-- function from mesecons_luacontroller -- 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. function digiline_global_memory.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 = digiline_global_memory.clean_and_weigh_digiline_message(k, back_references) v, v_cost = digiline_global_memory.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