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:
1 parent
752e36b526
commit
16b9023f60
83 files changed
+1574
-17
No files matched your search
+106
-16
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user