Skip to content

certify: the oracle never panics, and the release legs after #507 - #534

Merged
gafferongames merged 4 commits into
mainfrom
certify-red-507
Sep 4, 2026
Merged

certify: the oracle never panics, and the release legs after #507#534
gafferongames merged 4 commits into
mainfrom
certify-red-507

Conversation

@gafferongames

Copy link
Copy Markdown
Contributor

Certification on main went red at bafdb69 (run 33887252419), the first full
Certify after #507 landed the id-table wire. Thirteen of fifteen jobs failed on
two causes.

1. The oracle panicked, so it was not an oracle

Every test and inline-gate leg, and tables-cpp-release, died in the same
place:

harness: FAILED after 99100 mutants: the oracle PANICKED on the mutant: runtime error: slice bounds out of range [8:6]
  internal/tablewire.(*wireReader).sub  decode.go:189
  internal/tablewire.(*wireReader).arrayBody  decode.go:675

The cause. An array, map or keyed body is framed by its own L, and the
three readers compute end := r.off + bodyLen and later span the elements with
r.sub(end - r.off). Between those two lines they read the body's header, the
element kind byte and the count, and the count is a canonical LEB128 read
against the PARENT buffer rather than against end. A body's L of 2 is the
shortest that carries a header at all, but a LEB128 count is up to ten bytes,
so a count spelled wide leaves the cursor past the end its own L set and the
span goes negative. Go slices with it and panics.

The C++ reference has the same shape at all six of its sites, but its span is
an int64_t size on a reader whose has() is offset + bytes <= size, so a
negative size reads nothing. It decodes no elements and reports malformed on
the first one it was asked for. That is the correct answer and the oracle now
gives it: subTo(end) clamps the span at the cursor, so the body covers no
elements instead of a negative number of them.

The fix is at the class, not the site. All three body readers go through
subTo now, so the invariant is stated once and named.

The reference agrees on the mutant. Replayed alone, and across the whole
113652-mutant run: 0 divergences. There is no second defect here.

The mutant is pinned. testdata/wire/tables/fuzz-vectors/ is a new corpus
of mutants the fuzzer has already gone red on, seeded into every run by name,
fed exactly as it is and before the random pass. It holds one vector,
stream_count_past_body. Pinned vectors are never mutated and the random pass
never draws from them, so the mutant sequence is still a function of the corpus
and the seed alone and pinning one cannot move a red that was already there.

A red is now a seek. The run seed was already deterministic (SEED ?= 24845619678 in the Makefile), which is why this reproduced on the first try
locally, but the failure message printed neither it nor the mutant. It prints
both now, the bytes as hex up to 4 KB and the SHA-256 above that, so a red in a
CI log a person cannot re-run is still a red they can replay.

The negative control is make tables-wire-fuzz-oracle-negative-control,
which joins tables-wire-fuzz-negative-control and so runs in make test. It
removes the clamp from a copy of decode.go through a Go build overlay and
requires the run to go red on the panic AND on the pinned vector by name:

harness: FAILED after 93651 mutants: the oracle PANICKED on the mutant: runtime error: slice bounds out of range [8:6]
  corpus seed stream_count_past_body (streamdemo.Feed), pass vector #0, run seed 24845619678, mutant of 119 bytes
negative control: removing the oracle's body-span clamp turns the pinned vector RED

2. The port gates that read the id-table corpus

tables-elixir-release, tables-dart-release, tables-java-release and
tables-js-release failed on one cause: they run targets that hold a port's
codec to testdata/wire/tables, which is the id-table form now, while the port
still writes the wire's previous form. tables-cpp-release was not a fifth
cause. It was the wire fuzzer above.

#507 already ruled on this shape and spelled it as dormancy: the conformance
harness reports those ports' wire, report and json-* surfaces ABSENT, and
each port's conformance-negative-control-<lang> is a dormant echo naming
issues #511 to #518. What was missed is every gate that calls the port's wire
codec directly rather than through the harness. This applies the same rule to
those, by name, citing the same issue per port.

It reaches further than the release legs. make test runs the port legs
after the wire fuzzer, so the panic above masked them: with the oracle fixed,
test-c, test-cs, test-go, test-js and test-java all go red on the same
cause. Those are fixed here too, or the run would only fail later.

port issue dormant by name
Go #511 six AllocsPerRun gates and TestSoak in test/go-tables
C #512 tables-c-soak, tables-c-soak-negative-control
C# #513 tables-cs-leg (the inline dotnet run in test-cs, now a named target)
Dart #514 tables-dart-soak, -soak-negative-control, tables-dart-alloc, -alloc-negative-control, tables-dart-usage
Elixir #515 tables-elixir-alloc-audit, -alloc-pin, -alloc-negative-control, tables-elixir-soak, -soak-negative-control
JavaScript #516 tables-js-leg, tables-js-json-differential, -json-differential-negative-control
Java #517 tables-java-alloc, -alloc-negative-control, tables-java-soak, -soak-negative-control
Rust #518 nothing: no Rust gate reads the table wire corpus

Each dormant target keeps the comment that says what it is and gains the same
one-line echo #507 wrote, so grep dormant make/*.mk is the wake list for
#511 to #518.

3. The zero-cost gate, which was the next red behind the panic

With the oracle fixed, make test reaches tables-zero-cost and it fails:

450:static const uint64_t kTableNodeTableFieldId = 0xFFFFFFFFFFFFFFFFull;
1027:        if ( field_id == kTableNodeTableFieldId && r.nested )
ZERO-COST GATE FAILED: pointer or map machinery leaked into build/tables-generated/examples/GuardedTable.h

