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.
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
Related
I feel like this should be simple, but can't seem to solve it. I have a function in Lua that's designed to validate a confirm code in a survey. Basically, if the ID is valid, then we can grab lots of data from that code, but if it's not a valid code, the script will break because it'll be populating nil values.
So, I basically need an error check — if the function can run properly, then run it. If it can't then I need to ask for a new code.
I've tried using pcall which feels like is exactly for this. I'm working off the Lua documentation:
if pcall(foo) then
-- no errors while running `foo'
...
else
-- `foo' raised an error: take appropriate actions
...
end
On my end, that means I have a function:
function populate()
... doing lots here to unencrypt and parse the ID someone gives and populate variables
end
Then I'm running the follwing:
if pcall(populate) then
print('no errors!') -- Just printing as a test, if there's no error, I'll run the script
else
print('Oh snap theres an error!) -- I'll change this to ask the user for a valid ID and then try again
end
What am I missing? I know it's going to be simple. But the last part of my code always returns the "Oh snap..." no matter what.
Thanks in advance, I have a super complex code running that I was able to build from just reading responses to other questions, but can't seem to get this simple part to work. Entirely possible I'm missing the point of pcall.
What do you expect?
If i am unsure what happen or which return value i should use for another condition i normally test/check in Lua Standalone...
€ lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
> pcall(string.gsub,'foo','%l','bar') -- Check for lowercases
true barbarbar 3
> pcall(string.gsub,'foo','%u','bar') -- Check for uppercases
true foo 0
> -- In this case i have to use maybe the third return value to decide what to do?
> -- OK -- Lets go...
> a, b, c = pcall(string.gsub,'foo','%u','bar') -- Check for uppercases
> if a and c == 0 then print('Only Kiddies here!') return false end
Only Kiddies here!
false
>
I'm attempting to constantly read and parse a log file (Minecraft log file) by using io.popen in tandem with Ubuntu's tail command so that I can send some messages upon certain events.
Now, I have mostly everything working here, except one small issue. After a while of reading, the entire program just freezes.
Here is the relevant code:
-- Open the tail command, return a file handle for it.
local pop = io.popen(config.listen_command)
-- Simply read a single line, I've pulled this into its own
-- function so that if this ever needs changing I can do so
-- easily.
local function get_line()
logger:log(4, "READ LINE")
return pop:read("*l")
end
-- For each line in the log file, check if it matches any
-- of a list of patterns, return the matches and the
-- pattern information if so.
local function match_line()
local line = get_line()
logger:log(4, "Line: %s", line)
-- This all works, and I've tested that it's not freezing
-- here. I've just included it for completion of the call
-- -stack.
for event_type, data in pairs(config.message_patterns) do
for event_name, pattern in pairs(data) do
local matches = {line:match(pattern)}
if matches[1] then
return event_type, event_name, matches
end
end
end
end
-- The main loop, simply read a line and send a message
-- if there was a match.
logger:log(4, "Main loop begin.")
while true do
local event_type, event_name, matches = match_line()
-- ...
-- The rest of the code here is not relevant.
config.listen_command = "tail -F --lines=1 latest.log"
The issue is in the get_line function. After a while of reading the log file, it completely freezes on the pop:read("*l"). It prints the READ LINE message, but never prints the Line: <whatever data here> line.
This is a really strange issue that I've been getting really confused about. I've tried swapping to different distributions of Lua (Luvit, LuaJIT, Lua) and a very large amount of debugging, changing small things, rerunning, ... But I cannot think of anything that'd be causing this.
Perhaps there's something small I've missed.
So my question here is this: Why is pop:read("*l") freezing, even though more data is being outputted to the logfile? Is there a way to fix this? Perhaps to detect if the next read will freeze indefinitely, so I can try closing the popen'ed file and re-open it (or to preferably stop it happening altogether?)
I'm building, or trying to build, a mod called More Drops (DS). I ported it to Don't Starve from "Don't Starve Together", so my lua knowledge is very limited, but I've been working on this thing for days and can't figure it out, so I need to ask for help.
I figured out how to expand on the original mod authors work by adding new trees and resources, but when it comes to "hackable" resources, I'm stumped.
So this is what I was trying, and tons of variations on this. This doesn't work and I don't understand why, and I'll show you what does work in the next two.
--bamboo
if inst.name == "Bamboo Patch" and utils.LootRandom(bambooChance) then
utils.DoTimes(GetModConfigData("bambooAmount", KnownModIndex:GetModActualName("(JEM) More Drops DS")), inst.components.lootdropper.SpawnLootPrefab, inst.components.lootdropper, "bamboo")
end
This does work
--Spawn extra logs for tall trees
if inst.components.growable.stage == 3 and utils.LootRandom(logChance) then
utils.DoTimes(GetModConfigData("extralogstall", KnownModIndex:GetModActualName("(JEM) More Drops DS")), inst.components.lootdropper.SpawnLootPrefab, inst.components.lootdropper, "log")
end
And this works as well
if inst.name == "Evergreen" and inst.components.growable.stage ~= 1 then
if utils.DropLootRandom(inst, "pinecone", seed_chance) then print("Dropped seed") end
end
Now I'm not sure how much of the files I should paste here, but here's the start of the trees.lua just to add a bit more context
local utils = require("utils")
local logChance = GetModConfigData("logChance", KnownModIndex:GetModActualName("(JEM) More Drops DS"))
local function ImproveTree(inst)
--Do these when the tree is fully chopped
local seed_chance = GetModConfigData("treeseedchance", KnownModIndex:GetModActualName("(JEM) More Drops DS"))
local egg_chance = GetModConfigData("eggChance", KnownModIndex:GetModActualName("(JEM) More Drops DS"))
local coconut_seed_chance = GetModConfigData("coconutChance", KnownModIndex:GetModActualName("(JEM) More Drops DS"))
local bambooChance = GetModConfigData("bambooChance", KnownModIndex:GetModActualName("(JEM) More Drops DS"))
local oldonfinish = inst.components.workable.onfinish
inst.components.workable:SetOnFinishCallback(function(inst, chopper)
All of the info is called in modmain.lua correctly as far as I can tell, I made sure to add things in the same way as the working code.
I expected this to drop some extra bamboo! I've tried so many different ways, even made my own choppable.lua file and added it the same way the original author added pickable.lua, but I figured it's gotta be a bit more like trees than regular resources, since you use a tool and it drops the item on the ground rather than adding to invetory.
If you want to see the whole mod, I've uploaded it here: https://www.dropbox.com/s/ahss2d0s861j2an/%28JEM%29%20More%20Drops%20DS.7z?dl=0
Update:
Some further reading (local variable cannot be seen in a closure across the file?) gave me an "aha!" moment as to exactly why my code didn't work.
In Lua, local x is visible to anything within the same scope - things in the same function, if/then structures, for loops, other functions called from that function, etc, but not other modules, even if they're called from within the local's scope. I haven't found a better explanation than "because reasons", but simply knowing that modules are an exception to the "normal" scope behavior at least puts my mind at ease.
Original post:
(Terribly sorry if there's an answer for this out there; I've been Googling for a couple of hours and come up short.)
I've written a GUI library for my preferred audio software (Reaper). There's a strong potential for scripts to be running during recording/playback, so performance is a big issue, and I'm trying to keep everything Local where possible. Easy enough, in general, but I'm having a bit of trouble when it comes to using the GUI library + element classes in a script.
Main GUI module:
-- Core.lua --
local function GUI_table()
local GUI = {}
-- Template for GUI elements
GUI.Element = {}
function GUI.Element:new(name)
local elm = {}
setmetatable(elm, self)
self.__index = self
return elm
end
...add a bunch of GUI.do_this = function()....
return GUI
end
GUI = GUI_table()
All of the GUI elements are separate files of the same form:
-- Class - Button.lua --
if not GUI then throw_a_missing_library_error_and_quit end
GUI.Button = GUI.Element:new()
function GUI.Button:draw()
...etc...
I'm currently loading them from the parent script via loadfile("Core.lua")(). This works well enough, but it places GUI in the global table with the associated overhead for lookups. Trying to rewrite things so that GUI can be local has, thus far, not gone well. I've tried:
local GUI
loadfile("Core.lua")()
loadfile("Class - Button.lua")()
...
Fails because the main script's GUI calls all go to the local _GUI, but loaded files can't see or add to it because of scoping.
loadfile("Core.lua")()
loadfile("Class - Button.lua")()
local GUI = GUI
Runs fine, but doesn't make a difference performancewise: Errors in module code still trace back to the module (i.e. "line 23 in Class - Button.lua"), which leads me to assume the modules are still held within their own scope and my local GUI isn't actually being touched.
I've also tried having Core.lua return the GUI table directly, so the main script can have local GUI = loadfile("Core.lua")(), but I ran into trouble giving the element modules access to it, as above. I know, scoping.
So, given all of the above, is there a "correct" way to write/structure the modules so that everything ends up in a local GUI? I get the impression that the defunct module(..., package.seeall) functionality would have solved this... maybe not though.
Cheers.
This is a problem with circular dependencies. I notice that Element and Button go inside GUI, but they don't depend on GUI for their construction. Putting Element in its own file will allow you to make everything local.
-- Core.lua --
local GUI = {}
-- Template for GUI elements
GUI.Element = require 'Element'
GUI.Button = require 'Button'
return GUI
-- Element.lua --
return {
new = function(self, name)
local elm = {}
setmetatable(elm, self)
self.__index = self
return elm
end,
}
-- Class - Button.lua --
Button = require('Element'):new()
function Button:draw()
end
return Button
Ok, I'm wanting to know if there's a way of running scripts from an external source with Lua. Be it another file in .txt format or from pastebin, what have you, and run the code and wait for said code to finish, and then continue on with the rest of the function. I'm not quite sure at all about how it'd work, but this is basically the idea I'm going by and isn't actual code.
function runStuff()
print("checking for stuff")
run.script(derp.txt)
wait for script
if run.script == finished
continue program
elseif nil
print("looks like this script sucks.")
end
end
runStuff()
And for example what "derp.txt" contains is:
function lookFile()
if herp.txt exists then
rename herp.txt to herp.lua
else
nil
end
end
lookFile()
I'm still new to Lua so I'm coding like a moron here, but you get my picture hopefully. I'm working on a project that'll act like an installer package for repositories based from pastebin or anywhere else that'll supply raw format outputs of lua scripts and I'm going to use that idea for it to call on scripts to run externally. So when I supply a "First Time Run" version of the program, it'll call out to a lua script that'll call to another lua script and install that script, then close.
This is for minecraft, mind you. ComputerCraft made me take interest in Lua, but anyway, hopefully you got the gist of what I'm trying to figure out. Hopefully that's doable and if not, I'll just have to figure something else out.
To load and execute a Lua code fragment you can use something like this:
local http = require("socket.http")
local response, err = http.request("url to some Lua code")
if not response then error(err) end
local f, err = (loadstring or load)(response)
if not f then error(err) end
print("done with "..response)
-- make sure you read on (in)security implications of running remote code
-- f() -- call the function based on the remote code
You probably need to use sandboxing.