From b545646922bea406a37a2cd53c35588b63cfe301 Mon Sep 17 00:00:00 2001 From: James Roseborough Date: Fri, 17 Apr 2015 02:59:50 +0000 Subject: [PATCH] Add utilities and sample code to load luaj in custom class loader for strong sandboxing, and use of orphaned threads. --- README.html | 32 +++- build.xml | 6 +- .../jse/CollectingOrphanedCoroutines.java | 53 ++++++ examples/jse/SampleUsingClassLoader.java | 95 +++++++++++ .../org/luaj/vm2/server/DefaultLauncher.java | 105 ++++++++++++ src/jse/org/luaj/vm2/server/Launcher.java | 70 ++++++++ .../org/luaj/vm2/server/LuajClassLoader.java | 157 ++++++++++++++++++ 7 files changed, 513 insertions(+), 5 deletions(-) create mode 100644 examples/jse/CollectingOrphanedCoroutines.java create mode 100644 examples/jse/SampleUsingClassLoader.java create mode 100644 src/jse/org/luaj/vm2/server/DefaultLauncher.java create mode 100644 src/jse/org/luaj/vm2/server/Launcher.java create mode 100644 src/jse/org/luaj/vm2/server/LuajClassLoader.java diff --git a/README.html b/README.html index 0feaabad..f7ac2336 100644 --- a/README.html +++ b/README.html @@ -462,7 +462,29 @@ multiple threads see examples/js

As an alternative, the JSR-223 scripting interface can be used, and should always provide a separate Globals instance -per script engine instance by using a ThreadLocal internally. +per script engine instance by using a ThreadLocal internally. + +

Sandboxing

+Lua and luaj are allow for easy sandboxing of scripts in a server environment. +

+Considerations include +

+ +Luaj provides sample code covering various approaches: +

4 - Libraries

@@ -535,7 +557,8 @@ Luaj uses WeakReferences and the OrphanedThread error to ensure that coroutines are properly garbage collected. For thread safety, OrphanedThread should not be caught by Java code. See LuaThread and OrphanedThread -javadoc for details. +javadoc for details. The sample code in examples/jse/CollectingOrphanedCoroutines.java +provides working examples.

Debug Library

