Bounded at every level
The isolation model is borrowed from operating systems rather than from the web. A
supervisor loads the configuration and spawns a pool of worker processes for each site.
Every worker is its own operating-system process, with its own tokio runtime, its own Lua
virtual machine and its own SQLite connection, so one site's fault cannot reach
another's. A single worker is already a multi-threaded runtime that fills the cores it is
given, so the pool, one worker by default and configurable per site, is for isolation and rolling
restart, not for parallelism.
Inside each worker, the site's own Lua and CAST run as a guest that cannot open a file,
call the operating system or load code. Every side effect is performed by the host, at the
guest's request and never on its own authority. Its TLS is rustls, with no libssl or
libcrypto linked, which remains the most dependable way to survive OpenSSL's next advisory.
Capability isolation is only half of it, because a guest that cannot open a file can
still allocate without bound or spin forever. So every request also runs under a budget: a
memory ceiling and a wall-clock deadline, sixty-four megabytes and five seconds by default,
both configurable per site. A handler that breaches either fails that one request while the
worker carries on serving the next. The deadline is wall clock rather than processor time,
and the guest is interrupted between virtual-machine instructions rather than at an exact
moment, so this is a backstop against runaway code and not a real-time guarantee.
The database is bounded the same way, and it was not always. ZERVO+ guarded that
boundary by searching submitted SQL for forbidden table names, which is what most
application servers do, and a pre-release security audit defeated it in one line:
ATTACH names no forbidden table, because it brings its own database with it.
The escape crossed between sites on a shared host, which is the boundary that mattered. It
never reached a public release, there not having been one, and it is closed. The guard is
now SQLite's own authorizer, which sits inside the parser and judges resolved names, so
quoting, comments and whitespace have nothing to work with: ATTACH,
PRAGMA and ZERVO+'s own internal tables are refused, while a site's ordinary
queries are untouched.
The same audit closed two remote denial-of-service advisories in the QUIC stack, which
mattered here more than most: a QUIC endpoint comes up on every TCP bind and there is no
switch to turn HTTP/3 off. One advisory stays open with no fix available, a timing side
channel in rsa, reached only when an RSA SSH key decrypts a backup archive.
That is a single local interactive operation rather than the repeated decryptions the
attack needs, and an ed25519 key avoids it altogether.
Audited before release, then, with dependencies checked against the RustSec advisory
database. Not fuzzed, not penetration tested, and the libraries it trusts were not audited
themselves.
When a worker dies, the supervisor starts a new one. When it dies three times in sixty
seconds, the supervisor stops trying and leaves it down, because a restart loop that hides a
fault is worse than an outage that shows one. Safe, then, by construction; whether the
language behind that sandbox is a pleasure to write or a chore is the next question.