Sandboxing
Mix.install([
{:lua, "~> 1.0.0-rc.0"}
])
The default sandbox
Lua.new/0 sandboxes a curated list of functions (os.execute, os.exit,
os.getenv, require, loadfile, …). Calling a sandboxed function
raises, which pcall turns into a false, message pair so the script can
recover in-band.
{[ok?, message], _lua} =
Lua.eval!(Lua.new(), ~S[return pcall(function() return os.getenv("HOME") end)])
{ok?, message}
{false, "Lua runtime error: os.getenv(_) is sandboxed"}
Allowing specific operations
Lua.new(exclude: [...]) lifts the sandbox for specific paths while keeping
everything else locked down. Here we allow os.getenv only.
allowed = Lua.new(exclude: [[:os, :getenv]])
{[kind], _lua} = Lua.eval!(allowed, ~S[return type(os.getenv("HOME"))])
kind
"string"
os.execute is still blocked on the allowed VM — we only lifted getenv.
{[exec_ok?, _message], _lua} =
Lua.eval!(allowed, ~S[return pcall(function() return os.execute("echo hi") end)])
exec_ok?
false
Replacing or disabling the sandbox
Lua.new(sandboxed: [...]) replaces the whole sandbox list, and
Lua.new(sandboxed: []) disables sandboxing entirely. Reach for these only
when you fully trust the script you are running.
Bounding CPU work
Sandboxing controls which functions a script may call, but it does not
stop a script from spinning forever (while true do end) or recursing
without bound. Two options give you deterministic limits without wrapping
each evaluation in a host Task plus a wall-clock timeout.
Call depth
Lua.new(max_call_depth: n) caps the depth of nested function calls.
Recursing past the cap raises a catchable "stack overflow" runtime error
instead of letting the recursion exhaust the host process. The default is
:infinity (no limit).
Instruction budget
Lua.new(max_instructions: n) caps the number of VM instructions a single
evaluation may execute. When a script exceeds the budget it raises a
catchable "instruction budget exceeded" runtime error — so a runaway loop
terminates deterministically inside the VM:
{[ok?, message], _lua} =
Lua.eval!(Lua.new(max_instructions: 1000), ~S[return pcall(function() while true do end end)])
{ok?, message}
{false, "instruction budget exceeded"}
The budget is enforced at loop back-edges and call boundaries, so the
default :infinity carries no per-instruction cost, and it applies to both
the interpreter and the compiled-dispatcher execution paths. Each
top-level evaluation gets a fresh budget, and because the error is an
ordinary runtime error, pcall recovers from it in-band like any other.