- bounded wire reader/writer (malformation -> reject, overflow -> fail; no over-read on attacker-controlled bytes) - wo_tls_parse_server_hello: extracts negotiated suite + server x25519 key share; rejects HelloRetryRequest, unsupported suite/group, non-1.3 selected_version, and any truncation - wo_tls_build_client_hello: ClientHello offering TLS 1.3 / x25519 / RSA-PSS+RSA-PKCS1+ECDSA-P256, SNI, 32-byte legacy session id - KAT: ServerHello parser vs RFC 8448 recorded message (suite 0x1301 + server pubkey byte-exact), malformed rejected; ClientHello builder structural + SNI/keyshare present + too-small refused, and validated byte-for-byte spec-valid by an independent python parser. test_tls 71 pass, ASan/UBSan clean Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 541c71bca1655f370b145621d272d2e8bdb6c7ce) |
||
|---|---|---|
| .. | ||
| bench | ||
| src | ||
| test | ||
| Makefile | ||
| README.md | ||
| wo-rt.c | ||
runtime/ — wovm, the writeonce bytecode VM
The C11 register VM that loads and runs .wob images. Sibling of the OCaml
woc compiler (compiler/README.md): woc emits the
image, wovm executes it, and woc build appends an image to a copy of this
binary to produce one self-contained executable. libc only, direct syscalls, no
libraries.
The embedded database engine (database/) is
statically linked into every wovm and every test binary — one binary, no
separate database process.
Reasoning under the code: src/CODE-LOGIC.md. Normative
contracts: docs/plan/oop-vm/00-wob-format.md
(format, opcodes, builtin ids — src/wob.h is its machine-readable twin) and
08-builtin-surface.md (what each
builtin means in source terms).
wo-rt.cin this directory is not part of that toolchain. It is the retired io_uring event-loop reference prototype the runtime's design was read off, kept for reading. See the "Historical" section at the bottom of this page — nothing in the shipped build compiles it.
Build, test
just wovm-build # -> runtime/wovm (the release binary)
just wovm-test # unit suites, both dispatch flavors, + CLI smoke, ASan+UBSan
# in runtime/ directly:
make wovm # the release binary
make test # unit suites, computed-goto dispatch
make test-iso # the same suites under -DWO_ISO_C (plain switch)
make wovm-asan # a separate sanitized binary, for corpus fixtures needing a leak/UB proof
make wovm-tsan # thread-sanitized, for the fiber/actor demos
Across both halves of the toolchain: just oop-e2e (the woc + wovm
conformance corpus) and just oop-accept (the full milestone gate — compile-time
budget, corpus under ASan, single-binary smoke, both unit suites, one command).
Shipped features
- Register interpreter — fixed 32-bit instructions, Lua-style window-overlap
calls (callee r0 = caller slot A), dual dispatch: computed goto under GNU C,
switchunder-DWO_ISO_C. Both flavors are gated so neither rots. - Owned objects with a runtime borrow word — shared-reader count /
exclusive sentinel in every 16-byte header; violations trap
T_BORROW. The compiler elides provable sites; the VM enforces the residual ones. - Inferred GC, incremental tri-color mark-sweep — GC-ness is a compiler
inference, never an annotation (
@gcis rejected outright, WO-E104). Per-shard traced list, snapshot-at-beginning roots, Yuasa deletion barrier, budgeted mark and sweep slices — no stop-the-world by construction. Reference counting and the Bacon–Rajan trial-deletion collector that shipped in iteration 2 were both removed by iteration 7b.wovmpumps the collector to quiescence after the entry returns (WO_GC_BUDGETsteps per call, default 64;WO_GC_TRACE=1prints one stderr line per step). - Deterministic drops — kind-directed drop plans (scalar/owned/gcref/text/ bytes/float/multi/map), recursive over class fields and container elements.
- Trap unwinding that never leaks — per-method drop tables (pc → owned/gc
register masks); a trap walks every frame and frees what was live; structured
error
{code, line, method, msg}via line tables, catchable withtry/catch. - Validating loader — bounds-checked parse, aligned copies, const-string interning, full static validation (opcodes, registers, indexes, jump targets, terminators, builtin arity, call windows, sorted vtables). What the loader accepts, the interpreter trusts — no UB on any input.
- Structural interfaces —
ICALLbinary-searches sorted (class, slot, method) vtable triples by receiver class. - Fibers and shard actors — reduction-budget preemption, pinned per-core
shards, ownership-move message sends, bounded mailboxes (
WO_MAILBOX, default cap 1024) with a catchableWO_T_ACTORtrap on overflow,callparking the caller for a typed scalar reply, and actor death that traps callers rather than hanging them. Blocking stdlib calls park the fiber; the shard runs someone else. - The systems stdlib and the engine — six module namespaces (
fs,time,env,net,proc,json) plus the free builtins: text and containers, theFloat/Bytesbridges, base64, and the digestssha1/sha256/hmac_sha256.WO_B_MAXis 95. Database builtins reach the linked engine directly; the legacyDB_STUBopcode survives only for images emitted against no engine. - CLI contract —
wovm app.wob: exit 0 = ran; exit 1 = trap, one stderr linetrap CODE in METHOD at line N: MESSAGE; exit 2 = usage/load failure.wovm --versionprintswovm <VERSION>.WO_HEAP_MBoverrides the 64 MiB arena;WO_DATAopts into durability;WO_SHARDSsets the shard count. Run with no.wobargument,wovmchecks its own trailer for an appended image (woc build's single-binary output) — a recognized-but-corrupt trailer fails clearly on exit 2, never a crash.
Interpreter ceilings, all in src/wob.h: 64 registers per frame
(WO_MAX_REGS), a 4096-slot value stack (WO_STACK_SLOTS), 256 frames
(WO_MAX_FRAMES), 64 shards (WO_MAX_SHARDS). The image format is at
WOB_VERSION 6 (iteration 36's bitwise opcodes 42–46 moved it last).
File map
| File | What it is |
|---|---|
src/wob.h |
the .wob contract: opcodes, field kinds, trap codes, builtin ids, limits, the 16-byte object header |
src/obj.h/.c |
arena allocator (16-byte size-class free lists ≤ 1024 B, malloc above), wo_rt context, object creation, wo_str |
src/borrow.h/.c |
the borrow word: acquire shared/exclusive, unconditional releases |
src/cont.h/.c |
native containers: growable multi, linear-scan map (content-compared text keys) — deliberate KISS, and O(n) per lookup |
src/gc.h/.c |
the kind-directed drop dispatcher plus the incremental tri-color mark-sweep for traced objects |
src/loader.h/.c |
.wob parse + full static validation + the mmap file path |
src/vm.h/.c |
the interpreter: dispatch, frames, traps, drop-map unwinding, ICALL, shard/engine startup |
src/builtin.h/.c |
the builtin dispatcher — the single entry point the interpreter calls, forwarding to sysio.c and json.c |
src/sysio.c |
the OS half: fs, time, env, net, proc |
src/json.c |
json.encode / json.decode, driven by class metadata |
src/crypto.c/.h |
SHA-1, SHA-256, HMAC-SHA256 (iteration 34), vector-verified |
src/park.c/.h |
the park plane: raw io_uring ABI (no liburing) and the epoll fallback, fiber parking, per-call deadlines |
src/main.c |
the wovm CLI: find an image (argument or embedded trailer), build argv, call the entry, map its result to an exit code, pump the collector |
test/t.h |
20-line assert harness (no framework) |
test/wob_build.h/.c |
in-memory .wob assembler — a second, independent encoding of the format, so builder/loader disagreements fail tests |
test/test_*.c |
18 suites, one binary each, ASan+UBSan: arena, borrow, builtin, cont, crypto, cycle, fiber, icall, loader, mailbox, obj, objops, rc, table, unwind, vm, wal, wobbuild |
test/mkwob.c |
fixture generator for the CLI smoke |
test/cli_smoke.sh |
end-to-end exit-code/stderr-shape check |
Note where io_uring is and is not: src/park.c drives it for the fiber and
network plane, while the WAL commit path in database/src/wal.c is still a plain
per-commit fdatasync. Moving the WAL onto the rings is iteration 23.
Debugging
VS Code: .vscode/launch.json ships four configs (needs the C/C++ extension,
ms-vscode.cpptools):
- wovm: debug hello.wob — rebuilds
wovmat-O0 -g, regenerates fixtures, breaks anywhere in the VM. - wovm: debug a .wob file — same, prompts for the image path.
- wovm: debug unit test (ISO dispatch) — pick one of the ASan test
binaries (already
-g), step through it. - wo-rt: debug server (1 shard) — the retired event-loop reference at
-O0 -g,WO_THREADS=1so one shard owns everything.
How to work on the VM under a debugger:
- Step the ISO flavor, not the computed-goto one. The goto interpreter jumps
label-to-label and single-stepping is disorienting. Test binaries have an ISO
twin (
build/iso_test_*, plainswitch) wherenext/stepbehave normally. Forwovmitself, buildmake -B wovm CFLAGS='-O0 -g -std=c11 -DWO_ISO_C'. break vm_trap— one breakpoint catches every trap at the moment of failure, with the trapping frame intact (vm->frames[vm->depth-1],pcalready rewound to the faulting instruction).vm_unwindis the next frame down if you're chasing a leak-on-trap.- Other load-bearing breakpoints:
wo_load_buf(validation rejects),recv_check(residual field checks),wo_builtin(all builtins),wo_gc_step(mark-sweep slices). - ASan under gdb:
ASAN_OPTIONS=abort_on_error=1makes the first report SIGABRT so the debugger stops on it with the full stack; without gdb the report alone usually names the exact free you missed. - CLI knobs:
WO_HEAP_MB=1 ./wovm app.wobforces earlyT_OOMpaths; exit codes 0/1/2 are stable for scripting. - gdb without VS Code:
gdb --args ./wovm build/hello.wob, orgdb ./build/iso_test_unwind.
Historical: wo-rt.c
A single-file C server that was the runtime-layer reference prototype —
built to find out which kernel primitives a writeonce runtime should stand on,
by writing them with no abstraction in the way. Its design conclusions are what
src/park.c and the shard model implement. It is not part of the toolchain, no
gate builds it, and it shares no code with wovm.
It reached phase F of its own plan
(docs/plan/exploration/c-runtime/00-plan.md,
phases A→F all shipped): WO_THREADS pinned threads, each owning a raw io_uring
ring (io_uring_setup + mmap'd SQ/CQ rings + io_uring_enter, no liburing), its
own SO_REUSEPORT listener with multishot accept, its own keep-alive
connections, and its own slice of one mlock'd mmap arena — shared-nothing, no
locks, one io_uring_enter per loop tick in steady state. Writes followed the
dual-write order: RAM apply, framed WAL record to a per-shard fallocate'd log,
one group-commit fdatasync per tick, HTTP ack only after the fsync
completes. Boot replayed each shard's snapshot + WAL tail in parallel before
any accept armed; ./wo-rt wal-check <file> validated a log offline.
Build and poke it, if you want to read it running:
make -C runtime wo-rt # cc -O2 -Wall -Wextra -std=c11 -pthread, no libraries
./runtime/wo-rt # 127.0.0.1:8085 (WO_PORT=9000 WO_THREADS=4 to override)
Its measured numbers, kept as the historical record they are — bench client
bench/bench.c (keep-alive, only 2xx counted), Go reference in bench/goref/,
20-core Linux 6.14, tmpfs data dir. These are not wovm numbers; the
shipped VM's measurements live in bench/baseline.json and
docs/plan/perf-targets.md.
| Benchmark | wo-rt-c (8 shards, durable WAL, io_uring group commit) | Go net/http (go1.25.1, 20 cores, no durability) |
|---|---|---|
GET /healthz |
859,033 req/s · p50 71 µs · p99 159 µs | 336,444 req/s · p50 70 µs · p99 1,277 µs |
GET / (JSON) |
671,312 req/s · p99 180 µs | — |
POST write (tmpfs) |
618,343 commits/s — fsync-acked · p99 194 µs | 320,516 req/s — RAM only, no WAL · p99 1,581 µs |
| 10,000 idle conns | 0 errors | 0 errors |
Honest caveats as recorded then: net/http does full general-purpose HTTP and
this parser was minimal; tmpfs makes fsync nearly free, so the durable column
flatters itself. ACID probes: three kill -9 rounds mid-bench at ~2M commits all
showed WAL records ≥ acked; 300 concurrent commits → 300 distinct ids; torn-tail
records dropped whole by CRC. The crash-under-load test found two real bugs the
lighter phase-D test missed — an ack-before-fsync race and an fd-reuse ABA hazard
in ack parking — which is what the phase existed for.
Deliberate simplifications it never outgrew: single-shot RECV re-armed per
request, one outstanding SQE per connection, fixed-size buffers, naive "title"
extraction instead of a JSON parser, no timerfd. Requires kernel ≥ 5.19
(multishot accept).
Design docs it fed:
c-runtime/01-architecture.md
(the runtime traced through one memory address, plus seven improvement
proposals),
c-runtime/02-single-binary.md
(how a single binary runs on this runtime),
blue-green-vm/00-vision.md
(port-free transports, fibers, embedded source, two-VM hot swap), and the
kernel-primitive cards under
exploration/linux/.