This is #507's too, and it is a false positive rather than a leak. The reserved
node-table id is in every generated unit because every reader of the id-table
form owes §3.1's refusal of that id inside a NESTED body, whether or not its own
closure carries a pointer: the body it is handed may have been written by a unit
that does (§4). What a pointer-free unit pays for the rule is one static const uint64_t and one comparison, which is none of the things the gate's own comment
says it holds (no builder, no arena, no handles, no lifecycle surface, no extra
descriptor columns).

The scan is BY SYMBOL now rather than by line, TableNode is matched with its
whole spelling so a node symbol nobody has written yet is still refused, and
exactly one spelling is sanctioned by name. Across the whole pointer-free scan
set that constant is the only TableNode spelling present, so nothing else is
let through.

The negative control, make tables-zero-cost-negative-control, is a new
target in make test: it plants TableNodeMap in a copy of a scanned header
and requires the same scan to refuse it.

TableNodeMap
negative control: a planted node symbol turns the zero-cost scan RED, and the reserved id alone does not

Two things noticed and not fixed here

  • Some gates are green and vacuous. tables-js-soak, tables-js-alloc and
    Go's TestLoadAllocatesNothing load a golden the port now refuses and
    measure the refusal instead of the read path. They are green, so they are not
    this red, but they are measuring nothing until the port wakes.
  • The map body's site is not fuzzed. test/tables/wire_fuzz_main.cpp has no
    codec for the maps unit, so mapField is fixed here by inspection and by
    the class rather than by the fuzzer. Adding the unit to the leg would close
    that.

What was run

make test locally, one C++ build at a time. bench/LOCK was read first and
no locked path is touched: the diff has nothing under internal/codegen/c/,
internal/codegen/cpp/, generated/c/, generated/cpp/ or
generated/c-ludicrous/.

gafferongames and others added 4 commits September 5, 2026 02:01
THE ORACLE PANICKED, so it was not an oracle. An array, map or keyed body is
framed by its own `L`, and the three readers compute `end := r.off + bodyLen`
and later span the elements with `r.sub(end - r.off)`. Between those two lines
they read the body's header, whose count is a canonical LEB128 read against the
PARENT buffer rather than against `end`. A body's `L` of 2 is the shortest that
carries a header at all, but a LEB128 count is up to ten bytes, so a count
spelled wide leaves the cursor past the end its own `L` set and the span goes
negative.

The C++ reference has the same shape at every one of its sites, but a negative
span there is a reader whose `has()` refuses, so it decodes no elements and
reports malformed on the first one asked for. `subTo(end)` gives the oracle that
same answer by clamping the span at the cursor, and all three body readers go
through it, so the invariant is stated once.

The reference agrees on the mutant, replayed alone and across the whole run: 0
divergences.

THE MUTANT IS PINNED. testdata/wire/tables/fuzz-vectors/ is a corpus of mutants
the fuzzer has already gone red on, seeded into every run by name and fed
exactly as it is, before the random pass. Vectors are never mutated and the
random pass never draws from them, so the mutant sequence stays a function of
the corpus and the seed alone.

A RED IS A SEEK. The run seed was already deterministic, but the failure
message printed neither it nor the mutant. It prints both now, the bytes as hex
up to 4 KB and the SHA-256 above that.

The negative control is tables-wire-fuzz-oracle-negative-control, which joins
tables-wire-fuzz-negative-control and so runs in `make test`: it removes the
clamp through a Go build overlay and requires the run to go red on the panic
AND on the pinned vector by name.

THE PORT GATES THAT READ THE ID-TABLE CORPUS. Four release legs failed on one
cause: they hold a port's codec to testdata/wire/tables, which is the id-table
form now, while the port still writes the wire's previous form. #507 already
ruled on that shape and spelled it as dormancy; what it missed is every gate
that calls a port's wire codec directly rather than through the conformance
harness. The same rule now covers those, by name, citing the same issue per
port: #511 Go, #512 C, #513 C#, #514 Dart, #515 Elixir, #516 JavaScript, #517
Java. No Rust gate reads the corpus.

It reaches past the release legs: `make test` runs the port legs after the wire
fuzzer, so the panic masked them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d id

The gate is the next red on main behind the wire fuzzer's panic, and it is
#507's too. The id-table form's reserved node-table id is `kTableNodeTableFieldId`
in every generated unit, because every reader of the form owes §3.1's refusal of
that id inside a NESTED body whether or not its own closure carries a pointer:
the body it is handed may have been written by a unit that does (§4). The
gate's `TableNode` token matched the constant and called it pointer machinery.

What a pointer-free unit pays for the rule is one `static const uint64_t` and
one comparison. No arena, no builder, no handle, no lifecycle surface and no
extra descriptor column, which is the claim this gate holds.

So the scan is BY SYMBOL now rather than by line, `TableNode` is matched with
its whole spelling so a node symbol nobody has written yet is still refused, and
exactly one spelling is sanctioned by name. tables-zero-cost-negative-control
plants TableNodeMap in a copy of a scanned header and requires the same scan to
refuse it, so the one sanctioned symbol is shown to be the only one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The four Elixir sabotage variables and the three Dart GC-count variables
existed for recipes that are now dormant echoes, so they are scaffolding with
nothing behind it. The tunables stay: DART_SOAK_SECONDS, JAVA_SOAK_SECONDS and
their kind are the knobs each gate wakes with.

The failure-message const also moves above wireFailure's own doc comment, which
it had split.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The next red behind the zero-cost gate, and the control caught itself: #507
hardened the map's N-against-L check to compare unsigned, the way §3 requires
every length, count and index on this wire to be compared, and the control's sed
still named the signed spelling. Its own "the sabotage patched nothing" guard is
what said so, which is the guard doing its job.

The sabotage now names the current line and the gate goes red on three cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant