Skip to content

Releases: Aloecraft-org/diluvium

Diluvium 5.5.1_build12p1

Choose a tag to compare

@github-actions github-actions released this 02 Sep 00:37
515160f

[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_usage keeps counting past a budget the guest tried to ignore.
    insn_used advanced 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 from dv_usage was reading a number the defect
    controlled. usage_keeps_counting_past_the_budget holds it, asserting
    strictly greater than the limit, because the frozen value was the
    limit exactly.

Fixed

  • A guest pcall permanently 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_error raises an ordinary catchable Lua error, so a guest's own
    pcall caught it — and with the hook already cleared nothing re-armed
    it, because dv_run and dv_restore are 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 returned DV_DONE. The hook now stays
    armed, so it fires again within DV_HOOK_STEP and the budget bounds
    the work again. a_budget_survives_a_guest_pcall holds 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 buys DV_HOOK_STEP
    instructions and the loop repeats. Lua has no uncatchable error.
    Closing it needs one of: a pcall/xpcall that refuses to catch once
    exceeded is set, which is a core-file patch and so a
    CORE_PATCH_ALLOWLIST decision rather than a change; or a
    process-level watchdog, since a host inside dv_run has no way in --
    dv.h exposes 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

Choose a tag to compare

@github-actions github-actions released this 01 Sep 16:55
51de834

[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_check and make dshim_race_tsan, over a
    new test/dshim_race_check.c: several threads released together
    into dv_new in 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 of diluvium-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.md stays normative
    for both implementations. New doc/DRT.md covers the split and
    analyses what the C host still does that DRT does not yet -- local
    exec/run most 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.c is now a frozen differential-test reference that
    diluvium-drt reimplements and benchmarks against, and its SPEC
    records that this repository deletes it once that port passes
    acceptance -- so diluvium-swarm-* is deliberately not a release
    artifact, and make build_swarm_lib/dvs_check stay 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 concurrent dv_new (SIGSEGV in strcmp).

    diluvium_shim_addcont and diluvium_snap_addcont scanned 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
    NULL for the next scan to hand to strcmp; 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.h to 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
    f137b308c4dce917b24c71ab41add61606945e58 or earlier has an
    affected copy.

  • ds_learnconts's once-flag raced. A plain static 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

Choose a tag to compare

@github-actions github-actions released this 24 Aug 15:59
6206b0f

[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 way jwt_sign owns exp, and the deployment's
    uris are 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. Without key, 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 in json.encode, and a named
    permanent in snapshots, so x == msgpack.null still holds after
    a wake. Decode never produces it: null -> nil stays.
    host.sql.NULL re-exports it where a statement binder will look;
    a nil param stays refused, because an accidental nil is still a
    bug worth catching.
  • json.encode honours msgpack.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 as host.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 on host.time(). Granted as host:time/monotonic.

Changed

  • The listener's headers and response_headers allowlists 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.encode of an msgpack.ext wrapper 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

Choose a tag to compare

@github-actions github-actions released this 18 Aug 23:38
7dfe1d5

[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 by response_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/get and rest/post results
    carry a headers map: 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's spawned event lands; a denial raises with the refused
    capability in the message; handle.kill() awaits its outcome;
    host.events drains child exits, faults and budget overruns,
    buffered through any outcome wait. No child.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 via load.
    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.lua proves the checks can fail on a non-secure
    build of the same table.
  • An assistant-shaped example -- host/assistant.host.lua and
    host/assistant.lua: listener + rest + vault + spawn wired with
    honest bounds, including the raised call_timeout_ms an 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

Choose a tag to compare

@github-actions github-actions released this 15 Aug 16:16
760783d

[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.sh and host/Dockerfile.musl build 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

Choose a tag to compare

@github-actions github-actions released this 15 Aug 14:34
0d6180f

[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_PENDING is 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.json beside it. Manifests are parsed
    with the runtime's own strict JSON decoder in a lua_State the host
    owns, so nothing is vendored and no schema validator is embedded --
    the runtime reads the flat metadata and reads past args/result.
    Per-plugin max_inflight bounds what a serial plugin is handed;
    call_timeout_ms is 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 over postMessage in a Worker). Plus
    plugins/dvplug.h, a dependency-free single-header kit for writing
    a plugin in C, and plugins/README.md.
  • host.capabilities() and the visibility field, 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_sleep folds the
    listener's sockets, the plugin channels and a plain timeout into a
    single poll(). 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.call and host.try take an optional waitms, which roundtrip had always accepted.
  • dh_call_fn carries the request's correlation token, so a connector that defers can name the call it is answering.
  • DH_MAX_CONNECTORS is 16, since every plugin claims a slot beside the built-ins.

Fixed

  • The generic host builds on macOS. dhost_crypto.c called
    getrandom(2) unguarded; that is Linux's, and <sys/random.h> is
    spelled the same on macOS but declares getentropy instead, so the
    /dev/urandom fallback underneath it was unreachable -- the file
    never compiled. Split at compile time. Pre-existing and invisible
    because host_check did 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.
  • realpath was handed a 512-byte buffer in host_check; it writes up to PATH_MAX. Found by adding host_check to sanitize_checks, which _FORTIFY_SOURCE catches and ASan does not.

Security

  • A plugin is exec'd with execv from an absolute path, never
    execvp: a plugin path comes from a manifest an operator wrote, and
    resolving it through PATH is 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 swap diluvium-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

Choose a tag to compare

@github-actions github-actions released this 13 Aug 15:13
c8f8f4f

[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

Choose a tag to compare

@github-actions github-actions released this 12 Aug 19:17
9d887d1

[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

Choose a tag to compare

@github-actions github-actions released this 12 Aug 07:43
4ddd65f

[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.lua configuration 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/query reads, sql/exec writes, 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, conn echoed like a hostcall token).
    examples/discofetch/swarmd.c is the bespoke ancestor this
    retires.
  • The swarm layer reaches WebAssembly. dvs.c joins both wasm
    archives, and diluvium_swarm_wasi.wasm is a new artifact
    carrying it plus dvs_shim.c: a JavaScript host cannot make the
    C function pointers dvs_new wants, so the trampolines live on
    the C side as mandatory "env" imports and dvsjs_new stands in
    as the constructor. A separate module on purpose -- mandatory
    imports linked into diluvium_wasi.wasm would have broken
    wasmtime diluvium_wasi.wasm and every other pure-WASI
    consumer, which keep the module they had. The JS binding always
    supplies the trampolines, setSwarmHost installs the real host,
    and instantiate is 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.md and doc/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
    restored pcall'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_hibernation is 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_restore arms the count hook and
    the header carries insn_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's sizeof; 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 inside luaF_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_settbc refuses 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 permanent dendpoint.refmt; bind adopts 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-bit unsigned 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.md now 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_STDLIB and nothing better, which is exactly why
    that flag is scaffolding rather than a configuration.

  • DV_FLAG_UNSAFE_DEBUG is a real hole, deliberately.

    Unchanged from 5.5.1_build4: a host that sets it gets the whole
    debug library 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.funcidx of 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 in doc/Hibernate.md so 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...

Read more

Diluvium 5.5.1_build4

Diluvium 5.5.1_build4 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 11 Aug 19:04
6aa17c7

[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_STDLIB in dv_config.flags: put io, os and
    package back, with dofile and loadfile. 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
    and dv_restore refuses with the permanents-set message. That is the
    right answer -- a program captured holding io.open cannot wake
    somewhere there is none. It does cross DV_FLAG_UNSAFE_DEBUG,
    because there the names are all still present.

  • dvs_allow_unsafe_stdlib in the swarm layer, and flag attenuation
    with it.

    Flags were the one authority in that layer that did not narrow:
    build zeroed its dv_config and never consulted the parent, so a
    sealed supervisor spawned children that had os.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 carrying sealed = true
    narrows further, which is the supervisor that needs os.time itself
    but hands its workers an instance without it. There is no way to
    widen -- section 9.3 applied to flags.

  • DV_FLAG_UNSAFE_DEBUG in dv_config.flags: open the whole debug
    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, os or package, or dofile and
    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_new has shipped exactly once, in 5.5.1_build3, which is flagged
    prerelease, is not latest, 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 carrying dv_new becomes latest. Deciding it
    here also means the DV_FLAG_SEALED of 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's dv_endpoint_queue
    went on naming the dead handle as the buffer to drain. Reachable by
    accident rather than by trying: endpoint.bind and queue.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_wasm named 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 check exited 0 having examined nothing when the
    fork point was unreachable, which a shallow clone does.
    test.yml's include_skipped named 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. And disabled was the one row of 6.4 with no host-side
    assertion: deleting the enabled check in diluvium_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 as faulted and 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, dofile and loadfile were all reachable by a program
    loaded into an instance; io.open('/etc/passwd') returned a file
    handle. dv_new called luaL_openlibs, and no section of
    doc/Messaging.md ever 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.md under "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 = false hid it from getmetatable
    but not from debug.getmetatable, and the metatable itself sat in
    the registry under its own __name, which debug.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.getregistry is 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 a lua_State has, 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_exceeded reported false and
    ...

Read more