Files
alkstore/tasks/fuzz-target-every-parse.md
T

11 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
fuzz-target-every-parse Fuzz target — `every_parse` (the three `@every` parsers, differential) completed
fuzz-scaffold
fuzz-parser-exposure
moderate medium component implementation
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<i64, String>),
  • Postgres: alkstore-postgres's re-export (Result<i64> → InvalidSpec),
  • Mem: alkstore-mem's re-export (Result<i64> → InvalidSpec).

Input: arbitrary &[u8] → UTF-8-lossy Strings (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

  • fuzz_targets/every_parse.rs + shared/src/every_parse.rs exist; cargo fuzz build builds the target
  • 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)
  • Seed corpus committed, deterministic-generated, gitignore negation in place
  • cargo test --manifest-path fuzz/shared/Cargo.toml green; shared clippy/fmt gates green
  • 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)
  • 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<i64, String> 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.