Script
Granted capabilities
Result
Output (via print)
Runs fully client-side, nothing leaves the page. A faithful JavaScript port of the Rust sandbox, with the same traps, the same gates, and the same containment behavior.
Rust · untrusted-code sandbox
Terrarium is a from-scratch scripting sandbox that runs untrusted code without letting it hang you, exhaust your memory, blow your stack, or touch anything you did not hand it. Fuel bounds time, a byte budget bounds allocation, a depth cap bounds recursion, and capabilities gate every effect. For any input script, run returns a value or a typed trap, never a panic, a hang, or an escape. Type a script below, set the limits, choose what to grant, and watch the meters and the trap.
print, input, now, len. Anything ungranted is denied. Watch the verdict and the fuel and memory meters when a limit is hit.Runs fully client-side, nothing leaves the page. A faithful JavaScript port of the Rust sandbox, with the same traps, the same gates, and the same containment behavior.
The whole project is judged on one claim: for any input script, including adversarial and malformed ones, run returns in bounded time with a value or a typed trap. It never panics the host process, never hangs, never overflows the host stack, and never performs an effect outside the granted capabilities.
# tests/fuzz.rs throws 10,000 programs per run across five fixed seeds, # with no capabilities granted, and asserts the three invariants fuzz report: seeds=5 iterations=10000 host_panics=0 hangs=0 escapes=0 traps=9933 values=67
An infinite loop traps OutOfFuel. An allocation bomb traps OutOfMemory before it touches real memory. Unbounded recursion traps StackOverflow instead of a host segfault. An ungranted effect traps CapabilityDenied. Integer overflow wraps, division by zero is a trap, and a garbage source is a clean parse error. tests/containment.rs pins each hostile scenario to its exact trap.
Real sandboxes solve the same problem in production, and they are large. Terrarium is the readable version, an interpreter whose whole reason to exist is the four bounds.
A production sandbox with fuel metering to bound execution and linear-memory limits to bound allocation. Robust and fast, but it means compiling your code to Wasm and embedding a full runtime.
Filters the system calls a process may make, bounding effects at the kernel boundary. Powerful for OS-level isolation, but it is Linux-specific and coarse, and it says nothing about time, memory, or recursion inside the process.
Embed untrusted JavaScript with per-isolate heap limits. Battle tested, but you are carrying an entire JavaScript engine to get it.
A from-scratch interpreter, dependency-free, where fuel, a byte budget, a depth cap, and capabilities are the product rather than an afterthought. For any input script, run returns a value or a typed trap, never a panic, a hang, a host stack overflow, or an escape.
The bounds that contain it
Each bound turns a classic way to take down a host into a named, recoverable trap. The fuzzer and the containment test pin every one of them.
Every evaluation step costs one unit of fuel. Run out and you get Trap::OutOfFuel, so an infinite loop stops in bounded time.
Strings, arrays, maps, and call frames are charged against a byte budget before they are allocated. Exceed it and you get Trap::OutOfMemory before real memory is touched.
Recursion is counted. Too deep and you get Trap::StackOverflow rather than a host segfault.
A script has no ambient authority. Effects like print, input, now, and len are reachable only if the host granted that exact name; everything else is Trap::CapabilityDenied.
Every failure surfaces as a named trap, from DivByZero to Type to Parse. A host panic would count as an Internal trap, and the fuzzer holds that count at zero.
Integer overflow wraps and is documented, division or modulo by zero is a trap, and garbage source is a clean parse error. Same input, same outcome, every run.
Build a Sandbox::new(Limits::default()), grant only the capabilities you want, and call run. You get back the result value or a typed Trap, plus whatever the host captured. Tune limits with Limits::default().with_fuel(N).
terrarium run <file> [--fuel N] [--mem BYTES] [--depth D] [--grant cap,cap] [-- args...]. Each run prints the value or the trap, with the fuel and memory it spent.
Small on purpose: integers, booleans, strings, nil, let and assignment, arithmetic and short-circuit logic, if and while, functions with recursion, and arrays and maps. Builtins are reachable only through capabilities.
tests/fuzz.rs throws ten thousand programs per run across five fixed seeds; tests/containment.rs pins each hostile scenario to its exact trap.
# build and run the test suite, including the fuzzer and containment gate cargo build cargo test # run a script under limits, granting only what it needs terrarium run examples/normal.tr --grant print terrarium run examples/loop.tr --fuel 100000 terrarium run examples/escape.tr