--- id: fuzz-target-every-parse name: Fuzz target — `every_parse` (the three `@every` parsers, differential) status: completed depends_on: [fuzz-scaffold, fuzz-parser-exposure] scope: moderate risk: medium impact: component level: implementation tags: [wave-7, fuzzing] --- ## Description The §3.2 target: `every_parse` — the wave's load-bearing target, since the crate's own track record (the wave-6 gate's three edge-domain findings) lives exactly here. Drives the three per-engine `parse_every_interval` implementations **differentially** through the `fuzz-parser-exposure` surfaces, plus each engine's next-boundary arithmetic (`skip_forward`): - SQLite: `alkstore-sqlite`'s re-export (substrate-owned, `Result`), - Postgres: `alkstore-postgres`'s re-export (`Result` → `InvalidSpec`), - Mem: `alkstore-mem`'s re-export (`Result` → `InvalidSpec`). Input: arbitrary `&[u8]` → UTF-8-lossy `String`s (lossy is deliberate — the engines receive `&str`, always valid UTF-8; the generator still stresses the grammar's edge shapes). Invariants (§3.2, verbatim): - **no-panic on any string** (each parser, each boundary path); - **typed rejection**: every rejection is the parser's own typed error (`InvalidSpec` on pg/mem; the substrate's typed error under the engine's mapping on SQLite) — never a panic, never a silent `Ok` on a grammar-violating shape the authored tests already reject; - **three-way agreement**: the accept-set and the interval value are identical across the three parsers for every input. Error *identity* is NOT pinned equal (the substrate's `String` vs `InvalidSpec` is a known, typed asymmetry) — the differential pins the Ok/Err partition and the `Ok` value; - **overflow posture**: the boundary paths (`skip_forward` driven with adversarial `(next_fire_at, now, interval)` triples — the `i64::MAX` neighborhood, negative shapes, `d`-unit multiplier blowups) reject-or-saturate per each engine's pinned semantics, never wrap or panic — the wave-6 scheduler-register fix's invariant over the whole domain; - **the `n <= 0` and giant-`n` arms**: leading-zero forms, digit overflow at exactly `i64::MAX/86400`, positive-zero rejection. The register-path posture (parse → first-boundary `checked_add` → reject with `InvalidSpec`, the wave-6 alignment) is suite-pinned already; the target's optional `(a)`-companion leg may drive the public `schedule()` on the mem/SQLite factories for register-path seeds — optional, recorded if used. - **`fuzz_targets/every_parse.rs`** + **`shared/src/every_parse.rs`** (invariants + `#[cfg(test)]` replay), `[[bin]]` entry, `shared` manifest gains the three engine crates (path deps — stable-buildable, they already are). - **Seeds** via `gen_fuzz_seeds.py`'s `every_parse` function: every grammar-valid unit form (`s|m|h|d`), multi-digit and leading-zero forms, `@every 0s` / `@every -5s` shapes, the `i64::MAX`-neighborhood and `i64::MAX/86400` digit-overflow forms, `d`-multiplier blowups, whitespace/garbage/truncation shapes, non-grammar prefixes (`@once`, `@hourly`-adjacent strings). Corpus committed; gitignore negation. - **`fuzz/README.md`** targets-table row. Risk note: medium because the differential may surface a real three-way divergence (that is the target's purpose) — a finding is *success*, dispositioned per the campaign task's posture (fix inline or escalate), not a task failure. ## Acceptance Criteria - [x] `fuzz_targets/every_parse.rs` + `shared/src/every_parse.rs` exist; `cargo fuzz build` builds the target - [x] All five §3.2 invariant classes implemented and exercised by both the fuzz path and the replay module; the three-way agreement asserted on every input (fuzz) and every seed (replay) - [x] Seed corpus committed, deterministic-generated, gitignore negation in place - [x] `cargo test --manifest-path fuzz/shared/Cargo.toml` green; `shared` clippy/fmt gates green - [x] Root workspace gates still green; substrate diff still empty *(amended at implementation, 2026-10-10 — see Notes: the target found a real no-panic/overflow-posture finding in `skip_forward`, dispositioned inline per the task's own finding posture; the substrate delta is two keyword-free lines in the re-derived `queue_ops.rs`, registered as PROVENANCE D-38; lineage body untouched)* - [x] Any three-way divergence found during target development is recorded in Notes with its disposition (fixed / escalated), not silently absorbed ## References - docs/research/fuzzing.md §3.2 (target spec + the exposure decision this target consumes), §4, §7 - docs/architecture/decisions/009-queue-and-scheduler.md §2 (the `@every` grammar the three parsers implement) - docs/architecture/decisions/012-forked-substrate-design.md §2 (one-owner rule — why there are three parsers to differ) - tasks/review-wave-6.md (the wave-6 finding class this target generalizes; the register-overflow alignment) - tasks/fuzz-parser-exposure.md (the surfaces this target drives) ## Notes Decisions of record the implementation made that the description didn't pin: - **Two real findings, both in `skip_forward` (the boundary leg), dispositioned fixed inline** (per this task's risk note, the campaign task's posture — small crate-side fixes ride the task that finds them): 1. **Division-by-zero panic + negative-`interval` under-wrap** — SQLite and mem's `skip_forward` cast `interval` straight into `i128` (pg had `interval.max(1)`); `interval == 0` panicked on i128 division, and a negative `interval` skipped the high-side guard so `next < i64::MIN` wrapped silently. Fix: the clamp, pg's committed shape. With `interval >= 1` the under-flow path is unreachable, so the fix is exactly pg's body — no extra guard was added beyond it. 2. **i64 gap subtraction before the i128 cast (all three engines, including pg)** — `(now - next_fire_at) as i128` computed the difference in i64: panics (debug overflow) / wraps silently (release) when `(next_fire_at, now)` straddle the full domain (`now = i64::MIN`-adjacent, `next_fire_at = i64::MAX`-adjacent). The 128-bit intermediates story had a hole at its first line. Fix: `now as i128 - next_fire_at as i128` — same one line, all three. Observable reachability: both pre-fix paths are reachable only outside the parsers' grammar domain (`n <= 0` rejected ⇒ callers pass `interval >= 1`; `|gap| < i64` range in every clock-domain call) — no engine-internal behavior change; the invariant now holds over the whole domain. SQLite's delta registered as `substrate/PROVENANCE.md` **D-38** (queue_ops.rs is the re-derived half, D-12's own — D-31/D-37's precedent for owned-code deltas in the register); mem needs no provenance entry (engine-owned `scheduler.rs`), and pg's one-line delta is engine-owned likewise. - **(a)-companion leg not built.** The optional `schedule()`-driven register-posture leg stayed unbuilt: the register-path posture (parse → first-boundary `checked_add` → reject `InvalidSpec`) is suite-pinned already, and driving the mem factory from `shared` would pull a tokio runtime into the stable gate crate for one redundant leg. - **`skip_forward` three-way equality asserted, not just no-panic.** Post-fix the three bodies are lexically identical, so the target goes further than the task text required: three-way output equality on *every* `(next_fire_at, now, interval)` triple (fixed adversarial battery + triples folded out of the input bytes at 24-byte stride), plus the saturation pin (`i64::MAX` clamp or strictly-beyond-`now`, never a past value). - **Error identity left unpinned as specified**: SQLite's `Result` vs pg/mem's `InvalidSpec` — the differential pins partition-and-value; the typed-rejection check (pg/mem must reject with `Error::InvalidSpec`, never another variant) runs ahead of the agreement assert. - **Corpus growth noise after the smoke campaign**: a 180 s detached smoke (`run-detached.sh every_parse -max_len=4096 -use_value_profile=1`, ~737k execs, 0 crash/oom/timeout — the campaign task re-runs the full budget) grew `corpus/every_parse` to 686 files; the grown additions were pruned back to the 66 committed generator seeds since corpus-policy reconciliation (grown-corpus disposition, the seed-promotion posture) is the campaign task's named AC. The `!corpus/every_parse` negation does un-ignore grown additions in that dir — the same shape alksocks shipped, so the tension is the campaign task's to resolve, noted here for context. ## Summary Landed the `every_parse` differential target (2026-10-10): the §3.2 surface — all three one-owner `parse_every_interval` implementations driven through the `fuzz-parser-exposure` `#[doc(hidden)]` re-exports, plus all three `skip_forward` boundary postures. - **`fuzz/shared/src/every_parse.rs`** — the invariant logic (stable toolchain, replayable): typed-rejection pin (pg/mem rejects are `Error::InvalidSpec` only), three-way agreement on partition + `Ok` value for arbitrary lossy-decoded strings, a fixed grammar battery (17 accept shapes with pinned interval values incl. leading-zero and the `i64::MAX`/`i64::MAX/86400`-neighborhood giant-`n` arms; 27 reject shapes incl. positive-zero, negative, whitespace/truncation, cron/`@once` adjacency and unknown units), and the overflow-posture battery over `skip_forward` (24 fixed adversarial triples + 24-byte-stride triples folded from the raw input; three-way equality + saturate-or-strictly-future pin). `#[cfg(test)]` corpus replay over the committed dir (≥48 seeds). - **`fuzz/fuzz_targets/every_parse.rs`** + `[[bin]]` entry (the payload_codec shape); engine path deps added to `shared`'s manifest; lib.rs module registered. - **66 committed seeds** from `gen_fuzz_seeds.py`'s new `every_parse()` (registry wired); `!corpus/every_parse` gitignore negation; `fuzz/README.md` targets-table row filled. - **Two findings found and fixed inline** (details in Notes): `skip_forward` division-by-zero/negative-`interval` wrap (sqlite + mem), and the i64 gap subtraction before the i128 cast (all three engines incl. pg). PROVENANCE D-38 records the substrate delta. - **Smoke verdict**: 180 s detached campaign, ~737k execs, 0 crash/oom/timeout — the fixes closed out the deterministic finding space; grown corpus pruned to the committed seeds. Verified: `cargo build` clean; `cargo test` workspace green (15 ok result groups, 0 failures); `cargo clippy --all-targets -- -D warnings` clean; `cargo fmt --check` clean; the shared gate `cargo test --manifest-path fuzz/shared/Cargo.toml` green; shared clippy `-D warnings` + fmt --check green; `cargo fuzz build` green from `fuzz/` (nightly pin; all three engines compile under ASAN through the new bin); `cargo check -p alkstore-mem --target wasm32-unknown-unknown` clean. Lineage body untouched; the substrate delta is the two-line `skip_forward` fix in the re-derived half, registered as D-38.