4

I am experimenting with a game mechanic in which players can run scripts on in-game computers. Script execution will be resource limited at a gameplay level to some amount of instructions per tick.

The following proof-of-concept demonstrates a basic level of sandboxing and throttling of arbitrary user code. It successfully runs ~250 instructions of poorly crafted 'user input' and then discards the coroutine. Unfortunately, the Java process never terminates. A little investigation in shows that the LuaThread created by LuaJ for the coroutine is hanging around forever.

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

The docs suggest that a coroutine that is considered unresumable will be garbage collected and an OrphanedThread exception will be thrown, signalling the LuaThread to end - but this is never happening. My question is in two parts:

  • Am I doing something fundamentally wrong to cause this behaviour?
  • If not, how should I handle this situation? From the source it appears that if I can get a reference to the LuaThread in Java I may be able to forcibly abandon it by issuing an interrupt(). Is this a good idea?

Reference: Lua / Java / LuaJ - Handling or Interrupting Infinite Loops and Threads

EDIT: I have posted a bug report over at the LuaJ SourceForge. It discusses the underlying issue (threads not being garbage collected as in the Lua spec) and suggests some ways to work around it.

Community
  • 1
  • 1
David Lewis
  • 161
  • 6

1 Answers1

3

It seems to be a limitation of LuaJ. I submitted a ticket earlier this year on Sourceforge as I see you've also done. The LuaThread class doesn't store references to the Java threads it creates, so you can't interrupt() those threads without modifying the LuaJ core to expose them:

new Thread(this, "Coroutine-"+(++coroutine_count)).start();

It may be dangerous to interrupt those threads without adding appropriate cleanup code to LuaJ.


Documentation that you provided for OrphanedThread also tells us that scope is the defining condition:

"Error sublcass that indicates a lua thread that is no longer referenced has been detected. The java thread in which this is thrown should correspond to a LuaThread being used as a coroutine that could not possibly be resumed again because there are no more references to the LuaThread with which it is associated. Rather than locking up resources forever, this error is thrown, and should fall through all the way to the thread's Thread.run() method."

Your code example doesn't cause all LuaThread references to disappear, so you shouldn't expect an exception to be thrown. CoroutineLib documentation indicates: Coroutines that are yielded but never resumed to complete their execution may not be collected by the garbage collector, so an OutOfMemoryError should actually be expected from the code you listed on SourceForge, if I'm not mistaken. LuaThread:52 also specifies: Applications should not catch OrphanedThread, because it can break the thread safety of luaj., which is yet another obstacle.


There also seem to be differences between empty and non-empty while loops in Lua/J. IIRC, empty loops (while true do end) don't obey all coroutine hook/tick rules. *Because no actions occur in an empty loop, there's no opportunity for certain hooks to occur (I need to test this again so please correct me otherwise!).

A forked version of LuaJ with the functionality we're looking for is used in the ComputerCraft mod for Minecraft, though it's designed only for the mod and isn't open source.

xikkub
  • 1,641
  • 1
  • 16
  • 28
  • It's hard to know what the documentation means when it says "there are no more references to the LuaThread with which it is associated." If it means the LuaThread generated in-library when I call coroutine.create() in Lua, then I never hold a reference to that in my Java code. So, it is staying alive due to some quirk under the hood of LuaJ. Regarding empty while loops, they do still increment the instruction counter and trigger debug hooks. – David Lewis Jul 08 '14 at 04:37