From 5d084a51b9bb070ecfe44411a5184c2f5ff8bcc9 Mon Sep 17 00:00:00 2001 From: IonutParau Date: Mon, 26 May 2025 18:07:20 +0200 Subject: [PATCH] basic sandbox progress --- src/sandbox.lua | 440 +++++++++++++++++++++++++++++++++++++++++++--- src/testLuaArch.c | 35 ++++ 2 files changed, 451 insertions(+), 24 deletions(-) diff --git a/src/sandbox.lua b/src/sandbox.lua index 5dc6f79..e775a80 100644 --- a/src/sandbox.lua +++ b/src/sandbox.lua @@ -1,35 +1,427 @@ -- sandbox stuff -local clist = component.list -function component.list(type, exact) - local m = clist() - if not type then return m end - local t = {} - for addr, kind in pairs(m) do - if exact then - if type == kind then - t[addr] = kind - end - else - if string.match(kind, type) then - t[addr] = kind +local function copy(v, p) + if type(v) == "table" then + local t = {} + for key, val in pairs(v) do + t[key] = copy(val) + end + if p then + for key, val in pairs(p) do + if not rawget(t, key) then + t[key] = copy(val) + end end end + return t + else + return v end - setmetatable(t, { - __call = function() - return next(t) +end + +local function nextDeadline() + return computer.uptime() + 5 +end + +local bubbleYield = false +local timeout = nextDeadline() + +local function tooLong() + return computer.uptime() >= timeout +end + +local tooLongWithoutYielding = "too long without yielding" + +local function nextHeatUp() + return computer.uptime() + math.random() * 2 + 0.1 +end + +local heatInc = nextHeatUp() + +debug.sethook(function() + if computer.uptime() >= heatInc then + heatInc = nextHeatUp() + computer.addHeat(math.random() * 3) + end + + if tooLong() and not bubbleYield then + bubbleYield = true + error(tooLongWithoutYielding) -- here it is an actual string + end +end, "c", 20000) -- no bogo mips, the check is cheap anyways + +local function resume(co, val1, ...) + while true do + local t = {coroutine.resume(co, val1, ...)} + if bubbleYield then -- yield was meaningless + coroutine.yield() -- carry through + else + return table.unpack(t) -- yield the user cares about + end + end +end + +local function yield() + bubbleYield = true + coroutine.yield() +end + +local function ensureYields() + if bubbleYield then + coroutine.yield() + end +end + +local function checkArg(idx, v, ...) + local bad = true + local n = select("#", ...) + for i=1,n do + local t = select(i, ...) + if type(v) == t then bad = false break end + end + if not bad then return end + local msg = string.format("bad argument #%d (%s expected, got %s)", idx, table.concat({...}, " or "), type(v)) + error(msg, 3) +end + +local libcomponent + +local componentProxy = { + __pairs = function(self) + local method + return function() + method = next(self, method) + if method then + return method, self[method] + end + end + end, +} + +local componentCallback = { + __call = function(self, ...) + return libcomponent.invoke(self.address, self.name, ...) + end, + __tostring = function(self) + return libcomponent.doc(self.address, self.name) or "undocumented" + end +} + +libcomponent = { + invoke = function(addr, method, ...) + checkArg(1, addr, "string") + checkArg(2, method, "string") + + while true do + local r = {pcall(component.invoke, addr, method, ...)} + + -- in this situation, either the temperature is above 100 C and we throttle + -- or the call budget has been filled and we dont care + if computer.isOverheating() or computer.isOverworked() then + local ok = pcall(yield) + assert(ok, "component explicitly requested to be suspended") + end + + if computer.getState() == states.blackout then + -- oops, powerout + local ok = pcall(yield) + assert(ok, "blackout") + end + + if computer.getState() ~= states.busy then + -- busy gets to try again + if r[1] then + return table.unpack(r, 2) + end + return nil, r[2] + end + end + end, + list = function(filter, exact) + checkArg(1, filter, "string", "nil") + local t = component.list() + if not filter then return t end + local list = {} + for addr, kind in pairs(t) do + if type(exact) == "boolean" and exact then + if kind == filter then + list[addr] = kind + end + elseif rawequal(exact, "pattern") then + if string.match(kind, filter) then + list[addr] = kind + end + else + if string.find(kind, filter, nil, true) then + list[addr] = kind + end + end + end + + local key = nil + return setmetatable(list, { + __call = function() + key = next(list, key) + if key then + return key, list[key] + end + end, + }) + end, + methods = component.methods, + fields = component.fields, + doc = component.doc, + slot = component.slot, + type = component.type, + proxy = function(addr) + checkArg(1, addr, "string") + if not component.type(addr) then return nil, "no such component" end + local proxy = setmetatable({ + address = addr, + type = component.type(addr), + slot = component.slot(addr), + fields = {}, + }, componentProxy) + local methods = component.methods(addr) + for method in pairs(methods) do + proxy[method] = setmetatable({address = addr, name = method}, componentCallback) + end + return proxy + end, +} + +local libcomputer = { + isRobot = function() + return libcomponent.list("robot", true) ~= nil + end, + address = computer.address, + tmpAddress = computer.tmpAddress, + usedMemory = computer.usedMemory, + freeMemory = computer.freeMemory, + totalMemory = computer.totalMemory, + uptime = computer.uptime, + energy = computer.energy, + maxEnergy = computer.maxEnergy, + users = computer.users, + -- these 2 are not actually implemented + -- TODO: implement them + addUser = computer.addUser, + removeUser = computer.removeUser, + + shutdown = function(reboot) + computer.setState(reboot and states.REPEAT or states.closing) + yield() + end, + pushSignal = computer.pushSignal, + pullSignal = function(timeout) + local deadline = computer.uptime() + (type(timeout) == "number" and timeout or math.huge) + + repeat + yield() -- give executor a chance to give us stuff + local s = table.pack(computer.pullSignal()) + if s.n > 0 then + return table.unpack(s) + end + until computer.uptime() >= deadline + end, + beep = computer.beep, + getDeviceInfo = function() + return {} -- yup + end, + getProgramLocations = function() + return {} -- yup + end, + + getArchitectures = computer.getArchitectures, + getArchitecture = computer.getArchitecture, + setArchitecture = function(...) + computer.setArchitecture(...) -- also sets state to SWITCH + yield() + end, + getTemperature = computer.getTemperature, +} + +local sandbox +sandbox = { + assert = assert, + error = error, + getmetatable = function(t) + if type(t) == "string" then -- HUGE security problem + return nil -- fixed + end + return getmetatable(t) + end, + ipairs = ipairs, + load = function(ld, source, _, env) -- mode is ignored as bytecode is just fully illegal for now + return load(ld, source, "t", env or sandbox) + end, + next = next, + pairs = pairs, + pcall = function(...) + if tooLong() then + yield() + return false, tooLongWithoutYielding + end + local t = {pcall(...)} + ensureYields() -- if it took too long, this will make it yield + return table.unpack(t) + end, + rawequal = rawequal, + rawget = rawget, + rawlen = rawlen, + rawset = rawset, + select = select, + setmetatable = function(t, mt) + if type(mt) ~= "table" then + return setmetatable(t, mt) + end + -- we do mutate the metatable but this field shouldn't exist anyways + mt.__gc = nil + return setmetatable(t, mt) + end, + tonumber = tonumber, + tostring = tostring, + type = type, + _VERSION = _VERSION, + xpcall = function(f, msgh, ...) + checkArg(1, f, "function") + checkArg(2, msgh, "function") + + -- to prevent infinite loops we simply terminate the error handler if it took too long. + local function errorCapture(ff, ...) + --ensureYields() -- you can't yield in xpcall... + -- Immediately dont care + if tooLong() then + return nil, tooLongWithoutYielding + end + -- This would mean you shutdown in the errorCapture. + -- In vanilla OC, that does nothing. + -- In here, it returns a suspended error and then yields eventually. + if bubbleYield then + return nil, "suspended" + end + + return xpcall(ff, function(...) + if tooLong() then + return tooLongWithoutYielding + else + return select(2, errorCapture(msgh, ...)) + end + end, ...) + end + + local t = {errorCapture(f, ...)} + pcall(ensureYields) -- it can fail if we are doing xpcall in xpcall. + return table.unpack(t) + end, + + coroutine = { + create = coroutine.create, + resume = resume, + running = coroutine.running, + status = coroutine.status, + yield = coroutine.yield, + wrap = function(f) + -- uses the correct resume + local co = coroutine.create(f) + return function(...) + local result = {resume(co, ...)} + if result[1] then + return table.unpack(result, 2) + else + error(result[2], 0) + end + end end, - }) - return t + isyieldable = coroutine.isyieldable, + }, + + string = copy(string), + table = copy(table), + math = copy(math, { + -- patch table + atan2 = math.atan, + ldexp = function(a, e) return a * (2.0 ^ e) end, + pow = function(a, b) return a ^ b end, + }), + + os = { + clock = os.clock, + date = os.date, + difftime = function(t2, t1) return t2 - t1 end, -- thanks UNIX + time = function(t) + checkArg(1, t, "table", "nil") + return os.time(t) + end, + }, + + debug = { + getinfo = function(...) + local result = debug.getinfo(...) + if result then + return { + source = result.source, + short_src = result.short_src, + linedefined = result.linedefined, + lastlinedefined = result.lastlinedefined, + what = result.what, + currentline = result.currentline, + nups = result.nups, + nparams = result.nparams, + isvararg = result.isvararg, + name = result.name, + namewhat = result.namewhat, + istailcall = result.istailcall, + -- believe it or not, this IS NOT safe. + -- They may use this to re-call machine.lua which would reset the hook and timeout. + -- TODO: make this safe. + --func = result.func, + } + end + end, + traceback = debug.traceback, + -- we only allow the first return, aka not the value. + -- Otherwise, some dumb shmuck could do nasty stuff. + -- TODO: make them not need this. + getlocal = function(...) return (debug.getlocal(...)) end, + getupvalue = function(...) return (debug.getupvalue(...)) end, + }, + + utf8 = copy(utf8), + checkArg = checkArg, + component = libcomponent, + computer = libcomputer, +} +sandbox._G = sandbox + +local function bootstrap() + local eeprom = libcomponent.list("eeprom")() + assert(eeprom, "no eeprom") + + local code = libcomponent.invoke(eeprom, "get") + assert(code and #code > 0, "empty eeprom") + + return assert(load(code, "=bios", "t", sandbox)) end -for addr, kind in pairs(component.list()) do - if kind == "eeprom" then - local data = component.invoke(addr, "get") - assert(load(data, "=eeprom"))() - return +coroutine.yield() -- startup delay + +local f = bootstrap() +local co = coroutine.create(f) + +while true do + timeout = nextDeadline() + bubbleYield = false + + local ok, err = resume(co) + + if not ok then + error(err) + elseif coroutine.status(co) == "dead" then + error("computer halted", 0) + else + coroutine.yield() end end -error("no bios") +error("machine halted") diff --git a/src/testLuaArch.c b/src/testLuaArch.c index 809389a..b4bb9b8 100644 --- a/src/testLuaArch.c +++ b/src/testLuaArch.c @@ -272,6 +272,19 @@ static int testLuaArch_computer_users(lua_State *L) { return i; } +static int testLuaArch_computer_getState(lua_State *L) { + nn_computer *c = testLuaArch_getComputer(L); + lua_pushinteger(L, nn_getState(c)); + return 1; +} + +static int testLuaArch_computer_setState(lua_State *L) { + nn_computer *c = testLuaArch_getComputer(L); + int s = luaL_checkinteger(L, 1); + nn_setState(c, s); + return 1; +} + static int testLuaArch_component_list(lua_State *L) { nn_computer *c = testLuaArch_getComputer(L); size_t len = 0; @@ -446,6 +459,10 @@ void testLuaArch_loadEnv(lua_State *L) { lua_setfield(L, computer, "popSignal"); lua_pushcfunction(L, testLuaArch_computer_users); lua_setfield(L, computer, "users"); + lua_pushcfunction(L, testLuaArch_computer_getState); + lua_setfield(L, computer, "getState"); + lua_pushcfunction(L, testLuaArch_computer_setState); + lua_setfield(L, computer, "setState"); lua_setglobal(L, "computer"); lua_createtable(L, 0, 10); @@ -465,6 +482,24 @@ void testLuaArch_loadEnv(lua_State *L) { lua_pushcfunction(L, testLuaArch_component_type); lua_setfield(L, component, "type"); lua_setglobal(L, "component"); + + lua_createtable(L, 0, 7); + int states = lua_gettop(L); + lua_pushinteger(L, NN_STATE_SETUP); + lua_setfield(L, states, "setup"); + lua_pushinteger(L, NN_STATE_RUNNING); + lua_setfield(L, states, "running"); + lua_pushinteger(L, NN_STATE_BUSY); + lua_setfield(L, states, "busy"); + lua_pushinteger(L, NN_STATE_BLACKOUT); + lua_setfield(L, states, "blackout"); + lua_pushinteger(L, NN_STATE_CLOSING); + lua_setfield(L, states, "closing"); + lua_pushinteger(L, NN_STATE_REPEAT); + lua_setfield(L, states, "REPEAT"); + lua_pushinteger(L, NN_STATE_SWITCH); + lua_setfield(L, states, "switch"); + lua_setglobal(L, "states"); } testLuaArch *testLuaArch_setup(nn_computer *computer, void *_) {