load, Chunks, Bytecode and Sandboxing

-- load compiles a string (or reader function) into a function
local f = load("return 2 + 3")
print(f())                                       --> 5

local add = load("local a, b = ...; return a + b")
print(add(4, 5))                                 --> 9

local fn, err = load("return +")                 -- syntax error
print(fn, err)                                   --> nil  [string "return +"]:1: unexpected symbol near <eof>

-- Restrict mode and environment
local env = {math = math}
local calc = load("return math.sqrt(x)", "calc", "t", setmetatable(env, {__index = {x = 16}}))
print(calc())                                    --> 4.0

dofile("script.lua")                             -- load and run (errors propagate)
local chunk = loadfile("script.lua", "t")        -- compile only

-- Bytecode
local bytecode = string.dump(function(a) return a * 2 end)
local restored = load(bytecode, "dumped", "b")   -- mode "b" = binary only
print(restored(21))                              --> 42
-- strip debug info: string.dump(f, true)

30.1 Building a Safe Sandbox

Never load untrusted bytecode. Exclude load, loadstring, dofile, require, io, os, debug, setmetatable/getmetatable (string metatable access), collectgarbage, and rawset from sandboxes. Memory limits require a custom allocator (C API: lua_setallocf).

local safe_env = {
    print = print, pairs = pairs, ipairs = ipairs, next = next, type = type,
    tostring = tostring, tonumber = tonumber, select = select, error = error, pcall = pcall,
    string = {format = string.format, sub = string.sub, upper = string.upper, lower = string.lower,
              len = string.len, find = string.find, gsub = string.gsub, match = string.match, gmatch = string.gmatch},
    table = {insert = table.insert, remove = table.remove, sort = table.sort, concat = table.concat, unpack = table.unpack},
    math = math,
}
local function run_sandboxed(code, max_instructions)
    local fn, err = load(code, "user", "t", safe_env)   -- "t": refuse bytecode (malicious bytecode can crash the VM)
    if not fn then return nil, err end
    local co = coroutine.create(fn)
    debug.sethook(co, function() error("instruction limit exceeded") end, "", max_instructions or 1e6)
    local ok, result = coroutine.resume(co)
    return ok and result or nil, (not ok) and result or nil
end
print(run_sandboxed("return 1 + 1"))
print(run_sandboxed("while true do end", 1e5))
print(run_sandboxed("os.exit()"))