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.
29 KiB
Plan: alktype fuzzing
Adopted from alkhttp's docs/plans/fuzzing.md (the pattern is operational
in six sibling crates: alkcall, alktty, alktunnels, alksocks, alkhttp —
each with the same fuzz/ layout, the detached runner, and the
corpus-replay-as-plain-test gate). The rationale research lives in
alkcall's docs/research/fuzzing.md (tool landscape, comparable-crate
survey, the no-hosted-CI policy §7.9); this plan stays focused on what
alktype fuzzes and in what order.
Rationale for this crate in one paragraph (the detailed version applies
by reference from the two docs above): alktype is the binary engine the
alk* family consumes — alkcall's hub/spoke accepts BAST schema documents
from arbitrary internet peers, and those documents flow into this
crate's compile paths downstream; both untrusted-input shapes exist here
(attacker-shaped JSON BAST docs → AlkTypeEngine::compile, and
attacker-shaped byte buffers read according to a schema →
validate_bytes / SequentialReader / tunion dispatch /
materialize), and the byte side is hand-rolled decode
(data_access, tunion discriminators, indirect {offset,length}
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: 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_arrayreturnedOkfor 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 inplan_read_arraywith an end-vs-buffer bounds check + Access error naming the array field, regression testarray_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). Thefuzz/workspace, 136 committed seeds (38bast_compile+ 98data_access), the detached runner, the corpus-replay gate (AGENTS.md checklist gains the line), nightly pinned subtree, explicit root[workspace]exclusion, publish-exclude gain,json.dict.cargo fuzz buildclean; corpus replay 4/4 green; main crate untouched (569 tests, clippy-D warningsclean). One dict-format fix on the way: libFuzzer's dictionary parser does not accept\uescapes ("\u0000"→ the\xABform) and needs fully quoted lines — caught by the campaign launcher, not the fuzzer. - 2026-09-30 — smoke campaigns clean (see §5).
data_accesssaturated (pure decode core, the alkcallchunk_headerprofile);bast_compilestill discovering coverage at budget end (longer campaigns keep paying). No crate findings — the §6 candidates (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.
1. Why alktype fuzzes (the if)
- Downstream of the trust boundary. alktype is compiled against in alkcall (the integration crate), whose peers are untrusted and whose wire payloads carry schema-shaped JSON. A panic on a malicious BAST doc or bytes read under one is the quinn-CVE class (RUSTSEC-2026-0037) at one further hop: the alk* stack parses documents it never vetted, and alktype is where they get walked.
- Both input shapes, one crate. Sibling crates each had mostly one
parse shape (wire bytes); alktype has the schema-JSON shape and
the raw-buffer shape, plus two-input combined paths
(
read_field/write_field,materialize_alignedare doc+bytes). - Infrastructure is proven and cheap; the crate is the simplest
consumer yet. Six siblings run the layout; alktype is sync, has
zero
unsafe, zerounwrap/expectoutside tests, and no allocation-from-wire-count anywhere (grep-verified inventory). The marginal cost is target logic only.
Honest caveat (alkcall §1's shape): the code is already well
hardened — checked_add/check_bounds everywhere, parse-time caps
(MAX_ARRAY_ELEMENTS, MAX_ARRAY_BYTES, MAX_ALIGN = 4096,
MAX_LENGTH), MAX_GRAPH_DEPTH/MAX_COMPILE_DEPTH = 128, meta-schema
gate before any walker. Expected yield is low-moderate: the residual
candidates in §6 are the first things to probe; a clean first campaign
is the successful negative result — "we think the engine is robust"
converted into a demonstrated property.
2. Infrastructure (identical to the siblings)
Layout (copy of alkcall/alkhttp):
fuzz/
├── Cargo.toml alktype-fuzz (nightly-only bins; own [workspace])
├── rust-toolchain.toml pins nightly + llvm-tools for this subtree only
├── fuzz_targets/ thin fuzz_target! wrappers (3 lines each)
├── shared/ alktype-fuzz-shared — STABLE-toolchain library:
│ invariant logic + corpus-replay tests
├── corpus/<target>/ committed seeds (generated by gen_fuzz_seeds.py)
├── artifacts/ gitignored crash/oom/timeout artifacts + logs
├── gen_fuzz_seeds.py deterministic seed generator (quiche pattern)
├── json.dict JSON/BAST token dictionary (bast_compile)
├── run-detached.sh detached campaign runner (copied from the siblings)
└── README.md operational cheat-sheet
Load-bearing details (all six siblings hit these; alkcall's doc is the deep reference):
- Invariant logic lives in
fuzz/shared/, not the target binaries. The stable-toolchain shared crate replays every committed seed through the identical invariant functions as plaincargo test— the standing fuzz gate (alkcall §7.9 tier-3 deliverable; no hosted CI in this repo by policy). Thefuzz_target!binaries are thin wrappers. - Root
Cargo.tomlneeds an explicit[workspace]table (members = ["."],exclude = ["fuzz"]); without it auto-discovery pullsfuzz/shared/into the main workspace and the stable toolchain builds nightly-consumed dev-deps. alkcall hit this trap. fuzz/rust-toolchain.tomlpins nightly socargo fuzz buildworks from any CWD; nightly stays confined tofuzz/, MSRV 1.85 untouched here.fuzz/joins the publishexcludelist..gitignoreadditions:fuzz/artifacts/, grown-corpus dirs (committed seeds stay).- Detached runner (non-negotiable operating rule). Campaigns never
run as a foreground child of an agent session; the runner pins
-fork=1 -rss_limit_mb=2048 -malloc_limit_mb=2048 -timeout=25and detaches viasetsid+nohup+ log redirect; the agent polls the log and artifact directory, never waits. Copied from the siblings. - No feature-gating needed in
fuzz/shared: alktype hasdefault = []and no feature flags, so the shared crate rides the main crate build unconditionally (unlike alkhttp's gatedopenapi/mcp). - Exposure needs are minimal. The inventory found every target
entry point already
pub(compile,data_access::*,SequentialReader,LayoutBuilder,tunion::*,materialize::*,validate_bast_doc,build_validator). No#[cfg(fuzzing)]hub is expected — the first choice remains a minimalfuzzinghub only if a needed item turns outpub(crate), per the sibling pattern (alkcall never needed one).
Verification-gate change: cargo test --manifest-path fuzz/shared/Cargo.toml (corpus replay) joins AGENTS.md's verification
checklist, as the siblings did.
3. Target inventory (5, in waves)
| # | Target | Drives | Input style | Status |
|---|---|---|---|---|
| 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 |
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)
AlkTypeEngine::compile (src/engine.rs:127-176) fans out through the
entire untrusted-JSON surface: bast_meta::validate_bast_doc →
BastDoc::new → ValidationPlan::compile → LayoutBuilder::new +
ReadPlan::compile (packed) or OffsetMap::compute (aligned) →
validation::build_validator (when json_schema is Some).
- Drives: raw bytes →
serde_json→compile(value, root_name, mode, None)in both modes; a second lane feedsSome(schema)with a second attacker-shaped JSON value for the jsonschema-build path. - Invariants:
- no-panic on any JSON document, both modes;
- compile is always
Result— every rejection is a cleanAlkTypeError(Schema/Offset/Validationpayload classes,src/error.rs:11-28), never a panic or a silent bogus engine; - meta-schema gate ordering: any doc that fails
validate_bast_docmust surfaceSchema(...)and must never reach layout/plan walks (shape partition); - parse-time caps hold exactly:
align > 4096, arrays aboveMAX_ARRAY_ELEMENTS/MAX_ARRAY_BYTES,maxLength > MAX_LENGTH, depth > 128, and$refcycles all reject at compile with the documented error classes (the walk-guardcheck_ref_graph, compile-depth, and cycle-seenmachinery pinned by adversarial corpus entries); - if compile fails in packed it must also fail in aligned (mode independence of the schema-gate layer — the parse layers are shared; divergence means a mode-specific parse bug);
- a successfully compiled engine's
endian()equals the root struct's declared endianness.
- Seeds: the full BAST feature menu (each kind, endian ×2, TUnion
byte/field/enum discriminators, records, arrays, string/bytes
encodings,
$refdiamond), each reject-class corpus entry, plus the hostile menu in §4.
Target 2 — data_access (the decode core)
Every byte-touching decode funnels through read_array<const N: usize>
(src/data_access.rs:48-75): checked_add(N) → check_bounds →
.get(..) → try_into. The widest attacker-influenced values in the
crate are read_bytes_indirect's absolute {offset,length} pair
(src/data_access.rs:328-350).
- Drives: the
pubread functions directly with the fuzzer choosing buffer, offset (including far-past-end and huge values), and endianness; lanes forread_bytes/read_string(u32 length prefix),read_bytes_indirect/read_string_indirect(the{offset,length}pair),read_enum,read_boolstrictness, and each fixed-width kind from the macro family. - Invariants:
- no-panic for any (buffer, offset, endian) triple;
boolaccepts exactly 0x00/0x01 and rejects everything else (:134-144— the strictness is contract, pin it);- invalid UTF-8 in
read_stringerrors (Access), never a lossy silently-corrupting parse (:195-208); - bounds partition: an error implies
checked_add-overflow orend > buffer_lenwith the offendingfield_pathnamed; an Ok implies the field sits fully inside the buffer; - nothing before/end-of-buffer is read: the decode consumes exactly its declared width (offset unchanged on error paths);
read_bytes_indirect's data region always satisfiesdata_offset + data_length ≤ buffer_lenon Ok, and neither field can push arithmetic past the buffer without an error (the twou32widening casts at:334, :341widening-only, verified by the partition).
Target 3 — read_opseq (stateful, wave 2)
SequentialReader is stateful against attacker bytes (mutable cursor:
field_index, position; src/sequential_reader.rs:144-148) and
fuzzer-reachable operations are read_next, read_next_borrowed,
read_field (out-of-order names), reset (:157-300).
- Drives:
#[derive(Arbitrary)]op sequences (Next, Field(name choice), Reset, End) against a compiled plan — the plan built once per exec from a fixed small schema menu, bytes adversarial. - Invariants:
- no-panic over any op interleaving and any buffer;
- cursor discipline: a failed read leaves the reader usable (a
subsequent
resetrestores the exact initial state; cursor never exceeds the buffer); read_nextreturns fields exactly in plan order andNoneexactly at plan end; interleavedread_fieldfor any field at any cursor state never panics and never mutates the sequential cursor (its offset argument comes from the plan, not the reader);- record-count spin bound: wire-controlled
countloops (src/materialize.rs:439-442,:817-820,src/sequential_reader.rs:985-988) consume ≥ 4 verified bytes per iteration, so iterations are bounded byremaining_bytes / 4— a hostile count fails fast withAccess(encode as an explicit per-exec assertion, not just no-panic/OOM); - engine-issued readers are independent: two readers over the same plan and buffer never observe each other's cursors (ADR-007's owned-fresh-reader contract).
Target 4 — layout_build (wave 2)
LayoutBuilder::new parses once (src/layout_builder.rs:154-156);
build(&HashMap<String, usize>) (:189) is repeatable with
attacker-shaped var_sizes driving write-position arithmetic in
walk_struct.
- Drives: a fixed schema menu containing every variable-width
encoding ×
#[derive(Arbitrary)]var_sizesmaps and write values (FieldValueshapes). - Invariants:
- no-panic across adversarial size maps (zero, huge, mismatched
with
max_length/countdeclarations); - every failed write leaves the buffer untouched (byte-equal to the pre-call snapshot) or documented-partial exactly where the contract allows — pin the actual contract the code implements;
- field positions from a successful
buildare disjoint and in-bounds for the reported total size; data_offset/lengthpairs written bywrite_string_indirect/write_bytes_indirectalways satisfy the read-sideread_*_indirectbounds partition above — the write side and the read side of the pair are one contract (round-trip pair;:373-417guards verified by:733-750-style assertions).
- no-panic across adversarial size maps (zero, huge, mismatched
with
Target 5 — validate_pair (two-input structured, wave 3)
The integration target: schema and bytes are both adversarial.
- Drives:
#[derive(Arbitrary)](doc, bytes) — compile once per exec with whichever mode the fuzzer picks, then drivevalidate_bytes,read_field(arbitrary field paths, including junk paths),materialize_packed/materialize_aligned, andread_nextunder the compiled plan. - Invariants:
- no-panic for any (doc, bytes) pair, either mode;
- validate/read/materialize agreement lattice:
validate_bytesOk ⇒materialize_*Ok and everyread_fieldover a declared path Ok;materialize_*error ⇒validate_byteserror on the same buffer (exact agreement direction pinned per the code's actual contract — determine the strict/loose ordering from thevalidate_bytesimplementation, don't assume); - non-finite floats (NaN/Inf) surfaced by
materializeare alwaysAccesserrors, never silentlyNull/0.0(src/materialize.rs:876-883); - unknown field-path strings always error with
Accessnaming the path, never panic, never index the map by substring drift; Valueoutput is serde-safe:materialize_*output round-trips throughserde_json::to_vecand back to a structurally equalValue(structural only — this crate's serde_json builds withpreserve_order, so object key order is preserved; byte-identity round-trips are acceptable only where the docs say lossless, per the alkcall §7.3 false-positive trap when they don't).
4. Corpus policy
Committed hand-made seeds per target, generated by
fuzz/gen_fuzz_seeds.py (deterministic, in-tree, quiche pattern);
grown corpora and artifacts gitignored. Seed menus:
bast_compile: a minimal valid packed doc and aligned doc; everyAlkTypeKindonce; each reject class (align4097/65536/u32-max,countover cap,maxLengthover cap, depth-129 nesting both inline-nested and via$refchains,$refcycle,$refto missing def, root not a struct, missingtype, unknown kind string, duplicate field names first-wins probe, non-object doc, deeply-nested JSON at serde_json's own 128 limit);json.dictcarries the BAST token set.data_access: minimal valid encodings per kind per endianness; truncation at every prefix length (1..N-1 for each width);len = 0/MAX_LENGTH/u32::MAXprefixes; the indirect pair at {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, 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/mismatchedvar_sizes; overwrite-everything write; indirect-pair overflow write, plus the write-then-read pair fixture.read_opseqseeds 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 adecode_lands_on_the_intended_variantsreplay 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.
Stateful Arbitrary seeds are hand-encoded against the arbitrary
1.4.x derive layout with per-element keep-going bytes, pinned by
seed-decode tests (alkhttp's target-4 pattern).
5. Campaign + gate policy
Identical to the siblings (alkcall §7.9 posture; no hosted CI in this repo):
- Corpus replay is the standing fuzz gate:
cargo test --manifest-path fuzz/shared/Cargo.toml— joins AGENTS.md's verification checklist. - Campaigns run detached via
fuzz/run-detached.sh; budget 10 min per target for a smoke campaign, 30–45 min before a release or after touchingsrc/data_access.rs,src/schema.rs, the compile walks, or the sequential reader. - Grown corpora stay gitignored (hash-named files ignored via
pattern; committed
seed-*files stay); merge worthy entries into seeds only deliberately. - libFuzzer flags worth pinning:
-rss_limit_mb=2048,-malloc_limit_mb=2048,-timeout=25,-max_len=65536,-use_value_profile=1, and-dict=json.dicton the JSON targets (alkcall §7.2's set, minus the CI-tier concerns). - Nightly stays confined to
fuzz/; the main crate's stable build, MSRV, and wasm target are untouched —cargo fuzz buildmust never be a prerequisite forcargo 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 adversarialvar_sizesmap 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):
bast_compile— 543k execs at ~1.1k/s (each exec compiles two engines through the whole fan-out — meta gate, parse, plans, layout walks — in both modes, so per-exec work is heavy), coverage 10,830 edges / 25,326 features / 1,293 in-memory corpus entries, still growing at budget end — longer campaigns keep paying.data_access— 3.5M+ execs at ~7–8k/s, coverage saturated at 379 edges / 574 features / 37 corpus entries (the pure-decode-core ceiling is fully enumerated; the alkcallchunk_headerprofile).- Both exited 0 with empty artifact directories;
oom/timeout/crash: 0/0/0on every fork job. - Seeds regenerate deterministically:
python3 fuzz/gen_fuzz_seeds.py. - Toolchain notes live in
fuzz/README.md; nightly stays confined tofuzz/.
6. Pre-fuzzing candidate findings (confirm or refute)
These are pre-fuzzing code-review findings from the 0.3.0 inventory, verified against the code. They define what the targets must encode as invariants and are the first corpus entries to add; the fuzzer confirms or refutes them.
-
Wire-controlled
Recordcount loops (src/materialize.rs:439- 442,:817-820,src/sequential_reader.rs:985-988): the only buffer-derived loop counts (read_u32(..)? as usizethenfor i in 0..count). Each iteration performs at least one bounds-checked read, so a hostile count should fail fast withAccess— bounded byremaining_bytes / 4, no allocation, no spin. Correct as designed if and only if that holds; theread_opseqtarget encodes it as an explicit assertion (§3 target 3) so the fuzzer can break it the moment any per-entry cost stops being≥ 4 verified bytes. -
Attacker-controlled absolute
{offset,length}pairs (read_bytes_indirect/read_string_indirect,src/data_access.rs:307-350): the widest attacker-influenced values in the crate (eachu32, up to 2³²−1). The pattern (widening cast →checked_addpair-sum → full bounds check) looks correct; campaign confirms the bounds partition on every (buffer, pair) input, both sides of the write/read contract (§3 target 2 / target 4). -
Duplicate field-path tolerance by design (
OffsetMap::build_ index,src/offset_map.rs:199-205: first-wins;BastStruct:: parsedoes not reject duplicates): ambiguous lookups are documented behavior. Encode as an invariant — a successful engine'sread_fieldresolves duplicates deterministically (first wins) — so a future "reject duplicates" change shows up as a deliberate contract change, not silent drift. -
align_up/round_upplain+arithmetic (src/offset_map.rs: 618-639): un-checked+ align - remin a field of schema-controlled values. Overflow-infeasible today because align is capped at parse (MAX_ALIGN= 4096) and the running offset is monotonically checked — abast_compilecorpus entry with align at the cap pinning the boundary keeps it that way if the cap ever moves. -
bast_validation::validate_valuerecompiles aValidationPlanper call (src/bast_validation.rs:64-65): a repeated-op DoS surface if any consumer compiles-per-call. Not a bug in this crate's API; fuzz targets compile once per exec, and the doc records the compile cost as the consumer's responsibility. -
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:898pre-fix) computedendand returnedOkwithout the check. A truncated fixed-stride array reportedOk(Some(...))withelement_start + count × stridepast 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 returningOkover a short buffer. Found by hand-running theread_opseqdrive before the campaign; fixed with an end-vs-buffer check returningAccessnaming the array field; regression test insrc/sequential_reader.rs. The wave-2 seeds' array-truncation fixtures keep the boundary pinned. -
Packed-mode
encoding: offset-indirectis a silent no-op — PINNED, open design question (wave 2).BastField::parserecords 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 testpacked_mode_is_always_inline_length_prefixedpins 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. 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
- ✅ Wave 1 (2026-09-30, commit
ed41d77) — infra (workspaceexclude, toolchain pin, runner, seed generator, README,.gitignore) + targets 1–2 + corpora (136 seeds) + corpus replay + AGENTS.md gate + smoke campaigns (clean, see §5). - ✅ 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_arraybounds check, §6 candidate 6); packed-mode offset-indirect pinned as a documented no-op (§6 candidate 7). - 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. - Everything else inherited verbatim: no hosted CI, OSS-Fuzz out, no Actions/workflow files anywhere in the repo (alkcall §7.9).
8. References
- alkcall
docs/research/fuzzing.md— rationale, tool landscape, campaign containment (§7.6), no-hosted-CI policy (§7.9) - alkhttp
docs/plans/fuzzing.md— the live pattern this plan copies (waves, findings log,fuzzinghub convention) - RUSTSEC-2026-0037 / CVE-2026-31812 (quinn-proto) — the remote-DoS class this crate's peers are exposed to
- Internal: ADR-002 (layout modes), ADR-004 (error/validation
strategy), ADR-006 (aligned-mode variable-field rejection), ADR-008
(aligned-mode TUnion rejection), ADR-010 (
validate_bytes, materialize-then-validate)