fuzz: wave 2 — stateful read_opseq + layout_build targets, seeds, one engine fix

Targets 3-4 of docs/plans/fuzzing.md, per the sibling layout:

- fuzz/shared/src/read_opseq.rs — SequentialReader op sequences
  (Next/NextBorrowed/Field/Reset/End, Arbitrary-derived) over hostile
  buffers under the fixed packed schema menu. Invariants: cursor
  discipline (failed read leaves position untouched, state replay
  deterministic), None sticky at plan end, plan-order full walks with
  a spin bound, read_field leaves a usable reader, ADR-007 reader
  independence (shared Arc, isolated cursors), and the plan §6-1
  record-count ≥4-verified-bytes bound encoded as an explicit End-op
  assertion.
- fuzz/shared/src/layout_build.rs — LayoutBuilder::build with
  adversarial var_sizes over a five-schema menu (string/bytes, nested
  struct, byte-disc union, record+array, fixed control). Invariants:
  Offset-class failures only, position disjointness + total-size
  bounds, variable fields record their 4-byte prefix, failed writes
  leave the buffer byte-identical, write→read pair round trip.
- derive_var_sizes discovers the synthetic keys ('p.__discriminator')
  the builder actually wants by parsing the quoted key from the
  Offset reason.
- 73 committed seeds (58 read_opseq + 15 layout_build) hand-encoded
  against the pinned arbitrary 1.4.2 derive layout (4-byte LE
  multiply-shift variant selectors, keep-going vec elements,
  take-rest last field) and pinned by decode_lands_on_the_intended_variants
  replay tests; gen_fuzz_seeds.py mirrors the encoders.
- Engine fix (finding W2-1): plan_read_array returned Ok for a
  fixed-stride array whose count*stride window extended past the
  buffer — the struct/union arms bounds-check, the array arm did not;
  a truncated array deferred the failure to the next field (wrong
  path) or masked it entirely as an Ok walk. Now an Access error
  naming the array, regression test in sequential_reader.rs.
- Packed-mode 'encoding: offset-indirect' pinned as the documented
  inline-length-prefix no-op (finding W2-2, bast-format.md Default
  strategy selection); open design question recorded as plan §6-7.

Verification: fuzz corpus replay 19/19; main crate 570 tests incl.
the new regression; clippy -D warnings clean (crate + shared); wasm
build clean; cargo fuzz build clean (nightly confined to fuzz/).
Smoke campaigns (10 min detached each): read_opseq 52.1k execs exit 0
empty artifacts, layout_build 42.4k execs exit 0 empty artifacts; no
crash/oom/timeout on any fork job.
This commit is contained in:
glm-5.3-flash committed 2026-09-30 06:26:05 +00:00
1 parent 752e36b526
commit 16b9023f60
83 files changed
+1574 -17

No files matched your search

