@ -197,369 +197,7 @@ end
-- Parsing and running --
-------------------------
local function saf e_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 sav e_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 )