Skip to content
Runtime

How to run Lua code in a sandbox

Install Lua 5.4 or LuaJIT in a Linux microVM. Write the script, then run it with lua5.4 under a time limit and with the network off.

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,64
Pythonfrom 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.

Facts on this page were checked on 26 October 2026.

Related.

100 free hoursin your first 30 days.

of a 1 GB machine

  • No credit card
  • Four sandboxes at once, 2 vCPU and 4 GiB each
  • Then prepaid credit from $10, no plan fee
Start free, no card
Start free, no card