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.
_G. And after that you're "shallow-restore"_Gfrom its copy. After that, in your global env table the field_Gis not equal to the table itself. That's not good. - Egor Skriptunoff