On Runtime, Lua runs in a microVM of its own, so there is no need to strip the
standard library to make it safe. Each sandbox is a Firecracker microVM that
runs Ubuntu 24.04.5. There, sudo apt-get install -y lua5.4 installs the
interpreter in one command.
In-process sandboxing versus a microVM
Lua is often embedded in other programs. The usual way to run untrusted Lua is
to load it with a restricted environment that leaves out os, io and
require. That guards only the functions you remembered to remove. By itself,
it does not limit CPU time or memory. The code also still runs inside your
process.
| Concern | Restricted _ENV in your process |
A Runtime sandbox |
|---|---|---|
os.execute, io.open |
Removed by hand, one by one | Allowed; they reach only a throwaway machine |
| Endless loop | Needs a debug hook or a watchdog | timeoutMs ends the command |
| Memory | Needs a custom allocator | The sandbox's memoryMiB, enforced on the host |
| Network | Whatever your process can reach | Off with internet: false; private addresses refused |
| A bug in the interpreter | Runs with your process's rights | Contained by the microVM's own kernel |
If the Lua must call back into your application, keep an embedded interpreter for that part. For scripts that only compute, a sandbox is the simpler boundary (security).
Lua packages on Ubuntu 24.04
Lua is not in the default image. The versions below are from Ubuntu's archive, read on 26 October 2026:
| Package | Version on noble | Command |
|---|---|---|
lua5.4 |
5.4.6 | lua5.4, luac5.4 |
luajit |
2.1.0 (git 20231223) | luajit |
lua5.1 |
5.1.5 | lua5.1 |
luarocks |
3.8.0 | luarocks |
Run a script
First check the syntax with luac5.4 -p. It loads the file without running it
(luac manual). Then run the script,
with its input passed as arguments.
TypeScriptimport { Sandbox } from "withruntime";const script = `local n = tonumber(arg[1])local squares = {}for i = 1, n do squares[#squares + 1] = i * i endprint(table.concat(squares, ","))`;await using sbx = await Sandbox.create({ timeoutSeconds: 600, onLeaseEnd: "stop" });await sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q lua5.4", { check: true, timeoutMs: 300_000,});await sbx.network.set({ internet: false });await sbx.files.write("/workspace/squares.lua", script);const syntax = await sbx.exec(["luac5.4", "-p", "squares.lua"]);if (syntax.exitCode !== 0) throw new Error(syntax.stderr);const run = await sbx.exec(["lua5.4", "squares.lua", "8"], { timeoutMs: 10_000 });console.log(run.exitCode, run.timedOut, run.stdout); // 0 false 1,4,9,16,25,36,49,64Pythonfrom withruntime import Sandboxscript = """local n = tonumber(arg[1])local squares = {}for i = 1, n do squares[#squares + 1] = i * i endprint(table.concat(squares, ","))"""with Sandbox.create(timeout_seconds=600, on_lease_end="stop") as sbx: sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q lua5.4", check=True, timeout_ms=300_000) sbx.network.set(internet=False) sbx.files.write("/workspace/squares.lua", script) syntax = sbx.exec(["luac5.4", "-p", "squares.lua"]) if syntax.exit_code != 0: raise SystemExit(syntax.stderr) run = sbx.exec(["lua5.4", "squares.lua", "8"], timeout_ms=10_000) print(run.exit_code, run.timed_out, run.stdout)Arguments arrive in Lua's arg table. The array form runs lua5.4 with no
shell in between. So an argument such as 8; rm -rf ~ stays one string
(run commands).
Many scripts, one sandbox or many?
Scripts from the same user can share a sandbox. Write each script to its own
file, and run them in turn. Scripts from different users should not share a
sandbox, because a script can leave files behind for the next one.
Start one sandbox per user or per job, stop it after, and let onLeaseEnd: "stop" clean up one you forget.
TypeScriptimport { Sandbox } from "withruntime";const jobs = ["print(1 + 1)", "print(('lua'):upper())", "while true do end"];await using sbx = await Sandbox.create({ image: "lua", network: { internet: false } });for (const [i, code] of jobs.entries()) { await sbx.files.write(`/workspace/job${i}.lua`, code); const run = await sbx.exec(["lua", `job${i}.lua`], { timeoutMs: 5_000 }); console.log(i, run.timedOut ? "timed out" : run.stdout.trim());}This uses lua, the name the image built below links to Lua 5.4; the third
job is stopped after five seconds and the loop carries on.
LuaRocks, then no network
luarocks install fetches rocks from the web. It may compile C code with the
image's gcc. Install with the network on. Then turn the network off:
Pythonfrom withruntime import Sandboxwith Sandbox.create(image="lua", timeout_seconds=900, on_lease_end="stop") as sbx: sbx.exec("sudo luarocks --lua-version=5.4 install dkjson", check=True, timeout_ms=300_000) sbx.network.set(internet=False) run = sbx.exec(["lua", "-e", 'print(require("dkjson").encode({ok = true}))']) print(run.stdout)Start every sandbox with Lua
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({ name: "lua", recipe: { apt: ["lua5.4", "liblua5.4-dev", "luarocks", "luajit"], commands: ["ln -sf /usr/bin/lua5.4 /usr/local/bin/lua"], },});await using sbx = await runtime.sandboxes.create({ image: "lua", network: { internet: false } });console.log((await sbx.exec(["lua", "-v"])).stdout);Building an image is free. A stored image is billed by its size. An account without credit stores its first three images at no charge (custom images). A sandbox waiting for its next script costs $0.03125 an hour at 2 vCPU and 4 GiB, because Runtime bills the CPU in use (pricing). New new accounts get 100 free hours of a 1 GB machine in their first 30 days. No payment card is needed.
See also running untrusted LLM code, turning off a sandbox's internet and how to run Bash scripts in a sandbox.
Sources
Checked 26 October 2026.
- Ubuntu 24.04 (noble) packages: lua5.4 and its file list, luajit, lua5.1, luarocks, liblua5.4-dev
- luac manual, Lua 5.4
Facts on this page were checked on 26 October 2026.