-- lib/supple/comms.lua -- -- Sandbox (for) Untrusted Procedure Partitioning (in) Lua Engine -- -- Management of communications between host and sandbox -- -- Copyright 2012 Daniel Silverstone -- -- For licence terms, see COPYING -- local luxio = require "luxio" local capi = require "supple.capi" local request = require "supple.request" local objects = require "supple.objects" local track = require "supple.track" local unpack = unpack local tonumber = tonumber local error = error local getinfo = debug.getinfo local concat = table.concat local xpcall = xpcall local gc = collectgarbage local fd = -1 local function set_fd(_fd) fd = _fd end local function send_msg(msg) if (#msg > 99999) then error("Message too long") end local msglen = ("%05d"):format(#msg) track.record("XMIT", msg) luxio.write(fd, msglen .. msg) end local function recv_msg() local len, errno = luxio.read(fd, 5) if type(len) ~= "string" then error(luxio.strerror(errno)) end if #len < 5 then error("Unable to read 5 byte length") end len = tonumber(len) if len == nil or len < 1 or len > 99999 then error("Odd, len didn't translate properly") end local str = luxio.read(fd, len) if type(str) ~= "string" or #str ~= len then error("Unable to read " .. tostring(len) .. " bytes of msg") end track.record("RECV", str) return str end local function captcha(msg) local traceback = {} local level = 2 local info = getinfo(level, "Snlf") local function in_supple() return info.short_src:match("/supple/[^%.]+%.lua$") end while info and info.func ~= xpcall do -- if info.currentline > 0 and not in_supple() then if true then local ttype, tag = objects.find_tag(info.func) if ttype then info.name = tag end local line = ("\t%s:%d in function %s"):format(info.short_src, info.currentline, info.name or "") traceback[#traceback+1] = line end level = level + 1 info = getinfo(level, "Snlf") end return {msg, (concat(traceback, "\n") or "") .. "\nComms track:\n" .. track.stop()} end local limits = {} math.randomseed(os.time()) math.randomseed(math.random(luxio.getpid())) local count_per_hook = 10 + (math.random(5) - 3) local limiting = false local function limit_hook() local inside = getinfo(2, "Snlf") if inside.short_src == "[C]" or inside.name == "(tail call)" then return end if limiting and not inside.short_src:match("/supple/[^%.]+%.lua$") then if limits.count then limits.count = limits.count - count_per_hook if limits.count < 0 then limiting = false error("Used too many instructions", 2) end end if limits.memory then if limits.memory < gc "count" then limiting = false error("Exceeded memory usage limit", 2) end end end end local function wait_for_response() repeat -- We *MUST* disable GC for the duration of receiving and parsing the -- message, lest we send a __gc transaction between the first and second -- read operations. Or indeed if we GC and object between receiving the -- message and deserialising and thus anchoring the object. If we error -- out here, we don't really care about restarting gc since we won't -- survive long anyhow. gc "stop" -- Also anchor all the objects just in case local anchor = objects.get_object_anchor() -- Now receive and deserialise (thus anchoring relevant objects) local back = request.deserialise(recv_msg()) -- Now that the request is anchored, release the extra anchor and -- re-allow GC anchor = nil gc "restart" -- And get on with parsing the message we received.. -- back could be three things -- an error (raise it) if back.error then local msg = back.message if back.traceback and back.traceback ~= "" then msg = msg .. "\n" .. back.traceback end error(msg, 2) end -- A result, return it if back.error == false then return unpack(back.results) end -- A method call, call it local function safe_method(fn) local ok, res = xpcall(fn, captcha) local resp if not ok then resp = request.error(unpack(res)) else resp = request.response(unpack(res)) end -- Force anything no longer anchored to be GC'd before we reply gc "collect" send_msg(resp) end if back.method == "__gc" then -- __gc is the garbage collect mechanism objects.forget_mine(back.object) -- Force anything no longer anchored to be GC'd before we reply gc "collect" send_msg(request.response()) elseif back.method == "__call" then -- __call is the function call mechanism safe_method(function() local obj = objects.receive { tag = back.object } return {obj(unpack(back.args))} end) elseif back.method == "__len" then safe_method(function() local obj = objects.receive { tag = back.object } return {#obj} end) elseif back.method == "__index" then safe_method(function() local obj = objects.receive { tag = back.object } return {obj[back.args[1]]} end) elseif back.method == "__newindex" then safe_method(function() local obj = objects.receive { tag = back.object } obj[back.args[1]] = back.args[2] return {} end) elseif back.method == "__next" then safe_method(function() local obj = objects.receive { tag = back.object } return {next(obj, back.args[1])} end) else safe_method(function() local obj = objects.receive { tag = back.object } local meth = capi.raw_getmm(obj, back.method) if not meth then error("Unknown or disallowed method: " .. back.method, "") end return {meth(obj, unpack(back.args))} end) end until false end local function make_call(object, method, ...) gc "stop" track.enter("make_call", object, method) local req = request.request(object, method, ...) send_msg(req) local ret = {wait_for_response()} track.leave("make_call", object, method) gc "restart" return unpack(ret) end local function set_limits(newlimits) limits = newlimits debug.sethook() debug.sethook(limit_hook, "c", count_per_hook) limiting = true end return { call = make_call, _wait = wait_for_response, _set_fd = set_fd, set_limits = set_limits, }