Releases: Aloecraft-org/diluvium
Release list
Diluvium 5.5.1_build12p1
[5.5.1_build12p1] - 2026-09-02
v5.5.1_build12p1 · Lua 5.5.1 · bytecode format 0x46
A patch release on build12: the instruction budget was switched off
by its own first firing.
A guest that tripped the budget inside its own pcall ran unbounded
from then on, for the life of the instance, and dv_usage reported it
sitting exactly at its limit the whole time. Two lines of Lua were
enough. One line of C fixes it. Read the "what this does not fix"
note below before treating the whole class as closed.
Changed
dv_usagekeeps counting past a budget the guest tried to ignore.
insn_usedadvanced only in the hook, so it froze the moment the hook
cleared itself: an escaped instance reported exactly its limit —
the healthiest possible reading — while running on. A supervisor
measuring saturation fromdv_usagewas reading a number the defect
controlled.usage_keeps_counting_past_the_budgetholds it, asserting
strictly greater than the limit, because the frozen value was the
limit exactly.
Fixed
-
A guest
pcallpermanently disabled the instruction budget. The
count hook cleared itself before raising:inst->exceeded = 1; lua_sethook(L, NULL, 0, 0); /* once is enough; the error is on its way */ luaL_error(L, "instruction budget of %I exceeded", ...);
luaL_errorraises an ordinary catchable Lua error, so a guest's own
pcallcaught it — and with the hook already cleared nothing re-armed
it, becausedv_runanddv_restoreare the only other sites that arm
it and neither is reachable again on a running instance. So:pcall(function() while true do end end) -- trips the budget once while true do end -- then runs unbounded
Measured against a 1,000,000-instruction budget, a program doing
30,000,000 instructions of work returnedDV_DONE. The hook now stays
armed, so it fires again withinDV_HOOK_STEPand the budget bounds
the work again.a_budget_survives_a_guest_pcallholds it.
Known issues
-
What this does not fix: a guest that catches in a loop still spins.
Stated plainly so the class is not filed as closed. The fix bounds the
work a guest can do past its budget; it does not make the error
uncatchable and it does not return control to the host.while true do pcall(function() while true do end end) end
still never leaves
dv_run— each catch buysDV_HOOK_STEP
instructions and the loop repeats. Lua has no uncatchable error.
Closing it needs one of: apcall/xpcallthat refuses to catch once
exceededis set, which is a core-file patch and so a
CORE_PATCH_ALLOWLISTdecision rather than a change; or a
process-level watchdog, since a host insidedv_runhas no way in --
dv.hexposes no interrupt. The practical bound today is operational:
run the host under a supervisor that restarts it, and alert on the
restart count rather than absorbing it.
Upgrading
Nothing to do, and no format movement. One behaviour change worth
knowing: a guest that catches the budget error in a pcall no longer
continues past it — the error is re-raised roughly every 1,000
instructions until the program unwinds. A program that relied on
catching the budget error and carrying on was relying on the defect.
Built from commit 515160f64587.
Download guide
| Platform | File | Notes |
|---|---|---|
| Linux (standard) | diluvium_linux_static_x86_64 |
Static; works on Ubuntu, Fedora, CentOS, Arch |
| Raspberry Pi 3/4/5 | diluvium_linux_static_aarch64 |
64-bit OS only |
| Raspberry Pi Zero | diluvium_linux_static_armv7l |
32-bit ARM |
| macOS (Apple silicon) | diluvium_darwin_arm64 |
M1/M2/M3/M4 |
| macOS (Intel) | diluvium_darwin_x86_64 |
|
| Windows | diluvium_windows_x86_64.exe |
64-bit |
| WASI | diluvium_wasi.wasm |
Needs a runtime with wasm exception-handling (wasmtime 28+ with -W exceptions=y) |
| Browser | libdiluvium_wasm_unknown.a |
wasm32-unknown-unknown static library |
Each platform also ships a diluvium_compiler_* binary (luac
with the -r analysis report flag) and a libdiluvium_*.a static
library for embedding.
Verifying a download
sha256sum -c SHA256SUMS.txt --ignore-missing
BUILDINFO.txt records the exact commit, build time and workflow run.
Diluvium 5.5.1_build12
[5.5.1_build12] - 2026-09-01
v5.5.1_build12 · Lua 5.5.1 · bytecode format 0x46
A crash fix for hosts that create instances on more than one
thread. dv_new appended to two process-global arrays with no
synchronisation at all, so two threads creating their own
instances at the same moment could kill the process -- inside
strcmp, in a registry scan, in the first microseconds of a fresh
process. Nothing else changes: no new surface, no format
movement, and a single-threaded host behaves exactly as it did on
build11.
Added
make dshim_race_checkandmake dshim_race_tsan, over a
newtest/dshim_race_check.c: several threads released together
intodv_newin a fresh process, repeated over many
executions, asserting both that nothing crashes and that every
continuation name is still registered afterwards. The
ThreadSanitizer lane is the one that decides -- ASan, UBSan and
valgrind's default tool detect no data races, which is why the
existing sanitizer sweep was silent on this -- and it reports
the race on the unfixed tree within the first execution.
Changed
- The generic host (
host/,make build_host) is deprecated in
favour ofdiluvium-drt.
Documentation only -- the host is still built, still shipped, and
behaves exactly as it did; there is no runtime deprecation notice,
because a deployment's log is the wrong place for a warning nobody
can act on without changing runtimes.doc/Host.mdstays normative
for both implementations. Newdoc/DRT.mdcovers the split and
analyses what the C host still does that DRT does not yet -- local
exec/runmost notably, which appears in none of DRT's own tracking. doc/Messaging.md§12.1's artifact list is marked superseded in
part.src/dvs.cis now a frozen differential-test reference that
diluvium-drtreimplements and benchmarks against, and its SPEC
records that this repository deletes it once that port passes
acceptance -- sodiluvium-swarm-*is deliberately not a release
artifact, andmake build_swarm_lib/dvs_checkstay in the test
sweep where a reference implementation belongs. Stated because the
stale line reads as a broken release otherwise.
Fixed
-
A data race in the named-continuation registries could kill
the process on concurrentdv_new(SIGSEGV instrcmp).
diluvium_shim_addcontanddiluvium_snap_addcontscanned and
appended to process-global arrays with no lock, atomic or
once-guard. Two threads could claim the same slot and both
increment the count, leaving an entry whose name was still
NULLfor the next scan to hand tostrcmp; separately,
nothing ordered the name store before the increment, so a
scanner could see the larger count and the older name. Both are
closed by a mutex (src/dsync.h: pthreads, SRWLOCK on Windows,
a no-op where there are no threads) held across the whole
scan-then-append -- and across the four readers too, which had
the same exposure.This was reachable from a host obeying
dv.hto the letter:
the "one instance, one thread" rule is per instance and the
registries are per process, and the header said nothing about
the gap. It now does. Anyone holding a binary built from
f137b308c4dce917b24c71ab41add61606945e58or earlier has an
affected copy. -
ds_learnconts's once-flag raced. A plainstatic int done
let two threads run the body at once, which both widened the
window above and could leave the registry missing
baselib.pcall-- surfacing much later, and far from its cause,
as a snapshot refused for a continuation the process could
perfectly well have known. Now guarded, with the body held
across the guard so nobody proceeds against a half-filled
registry. -
msgpack's hex-byte formatter used a shared static buffer.
Found while fixing the above: two instances on two threads
decoding malformed bytes at the same moment could each read the
other's digits into their own error message. The caller supplies
the buffer now. A garbled message rather than a crash, but a
race on a guest-reachable path all the same.
Upgrading
Nothing to do. The snapshot format, the permanents fingerprint and
the bytecode format are all unmoved, so snapshots and compiled
chunks cross this build in both directions.
Hosts that worked around the crash by serialising instance
creation behind their own mutex can drop that once they are on
this build; the runtime does it now, and only around the
registration itself rather than around dv_new.
Built from commit 51de834e63c8.
Download guide
| Platform | File | Notes |
|---|---|---|
| Linux (standard) | diluvium_linux_static_x86_64 |
Static; works on Ubuntu, Fedora, CentOS, Arch |
| Raspberry Pi 3/4/5 | diluvium_linux_static_aarch64 |
64-bit OS only |
| Raspberry Pi Zero | diluvium_linux_static_armv7l |
32-bit ARM |
| macOS (Apple silicon) | diluvium_darwin_arm64 |
M1/M2/M3/M4 |
| macOS (Intel) | diluvium_darwin_x86_64 |
|
| Windows | diluvium_windows_x86_64.exe |
64-bit |
| WASI | diluvium_wasi.wasm |
Needs a runtime with wasm exception-handling (wasmtime 28+ with -W exceptions=y) |
| Browser | libdiluvium_wasm_unknown.a |
wasm32-unknown-unknown static library |
Each platform also ships a diluvium_compiler_* binary (luac
with the -r analysis report flag) and a libdiluvium_*.a static
library for embedding.
Verifying a download
sha256sum -c SHA256SUMS.txt --ignore-missing
BUILDINFO.txt records the exact commit, build time and workflow run.
Diluvium 5.5.1_build11
[5.5.1_build11] - 2026-08-24
v5.5.1_build11 · Lua 5.5.1 · bytecode format 0x46
The interop build. Everything here exists so a deployment can
speak protocols whose other end is not Diluvium: TURN REST
credentials for a WebRTC stack, webhook signature verification
under a provider's own secret, TOTP from guest-held per-user
secrets, JSON that can spell an empty array. One deliberate
snapshot-compatibility break rides it (see upgrading); everything
else is additive, and a deployment that configures none of it
behaves as it did on build10.
Added
- Guest-side digests.
bytes.sha256,bytes.sha1,
bytes.hmac_sha256,bytes.hmac_sha1-- raw bytes in and out,
composing with the codecs already there (TOTP truncates the raw
MAC, a TURN password base64s it). Guest-side because their key,
when there is one, is the caller's own -- a per-user TOTP secret
out of the program's database gains nothing from a hostcall but a
copy of itself in the log. SHA-1 is wire-interop only and says so
where it lives; nothing identity-shaped may use it. bytes.consteq: equal-or-not without a data-dependent branch once lengths agree, for comparing MACs, tokens and pins where==times its answer.- TURN REST credentials.
crypto/turn_credential {user, ttl?}
->{username, password, expires, uris?}: the use-auth-secret
scheme, HMAC-SHA1 under a shared secret configured as
connectors.crypto.turn(secret/secret_env/secret_file,
ttl,uris). The secret is held raw -- the TURN server holds
the same bytes -- and still never reaches a guest. The host owns
the expiry the wayjwt_signownsexp, and the deployment's
urisare echoed verbatim so the reply is a complete ICE server
entry: where the TURN server lives is deployment data, not
program code. - Named raw secrets for webhook verification.
connectors.crypto.secrets = { <name> = { secret | secret_env | secret_file } }(at most 8), selected per call as
crypto/hmac {data, key=<name>}. The derived subkey cannot
verify what a provider signed -- the peer holds specific bytes --
so these are raw, like TURN's.expect=<hex>(either case) turns
the call into a constant-time verification answering
{valid=bool}, the jwt_verify convention, so no guest writes the
comparison that leaks. Withoutkey, nothing changes. msgpack.null, the value that encodes as nil. A table cannot
hold nil, so an intentional null among positional values -- a
bound SQL NULL in a params array -- was a hole that changes the
table's shape. The sentinel is a value on the Lua side, msgpack
nil on the wire, JSON null injson.encode, and a named
permanent in snapshots, sox == msgpack.nullstill holds after
a wake. Decode never produces it: null -> nil stays.
host.sql.NULLre-exports it where a statement binder will look;
a nil param stays refused, because an accidental nil is still a
bug worth catching.json.encodehonoursmsgpack.as_array/as_map, at any
depth. The empty table was the encoder's one honest ambiguity
({}is a valid array and a valid object; it says object), and
the msgpack encoder already owned the answer: one wrapper tag,
now honoured by both codecs.{entries = msgpack.as_array({}), total = 0}encodes as{"entries":[],"total":0}. A non-empty
tagged array encodes exactly as it would untagged; a tag
contradicting the keys is an error, not a coercion.host.monotonic(): monotonic milliseconds -- deliberately the same unit ashost.time(). The epoch is the host process's own: good for intervals within a run (rate buckets, throttles, deadline loops), reset by a restart or a restore, never comparable to a persisted wall timestamp. Intervals here, records onhost.time(). Granted ashost:time/monotonic.
Changed
- The listener's
headersandresponse_headersallowlists take up to 16 names each (was 8) -- room for a CORS preflight set without a runtime release in the critical path. The refusal message now derives its number from the bound. json.encodeof anmsgpack.extwrapper now refuses by name. It previously encoded the wrapper's own internals (payload, kind, code) as a plain table, which was an accident of representation, not a contract; an ext has no JSON form.
Upgrading
Snapshots hibernated by 5.5.1_build10 or earlier do not restore
on this build. The bytes library grew C functions (sha256,
sha1, hmac_sha256, hmac_sha1, consteq) and msgpack grew
the null sentinel; new permanent names move the permanents
fingerprint, and restore requires an exact match. A resident swarm
upgrades by draining and restarting; a deployment whose root never
hibernates is unaffected. The break was taken once, deliberately,
at a release boundary, and every name that wanted it rode the same
crossing -- bytecode is untouched (LUAC_FORMAT stays 0x46), so
compiled chunks still load.
Built from commit 6206b0fbd236.
Download guide
| Platform | File | Notes |
|---|---|---|
| Linux (standard) | diluvium_linux_static_x86_64 |
Static; works on Ubuntu, Fedora, CentOS, Arch |
| Raspberry Pi 3/4/5 | diluvium_linux_static_aarch64 |
64-bit OS only |
| Raspberry Pi Zero | diluvium_linux_static_armv7l |
32-bit ARM |
| macOS (Apple silicon) | diluvium_darwin_arm64 |
M1/M2/M3/M4 |
| macOS (Intel) | diluvium_darwin_x86_64 |
|
| Windows | diluvium_windows_x86_64.exe |
64-bit |
| WASI | diluvium_wasi.wasm |
Needs a runtime with wasm exception-handling (wasmtime 28+ with -W exceptions=y) |
| Browser | libdiluvium_wasm_unknown.a |
wasm32-unknown-unknown static library |
Each platform also ships a diluvium_compiler_* binary (luac
with the -r analysis report flag) and a libdiluvium_*.a static
library for embedding.
Verifying a download
sha256sum -c SHA256SUMS.txt --ignore-missing
BUILDINFO.txt records the exact commit, build time and workflow run.
Diluvium 5.5.1_build10
[5.5.1_build10] - 2026-08-18
v5.5.1_build10 · Lua 5.5.1 · bytecode format 0x46
Assistant enablement, the additive half. The workload this build
serves is an AI assistant deployed as a swarm: inbound webhooks,
outbound LLM and API calls, a few credentials, bounded workers.
Everything here is additive -- new config keys, new result fields, new
library surface, new tooling -- and a deployment that configures none
of it behaves as it did on build9. The behaviour-changing half (rest
egress hardening, consuming the plugin wake policy, exec on the
deferred seam) is deliberately not here; doc/BUILD10.md §6 records
each with its reason.
Added
- Listener response headers. A reply may carry
headers = { ... },
gated per listener byresponse_headers-- a lowercase allowlist
mirroring build7's request side. The host-owned framing names
(content-length, connection, transfer-encoding, content-type) are
refused in the allowlist at config load. On the wire the header
name always comes from the allowlist, never from guest bytes, and a
value carrying a control byte, a name not listed, or a value past
the bound drops that header whole -- never truncated -- while the
response still answers. - Rest plugin response headers.
rest/getandrest/postresults
carry aheadersmap: names lowercased, repeats joined ", ", at
most 32 entries with names <= 64 and joined values <= 4096 bytes,
past a bound dropped whole. Both bindings, one manifest; the C
plugin's header comment had promised this shape since the file was
written, and now tells the truth. host.spawn,host.children,host.events-- the swarm half
of the build7 guest library. A spawn returns a handle when the
swarm'sspawnedevent lands; a denial raises with the refused
capability in the message;handle.kill()awaits its outcome;
host.eventsdrains child exits, faults and budget overruns,
buffered through any outcome wait. Nochild.push: the swarm does
not yet deliver endpoint references between instances (build7's
recorded cap4 gap), and the library does not pretend otherwise.script/mint_vault.lua-- mint a secrets vault as a compiled
secure function. Names arrive as environment-variable names (values
never ride argv); the output loads in a sealed guest viaload.
The claim is the secure-function claim and no more: obfuscation,
not encryption. The tool refuses to write a vault whose values
survive a plaintext or single-byte-xor search, and
test/test_vault.luaproves the checks can fail on a non-secure
build of the same table.- An assistant-shaped example --
host/assistant.host.luaand
host/assistant.lua: listener + rest + vault + spawn wired with
honest bounds, including the raisedcall_timeout_msan LLM round
trip needs and the caveats (fetch-per-use, snapshots) the vault
docs insist on.
Upgrading
Snapshots taken by 5.5.1_build9 restore on this build. The host
library grew members inside its module table, which is a named
permanent resolved to the runtime's own copy on restore; no permanent
name was added or moved, and no wire shape changed -- the reply
message's headers and the rest results' headers are new optional
fields old programs never send or read.
Nothing activates by itself. Response headers need a
response_headers allowlist in the listen block; host.spawn needs
the lifecycle capability the raw idiom already needed; the vault is
a tool you run, not a connector.
Built from commit 7dfe1d590488.
Download guide
| Platform | File | Notes |
|---|---|---|
| Linux (standard) | diluvium_linux_static_x86_64 |
Static; works on Ubuntu, Fedora, CentOS, Arch |
| Raspberry Pi 3/4/5 | diluvium_linux_static_aarch64 |
64-bit OS only |
| Raspberry Pi Zero | diluvium_linux_static_armv7l |
32-bit ARM |
| macOS (Apple silicon) | diluvium_darwin_arm64 |
M1/M2/M3/M4 |
| macOS (Intel) | diluvium_darwin_x86_64 |
|
| Windows | diluvium_windows_x86_64.exe |
64-bit |
| WASI | diluvium_wasi.wasm |
Needs a runtime with wasm exception-handling (wasmtime 28+ with -W exceptions=y) |
| Browser | libdiluvium_wasm_unknown.a |
wasm32-unknown-unknown static library |
Each platform also ships a diluvium_compiler_* binary (luac
with the -r analysis report flag) and a libdiluvium_*.a static
library for embedding.
Verifying a download
sha256sum -c SHA256SUMS.txt --ignore-missing
BUILDINFO.txt records the exact commit, build time and workflow run.
Diluvium 5.5.1_build9
[5.5.1_build9] - 2026-08-15
v5.5.1_build9 · Lua 5.5.1 · bytecode format 0x46
The host ships. Build 8 added a plugin channel and a generic host
that could defer a call, and published neither binary -- so using any
of it meant cloning the repository and building in a container. This
release carries diluvium_host_linux_static_x86_64 and
diluvium_rest_plugin_linux_static_x86_64 as release artifacts,
checksummed alongside everything else.
That gap was not academic. It cost three separate round trips
downstream, and every one of them looked the same from the outside: a
host older than the configuration it was handed refuses a key it does
not know, correctly and by name, and the refusal reads like a bad
config rather than an old binary.
So the other half of this release is being able to tell those apart.
diluvium-host --version answers which build it is, and the startup
banner leads with it, so the line above any refusal already says which
binary is doing the refusing.
Nothing in the runtime changed. This is build 8's feature set, made
obtainable.
Added
diluvium_host_linux_static_x86_64-- the generic host, fully
static musl, so it runs on an Alpine older than the builder with no
libc to match and no shared SQLite. This is the binary the capability
model lives in: connectors, the listener, the driven swarm. The
installer still ships only the CLI, which is a separate gap
(doc/BUILD7.md §5).diluvium_rest_plugin_linux_static_x86_64-- outbound HTTP and
HTTPS as a plugin, so build 8's plugin channel has something runnable
to point at. It links OpenSSL and the host links none, which is the
arrangement the channel exists to make possible.diluvium-host --version, and the build in the startup banner.
The build number now reaches a compiled artifact for the first time:
VERSION stays the single source of truth and the makefile passes it
in, so there is no second copy to drift.
Fixed
host/build-musl.shandhost/Dockerfile.muslbuild and export the rest plugin beside the host, rather than the host alone.
Upgrading
Snapshots taken by 5.5.1_build8 restore on this build, and so do
build7's: the permanents fingerprint has not moved since build 7, and
no guest-visible surface changed here at all.
The host and the rest plugin are x86_64 Linux only. The aarch64 and
armv7l CLI builds go through QEMU, and the host additionally needs
sqlite-static for those arches while the plugin needs
openssl-libs-static. Other platforms still build from source.
A consumer should pin a version and verify the checksum. Every
artifact is listed in the release's SHA256SUMS.txt. Following
latest means being unable to tell a Diluvium regression from your
own.
Built from commit 760783d23fd5.
Download guide
| Platform | File | Notes |
|---|---|---|
| Linux (standard) | diluvium_linux_static_x86_64 |
Static; works on Ubuntu, Fedora, CentOS, Arch |
| Raspberry Pi 3/4/5 | diluvium_linux_static_aarch64 |
64-bit OS only |
| Raspberry Pi Zero | diluvium_linux_static_armv7l |
32-bit ARM |
| macOS (Apple silicon) | diluvium_darwin_arm64 |
M1/M2/M3/M4 |
| macOS (Intel) | diluvium_darwin_x86_64 |
|
| Windows | diluvium_windows_x86_64.exe |
64-bit |
| WASI | diluvium_wasi.wasm |
Needs a runtime with wasm exception-handling (wasmtime 28+ with -W exceptions=y) |
| Browser | libdiluvium_wasm_unknown.a |
wasm32-unknown-unknown static library |
Each platform also ships a diluvium_compiler_* binary (luac
with the -r analysis report flag) and a libdiluvium_*.a static
library for embedding.
Verifying a download
sha256sum -c SHA256SUMS.txt --ignore-missing
BUILDINFO.txt records the exact commit, build time and workflow run.
Diluvium 5.5.1_build8
[5.5.1_build8] - 2026-08-15
v5.5.1_build8 · Lua 5.5.1 · bytecode format 0x46
Nothing blocks the shared thread, and a capability can live in
another program. Until now a connector answered inline: the host
called it and wrote the reply on the next line, so a connector that
needed time simply did not return, and exec sitting in a poll()
loop stalled every guest and the listener until its child exited. A
connector can now say later -- take the call, return, and answer
when the answer exists. Eight 200ms calls from one instance finish in
about 208ms rather than 1600.
The plugin channel is what that makes possible. A capability
answered by a separate program the host execs from an absolute path,
talking length-prefixed msgpack over a socketpair it inherits as fd 3,
described by a self-contained <name>.plugin.json manifest and wired
by a plugins table beside connectors. New capability without a new
Diluvium release.
plugins/rest is the first one, and closes the net gap build 7
deferred -- in C against OpenSSL and in JavaScript against fetch,
one manifest describing both, and a guest that cannot tell them apart.
The point is what diluvium-host did not have to learn: it links no
TLS, resolves no names, and opens no outbound socket.
host.capabilities() answers what this host can do and what of it
the caller may do, and keeps them apart. Every entry carries
granted, so a capability a deployment wires and a program may not
call is still listed rather than absent -- the difference between
"this host cannot" and "I may not", which had produced identical
silence. A visibility field (public, private, hidden, inherit;
public by default) decides what a caller is told exists, which is a
separate axis from what it may do.
The guest side needed no new surface for any of it: host.call already
reached any connector by name, and now forwards an optional timeout.
Added
- Deferred hostcall replies.
DH_CALL_PENDINGis a fourth
connector status meaning taken; exactly one reply is owed. The
host keeps a ledger keyed by (instance, token), sweeps it when an
instance dies so a dead program's work is abandoned rather than
computed, and reclaims an entry whose deadline elapses so a wedged
connector cannot hang a guest. - The plugin channel.
plugins = { name = { manifest = ... } }in
a deployment;<name>.plugin.jsonbeside it. Manifests are parsed
with the runtime's own strict JSON decoder in alua_Statethe host
owns, so nothing is vendored and no schema validator is embedded --
the runtime reads the flat metadata and reads pastargs/result.
Per-pluginmax_inflightbounds what a serial plugin is handed;
call_timeout_msis the host-side backstop. Errors carry one of
three classes --transport,plugin,capability-- because the
caller's retry decision differs for each. plugins/rest, outbound HTTP and HTTPS, in C (OpenSSL,
verifying the chain and the name) and in JavaScript (fetch, on
fd 3 under Node and overpostMessagein a Worker). Plus
plugins/dvplug.h, a dependency-free single-header kit for writing
a plugin in C, andplugins/README.md.host.capabilities()and thevisibilityfield, on the
deployment and per plugin. Discovery is gated on
host:capabilities/list, which is what makes an auditing agent
expressible: one that can report everything a swarm can reach while
being able to reach none of it.doc/Extending.md: adding a capability as a C connector, as a plugin, or as a Lab connector, side by side.doc/BUILD8.md: the plan, and the honest list of what it did not build.
Changed
- The host sleeps in exactly one place.
dh_host_sleepfolds the
listener's sockets, the plugin channels and a plain timeout into a
singlepoll(). Two sleeping polls in sequence would not deadlock,
but the first to sleep would delay the second by its whole timeout,
and "nothing blocks the shared thread" is only checkable if there is
one place the thread stops. host.callandhost.trytake an optionalwaitms, whichroundtriphad always accepted.dh_call_fncarries the request's correlation token, so a connector that defers can name the call it is answering.DH_MAX_CONNECTORSis 16, since every plugin claims a slot beside the built-ins.
Fixed
- The generic host builds on macOS.
dhost_crypto.ccalled
getrandom(2)unguarded; that is Linux's, and<sys/random.h>is
spelled the same on macOS but declaresgetentropyinstead, so the
/dev/urandomfallback underneath it was unreachable -- the file
never compiled. Split at compile time. Pre-existing and invisible
becausehost_checkdid not run in CI until this build put it
there. - A reply-queue accounting bug the deferral seam exposed. The pump
refused to drain a request when the reply queue was full, but a
deferred call is drained and not yet answered, so the queue looked
emptier than it was. Pending calls now count against the headroom. realpathwas handed a 512-byte buffer inhost_check; it writes up toPATH_MAX. Found by addinghost_checktosanitize_checks, which_FORTIFY_SOURCEcatches and ASan does not.
Security
- A plugin is exec'd with
execvfrom an absolute path, never
execvp: a plugin path comes from a manifest an operator wrote, and
resolving it throughPATHis an injection surface with nothing on
the other side of the trade. A relative path is refused by name at
config load. - There is no plugin authentication, deliberately. Containment
comes from a plugin being a narrow program rather than from
certifying it, and an attacker who can swap the plugin binary can
equally swapdiluvium-host. The channel has no filesystem path and
no port, so parentage is structural rather than negotiated. A
manifest checksum is recorded and logged at startup without being
enforced, which is forensics now and a one-line change later. - The rest plugin refuses credentials in a URL rather than laundering them into the host's log and the guest's message log, and refuses a header value carrying CR or LF rather than stripping it -- silently changing what was sent is worse than declining to send it.
Upgrading
Snapshots taken by 5.5.1_build7 restore on this build. Unlike
build7, which added a module table to the permanents set and moved the
fingerprint, everything build8 adds on the guest side is a Lua closure
inside the existing host table. ds_perm_walk names only C
functions and the fingerprint hashes the sorted name list, so it is
byte-identical. DILUVIUM_SNAP_FORMAT, DS_THREAD_VERSION and
LUAC_FORMAT are unchanged.
A deployment that wants host.capabilities() must grant it.
Discovery is a capability like any other and is gated on
host:capabilities/list; a program without it is denied by name. No
existing deployment grants it, because none could until now.
exec still answers synchronously. Build 8 made deferral possible
and did not convert exec, which is a behaviour change to a shipped
connector and belongs in its own build. A running child still stalls
every guest and the listener, and host/types/host.lua still says so.
Plugin binaries are not release assets. The plugin channel ships,
and nothing to run through it does: diluvium-host itself is still
not published either (build 7 §5's open item). Building both from
source is the path today.
Built from commit 0d6180f6283b.
Download guide
| Platform | File | Notes |
|---|---|---|
| Linux (standard) | diluvium_linux_static_x86_64 |
Static; works on Ubuntu, Fedora, CentOS, Arch |
| Raspberry Pi 3/4/5 | diluvium_linux_static_aarch64 |
64-bit OS only |
| Raspberry Pi Zero | diluvium_linux_static_armv7l |
32-bit ARM |
| macOS (Apple silicon) | diluvium_darwin_arm64 |
M1/M2/M3/M4 |
| macOS (Intel) | diluvium_darwin_x86_64 |
|
| Windows | diluvium_windows_x86_64.exe |
64-bit |
| WASI | diluvium_wasi.wasm |
Needs a runtime with wasm exception-handling (wasmtime 28+ with -W exceptions=y) |
| Browser | libdiluvium_wasm_unknown.a |
wasm32-unknown-unknown static library |
Each platform also ships a diluvium_compiler_* binary (luac
with the -r analysis report flag) and a libdiluvium_*.a static
library for embedding.
Verifying a download
sha256sum -c SHA256SUMS.txt --ignore-missing
BUILDINFO.txt records the exact commit, build time and workflow run.
Diluvium 5.5.1_build7
[5.5.1_build7] - 2026-08-13
v5.5.1_build7 · Lua 5.5.1 · bytecode format 0x46
The host guest library: a hostcall is a call. Reaching a
connector used to take a hand-rolled queue pair, a token, and a
request map -- ceremony every program repeated and none designed. The
new host global owns all of it: host.sql.open(name) returning a
database handle whose exec/query are plain calls,
host.crypto.hash/hmac/random/jwt_sign/jwt_verify, host.time(),
and host.call(name, args) for any connector by name. A non-ok
reply raises with the connector's own sentence in the message; the
try_ forms (db.try_exec, host.try) hand the status back
instead, so an expected denial stays expressible. The queues remain
the substrate and doc/Hostcall.md remains the protocol -- the
library multiplexes one lazily-declared pair, correlates replies by
token in any order, and is implemented as a Lua chunk so a future
await keyword changes its internals and no program. LuaCATS
definitions for every guest global (queue, msgpack, endpoint,
bytes, json, time, host) ship as types/guest.lua, so an
editor stops flagging them unknown.
The sql config grants a scope, not an application detail. A
deployment used to name an exact database file (path = "example.db"), resolved against whatever directory the host happened
to start in -- the config carrying the program's business, ambiguously.
Now connectors.sql grants a scope (a directory, resolved once,
canonically) and the program names its database inside it: args.db
on the wire, host.sql.open("name") in a program. A name with a
separator, a ./.., or one resolving (through a symlink) outside
the scope is DENIED, never clamped; multiple databases fall out for
free, opened on first use, nothing preallocated. The liars are
renamed and split: max_rows is max_result_rows (it always was a
per-query cap), and mode's two jobs are access
("read"/"readwrite" -- the grant, which wires or unwires sql/exec)
and create (the open-mode detail, defaulting to the write grant).
The old keys are refused with directions, not as anonymous typos.
Three connector gaps filled, host-side. The fs connector
(host:fs/read, host:fs/write; host.fs.read/write in a program)
works files inside a granted scope under the same discipline as sql
-- a path may descend into existing structure, but .., absolute
paths, and anything resolving (through a symlink) outside the scope
are denied, both directions refuse past max_bytes, and nothing
creates directories on the way. The exec connector
(host:exec/run; host.exec.run(argv, opts?)) is the honest escape
hatch, bounded because the instruction budget cannot reach a
subprocess: argv is a vector so there is no shell unless the program
names one, a wall-clock deadline (config ceiling, per-call at most
that) kills a runaway child, each output stream refuses past its byte
cap, and a nonzero exit is an answer, not an error -- granting exec
is leaving the sandbox, and the docs say so. And the listener now
forwards an allowlisted subset of request headers: config names
lowercase header names (empty by default), matching values arrive as
a headers map on each request message (present whenever an
allowlist is configured, so the shape is the config's decision),
repeats join per RFC 7230, and a value past the host's bound answers
431 rather than truncating.
Upgrading
Snapshots taken by 5.5.1_build6 and earlier are refused by this
build. The host module table joins the permanents set, which
moves the permanents fingerprint the snapshot header carries -- the
same mechanism, and the same clean format-mismatch refusal, as
build6's own library additions. Nothing half-loads, and there is no
converter. DILUVIUM_SNAP_FORMAT and DS_THREAD_VERSION are
unchanged from build6; only the permanents list grew.
Bytecode is unaffected. LUAC_FORMAT stays 0x46; chunks compiled
by build6 load without recompiling.
The host global is new. A program that used host as a global
name of its own now shadows the library; programs that kept to locals
are unaffected. Like every guest library it is present in the CLI
too, where a call fails loudly rather than answering -- nothing
drains the queues there.
queue.declare's size option is capacity. It always was; the
documented raw-hostcall idiom passed cap, which was silently
ignored, so those queues ran at the default 64. Programs following the
old example still work -- the option never did anything -- but the
examples now say capacity, and so should the code.
The sql connector's config changed shape. path gave way to
scope (a directory; the program names its database within it),
mode split into access and create, and max_rows is
max_result_rows. A build6 config is refused with a sentence naming
each replacement, and hostcalls now carry args.db -- raw-idiom
programs add it; programs on the host library say
host.sql.open("name") once and are done.
Built from commit c8f8f4f996f2.
Download guide
| Platform | File | Notes |
|---|---|---|
| Linux (standard) | diluvium_linux_static_x86_64 |
Static; works on Ubuntu, Fedora, CentOS, Arch |
| Raspberry Pi 3/4/5 | diluvium_linux_static_aarch64 |
64-bit OS only |
| Raspberry Pi Zero | diluvium_linux_static_armv7l |
32-bit ARM |
| macOS (Apple silicon) | diluvium_darwin_arm64 |
M1/M2/M3/M4 |
| macOS (Intel) | diluvium_darwin_x86_64 |
|
| Windows | diluvium_windows_x86_64.exe |
64-bit |
| WASI | diluvium_wasi.wasm |
Needs a runtime with wasm exception-handling (wasmtime 28+ with -W exceptions=y) |
| Browser | libdiluvium_wasm_unknown.a |
wasm32-unknown-unknown static library |
Each platform also ships a diluvium_compiler_* binary (luac
with the -r analysis report flag) and a libdiluvium_*.a static
library for embedding.
Verifying a download
sha256sum -c SHA256SUMS.txt --ignore-missing
BUILDINFO.txt records the exact commit, build time and workflow run.
Diluvium 5.5.1_build6
[5.5.1_build6] - 2026-08-12
v5.5.1_build6 · Lua 5.5.1 · bytecode format 0x46
Three guest libraries and the rest host-side.
The sealed sandbox met text edges -- a JWT segment, an HTTP body, a
config, a timestamp for a human -- with no tools for them, because the
libraries that would have them (os, and any C-module loader) are the
ones sealing takes away. Three new libraries fill that in, all pure
computation and so libraries rather than hostcalls: nothing about them
reaches outside the instance or is nondeterministic, so none needs a
capability.
bytes transcodes: tohex/fromhex, tobase64/frombase64,
tobase64url/frombase64url (the JWT and URL alphabet, unpadded),
and urlencode/urldecode (RFC 3986 percent-encoding). The decoders
are strict about the alphabet and a truncated group and name a bad
byte's position rather than skipping it. json is the whole-value
codec that sits beside msgpack: json.encode/json.decode, with
the array-or-object question settled by a documented heuristic and a
decoder that is bounded in depth and strict about the number grammar,
because it reads bytes a client sent. time is the pure half of the
clock -- the reading of it stays the host:time connector, but
time.iso, time.parse, time.fields and time.of convert between
Unix seconds, ISO 8601 and broken-down UTC fields, the calendar
arithmetic os.date used to do. crypto/random returning hex was the
first sign the gap was real; these close it.
Host-side, two additions since build5. The crypto connector gains
host:crypto/jwt_sign and host:crypto/jwt_verify alongside
random, hash and hmac: JWT-HS256, minted and checked with the
key in the host and never in a guest, the header fixed so
alg-confusion has no field to set, and the host owning iat/exp.
The configured secret signs nothing directly -- two subkeys are
derived from it, one for hmac and one for the JWT MAC -- so a
program holding only host:crypto/hmac cannot use it as an oracle to
forge a token, which it otherwise could. And the listener connector
accepts an array of blocks, so a deployment that cannot bind at
runtime pre-binds a block of ports up front; they share one poll and
one connection-token space, so replies route correctly whether the
ports share a queue or not.
Upgrading
Snapshots taken by 5.5.1_build5 and earlier are refused by a build
with these libraries. Their functions join the permanents set (they
must, or a program holding one could not be snapshotted at all), which
moves the permanents fingerprint the snapshot header carries -- the
same mechanism, and the same clean format-mismatch refusal, as
build5's endpoint-metatable move. Nothing half-loads, and there is no
converter. DILUVIUM_SNAP_FORMAT and DS_THREAD_VERSION are
unchanged from build5; only the permanents list grew.
Bytecode is unaffected. LUAC_FORMAT stays 0x46; chunks compiled
by build5 load without recompiling.
The bytes, json and time globals are new. A program that
used one of those as a global name of its own now shadows the library;
guests that kept to locals are unaffected, and the libraries are only
present inside an instance, as queue and msgpack are.
Built from commit 9d887d111c77.
Download guide
| Platform | File | Notes |
|---|---|---|
| Linux (standard) | diluvium_linux_static_x86_64 |
Static; works on Ubuntu, Fedora, CentOS, Arch |
| Raspberry Pi 3/4/5 | diluvium_linux_static_aarch64 |
64-bit OS only |
| Raspberry Pi Zero | diluvium_linux_static_armv7l |
32-bit ARM |
| macOS (Apple silicon) | diluvium_darwin_arm64 |
M1/M2/M3/M4 |
| macOS (Intel) | diluvium_darwin_x86_64 |
|
| Windows | diluvium_windows_x86_64.exe |
64-bit |
| WASI | diluvium_wasi.wasm |
Needs a runtime with wasm exception-handling (wasmtime 28+ with -W exceptions=y) |
| Browser | libdiluvium_wasm_unknown.a |
wasm32-unknown-unknown static library |
Each platform also ships a diluvium_compiler_* binary (luac
with the -r analysis report flag) and a libdiluvium_*.a static
library for embedding.
Verifying a download
sha256sum -c SHA256SUMS.txt --ignore-missing
BUILDINFO.txt records the exact commit, build time and workflow run.
Diluvium 5.5.1_build5
[5.5.1_build5] - 2026-08-12
v5.5.1_build5 · Lua 5.5.1 · bytecode format 0x46
Hibernation is on, and stable is true again.
5.5.1_build4 shipped stable: false for exactly one reason: an
ungated dv_restore with a known corruption path. That block --
profile C of the M0-M7 audit, hibernation at scale -- is closed.
Every confirmed finding from the audit is now fixed, each with a
named test that fails when its fix is removed, and hibernation is
on by default: park an instance, snapshot it, free it, restore the
bytes into a fresh instance and resume, with the failure paths as
tested as the happy one. A restored program that raises gets the
right error with a traceback; a restored pcall still catches and
re-arms the handler it displaced; a woken runaway is stopped with
both residencies charged against the one instruction budget; an
endpoint reference held across the gap still binds; and a
malformed snapshot is refused, never a crash -- the fuzzer's
known-failure set is empty and stays that way.
dvs_allow_hibernation survives as a host's opt-out. The comment
that made it off by default is struck rather than edited, which is
what its own text asked for.
The release also builds out the host story. doc/Hostcall.md
reserves the hostcall encoding -- correlation token required from
the first prototype -- and doc/Host.md states the protocol both
planned hosts implement, the lab JavaScript host and the generic C
one, with the acceptance test that a guest cannot tell them apart.
The swarm layer reaches WebAssembly: a new
diluvium_swarm_wasi.wasm carries it with the host vtable crossing
the boundary as "env" imports, so a swarm can be driven by a
JavaScript host -- which is what lab picks up. And the generic host
itself now exists (host/, make build_host): a deployment is a
supervisor program plus a typed *.host.lua configuration, not C,
with connectors for the wall clock, SQLite and an HTTP listener,
each off until wired and gated by the capability grammar.
Added
- The generic host (
host/,make build_host): the host
protocol as one configurable binary, so a deployment is a
supervisor program plus a*.host.luaconfiguration rather than
bespoke C. The config is Lua's syntax without its power --
evaluated in an empty environment, every unknown key refused by
name -- and typed by a LuaCATS schema (host/types/host.lua).
Three connectors, all off until wired and each gated by a
host:capability the same way queues are gated: the wall clock,
SQLite (sql/queryreads,sql/execwrites, confined to one
file by an authorizer), and an HTTP listener behind a
TLS-terminating load balancer (a request is a message, a message
is a response,connechoed like a hostcall token).
examples/discofetch/swarmd.cis the bespoke ancestor this
retires. - The swarm layer reaches WebAssembly.
dvs.cjoins both wasm
archives, anddiluvium_swarm_wasi.wasmis a new artifact
carrying it plusdvs_shim.c: a JavaScript host cannot make the
C function pointersdvs_newwants, so the trampolines live on
the C side as mandatory "env" imports anddvsjs_newstands in
as the constructor. A separate module on purpose -- mandatory
imports linked intodiluvium_wasi.wasmwould have broken
wasmtime diluvium_wasi.wasmand every other pure-WASI
consumer, which keep the module they had. The JS binding always
supplies the trampolines,setSwarmHostinstalls the real host,
andinstantiateis exported as the layer a host builds on. dvs_set_host_identity: a swarm can hold an identity, and every
snapshot it takes carries it. A foreign stamp is refused at
wake, and so is a missing one on a stamped host -- 10.10's
asymmetry, now true of the swarm layer and not only the ABI
under it. Not authentication; a label that fails loudly.diluvium_snap_headerusage: read a snapshot's instruction count
from its header without restoring it. In the header rather than
the payload so a host deciding whether it can afford to wake a
cached instance can ask the bytes -- the same reasoning that
made the budget a query at the swarm layer.doc/Hostcall.mdanddoc/Host.md: the hostcall request/reply
encoding with the correlation token reserved before any host
ships a handler, and the host protocol -- construction, drive
loop, roster, queue pump, hostcalls, hibernation policy,
shutdown -- written so the lab JavaScript host and the generic C
host are two implementations of one contract.- Tests for every path the audit found untested, each verified to
fail with its fix removed: a woken instance still budgeted (the
test that never existed), a restored program's traceback, a
restoredpcall's handler chain, all four host-identity stamp
quadrants, a nested coroutine refused by name and capturable
once dropped, an endpoint reference surviving hibernation, and a
woken program re-binding its own endpoint with ordering held
across the gap.
Changed
- Hibernation is on by default;
dvs_allow_hibernationis a
host's opt-out. The swarm's budget test the audit caught
certifying only readback now wakes what it caches and asserts
the counter came through. - The snapshot fuzzer labels mutants with the format-2 record
layout and its known-failure set is empty -- audit S2's two
crashers are refusals now, and adding an entry to that set needs
a better reason than a red run. - Refusal sentences widened where the checks did: a to-be-closed
slot can be refused for holding nothing closable, a permanents
mismatch speaks of named values rather than only C functions,
and restore refusals gained their own distinctness test.
Fixed
- Any error raised in any restored program corrupted memory
(audit finding 0, the reason hibernation shipped off). The
thread record now round-trips what it dropped, and
a_restored_program_can_raise-- the first test in the tree to
ever resume a restored thread into an error -- holds it. - A woken instance's instruction budget was never re-armed, and
the counter did not travel, so a budget quietly became
per-residency (finding 1).dv_restorearms the count hook and
the header carriesinsn_used; both residencies charge one
limit. - A restored program's error was correct but bare (old_errfunc).
The record carries the thread's error-handler slot and each
pcall frame's saved one, in slot units so the stream does not
depend on a build'ssizeof; validation refuses rather than
asserts, which is S2's lesson applied ahead of time. - Two malformed snapshots reached an abort instead of a refusal
(audit S2). A crafted to-be-closed list naming a non-closable
slot reached the raise insideluaF_newtbcupval, which escapes
the lock convention -- one extra unlock, the counter at -1, the
opposite of the leaked lock the audit predicted at the same
site.diluvium_shim_settbcrefuses it first now; the full
corpus runs 0 crashed. - Nested coroutines were captured rather than refused (finding
14). A program parked holding a suspended coroutine is refused
by name and told what to do -- let it finish or drop it -- and
the same program snapshots once it has. - An endpoint reference did not survive a snapshot, and
bind
refused it with a false statement (finding 12). The reference
metatable is the permanentdendpoint.refmt;bindadopts an
endpoint queue no token claims, which is how a woken program
re-binds its own endpoint and the only route by which the
host's drain path returns after a wake; and messages buffered
before a hibernate drain out in order after it. - A crafted to-be-closed count could send record reads four
gigabytes out of bounds on platforms with a 32-bitunsigned long(pre-existing; LP64 was never affected). Every marked
slot is a distinct stack slot, so a count above the slot count
is refused as not credible.
Known issues
-
A sealed program still has no way to ask for the time -- but
the encoding it will use is now fixed.The hostcall needs no ABI: it is a message on a queue the host
drains and an answer on a queue it pushes to, and
doc/Hostcall.mdnow reserves the request and reply shapes with
the correlation token required from the first prototype. What
does not exist yet is any host that connects a call: until one
ships, a program that genuinely needs a clock or a file has
DV_FLAG_UNSAFE_STDLIBand nothing better, which is exactly why
that flag is scaffolding rather than a configuration. -
DV_FLAG_UNSAFE_DEBUGis a real hole, deliberately.Unchanged from 5.5.1_build4: a host that sets it gets the whole
debuglibrary back and, with it, the endpoint forgery route
and the budget escape. Supported for trusted code; recorded in a
test rather than only here. -
The swarm's snapshot cache is in-memory only, and the swarm's
own topology has no serializer. A host may persist any
instance's snapshot bytes and restore them after a process
restart -- the header checks make stale or foreign bytes a
refusal, never a corruption -- but recording which bytes
belonged to what, and every parent, capability and budget
relationship around them, is the host's own bookkeeping.
Instance ids restart with a new swarm, so ids a program
remembers from a previous life are volatile and must not be
trusted across generations. -
The rebuilt
u2.funcidxof a restored vararg pcall frame
differs from the organic value by the argument count -- the
"agree by construction" claim at the reconstruction site is
measurably false for vararg callees. Benign on every tested
path, recorded indoc/Hibernate.mdso nobody extending the
frame rebuild trusts the comment.
Upgrading
Snapshots taken by 5.5.1_build3 or 5.5.1_build4 are refused by
this release. Three stream changes land together so the cost is
paid once: the snapshot header carries the instruction coun...
Diluvium 5.5.1_build4
[5.5.1_build4] - 2026-08-11 (prerelease)
v5.5.1_build4 · Lua 5.5.1 · bytecode format 0x46
The capability layer becomes a boundary.
5.5.1_build3 shipped as a pre-release for two stated reasons:
hibernation was switched off, and the capability layer was a
structuring device rather than a security boundary, because a program
could reach past it through the debug library. This release closes
the second one. A program loaded into an instance can no longer forge
an endpoint reference, read the runtime's registry, walk past a
protected metatable, or switch off the instruction budget it was given.
That last one was not on anyone's list. A lua_State has one hook slot
and the instruction budget is a count hook in it, so debug.sethook()
-- the documented way to clear a hook, one line, no setup -- disarmed
it: an instance limited to 200,000 instructions ran three million and
reported nought used. It was found while closing the forgery route and
is closed by the same change.
Still a pre-release, and now for one reason rather than two.
Hibernation is off by default and should stay off; the defect behind
that switch is unchanged and is described under Known issues. Every
other confirmed finding from the M0-M7 audit that is not part of
hibernation is now fixed -- twenty-nine of thirty-five, and the six
that remain are hibernation entire.
An instance is now sealed by default, and that is the change most
likely to affect you. Section 18.2 said the debug library was the one
item between a deployment and running programs it did not write. That was
wrong: dv_new opened every standard library, so an instance also had
os.execute, io.popen, io.open, package.loadlib, dofile and
loadfile, and a program that can start a process has no need to forge an
endpoint reference.
It is a default rather than a flag because it is what the design already
said. An instance reaches outside itself by yielding a request its host
answers -- queue.wait is that, and doc/Determinism.md calls the general
form a hostcall. io/os/package were a second boundary that arrived by
inheriting luaL_openlibs and was never decided anywhere. Having them
costs more than the obvious: the instruction budget stops meaning anything
(a subprocess costs no VM instructions) and the swarm stops being
replayable (inputs stop arriving through the message log), and neither
failure announces itself.
Beyond that: four checks that reported success without checking
anything, and a defect where destroying an endpoint queue made its
token permanently unusable.
Added
-
DV_FLAG_UNSAFE_STDLIBindv_config.flags: putio,osand
packageback, withdofileandloadfile. Scaffolding for
programs that predate the sealed default, not a supported
configuration -- see Changed and Upgrading.A snapshot does not cross this flag: the permanents fingerprint covers
the module tables, so a sealed instance and an unsealed one disagree
anddv_restorerefuses with the permanents-set message. That is the
right answer -- a program captured holdingio.opencannot wake
somewhere there is none. It does crossDV_FLAG_UNSAFE_DEBUG,
because there the names are all still present. -
dvs_allow_unsafe_stdlibin the swarm layer, and flag attenuation
with it.Flags were the one authority in that layer that did not narrow:
buildzeroed itsdv_configand never consulted the parent, so a
sealed supervisor spawned children that hados.execute. Now the
swarm has a ceiling (off by default), the root takes it, and a child
inherits its parent's set. A spawn request carryingsealed = true
narrows further, which is the supervisor that needsos.timeitself
but hands its workers an instance without it. There is no way to
widen -- section 9.3 applied to flags. -
DV_FLAG_UNSAFE_DEBUGindv_config.flags: open the wholedebug
library in this instance rather than the narrowed one. For profile A
hosts, whose programs are their own. See Upgrading.
Changed
-
An instance no longer has
io,osorpackage, ordofileand
loadfile. Breaking for a program that used them; see Upgrading
for the one-flag escape and what it costs.Taken now rather than deferred because the arithmetic only gets worse:
dv_newhas shipped exactly once, in 5.5.1_build3, which is flagged
prerelease, is notlatest, and is not on the release mirror -- so the
set of hosts affected is as small as it will ever be, and grows from
the moment a release carryingdv_newbecomeslatest. Deciding it
here also means theDV_FLAG_SEALEDof earlier drafts never ships and
is not deprecated one release after being introduced.
Fixed
-
Destroying an endpoint queue no longer makes its token permanently
unusable.Nothing removed an entry from the token-to-handle map, and
bind
short-circuits on it, so binding the same reference again returned
the destroyed handle and reported success. Pushing through it raised
"handle 4 has been destroyed", and the host'sdv_endpoint_queue
went on naming the dead handle as the buffer to drain. Reachable by
accident rather than by trying:endpoint.bindandqueue.destroy
are both in the guest table. Audit finding 11. -
Four checks that reported success without checking anything, which is
worse than an absent check because an absent check is visibly absent.dsnap_check's "every header refusal code has its own sentence"
walked to the code that was last when it was written, so the two
added since -- the two most likely to be reached by a hostile
snapshot -- were never inspected, and it compared each sentence only
against the fallback, so two codes could share one.make verify_wasmnamed four files no target produces and swallowed its
own exit status through| head, so its first step could not fail
and the target only ever failed for the wrong reason.
patch_series.sh checkexited 0 having examined nothing when the
fork point was unreachable, which a shallow clone does.
test.yml'sinclude_skippednamed six tests when three are
skipped, three of them recovered a release ago.Audit findings 18, 29, 30 and 31. Each is now confirmed to fail
against the mutation it was supposed to catch. -
Two assertions that could not fail, and a row of section 6.4 that
nothing checked from the host side.test_msgpack.lua's malformed-input loops counted
pcall(...) == nil, which cannot happen, so both were tautologies:
the decoder could have returned a partially built value for every
truncated input and the file would still have printed "0 failed".
They now assert the property -- each single byte decodes exactly when
it is a whole encoding, and every proper prefix of a real encoding is
refused. Anddisabledwas the one row of 6.4 with no host-side
assertion: deleting the enabled check indiluvium_queue_push_bytes
made a host push into a queue the program had disabled succeed
silently, and nothing turned red. Audit findings 9 and 10. -
The parent-visible half of build3's sticky-error fix now has a test.
The fix was in and asserted at the ABI level; what no test reproduced
was what a supervisor is told. A supervisor that spawns a worker
and asks for it to be hibernated in the same batch -- "start it
asleep" -- had the hibernate correctly denied, and the refusal was
left on the child, so when the child finished its work it was
reported asfaultedand a restart policy restarted it. Audit
findings 15 and 19.
Security
-
An instance had every standard library, which section 18.2 did not
say and this release did not intend.os.execute,os.remove,io.popen,io.open,package.loadlib,
require,dofileandloadfilewere all reachable by a program
loaded into an instance;io.open('/etc/passwd')returned a file
handle.dv_newcalledluaL_openlibs, and no section of
doc/Messaging.mdever decided the standard library surface -- it
was inherited and never looked at.This is not a smaller version of the forgery problem below. It is a
larger one, and it means 18.2's profile B was not reachable in
5.5.1_build3 for a reason nobody had written down. The fix is the
sealed default in Changed, not a flag you have to know about.Recorded as S1 in
doc/audit/M0-M7.mdunder "Found since the sweep". -
An endpoint reference can no longer be forged through the
registry.A reference is a table wearing a private metatable, and identity was
metatable equality.__metatable = falsehid it fromgetmetatable
but not fromdebug.getmetatable, and the metatable itself sat in
the registry under its own__name, whichdebug.getregistry
returns. So a program that had been given nothing could prime the
metatable into existence with any table at all, read it back out of
the registry, wrap guessed peer bytes in a table wearing it, and get
a live endpoint handle to any peer the host had pre-authorised. Every
message it pushed arrived.Section 7.3's "a reference cannot be forged" was false, and 9.3's
attenuation -- which treats a reference as a capability rather than a
guessable name -- rested on it. Audit finding 6, reproduced end to
end before it was fixed.No registry-side scheme fixes this while
debug.getregistryis open,
including keeping a weak-keyed set of the references the runtime
actually made: a program that can read the registry finds that table
too and adds itself to it. So the library narrows. See Upgrading. -
A program can no longer switch off its own instruction budget.
debug.sethook()takes the single hook slot alua_Statehas, and
the budget of section 9.4 is a count hook in it. One line, no setup,
no capability required: an instance given 200,000 instructions ran
three million to completion,dv_exceededreported false and
...