1
votes

Before I start let me clarify that I've looked through the existing questions regarding sandboxing, but none of the solutions I found really seemed to work for me.

What I am trying to implement is not necessarily a completely safe sandbox to run unsafe code, but provide an environment that emulates the functions of a game (Star Wars Empire at War to be exact) I'm modding that can be extended using Lua scripts. The game provides a bunch of global functions that I want to implement in this environment to be able to test my game extensions without launching the game itself, since its debugging capabilities are quite poor. The game also has some quirks. The relevant issue in this case is that using upvalues will produce corrupt save games, so this is why our Lua modules have to be globals. Since my game scripts are not contained in a single file and can require other files, call and declare their own functions using setfenv() to set an environment does not really work for me, since that applies only to the function you specify and not to any other functions it might call.

Now for my code itself. So far I've tried to implement a simple sandbox by simply deep cloning _G and restoring it to its former state afterwards (this is still WIP, I realize that you can still do table and environment modifications or potentially break out of the sandbox that aren't covered by this approach; as I said, this is not intended to run unsafe code).

This is in sandbox.lua:

local function run(func)
    -- tested deep_clone with luassert's assert.are_same(), so this should be fine
    local g_clone = deep_clone(_G)
    -- coroutine is needed because one of the game functions needs
    -- to be able to interrupt the script
    coroutine.wrap(function()
        func()
    end)()

    for k, v in pairs(_G) do
        if not g_clone[k] then
            _G[k] = nil
        else
            _G[k] = g_clone[k]
        end
    end
end

And this is my test script:

sandbox.run(function()
    require "src/declare_global" -- this declares A_GLOBAL = "TEST"
    print(A_GLOBAL) -- prints "TEST", everything is fine
end)

print(A_GLOBAL) -- prints nil, as intended

-- as I've restored _G to its former state I should be able to require 
-- the script again!
require "src/declare_global" 

print("package.loaded: "..tostring(package.loaded["src/declare_global"])) -- prints nil

print(A_GLOBAL) -- prints nil

As you can see my problem is that requiring the same file that simply declares a global after I did it in my sandbox doesn't work anymore. So there's probably an issue with restoring the state of _G, but I don't have any clue what it could be. The require call itself seems to work, though. If the script I'm requiring returns a function, I can still store it in a variable and execute it.

I'd really appreciate any suggestions on how to fix this.

1
If this sandbox doesn't exist to run unsafe code... what exactly is the goal of it? What do you need to prevent a function from doing? - Nicol Bolas
You're "deep-clone" _G. And after that you're "shallow-restore" _G from its copy. After that, in your global env table the field _G is not equal to the table itself. That's not good. - Egor Skriptunoff
@NicolBolas - The answer to your question is in the OP's question. - Egor Skriptunoff
@EgorSkriptunoff: I read the question. It doesn't explain what the purpose of the sandbox is. It explains what the sandbox should look like and that the script can't use locals, but nothing about what operations the sandbox should prevent. - Nicol Bolas
@NicolBolas - The word "sandbox" would better be replaced with "testing/debugging environment with ability to restore the globals after testing". - Egor Skriptunoff

1 Answers

0
votes

I found a solution to my problem. However, I feel this is an inefficient way to achieve what I want, so if somebody has a better suggestion I would love to hear it.

@EgorSkriptunoff's comment on my original question regarding the "shallow" the restoration of _G pointed me in the right direction, so I ended up writing a "deep restore" function, which seems to work fine with what I have tested so far.

local known_tables = {}

local function deep_restore(tab, backup)
    known_tables[tab] = true
    for k, v in pairs(tab) do
        if backup[k] == nil then
            tab[k] = nil
        elseif type(v) == "table" and not known_tables[v] then
            deep_restore(v, backup[k])
        elseif not type(v) == "table" then
            tab[k] = backup[k]
        end
    end
end


local function run(func)
    local g_clone = deep_clone(_G)

    coroutine.wrap(function()
        func()
    end)()

    deep_restore(_G, g_clone)
    known_tables = {}

end

Results:

sandbox.run(function()
    require "src/declare_global"
    print("Printing inside sandbox: "..A_GLOBAL) -- prints TEST
end)

print("This should be nil: "..tostring(A_GLOBAL)) -- nil
print("package.loaded should be nil: "..tostring(package.loaded["src/declare_global"])) -- nil

print("\nRequiring again...")
require "src/declare_global"
print("package.loaded: "..tostring(package.loaded["src/declare_global"])) -- true
print(A_GLOBAL) -- TEST