Skip to content

Commit 7cf49bf

Browse files
committed
fix: stale tasks_mod.g_state pointer in manually-driven VMContext tests; add GBC/task/compiler coverage
Runtime.activate() re-pins 8 process-global setActive pointers because Runtime is returned by value from test helpers and Zig does not guarantee that copy is elided -- every real run/call entry point calls activate() on its own final, stable address for exactly this reason. compiler_test.zig's ~28 tests that build a vm.VMContext by hand and call vm.run()/vm.callGlobal() directly (needed to drive gbc_reader.read() before there's a compiled program the normal entry points could run) instead re-pinned only 3 of those 8 pointers by hand -- chunk/globals/heap -- leaving tasks_mod.g_state (among others) pointed at a test helper's now-defunct stack frame. Undetected because nothing dereferenced the stale pointer under normal test runs; found when a coverage-audit test happened to trigger a GC while running through this exact path (vmAllocObject under -Dgc_stress=true, a CI-only lane the local pre-push hook never runs): collectGarbage's task-table walk read 0xAA-poisoned freed memory as temp_root_top and panicked. Fixed by replacing the manual 3-pointer triplet with rt3.activate() everywhere it occurred, and documented the hazard on activate() itself. Verified clean under both -Dgc_stress=true and -Dheap_paranoia=true (both CI-only, never run locally before now). Also closes three coverage gaps found in the same audit: - GBC reader malformed-wire header/section-table error paths (HeaderTooSmall, UnsupportedHeaderVersion, FormatMajorMismatch, NonZeroReserved, OptionsMismatch, VMFingerprintMismatch, TruncatedHeader, TruncatedBody, MalformedSectionTable, SectionOutOfBounds, MissingRequiredSection, BadConstantTag, FuncRefOutOfRange, TypeRefOutOfRange) via checksum-recomputing corruption tests on real round-tripped artifacts. - Task scheduler: MaxTasks=64 ceiling (TooManyTasks), a task spawning another task, and 5 concurrently-ready tasks' FIFO reply order -- previously only ever tested with <=2 tasks. - Compiler: MaxTypes=1024 ceiling (TooManyTypes). DuplicateField turned out to already be thoroughly covered by tests/spec/fail/ {009,017,216,217,218,219,220,221,222}, contrary to the audit's initial (grep-based, and wrong) claim of zero coverage.
1 parent c768d69 commit 7cf49bf

3 files changed

Lines changed: 543 additions & 45 deletions

File tree

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,50 @@
22

33
This changelog tracks notable language/runtime changes by implementation date.
44

5+
## 2026-08-20
6+
7+
### Fix — cap:net IPv6 CIDR policy rules crashed (or hit UB in release builds) for virtually every real prefix length
8+
9+
A coverage audit found `net_state.zig`'s `ipv6InCidr` had zero test
10+
coverage. The first CIDR test written for it (`"2001:db8::/32"`)
11+
immediately crashed: `bits` was typed `u3` (max representable value
12+
7), but `@min(remaining, 8)` legitimately equals 8 for any full byte
13+
of prefix — which is virtually every real-world IPv6 prefix length
14+
(8, 16, 24, 32, ..., 128), and any prefix length where an early byte
15+
is fully covered (e.g. `/12`). In Debug/ReleaseSafe this panicked
16+
("integer does not fit in destination type"); in ReleaseFast — what
17+
the shipped CLI actually runs — the same `@intCast` is undefined
18+
behavior instead of a panic, meaning an IPv6 CIDR deny rule could
19+
silently fail to match and let traffic through. Fixed by widening
20+
`bits` to `u4` (0-15 is ample for the real 1-8 range); the shift-amount
21+
computation immediately below already up-cast to `u4` before this fix,
22+
so it was a pure type-sizing bug, not a logic error. Added CIDR
23+
boundary tests (`/0`, `/32`-equivalent, `/128`-equivalent, in/out of
24+
subnet), wildcard-suffix subdomain-vs-lookalike-domain tests, and
25+
any-interface anti-bypass tests (`net_state.zig`).
26+
27+
### Fix — every manually-driven VMContext test carried a stale `tasks_mod.g_state` pointer, invisible outside `-Dgc_stress=true`
28+
29+
`Runtime.activate()` re-pins 8 process-global `setActive` pointers
30+
(chunk/globals/heap/vm/tasks_mod/fs_state/net_state/http_state)
31+
because `Runtime` is returned by value from test helpers and Zig does
32+
not guarantee that copy is elided — every real run/call entry point
33+
calls `activate()` on its own final, stable address for exactly this
34+
reason. `compiler_test.zig`'s ~28 tests that build a `vm.VMContext` by
35+
hand and call `vm.run()`/`vm.callGlobal()` directly (needed to drive
36+
`gbc_reader.read()` before there's a compiled program the normal entry
37+
points could run) instead re-pinned only 3 of those 8 pointers by
38+
hand — chunk/globals/heap — leaving `tasks_mod.g_state` (among others)
39+
pointed at a test-helper's now-defunct stack frame. Undetected because
40+
nothing dereferenced the stale pointer under normal test runs; found
41+
when a coverage-audit test happened to trigger a GC while running
42+
through this exact path (`vmAllocObject` under `-Dgc_stress=true`, a
43+
CI-only lane the local pre-push hook never runs): `vm_gc.zig`'s
44+
`collectGarbage` walked the task table through the stale pointer,
45+
read 0xAA-poisoned freed memory as `temp_root_top`, and panicked on
46+
the resulting garbage index. Fixed by replacing the manual 3-pointer
47+
triplet with `rt3.activate()` everywhere it occurred.
48+
549
## 2026-08-19
650

751
### GBC — in-function predicates with real captures already worked; docs were wrong (#5)

0 commit comments

Comments
 (0)