4

I'm using LuaJ to run user-created Lua scripts in Java. However, running a Lua script that never returns causes the Java thread to freeze. This also renders the thread uninterruptible. I run the Lua script with:

JsePlatform.standardGlobals().loadFile("badscript.lua").call();

badscript.lua contains while true do end.

I'd like to be able to automatically terminate scripts which are stuck in unyielding loops and also allow users to manually terminate their Lua scripts while they are running. I've read about debug.sethook and pcall, though I'm not sure how I'd properly use them for my purposes. I've also heard that sandboxing is a better alternative, though that's a bit out of my reach.

This question might also be extended to Java threads alone. I've not found any definitive information on interrupting Java threads stuck in a while (true);.

The online Lua demo was very promising, but it seems the detection and termination of "bad" scripts is done in the CGI script and not Lua. Would I be able to use Java to call a CGI script which in turn calls the Lua script? I'm not sure that would allow users to manually terminate their scripts, though. I lost the link for the Lua demo source code but I have it on hand. This is the magic line:

tee -a $LOG | (ulimit -t 1 ; $LUA demo.lua 2>&1 | head -c 8k)

Can someone point me in the right direction?

Some sources:

Community
  • 1
  • 1
xikkub
  • 1,641
  • 1
  • 16
  • 28

5 Answers5

7

I struggled with the same issue and after some digging through the debug library's implementation, I created a solution similar to the one proposed by David Lewis, but did so by providing my own DebugLibrary:

package org.luaj.vm2.lib;

import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;

public class CustomDebugLib extends DebugLib {
    public boolean interrupted = false;

    @Override
    public void onInstruction(int pc, Varargs v, int top) {
        if (interrupted) {
            throw new ScriptInterruptException();
        }
        super.onInstruction(pc, v, top);
    }

    public static class ScriptInterruptException extends RuntimeException {}
}

Just execute your script from inside a new thread and set interrupted to true to stop the execution. The exception will be encapsulated as the cause of a LuaError when thrown.

Seldon
  • 388
  • 2
  • 12
2

There are problems, but this goes a long way towards answering your question.

The following proof-of-concept demonstrates a basic level of sandboxing and throttling of arbitrary user code. It runs ~250 instructions of poorly crafted 'user input' and then discards the coroutine. You could use a mechanism like the one in this answer to query Java and conditionally yield inside a hook function, instead of yielding every time.

SandboxTest.java:

public static void main(String[] args) {
    Globals globals = JsePlatform.debugGlobals();

    LuaValue chunk = globals.loadfile("res/test.lua");

    chunk.call();
}

res/test.lua:

function sandbox(fn)
    -- read script and set the environment
    f = loadfile(fn, "t")
    debug.setupvalue(f, 1, {print = print})

    -- create a coroutine and have it yield every 50 instructions
    local co = coroutine.create(f)
    debug.sethook(co, coroutine.yield, "", 50)

    -- demonstrate stepped execution, 5 'ticks'
    for i = 1, 5 do
        print("tick")
        coroutine.resume(co)
    end
end

sandbox("res/badfile.lua")

res/badfile.lua:

while 1 do
    print("", "badfile")
end

Unfortunately, while the control flow works as intended, something in the way the 'abandoned' coroutine should get garbage collected is not working correctly. The corresponding LuaThread in Java hangs around forever in a wait loop, keeping the process alive. Details here:

How can I abandon a LuaJ coroutine LuaThread?

Community
  • 1
  • 1
David Lewis
  • 161
  • 6
0

I've never used Luaj before, but could you not put your one line

JsePlatform.standardGlobals().loadFile("badscript.lua").call();

Into a new thread of its own, which you can then terminate from the main thread?

This would require you to make some sort of a supervisor thread (class) and pass any started scripts to it to supervise and eventually terminate if they don't terminate on their own.

  • I know of no way to interrupt LuaValue.call(). Regardless of where you put it, whatever thread calls it will become uninterruptible. – xikkub Jul 05 '13 at 22:35
  • So I actually coded up what I proposed, and as mentioned above, it does not respond to a thread.interrupt() which totally sucks. – user2555167 Jul 06 '13 at 00:11
  • The notes from Oracle say: In some cases, you can use application specific tricks. For example, if a thread is waiting on a known socket, you can close the socket to cause the thread to return immediately. Unfortunately, there really isn't any technique that works in general. It should be noted that in all situations where a waiting thread doesn't respond to Thread.interrupt, it wouldn't respond to Thread.stop either. Such cases include deliberate denial-of-service attacks, and I/O operations for which thread.stop and thread.interrupt do not work properly. – user2555167 Jul 06 '13 at 00:12
0

EDIT: I've not found any way to safely terminate LuaJ's threads without modifying LuaJ itself. The following was what I came up with, though it doesn't work with LuaJ. However, it can be easily modified to do its job in pure Lua. I may be switching to a Python binding for Java since LuaJ threading is so problematic.

--- I came up with the following, but it doesn't work with LuaJ ---