The debug library is not included by default by @@ -982,6 +1005,9 @@ Files are no longer hosted at LuaForge.
  • Fix os.date("*t") to return hour in 24 hour format (fixes issue #45)
  • Add SampleSandboxed.java example code to illustrate sandboxing techniques in Java.
  • Add samplesandboxed.lua example code to illustrate sandboxing techniques in lua.
  • +
  • Add CollectingOrphanedCoroutines.java example code to show how to deal with orphaned lua threads.
  • +
  • Add LuajClassLoader.java and Launcher.java to simplify loading via custom class loader.
  • +
  • Add SampleUsingClassLoader.java example code to demonstrate loading using custom class loader.
  • Make string metatable a proper metatable, and make it read-only by default.
  • Add sample code that illustrates techniques in creating sandboxed environments.
  • Add convenience methods to Global to load string scripts with custom environment.
  • @@ -1001,6 +1027,8 @@ Files are no longer hosted at LuaForge.
  • negative zero is treated as identical to integer value zero throughout luaj
  • lua compiled into java bytecode using luajc cannot use string.dump() or xpcall()
  • number formatting with string.format() is not supported +
  • shared metatables for string, bool, etc are shared across Globals instances in the same class loader +
  • orphaned threads will not be collected unless garbage collection is run and sufficient time elapses

    File Character Encoding

    Source files can be considered encoded in UTF-8 or ISO-8859-1 and results should be as expected, diff --git a/build.xml b/build.xml index f8dc2590..b3f32a60 100644 --- a/build.xml +++ b/build.xml @@ -86,11 +86,11 @@ + excludes="**/script/*,**/Lua2Java*,**/server/*,lua*"/> + includes="**/script/*,**/Lua2Java*,**/server/*"/> - + Luaj API]]> Copyright © 2007-2008 Luaj.org. All Rights Reserved.]]> diff --git a/examples/jse/CollectingOrphanedCoroutines.java b/examples/jse/CollectingOrphanedCoroutines.java new file mode 100644 index 00000000..86e79287 --- /dev/null +++ b/examples/jse/CollectingOrphanedCoroutines.java @@ -0,0 +1,53 @@ +import org.luaj.vm2.Globals; +import org.luaj.vm2.LuaThread; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.jse.JsePlatform; + +/** Example that continually launches coroutines, and illustrates how to make +* sure the orphaned coroutines are cleaned up properly. +* +* Main points: +*
    • Each coroutine consumes one Java Thread while active or reference anywhere
    • +*
    • All references to a coroutine must be dropped for the coroutine to be collected
    • +*
    • Garbage collection must be run regularly to remove weak references to lua threads
    • +*
    • LuaThread.thread_orphan_check_interval must be short enough to find orphaned references quickly
    • +*
    +*/ +public class CollectingOrphanedCoroutines { + + // Script that launches coroutines over and over in a loop. + // Garbage collection is done periodically to find and remove orphaned threads. + // Coroutines yield out when they are done. + static String script = + "i,n = 0,0\n print(i)\n" + + "f = function() n=n+1; coroutine.yield(false) end\n" + + "while true do\n" + + " local cor = coroutine.wrap(f)\n" + + " cor()\n" + + " i = i + 1\n" + + " if i % 1000 == 0 then\n" + + " collectgarbage()\n" + + " print('threads:', i, 'executions:', n, collectgarbage('count'))\n" + + " end\n" + + "end\n"; + + public static void main(String[] args) throws InterruptedException { + // This timer controls how often each Java thread wakes up and checks if + // it has been orhaned or not. A large number here will produce a long + // delay between orphaning and colleciton, and a small number here will + // consumer resources polling for orphaned status if there are many threads. + LuaThread.thread_orphan_check_interval = 500; + + // Should work with standard or debug globals. + Globals globals = JsePlatform.standardGlobals(); + // Globals globals = JsePlatform.debugGlobals(); + + // Should work with plain compiler or lua-to-Java compiler. + // org.luaj.vm2.luajc.LuaJC.install(globals);; + + // Load and run the script, which launches coroutines over and over forever. + LuaValue chunk = globals.load(script, "main"); + chunk.call(); + } + +} diff --git a/examples/jse/SampleUsingClassLoader.java b/examples/jse/SampleUsingClassLoader.java new file mode 100644 index 00000000..cb64039d --- /dev/null +++ b/examples/jse/SampleUsingClassLoader.java @@ -0,0 +1,95 @@ +import java.io.InputStream; +import java.io.Reader; + +import org.luaj.vm2.Globals; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.jse.JsePlatform; +import org.luaj.vm2.server.Launcher; +import org.luaj.vm2.server.LuajClassLoader; + +/** Example of using {@link LuajClassLoader} to launch scripts that are blocked from + * interfering with globals from other scripts including shared static metatables. + *

    + * This technique is useful in a server environment to expose the full set of + * lua features to each script, while preventing scripts from interfering with + * each other. + *

    + * Because each Launch gets its own {@link LuajClassLoader}, it should be possible + * to include the debug library, or let scripts manipulate shared metatables, + * or luajava, which otherwise present challenges in a server environment. + *

    + */ +public class SampleUsingClassLoader { + + /** Script that manipulates the shared string metatable. + * When loaded by the {@link LuajClassLoader} via a {@link Launcher} + * created by that class, each instance of {@link LuajClassLoader} will + * have a completely separate version of all static variables. + */ + static String script = + "print('args:', ...)\n" + + "print('abc.foo', ('abc').foo)\n" + + "getmetatable('abc').__index.foo = function() return 'bar' end\n" + + "print('abc.foo', ('abc').foo)\n" + + "print('abc:foo()', ('abc'):foo())\n" + + "return math.pi\n"; + + public static void main(String[] s) throws Exception { + // The default launcher used standard globals. + RunUsingDefaultLauncher(); + RunUsingDefaultLauncher(); + // Example using custom launcher class that instantiates debug globals. + RunUsingCustomLauncherClass(); + RunUsingCustomLauncherClass(); + } + + static void RunUsingDefaultLauncher() throws Exception { + Launcher launcher = LuajClassLoader.NewLauncher(); + // starts with pristine Globals including all luaj static variables. + print(launcher.launch(script, new Object[] { "--------" })); + // reuses Globals and static variables from previous step. + print(launcher.launch(script, new Object[] {})); + } + + static void RunUsingCustomLauncherClass() throws Exception { + Launcher launcher = LuajClassLoader.NewLauncher(MyLauncher.class); + // starts with pristine Globals including all luaj static variables. + print(launcher.launch(script, new Object[] { "=========" })); + // reuses Globals and static variables from previous step. + print(launcher.launch(script, new Object[] { "" })); + } + + /** Example of Launcher implementation performing specialized launching. + * When loaded by the {@link LuajClassLoader} all luaj classes will be loaded + * for each instance of the {@link Launcher} and not interfere with other + * classes loaded by other instances. + */ + public static class MyLauncher implements Launcher { + Globals g; + public MyLauncher() { + g = JsePlatform.debugGlobals(); + // ... plus any other customization of the user environment + } + + public Object[] launch(String script, Object[] arg) { + LuaValue chunk = g.load(script, "main"); + return new Object[] { chunk.call(LuaValue.valueOf(arg[0].toString())) }; + } + + public Object[] launch(InputStream script, Object[] arg) { + LuaValue chunk = g.load(script, "main", "bt", g); + return new Object[] { chunk.call(LuaValue.valueOf(arg[0].toString())) }; + } + + public Object[] launch(Reader script, Object[] arg) { + LuaValue chunk = g.load(script, "main"); + return new Object[] { chunk.call(LuaValue.valueOf(arg[0].toString())) }; + } + } + + /** Print the return values as strings. */ + private static void print(Object[] return_values) { + for (int i =0; i + * Arguments are coerced into lua using {@link CoerceJavaToLua#coerce(Object)}. + *

    + * Return values with simple types are coerced into Java simple types. + * Tables, threads, and functions are returned as lua objects. + * + * @see Launcher + * @see LuajClassLoader + * @see LuajClassLoader#NewLauncher() + * @see LuajClassLoader#NewLauncher(Class) + * @since luaj 3.0.1 + */ +public class DefaultLauncher implements Launcher { + protected Globals g; + + public DefaultLauncher() { + g = JsePlatform.standardGlobals(); + } + + /** Launches the script with chunk name 'main' */ + public Object[] launch(String script, Object[] arg) { + return launchChunk(g.load(script, "main"), arg); + } + + /** Launches the script with chunk name 'main' and loading using modes 'bt' */ + public Object[] launch(InputStream script, Object[] arg) { + return launchChunk(g.load(script, "main", "bt", g), arg); + } + + /** Launches the script with chunk name 'main' */ + public Object[] launch(Reader script, Object[] arg) { + return launchChunk(g.load(script, "main"), arg); + } + + private Object[] launchChunk(LuaValue chunk, Object[] arg) { + LuaValue args[] = new LuaValue[arg.length]; + for (int i = 0; i < args.length; ++i) + args[i] = CoerceJavaToLua.coerce(arg[i]); + Varargs results = chunk.invoke(LuaValue.varargsOf(args)); + + final int n = results.narg(); + Object return_values[] = new Object[n]; + for (int i = 0; i < n; ++i) { + LuaValue r = results.arg(i+1); + switch (r.type()) { + case LuaValue.TBOOLEAN: + return_values[i] = r.toboolean(); + break; + case LuaValue.TNUMBER: + return_values[i] = r.todouble(); + break; + case LuaValue.TINT: + return_values[i] = r.toint(); + break; + case LuaValue.TNIL: + return_values[i] = null; + break; + case LuaValue.TSTRING: + return_values[i] = r.tojstring(); + break; + case LuaValue.TUSERDATA: + return_values[i] = r.touserdata(); + break; + default: + return_values[i] = r; + } + } + return return_values; + } +} \ No newline at end of file diff --git a/src/jse/org/luaj/vm2/server/Launcher.java b/src/jse/org/luaj/vm2/server/Launcher.java new file mode 100644 index 00000000..378f7c81 --- /dev/null +++ b/src/jse/org/luaj/vm2/server/Launcher.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2015 Luaj.org. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ +package org.luaj.vm2.server; + +import java.io.InputStream; +import java.io.Reader; + +/** Interface to launch lua scripts using the {@link LuajClassLoader}. + *

    + * Note: This class is experimental and subject to change in future versions. + *

    + * This interface is purposely genericized to defer class loading so that + * luaj classes can come from the class loader. + *

    + * The implementation should be acquired using {@link LuajClassLoader#NewLauncher()} + * or {@link LuajClassLoader#NewLauncher(Class)} which ensure that the classes are + * loaded to give each Launcher instance a pristine set of Globals, including + * the shared metatables. + * + * @see LuajClassLoader + * @see LuajClassLoader#NewLauncher() + * @see LuajClassLoader#NewLauncher(Class) + * @see DefaultLauncher + * @since luaj 3.0.1 + */ +public interface Launcher { + + /** Launch a script contained in a String. + * + * @param script The script contents. + * @param arg Optional arguments supplied to the script. + * @return return values from the script. + */ + public Object[] launch(String script, Object[] arg); + + /** Launch a script from an InputStream. + * + * @param script The script as an InputStream. + * @param arg Optional arguments supplied to the script. + * @return return values from the script. + */ + public Object[] launch(InputStream script, Object[] arg); + + /** Launch a script from a Reader. + * + * @param script The script as a Reader. + * @param arg Optional arguments supplied to the script. + * @return return values from the script. + */ + public Object[] launch(Reader script, Object[] arg); +} \ No newline at end of file diff --git a/src/jse/org/luaj/vm2/server/LuajClassLoader.java b/src/jse/org/luaj/vm2/server/LuajClassLoader.java new file mode 100644 index 00000000..3886c1f8 --- /dev/null +++ b/src/jse/org/luaj/vm2/server/LuajClassLoader.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright (c) 2015 Luaj.org. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ +package org.luaj.vm2.server; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * Class loader that can be used to launch a lua script in a Java VM that has a + * unique set of classes for org.luaj classes. + *

    +* Note: This class is experimental and subject to change in future versions. + *

    + * By using a custom class loader per script, it allows the script to have + * its own set of globals, including static values such as shared metatables + * that cannot access lua values from other scripts because their classes are + * loaded from different class loaders. Thus normally unsafe libraries such + * as luajava can be exposed to scripts in a server environment using these + * techniques. + *

    + * All classes in the package "org.luaj.vm2." are considered user classes, and + * loaded into this class loader from their bytes in the class path. Other + * classes are considered systemc classes and loaded via the system loader. This + * class set can be extended by overriding {@link #isUserClass(String)}. + *

    + * The {@link Launcher} interface is loaded as a system class by exception so + * that the caller may use it to launch lua scripts. + *

    + * By default {@link #NewLauncher()} creates a subclass of {@link Launcher} of + * type {@link DefaultLauncher} which creates debug globals, runs the script, + * and prints the return values. This behavior can be changed by supplying a + * different implementation class to {@link #NewLauncher(Class)} which must + * extend {@link Launcher}. + * + * @see Launcher + * @see #NewLauncher() + * @see #NewLauncher(Class) + * @see DefaultLauncher + * @since luaj 3.0.1 + */ +public class LuajClassLoader extends ClassLoader { + + /** String describing the luaj packages to consider part of the user classes */ + static final String luajPackageRoot = "org.luaj.vm2."; + + /** String describing the Launcher interface to be considered a system class */ + static final String launcherInterfaceRoot = Launcher.class.getName(); + + /** Local cache of classes loaded by this loader. */ + Map> classes = new HashMap>(); + + /** + * Construct a default {@link Launcher} instance that will load classes in + * its own {@link LuajClassLoader} using the default implementation class + * {@link DefaultLauncher}. + *

    + * The {@link Launcher} that is returned will be a pristine luaj vm + * whose classes are loaded into this loader including static variables + * such as shared metatables, and should not be able to directly access + * variables from other Launcher instances. + * + * @return {@link Launcher} instance that can be used to launch scripts. + * @throws InstantiationException + * @throws IllegalAccessException + * @throws ClassNotFoundException + */ + public static Launcher NewLauncher() throws InstantiationException, + IllegalAccessException, ClassNotFoundException { + return NewLauncher(DefaultLauncher.class); + } + + /** + * Construct a {@link Launcher} instance that will load classes in + * its own {@link LuajClassLoader} using a user-supplied implementation class + * that implements {@link Launcher}. + *

    + * The {@link Launcher} that is returned will be a pristine luaj vm + * whose classes are loaded into this loader including static variables + * such as shared metatables, and should not be able to directly access + * variables from other Launcher instances. + * + * @return instance of type 'launcher_class' that can be used to launch scripts. + * @throws InstantiationException + * @throws IllegalAccessException + * @throws ClassNotFoundException + */ + public static Launcher NewLauncher(Class launcher_class) + throws InstantiationException, IllegalAccessException, + ClassNotFoundException { + final LuajClassLoader loader = new LuajClassLoader(); + final Object instance = loader.loadAsUserClass(launcher_class.getName()) + .newInstance(); + return (Launcher) instance; + } + + /** + * Test if a class name should be considered a user class and loaded + * by this loader, or a system class and loaded by the system loader. + * @param classname Class name to test. + * @return true if this should be loaded into this class loader. + */ + public static boolean isUserClass(String classname) { + return classname.startsWith(luajPackageRoot) + && !classname.startsWith(launcherInterfaceRoot); + } + + public Class loadClass(String classname) throws ClassNotFoundException { + if (classes.containsKey(classname)) + return classes.get(classname); + if (!isUserClass(classname)) + return super.findSystemClass(classname); + return loadAsUserClass(classname); + } + + private Class loadAsUserClass(String classname) throws ClassNotFoundException { + final String path = classname.replace('.', '/').concat(".class"); + InputStream is = getResourceAsStream(path); + if (is != null) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] b = new byte[1024]; + for (int n = 0; (n = is.read(b)) >= 0;) + baos.write(b, 0, n); + byte[] bytes = baos.toByteArray(); + Class result = super.defineClass(classname, bytes, 0, + bytes.length); + classes.put(classname, result); + return result; + } catch (java.io.IOException e) { + throw new ClassNotFoundException("Read failed: " + classname + + ": " + e); + } + } + throw new ClassNotFoundException("Not found: " + classname); + } +}