Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sandboxing

This page states what a guest state isolates, what it does not isolate, and what a host program must add before it can run code that it does not trust.

What the guest cannot do

  • The guest cannot see host globals, host modules or host functions. The bridge copies primitives and passes compound values by reference, so a value reaches the other side only through the API.
  • The guest cannot corrupt the host state. An error in the guest returns as a string, and the host stack stays balanced.
  • A limit on memory is exact and is enforced by the allocator of the state. Refer to state:setMemoryLimit().
  • A count hook gives a reliable instruction limit while the debug and jit libraries are out of the guest. Refer to the warning below.

What a fresh guest state does not isolate

lua.new() opens every standard library, which is correct for a cooperating guest and wrong for an untrusted one. These facts are measured on the current version:

FactResult
require("ffi") in the guestOperates. The FFI gives raw memory and C calls, so it is a full escape.
io.open in the guestReads the host filesystem.
os.execute, os.exitOperate, and os.exit stops the host process.
package.loadlibOperates.
loadstring(string.dump(f))Loads bytecode, which ignores chunk modes.
Guest calls jit.on()Count hooks stop firing. A watchdog built only from a count hook then never runs.
Guest calls debug.sethook()Removes the hook of the host.

A watchdog is therefore only reliable when the guest has no jit library and no debug library. Both of them give the guest control over the hook mechanism itself.

The recipe for an untrusted guest

Run the code in a state that you prepare, and remove every path out:

  1. Build the global table for the guest by hand. Keep the safe core: assert, error, pcall, xpcall, select, type, tostring, tonumber, next, pairs, ipairs, unpack, setmetatable, getmetatable, rawget, rawset, rawequal, coroutine, table, string, math and bit.
  2. Leave out io, os, package, require, debug, jit, ffi, loadfile, dofile, getfenv, setfenv and string.dump.
  3. Provide load and loadstring as host functions that use chunk:setMode("text"), so the guest cannot load bytecode.
  4. Set a memory limit with state:setMemoryLimit().
  5. Set a deadline with state:setHook() and a count mask, with no jit and no debug in the guest.
  6. Know the depth of the call chain. The bridge limits it to 200 levels, and state:setChainLimit() changes that figure. A guest that recurses inside a tail call keeps its own stack flat, so without this limit it can stop the process through the C stack.
  7. Cap the output that host callbacks give back to the guest, for example the bytes of a print function, because the guest controls how often it calls them.

The boundary of this library

lua-sys isolates two lua_State instances from each other. It is not a security boundary for the LuaJIT virtual machine itself:

  • A hostile bytecode chunk can still damage the interpreter, because LuaJIT trusts its own bytecode. Refuse bytecode with a text mode.
  • A long chain of host to guest to host calls uses C stack space for each step. The bridge limits the chain to 200 levels by default, which is safe for a thread stack of 1 MB. Refer to state:setChainLimit().
  • The library gives no limit on execution time by itself. Use a hook, as in step 5 above.

For code that must not damage the host under any condition, add the operating system as a second boundary: run the whole program, or a child process, with a memory cap and a time limit from the outside. The in-process limits above then stop most accidents before they reach the operating system.