diff --git a/mesecons_luacontroller/depends.txt b/mesecons_luacontroller/depends.txt index acaa924..9fe6ca8 100644 --- a/mesecons_luacontroller/depends.txt +++ b/mesecons_luacontroller/depends.txt @@ -1 +1,2 @@ mesecons +mesecons_sandbox diff --git a/mesecons_luacontroller/init.lua b/mesecons_luacontroller/init.lua index 1c93e48..bd7ae87 100644 --- a/mesecons_luacontroller/init.lua +++ b/mesecons_luacontroller/init.lua @@ -197,369 +197,7 @@ end -- Parsing and running -- ------------------------- -local function safe_print(param) - local string_meta = getmetatable("") - local sandbox = string_meta.__index - string_meta.__index = string -- Leave string sandbox temporarily - print(dump(param)) - string_meta.__index = sandbox -- Restore string sandbox -end - -local function safe_date() - return(os.date("*t",os.time())) -end - --- string.rep(str, n) with a high value for n can be used to DoS --- the server. Therefore, limit max. length of generated string. -local function safe_string_rep(str, n) - if #str * n > mesecon.setting("luacontroller_string_rep_max", 64000) then - debug.sethook() -- Clear hook - error("string.rep: string length overflow", 2) - end - - return string.rep(str, n) -end - --- string.find with a pattern can be used to DoS the server. --- Therefore, limit string.find to patternless matching. -local function safe_string_find(...) - if (select(4, ...)) ~= true then - debug.sethook() -- Clear hook - error("string.find: 'plain' (fourth parameter) must always be true in a Luacontroller") - end - - return string.find(...) -end - -local function remove_functions(x) - local tp = type(x) - if tp == "function" then - return nil - end - - -- Make sure to not serialize the same table multiple times, otherwise - -- writing mem.test = mem in the Luacontroller will lead to infinite recursion - local seen = {} - - local function rfuncs(x) - if x == nil then return end - if seen[x] then return end - seen[x] = true - if type(x) ~= "table" then return end - - for key, value in pairs(x) do - if type(key) == "function" or type(value) == "function" then - x[key] = nil - else - if type(key) == "table" then - rfuncs(key) - end - if type(value) == "table" then - rfuncs(value) - end - end - end - end - - rfuncs(x) - - return x -end - --- The setting affects API so is not intended to be changeable at runtime -local get_interrupt -if mesecon.setting("luacontroller_lightweight_interrupts", false) then - -- use node timer - get_interrupt = function(pos, itbl, send_warning) - return (function(time, iid) - if type(time) ~= "number" then error("Delay must be a number") end - if iid ~= nil then send_warning("Interrupt IDs are disabled on this server") end - table.insert(itbl, function() minetest.get_node_timer(pos):start(time) end) - end) - end -else - -- use global action queue - -- itbl: Flat table of functions to run after sandbox cleanup, used to prevent various security hazards - get_interrupt = function(pos, itbl, send_warning) - -- iid = interrupt id - local function interrupt(time, iid) - -- NOTE: This runs within string metatable sandbox, so don't *rely* on anything of the form (""):y - -- Hence the values get moved out. Should take less time than original, so totally compatible - if type(time) ~= "number" then error("Delay must be a number") end - table.insert(itbl, function () - -- Outside string metatable sandbox, can safely run this now - local luac_id = minetest.get_meta(pos):get_int("luac_id") - -- Check if IID is dodgy, so you can't use interrupts to store an infinite amount of data. - -- Note that this is safe from alter-after-free because this code gets run after the sandbox has ended. - -- This runs outside of the timer and *shouldn't* harm perf. unless dodgy data is being sent in the first place - iid = remove_functions(iid) - local msg_ser = minetest.serialize(iid) - if #msg_ser <= mesecon.setting("luacontroller_interruptid_maxlen", 256) then - mesecon.queue:add_action(pos, "lc_interrupt", {luac_id, iid}, time, iid, 1) - else - send_warning("An interrupt ID was too large!") - end - end) - end - return interrupt - end -end - --- Given a message object passed to digiline_send, clean it up into a form --- which is safe to transmit over the network and compute its "cost" (a very --- rough estimate of its memory usage). --- --- The cleaning comprises the following: --- 1. Functions (and userdata, though user scripts ought not to get hold of --- those in the first place) are removed, because they break the model of --- Digilines as a network that carries basic data, and they could exfiltrate --- references to mutable objects from one Luacontroller to another, allowing --- inappropriate high-bandwidth, no-wires communication. --- 2. Tables are duplicated because, being mutable, they could otherwise be --- modified after the send is complete in order to change what data arrives --- at the recipient, perhaps in violation of the previous cleaning rule or --- in violation of the message size limit. --- --- The cost indication is only approximate; it’s not a perfect measurement of --- the number of bytes of memory used by the message object. --- --- Parameters: --- msg -- the message to clean --- back_references -- for internal use only; do not provide --- --- Returns: --- 1. The cleaned object. --- 2. The approximate cost of the object. -local function clean_and_weigh_digiline_message(msg, back_references) - local t = type(msg) - if t == "string" then - -- Strings are immutable so can be passed by reference, and cost their - -- length plus the size of the Lua object header (24 bytes on a 64-bit - -- platform) plus one byte for the NUL terminator. - return msg, #msg + 25 - elseif t == "number" then - -- Numbers are passed by value so need not be touched, and cost 8 bytes - -- as all numbers in Lua are doubles. - return msg, 8 - elseif t == "boolean" then - -- Booleans are passed by value so need not be touched, and cost 1 - -- byte. - return msg, 1 - elseif t == "table" then - -- Tables are duplicated. Check if this table has been seen before - -- (self-referential or shared table); if so, reuse the cleaned value - -- of the previous occurrence, maintaining table topology and avoiding - -- infinite recursion, and charge zero bytes for this as the object has - -- already been counted. - back_references = back_references or {} - local bref = back_references[msg] - if bref then - return bref, 0 - end - -- Construct a new table by cleaning all the keys and values and adding - -- up their costs, plus 8 bytes as a rough estimate of table overhead. - local cost = 8 - local ret = {} - back_references[msg] = ret - for k, v in pairs(msg) do - local k_cost, v_cost - k, k_cost = clean_and_weigh_digiline_message(k, back_references) - v, v_cost = clean_and_weigh_digiline_message(v, back_references) - if k ~= nil and v ~= nil then - -- Only include an element if its key and value are of legal - -- types. - ret[k] = v - end - -- If we only counted the cost of a table element when we actually - -- used it, we would be vulnerable to the following attack: - -- 1. Construct a huge table (too large to pass the cost limit). - -- 2. Insert it somewhere in a table, with a function as a key. - -- 3. Insert it somewhere in another table, with a number as a key. - -- 4. The first occurrence doesn’t pay the cost because functions - -- are stripped and therefore the element is dropped. - -- 5. The second occurrence doesn’t pay the cost because it’s in - -- back_references. - -- By counting the costs regardless of whether the objects will be - -- included, we avoid this attack; it may overestimate the cost of - -- some messages, but only those that won’t be delivered intact - -- anyway because they contain illegal object types. - cost = cost + k_cost + v_cost - end - return ret, cost - else - return nil, 0 - end -end - - --- itbl: Flat table of functions to run after sandbox cleanup, used to prevent various security hazards -local function get_digiline_send(pos, itbl, send_warning) - if not minetest.global_exists("digilines") then return end - local chan_maxlen = mesecon.setting("luacontroller_digiline_channel_maxlen", 256) - local maxlen = mesecon.setting("luacontroller_digiline_maxlen", 50000) - return function(channel, msg) - -- NOTE: This runs within string metatable sandbox, so don't *rely* on anything of the form (""):y - -- or via anything that could. - -- Make sure channel is string, number or boolean - if type(channel) == "string" then - if #channel > chan_maxlen then - send_warning("Channel string too long.") - return false - end - elseif (type(channel) ~= "string" and type(channel) ~= "number" and type(channel) ~= "boolean") then - send_warning("Channel must be string, number or boolean.") - return false - end - - local msg_cost - msg, msg_cost = clean_and_weigh_digiline_message(msg) - if msg == nil or msg_cost > maxlen then - send_warning("Message was too complex, or contained invalid data.") - return false - end - - table.insert(itbl, function () - -- Runs outside of string metatable sandbox - local luac_id = minetest.get_meta(pos):get_int("luac_id") - mesecon.queue:add_action(pos, "lc_digiline_relay", {channel, luac_id, msg}) - end) - return true - end -end - -local safe_globals = { - -- Don't add pcall/xpcall unless willing to deal with the consequences (unless very careful, incredibly likely to allow killing server indirectly) - "assert", "error", "ipairs", "next", "pairs", "select", - "tonumber", "tostring", "type", "unpack", "_VERSION" -} - -local function create_environment(pos, mem, event, itbl, send_warning) - -- Gather variables for the environment - local vports = minetest.registered_nodes[minetest.get_node(pos).name].virtual_portstates - local vports_copy = {} - for k, v in pairs(vports) do vports_copy[k] = v end - local rports = get_real_port_states(pos) - - -- Create new library tables on each call to prevent one Luacontroller - -- from breaking a library and messing up other Luacontrollers. - local env = { - pin = merge_port_states(vports, rports), - port = vports_copy, - event = event, - mem = mem, - heat = mesecon.get_heat(pos), - heat_max = mesecon.setting("overheat_max", 20), - print = safe_print, - interrupt = get_interrupt(pos, itbl, send_warning), - digiline_send = get_digiline_send(pos, itbl, send_warning), - string = { - byte = string.byte, - char = string.char, - format = string.format, - len = string.len, - lower = string.lower, - upper = string.upper, - rep = safe_string_rep, - reverse = string.reverse, - sub = string.sub, - find = safe_string_find, - }, - math = { - abs = math.abs, - acos = math.acos, - asin = math.asin, - atan = math.atan, - atan2 = math.atan2, - ceil = math.ceil, - cos = math.cos, - cosh = math.cosh, - deg = math.deg, - exp = math.exp, - floor = math.floor, - fmod = math.fmod, - frexp = math.frexp, - huge = math.huge, - ldexp = math.ldexp, - log = math.log, - log10 = math.log10, - max = math.max, - min = math.min, - modf = math.modf, - pi = math.pi, - pow = math.pow, - rad = math.rad, - random = math.random, - sin = math.sin, - sinh = math.sinh, - sqrt = math.sqrt, - tan = math.tan, - tanh = math.tanh, - }, - table = { - concat = table.concat, - insert = table.insert, - maxn = table.maxn, - remove = table.remove, - sort = table.sort, - }, - os = { - clock = os.clock, - difftime = os.difftime, - time = os.time, - datetable = safe_date, - }, - } - env._G = env - - for _, name in pairs(safe_globals) do - env[name] = _G[name] - end - - return env -end - - -local function timeout() - debug.sethook() -- Clear hook - error("Code timed out!", 2) -end - - -local function create_sandbox(code, env) - if code:byte(1) == 27 then - return nil, "Binary code prohibited." - end - local f, msg = loadstring(code) - if not f then return nil, msg end - setfenv(f, env) - - -- Turn off JIT optimization for user code so that count - -- events are generated when adding debug hooks - if rawget(_G, "jit") then - jit.off(f, true) - end - - local maxevents = mesecon.setting("luacontroller_maxevents", 10000) - return function(...) - -- NOTE: This runs within string metatable sandbox, so the setting's been moved out for safety - -- Use instruction counter to stop execution - -- after luacontroller_maxevents - debug.sethook(timeout, "", maxevents) - local ok, ret = pcall(f, ...) - debug.sethook() -- Clear hook - if not ok then error(ret, 0) end - return ret - end -end - - -local function load_memory(meta) - return minetest.deserialize(meta:get_string("lc_memory"), true) or {} -end - - -local function save_memory(pos, meta, mem) - local memstring = minetest.serialize(remove_functions(mem)) +local function save_memory(pos, meta, memstring) local memsize_max = mesecon.setting("luacontroller_memsize", 100000) if (#memstring <= memsize_max) then @@ -580,50 +218,23 @@ local function run_inner(pos, code, event) if overheat(pos) then return true, "" end if ignore_event(event, meta) then return true, "" end + local vports = minetest.registered_nodes[minetest.get_node(pos).name].virtual_portstates + local vports_copy = {} + for k, v in pairs(vports) do vports_copy[k] = v end + local rports = get_real_port_states(pos) + local pin = merge_port_states(vports, rports) + local port = vports_copy + -- Load code & mem from meta - local mem = load_memory(meta) + local mem = meta:get_string("lc_memory") local code = meta:get_string("code") + local success, port, mem = mesecons_sandbox.run(pin, port, mem, code) + if not success then return false, port end - -- 'Last warning' label. - local warning = "" - local function send_warning(str) - warning = "Warning: " .. str - end + set_port_states(pos, port) + save_memory(pos, meta, mem) - -- Create environment - local itbl = {} - local env = create_environment(pos, mem, event, itbl, send_warning) - - -- Create the sandbox and execute code - local f, msg = create_sandbox(code, env) - if not f then return false, msg end - -- Start string true sandboxing - local onetruestring = getmetatable("") - -- If a string sandbox is already up yet inconsistent, something is very wrong - assert(onetruestring.__index == string) - onetruestring.__index = env.string - local success, msg = pcall(f) - onetruestring.__index = string - -- End string true sandboxing - if not success then return false, msg end - if type(env.port) ~= "table" then - return false, "Ports set are invalid." - end - - -- Actually set the ports - set_port_states(pos, env.port) - - -- Save memory. This may burn the luacontroller if a memory overflow occurs. - save_memory(pos, meta, env.mem) - - -- Execute deferred tasks - for _, v in ipairs(itbl) do - local failure = v() - if failure then - return false, failure - end - end - return true, warning + return true end local function reset_formspec(meta, code, errmsg) diff --git a/mesecons_sandbox/sandbox.lua b/mesecons_sandbox/sandbox.lua index 25ed610..640ee1d 100644 --- a/mesecons_sandbox/sandbox.lua +++ b/mesecons_sandbox/sandbox.lua @@ -11,11 +11,39 @@ end local f_init = load_lib("_init") local f_fini = load_lib("_fini") -local f_helpers = load_lib("helpers") -local f_serialize = load_lib("serialize") +local l_helpers = load_lib("helpers") +local l_serialize = load_lib("serialize") local c_test = load_lib("test") -print("Testing") -local a,b,c,d,e,f = libluabox.run(0.1, 128, f_helpers, f_serialize, f_init, c_test, f_fini) -print(a,b,c,d,e,f) -print("Tested") +local function b(value) + if value then + return "true" + end + return "false" +end + +local function serialize_ports(key, port) + return string.format("%s = {a=%s, b=%s, c=%s, d=%s}", key, b(port.a), b(port.b), b(port.c), b(port.d)) +end + +function mesecons_sandbox.run(pin, port, mem, code) + print("Old ports:", dump(port)) + print("Old memory:", mem) + print("Code:", code) + local ok, port, mem, log = libluabox.run(1.0, 128, l_helpers, l_serialize, + serialize_ports("pin", pin), + serialize_ports("port", port), + mem, + f_init, code, f_fini) + print(minetest.serialize({ok, port, mem, log})) + if ok then + print("New memory:", mem) + print("New ports:", port) + end + if log then + print("Log: <<<") + print(log) + print(">>>") + end + return ok, minetest.deserialize(port, true), (mem or "") +end diff --git a/mesecons_sandbox/sandbox/_fini.lua b/mesecons_sandbox/sandbox/_fini.lua index 579e89a..3414b78 100644 --- a/mesecons_sandbox/sandbox/_fini.lua +++ b/mesecons_sandbox/sandbox/_fini.lua @@ -1,3 +1,4 @@ -local result = serialize({...}) +local port = serialize(port) +local mem = serialize(mem) local log = table.concat(log, "\n") -return result, log +return port, mem, log diff --git a/mesecons_sandbox/sandbox/_init.lua b/mesecons_sandbox/sandbox/_init.lua index 68df35f..9228623 100644 --- a/mesecons_sandbox/sandbox/_init.lua +++ b/mesecons_sandbox/sandbox/_init.lua @@ -1,4 +1,4 @@ -mem = ... +mem = (...) or {} log = {}