+106 -16
View File
@@ -22,13 +22,42 @@ pairs) — exactly the shapes where example tests miss off-by-one bugs.
The crate is fully synchronous, so targets are simpler than
alkcall/alkhttp's (no current-thread runtime shims anywhere).
**Status:** wave 1 implemented and verified (2026-09-30). The pre-fuzzing
inventory (§6) is verified against the code at 0.3.0. Targets 1–2 and
the full infrastructure are in-tree (commit `ed41d77`); both smoke
campaigns ran clean (10 min detached per target, §5); no findings.
**Status:** waves 1–2 implemented and verified (2026-09-30). The
pre-fuzzing inventory (§6) is verified against the code at 0.3.0.
Targets 1–4 and the full infrastructure are in-tree (commits `ed41d77`,
wave 1; wave 2); wave-1 smoke campaigns ran clean (§5); the wave-2
`read_opseq` campaign surfaced one real packing bug on its first
semantic fixture — the fixed-stride array window bounds check
(§6 candidate 6) — fixed same-day with a regression test.
Progress log:
- **2026-09-30 — wave 2 landed.** Targets 3 (`read_opseq`) and 4
(`layout_build`) with 73 committed seeds (58 + 15), decode-pin tests
for the arbitrary 1.4.2 derive encoding, and the plan §6 candidate-1
spin bound encoded as the explicit End-op assertion. Smoke campaigns:
see §5. **Finding W2-1 (fixed same session):** `plan_read_array`
returned `Ok` for a fixed-stride array whose declared window
(count × stride) extended past the buffer — the bounds check was
missing entirely from the array arm (struct/union arms had theirs).
A truncated array reported success with the failure deferred to the
*next* field read (wrong field path), or masked entirely when the
array was the last field. Found by hand-running the target-3 drive
before the campaign (the first semantic fixture); fixed in
`plan_read_array` with an end-vs-buffer bounds check + Access error
naming the array field, regression test
`array_truncated_below_fixed_stride_window_is_access_error_not_ok`.
**Finding W2-2 (pinned, not a bug):** packed mode compiles
`"encoding": "offset-indirect"` fields but the sequential reader
always reads them inline length-prefixed — this matches
bast-format.md §"Default strategy selection" ("Packed sequential
mode: always inline length-prefixing"), so the annotation is a
no-op in packed mode. Pinned as a corpus-replay invariant
(`packed_mode_is_always_inline_length_prefixed`) so any future
change to the packed reader's encoding awareness is deliberate. The
aligned materializer honors the encoding correctly. An upstream
question — should packed compile either honor the encoding or reject
the declaration — is recorded in §6 candidate 7.
- **2026-09-30 — wave 1 landed (commit `ed41d77`).** The `fuzz/`
workspace, 136 committed seeds (38 `bast_compile` + 98
`data_access`), the detached runner, the corpus-replay gate
@@ -46,6 +75,8 @@ Progress log:
(record-count loops, indirect pairs) held under the parser-level
drives; both remain encoded as wave-2/3 invariants in the stateful
targets.
- **2026-09-30 — wave-2 smoke campaigns (see §5).** Results recorded
there alongside the wave-1 numbers.
---
@@ -139,8 +170,8 @@ checklist, as the siblings did.
|---|---|---|---|---|
| 1 | `bast_compile` | `AlkTypeEngine::compile` both modes (via `bast_meta` → `BastDoc` → plans → layout → validator) | raw bytes → serde_json → BAST doc | implemented 2026-09-30 |
| 2 | `data_access` | the hand-rolled decode core (`src/data_access.rs`, read + write side) | raw `&[u8]` + chosen (offset, endian) | implemented 2026-09-30 |
| 3 | `read_opseq` | stateful `SequentialReader` op sequences over hostile bytes under a fixed plan | `#[derive(Arbitrary)]` op enum | planned (wave 2) |
| 4 | `layout_build` | `LayoutBuilder::build` with adversarial `var_sizes` | `#[derive(Arbitrary)]` map shapes | planned (wave 2) |
| 3 | `read_opseq` | stateful `SequentialReader` op sequences over hostile bytes under a fixed plan | `#[derive(Arbitrary)]` op enum | implemented 2026-09-30 |
| 4 | `layout_build` | `LayoutBuilder::build` with adversarial `var_sizes` | `#[derive(Arbitrary)]` map shapes | implemented 2026-09-30 |
| 5 | `validate_pair` | two-input structured: compile a schema once per exec, hammer hostile bytes through `validate_bytes`/`read_field`/`materialize` | `#[derive(Arbitrary)]` (doc, bytes) pair | planned (wave 3) |
### Target 1 — `bast_compile` (the whole schema side, one choke point)
@@ -313,12 +344,17 @@ grown corpora and artifacts gitignored. Seed menus:
{0,0}, {len, big}, {big, 0}, {u32::MAX, u32::MAX}; offset one-past-
end, offset u32-magnitude; 0x02 bool byte; invalid UTF-8 in a
string; enum value out of range; NaN/Inf bytes.
- `read_opseq` / `layout_build` (wave 2): the semantic fixtures —
full sequential walk, reset-mid-walk then full walk again, failed
read then reset, record loop with a hostile count under a real
buffer, junk field paths; zero/huge/mismatched `var_sizes`;
overwrite-everything write; indirect-pair overflow write, plus the
write-then-read pair fixture.
- `read_opseq` / `layout_build` (wave 2, **done 2026-09-30**): the
semantic fixtures — full sequential walk, reset-mid-walk then full
walk again, failed read then reset, record loop with a hostile count
under a real buffer, junk field paths; zero/huge/mismatched
`var_sizes`; overwrite-everything write; indirect-pair overflow
write, plus the write-then-read pair fixture. `read_opseq` seeds
also encode truncation at every prefix of the full-walk buffer. The
hand-written seeds are byte-encoded against the pinned arbitrary
1.4.2 derive layout and *pinned by decode tests* (both targets carry
a `decode_lands_on_the_intended_variants` replay test, the alkhttp
target-4 pattern).
- `validate_pair` (wave 3): hostile-schema/valid-bytes,
valid-schema/hostile-bytes, valid/valid — plus the §6 candidate
shapes as pinned reproducers.
@@ -350,6 +386,26 @@ repo):
MSRV, and wasm target are untouched — `cargo fuzz build` must never
be a prerequisite for `cargo test`/`clippy`/`build`.
Wave-2 smoke campaign results (2026-09-30, 10 min per target, §5
policy, detached):
- `read_opseq` — 52.1k+ execs at ~90/s, exit 0, empty artifact dir,
no oom/timeout/crash on any fork job. The typed-input decode plus
the per-op assertion work makes this the slowest target per exec in
the crate so far; coverage ~9,365 edges / 15,797 features / 293
in-memory corpus entries. The heavy semantic invariants
(replay-after-failure, full-walk spin bounds, reader independence,
plan-order assertion) run per op, not per exec — the campaign is
assertion-throughput-bound, not coverage-bound; the stateful search
(op × cursor × buffer shape) was still adding features at budget end.
- `layout_build` — 42.4k+ execs at ~84/s, exit 0, empty artifact dir,
no oom/timeout/crash on any fork job; coverage 9,432 edges / 19,204
features / 181 in-memory corpus entries. The adversarial `var_sizes`
map space over five schema menus exercised the missing-size /
unknown-discriminator / overflow rejection paths; the write-side
contracts (failed write leaves buffer byte-identical, positional
disjointness and bounds) held everywhere.
Wave-1 smoke campaign results (2026-09-30, 10 min per target, both
exited 0, artifact dirs empty — no crash/hang/OOM/leak):
@@ -413,13 +469,45 @@ confirms or refutes them.
crate's API; fuzz targets compile once per exec, and the doc
records the compile cost as the consumer's responsibility.
6. **Missing bounds check in `plan_read_array` — CONFIRMED and FIXED
(wave 2, finding W2-1, 2026-09-30).** The struct arm
(`plan_bounds_check`) and the union fixed-size arm both verify the
computed end against the buffer; `plan_read_array`
(`src/sequential_reader.rs:898` pre-fix) computed `end` and
returned `Ok` without the check. A truncated fixed-stride array
reported `Ok(Some(...))` with `element_start + count × stride` past
the buffer; the failure surfaced at the *next* field (naming the
wrong field in the error), or never when the array was the last
field — a completed walk returning `Ok` over a short buffer. Found
by hand-running the `read_opseq` drive before the campaign; fixed
with an end-vs-buffer check returning `Access` naming the array
field; regression test in `src/sequential_reader.rs`. The wave-2
seeds' array-truncation fixtures keep the boundary pinned.
7. **Packed-mode `encoding: offset-indirect` is a silent no-op —
PINNED, open design question (wave 2).** `BastField::parse`
records the annotation; the packed reader
(`plan_read_primitive`) ignores it and reads inline
length-prefixed — correct per bast-format.md's "Default strategy
selection" table, but nothing rejects the declaration in packed
mode (aligned mode consumes it; aligned rejects it on record
fields only). The wave-2 replay test
`packed_mode_is_always_inline_length_prefixed` pins the current
behavior. If the packed reader ever grows encoding awareness, the
pin flips deliberately; if the format instead wants the
declaration rejected in packed mode, that is a schema-gate change
with the mode-agreement invariant to re-verify.
None of these rises to the alkcall §6.2 / alkhttp FWD-20 class; they
are boundary-confirmations, which is exactly the expected profile of
this crate (§1 honest caveat). Smoke-campaign evidence so far: no
panics or OOM/timeouts on any of the 383k+ combined execs (2026-09-30,
§5); candidates 1, 2, and 4 held under the parser-level drives —
candidates 1 and 2 get their explicit stateful assertions in waves
2–3 (targets 3–5), and 4 keeps its boundary corpus entry.
2–3 (targets 3–5), and 4 keeps its boundary corpus entry. Candidate 3
(duplicate first-wins) is pinned by the wave-2 `layout_build` index
assertions and the existing crate tests; candidate 6 was confirmed as
a genuine bug and fixed in wave 2; candidate 7 is pinned with an open
design question.
## 7. Sequencing
@@ -427,9 +515,11 @@ candidates 1 and 2 get their explicit stateful assertions in waves
exclude, toolchain pin, runner, seed generator, README,
`.gitignore`) + targets 1–2 + corpora (136 seeds) + corpus replay +
AGENTS.md gate + smoke campaigns (clean, see §5).
2. **Wave 2** — targets 3–4 (stateful `read_opseq`, `layout_build`) +
seeds + smoke campaigns. Add regression fixtures for anything
wave 1 surfaced first (nothing so far).
2. ✅ **Wave 2 (2026-09-30)** — targets 3–4 (stateful `read_opseq`,
`layout_build`) + 73 seeds + decode-pin tests + smoke campaigns.
One real bug found and fixed first-session (`plan_read_array`
bounds check, §6 candidate 6); packed-mode offset-indirect pinned
as a documented no-op (§6 candidate 7).
3. **Wave 3** — target 5 `validate_pair` (the two-input structured
harness) + long (45 min) release-budget campaigns across all
targets; merge any curated inputs into seeds deliberately.