Here is a possible solution. I register a hook with debug.sethook that gets triggered on "count" events (these events occur even in a while true do end). I also pass a custom "ScriptState" Java object I created which contains a boolean flag indicating whether the script should terminate or not. The Java object is queried in the Lua hook which will throw an error to close the script if the flag is set (edit: throwing an error doesn't actually terminate the script). The terminate flag may also be set from inside the Lua script.

If you wish to automatically terminate unyielding infinite loops, it's straightforward enough to implement a timer system which records the last time a call was made to the ScriptState, then automatically terminate the script if sufficient time passes without an API call (edit: this only works if the thread can be interrupted). If you want to kill infinite loops but not interrupt certain blocking operations, you can adjust the ScriptState object to include other state information that allows you to temporarily pause auto-termination, etc.

Here is my interpreter.lua which can be used to call another script and interrupt it if/when necessary. It makes calls to Java methods so it will not run without LuaJ (or some other Lua-Java library) unless it's modified (edit: again, it can be easily modified to work in pure Lua).

function hook_line(e)
    if jthread:getDone() then
        -- I saw someone else use error(), but an infinite loop still seems to evade it.
        -- os.exit() seems to take care of it well.
        os.exit()
    end
end

function inithook()
    -- the hook will run every 100 million instructions.
    -- the time it takes for 100 million instructions to occur
    --   is based on computer speed and the calling environment
    debug.sethook(hook_line, "", 1e8)
    local ret = dofile(jLuaScript)
    debug.sethook()
    return ret
end

args = { ... }
if jthread == nil then
    error("jthread object is nil. Please set it in the Java environment.",2)
elseif jLuaScript == nil then
    error("jLuaScript not set. Please set it in the Java environment.",2)
else
    local x,y = xpcall(inithook, debug.traceback)
end

Here's the ScriptState class that stores the flag and a main() to demonstrate:

public class ScriptState {

    private AtomicBoolean isDone = new AtomicBoolean(true);
    public boolean getDone() { return isDone.get(); }
    public void setDone(boolean v) { isDone.set(v); }

    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                System.out.println("J: Lua script started.");
                ScriptState s = new ScriptState();
                Globals g = JsePlatform.debugGlobals();
                g.set("jLuaScript", "res/main.lua");
                g.set("jthread", CoerceJavaToLua.coerce(s));
                try {
                    g.loadFile("res/_interpreter.lua").call();
                } catch (Exception e) {
                    System.err.println("There was a Lua error!");
                    e.printStackTrace();
                }
            }
        };
        t.start();
        try { t.join(); } catch (Exception e) { System.err.println("Error waiting for thread"); }
        System.out.println("J: End main");
    }
}

res/main.lua contains the target Lua code to be run. Use environment variables or parameters to pass additional information to the script as usual. Remember to use JsePlatform.debugGlobals() instead of JsePlatform.standardGlobals() if you want to use the debug library in Lua.

EDIT: I just noticed that os.exit() not only terminates the Lua script but also the calling process. It seems to be the equivalent of System.exit(). error() will throw an error but will not cause the Lua script to terminate. I'm trying to find a solution for this now.

Community
  • 1
  • 1
xikkub
  • 1,641
  • 1
  • 16
  • 28
  • This looks promising. Have you tried using coroutine.yield()? Not sure if it will yield the current thread unless you set up coroutines, but I'm hopeful. I'll be trying your solution with yielding later and report back whether it works. – Sardtok Jul 16 '13 at 08:51
  • Looking at the code, using coroutines would not help. When the thread starts, it will simply run the code forever on a new thread, while the thread that started it waits for it to finish. Interrupting the starting thread is meant to kill the VM through the OrphanedThread error, but that wouldn't be much different from os.exit(). – Sardtok Jul 16 '13 at 12:42
  • Yeah I noticed. I hadn't gotten around to updating it. I may abandon LuaJ and try something like Jython instead. – xikkub Jul 17 '13 at 10:07
  • It would be nice if LuaJ exposed its threads to me or provided a way of cleanly terminating them, but I guess that's not option. – xikkub Jul 17 '13 at 10:19
  • 1
    I would have preferred if os.exit simply killed the Lua VM, not the enclosing JVM. There could be a setting for it in Globals, as most people would probably expect it to work like it always has. I'm going to post a feature request on LuaJ's SourceForge. – Sardtok Jul 22 '13 at 17:43
  • 1
    I took the liberty of creating the ticket myself. Thanks for your help. I hope this will be addressed. I very much like LuaJ. https://sourceforge.net/p/luaj/feature-requests/5/ – xikkub Jul 24 '13 at 06:53
0

Thanks to @Seldon for suggesting the use of custom DebugLib. I implemented a simplified version of that by just checking before every instruction if a predefined amount of time is elapsed. This is of course not super accurate because there is some time between class creation and script execution. Requires no separate threads.

class DebugLibWithTimeout(
    timeout: Duration,
) : DebugLib() {

    private val timeoutOn = Instant.now() + timeout

    override fun onInstruction(pc: Int, v: Varargs, top: Int) {
        val timeoutElapsed = Instant.now() > timeoutOn
        if (timeoutElapsed)
            throw Exception("Timeout")
        super.onInstruction(pc, v, top)
    }
}

Important note: if you sandbox an untrusted script calling load function on Lua-code and passing a separate environment to it, this will not work. onInstruction() seems to be called only if the function environment is a reference to _G. I dealt with that by stripping everything from _G and then adding whitelisted items back.

-- whitelisted items
local sandbox_globals = {
    print = print
}

local original_globals = {}
for key, value in pairs(_G) do
    original_globals[key] = value
end

local sandbox_env = _G
-- Remove everything from _G
for key, _ in pairs(sandbox_env) do
    sandbox_env[key] = nil
end

-- Add whitelisted items back.
-- Global pairs-function cannot be used now.
for key, value in original_globals.pairs(sandbox_globals) do
    sandbox_env[key] = value
end

local function run_user_script(script)
    local script_function, message = original_globals.load(script, nil, 't', sandbox_env)
    if not script_function then
        return false, message
    end

    return pcall(script_function)
end
Paulus Limma
  • 432
  • 6
  • 10