fuzz-target-every-parse: the §3.2 differential target — three parsers + skip_forward through the doc-hidden surfaces via shared invariants, 66 committed seeds + gitignore negation + README row; two real skip_forward findings fixed inline (division-by-zero/negative-interval wrap on sqlite+mem, i64 gap subtraction overflow on all three engines incl. pg) and registered as PROVENANCE D-38

This commit is contained in:
glm-5.3-flash committed 2026-10-10 22:01:13 +00:00
1 parent 1f6cc0970a
commit f7c1bd79b0
80 files changed
+1559 -18

No files matched your search

+2 -2
View File
@@ -252,8 +252,8 @@ fn fire_due(sched: &mut ScheduleState, queues: &mut QueueState, state: &State, n
/// in the future, grid-aligned (the schedule row's advance makes the
/// gap inspectable).
pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 {
let gap = (now - next_fire_at) as i128;
let interval = interval as i128;
let gap = now as i128 - next_fire_at as i128;
let interval = interval.max(1) as i128;
let next = (next_fire_at as i128) + (gap / interval + 1) * interval;
if next > i64::MAX as i128 {
i64::MAX
+1 -1
View File
@@ -262,7 +262,7 @@ fn next_every_boundary(spec: &str, after: i64) -> Result<i64> {
/// (the skipped boundaries never enqueue). 128-bit intermediates keep
/// the arithmetic saturation-safe; the result clamps at `i64::MAX`.
pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 {
let gap = (now - next_fire_at) as i128;
let gap = now as i128 - next_fire_at as i128;
let interval = interval.max(1) as i128;
let next = (next_fire_at as i128) + (gap / interval + 1) * interval;
if next > i64::MAX as i128 {
@@ -130,6 +130,7 @@ Cherry-picks: empty at scaffold, append-only forever.
| D-35 | drop | `Writer::try_acquire` cut (the non-blocking writer-slot try): upstream's embedding harness used it for try-shaped entry points its surface exposed; this engine's seam always blocks (`acquire`) — the writer-slot lease is a *parks-the-writer* lease by design (ADR-007's honest model), so a try-shaped acquire would contradict the posture rather than extend it. The probe tests driving it re-express their properties through `acquire`/`release`/`close` (the engine-reachable surface); the engine-level lease tests (`tx_tests`' lease/parking suite) pin the observable posture end-to-end | `schema.rs` (`Writer::try_acquire` + the three `writer_reader_tests` call sites) | ADR-007 (the caller-held-lease seam: `begin_tx` parks a concurrent writer — no try-shaped begin exists on the contract surface); ADR-012 §2 (nothing on the engine side reaches a try-acquire); wave-3 lint-removal obligation — cut, not suppressed | ours |
| D-36 | drop | `UpdateWatcher::spawn` and `SharedUpdateWatcher::new` cut (the default-cadence convenience constructors): upstream's embedding harnesses opened watchers without explicit config; this engine's single open path always constructs a `WatcherConfig` — from `SqliteOpts` (ADR-023 §4's `poll_interval` knob, `None` = the 1 ms default) — and passes it through `*_with_config`; a config-less twin would re-create two open postures (the mixed-flag inconsistency D-29 resolved at the URI layer). `WatcherConfig::default()` remains the default's single owner; the tests driving the cut constructors now build the config explicitly, pinning the same behavior | `watcher.rs` (both constructors + five watcher-test call sites) | ADR-023 §4 (the cadence knob rides the engine's opts — one open posture carries config); ADR-012 §2 (one construction path the engine reviews); wave-3 lint-removal obligation — cut, not suppressed | ours |
| D-37 | re-derivation | visibility-only widening on the re-derived `@every` surface: `parse_every_interval` (`pub(crate)` → `pub`) and `skip_forward` (module-private → `pub`) in `queue_ops.rs`, plus this subtree's facade re-export (`mod.rs`: `pub use queue_ops::{parse_every_interval, skip_forward}`), so the engine crate root can `#[doc(hidden)] pub use` both as the wave-7 fuzzing differential target's drive surface (`fuzz-parser-exposure`, target `every_parse`). Zero logic change, zero signature change, zero name change; `mod substrate` stays private, so containment holds — nothing leaves the crate except the doc-hidden root re-exports. A `pub use` re-export mechanically requires item-level `pub` (E0364/E0603), which is why the facade line is in `mod.rs`; the module doc's "nothing re-exported" boundary sentence was amended in the same commit to name this sole exception | `queue_ops.rs` (two visibility keywords); `mod.rs` (facade re-export line + boundary-doc exception sentence) | ADR-012 §3 (fidelity governs the ported honker body — `watcher.rs`/`schema.rs`/`ops.rs` untouched; `queue_ops.rs` is the re-derived half, owned contract-v1 code, so a visibility-only delta is legitimate); docs/research/fuzzing.md §3.2 (option (b): the `#[doc(hidden)]` narrow exposure); ADR-017 §3 (doc-hidden test surface, no stability guarantee — the `Job::from_row` precedent) | ours |
| D-38 | port delta | degenerate/adversarial-triple hardening on `skip_forward` (two legs, both aligning to the pg engine's committed clamp shape where it existed and extending pg itself): (1) the `interval` is clamped (`interval.max(1) as i128`) — pre-fix, `interval == 0` panicked on i128 division-by-zero, reachable only through degenerate/non-grammar callers (the parsers reject `n <= 0`, so every engine-internal call site passes `interval >= 1`); (2) the gap is computed in i128 (`now as i128 - next_fire_at as i128`) — pre-fix, `now - next_fire_at` was an **i64 subtraction** that panics in debug (overflow) / silently wraps in release when `(next_fire_at, now)` straddle the full domain (`now=i64::MIN`-adjacent, `next_fire_at=i64::MAX`-adjacent), feeding a wrapped gap to the 128-bit arithmetic and breaking the saturation guarantee. Found by the `every_parse` differential target (wave 7, tasks/fuzz-target-every-parse) driving the adversarial `(next_fire_at, now, interval)` space; pg needed leg (2) as well. Observable behavior unaffected in the reachable domain (|gap| < i64 range), saturation posture now holds over the whole domain. Zero signature change; lineage body untouched (`skip_forward` is the re-derived scheduler half, D-12's own) | `queue_ops.rs` (`skip_forward`, two lines) | docs/research/fuzzing.md §3.2 (the overflow-posture invariant: reject-or-saturate, never wrap or panic, over the whole domain); wave-6's register-overflow alignment (tasks/review-wave-6); alkstore-postgres/src/scheduler.rs `skip_forward` (the pinned clamp shape) | ours |
### Cherry-picks
+2 -2
View File
@@ -525,8 +525,8 @@ fn next_every_boundary(spec: &str, after: i64) -> Result<i64, String> {
}
pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 {
let gap = (now - next_fire_at) as i128;
let interval = interval as i128;
let gap = now as i128 - next_fire_at as i128;
let interval = interval.max(1) as i128;
let next = (next_fire_at as i128) + (gap / interval + 1) * interval;
if next > i64::MAX as i128 {
i64::MAX
+1
View File
@@ -1,5 +1,6 @@
target
corpus/*
!corpus/payload_codec
!corpus/every_parse
artifacts
coverage
+955 -3
View File
File diff suppressed because it is too large. Load diff
+7
View File
@@ -21,4 +21,11 @@ test = false
doc = false
bench = false
[[bin]]
name = "every_parse"
path = "fuzz_targets/every_parse.rs"
test = false
doc = false
bench = false
[workspace]
+1 -1
View File
@@ -28,7 +28,7 @@ libFuzzer targets for alkstore's parse/codec surfaces (see
| Target | Drives | Invariants |
|---|---|---|
| `payload_codec` | `encode_payload` / `payload_as::<T>` (`alkstore/src/payload.rs`) | no-panic on any byte sequence; every decode failure typed `Error::Codec`; deep nesting loud at serde_json's recursion limit; `Value` round-trip (serde_json canonicalization pinned); typed round-trip for a representative payload set |
| `every_parse` | the three `parse_every_interval` implementations (differential) | filled by its target task |
| `every_parse` | the three `parse_every_interval` implementations (differential, through the engines' `#[doc(hidden)]` re-exports) + each engine's `skip_forward` boundary arithmetic | no-panic on any UTF-8-lossy string (each parser, each boundary path); typed rejection (`Error::InvalidSpec` on pg/mem, the substrate's error under its mapping on SQLite); three-way agreement on the Ok/Err partition and the `Ok` interval value (error identity not pinned — the SQLite `Result<i64, String>` vs `InvalidSpec` asymmetry is known); overflow posture: `skip_forward` over adversarial `(next_fire_at, now, interval)` triples rejects-or-saturates, never wraps or panics; the `n <= 0` and giant-`n` arms (leading zeros, digit overflow at exactly `i64::MAX/86400`, positive-zero rejection) |
| `name_validation` | `validate_name` / `validate_shared_name` / `validate_local_name` (`alkstore/src/validation.rs`) | filled by its target task |
## Commands
+1
View File
@@ -0,0 +1 @@
@every 1s
+1
View File
@@ -0,0 +1 @@
@every 5s
+1
View File
@@ -0,0 +1 @@
@every 9s
+1
View File
@@ -0,0 +1 @@
@every 17s
+1
View File
@@ -0,0 +1 @@
@every 42m
+1
View File
@@ -0,0 +1 @@
@every 1m
+1
View File
@@ -0,0 +1 @@
@every 90m
+1
View File
@@ -0,0 +1 @@
@every 2h
+1
View File
@@ -0,0 +1 @@
@every 24h
+1
View File
@@ -0,0 +1 @@
@every 1d
+1
View File
@@ -0,0 +1 @@
@every 7d
+1
View File
@@ -0,0 +1 @@
@every 365d
+1
View File
@@ -0,0 +1 @@
@every 007s
+1
View File
@@ -0,0 +1 @@
@every 007m
+1
View File
@@ -0,0 +1 @@
@every 000000042h
+1
View File
@@ -0,0 +1 @@
@every 007d
+1
View File
@@ -0,0 +1 @@
@every 0s
+1
View File
@@ -0,0 +1 @@
@every 00m
+1
View File
@@ -0,0 +1 @@
@every 0000d
+1
View File
@@ -0,0 +1 @@
@every -5s
+1
View File
@@ -0,0 +1 @@
@every -0s
+1
View File
@@ -0,0 +1 @@
@every 9223372036854775807s
+1
View File
@@ -0,0 +1 @@
@every 9223372036854775806s
+1
View File
@@ -0,0 +1 @@
@every 9223372036854775808s
+1
View File
@@ -0,0 +1 @@
@every 9223372036854775807m
+1
View File
@@ -0,0 +1 @@
@every 9223372036854775807h
+1
View File
@@ -0,0 +1 @@
@every 9223372036854775807d
+1
View File
@@ -0,0 +1 @@
@every 18446744073709551616s
+1
View File
@@ -0,0 +1 @@
@every 18446744073709551616000000m
+1
View File
@@ -0,0 +1 @@
@every 106751991167300d
+1
View File
@@ -0,0 +1 @@
@every 106751991167301d
+1
View File
@@ -0,0 +1 @@
@every 106751991167299d
+1
View File
@@ -0,0 +1 @@
@every 2147483647d
+1
View File
@@ -0,0 +1 @@
@every 4294967295d
+1
View File
@@ -0,0 +1 @@
@every 100000000000d
+1
View File
@@ -0,0 +1 @@
@every
+1
View File
@@ -0,0 +1 @@
@every
+1
View File
@@ -0,0 +1 @@
@every 5s
+1
View File
@@ -0,0 +1 @@
@every 5s
+1
View File
@@ -0,0 +1 @@
@every 5s
+1
View File
@@ -0,0 +1 @@
@every 5 s
+1
View File
@@ -0,0 +1 @@
@every 5sx
+1
View File
@@ -0,0 +1 @@
@every 5S
+1
View File
@@ -0,0 +1 @@
@every +5s
+1
View File
@@ -0,0 +1 @@
@every .5s
+1
View File
@@ -0,0 +1 @@
@every 3.5s
+1
View File
@@ -0,0 +1 @@
@every 5.0s
+1
View File
@@ -0,0 +1 @@
@every 5 s
+1
View File
@@ -0,0 +1 @@
@every és
+1
View File
@@ -0,0 +1 @@
ÿ @every ÿ 5s þ
+1
View File
@@ -0,0 +1 @@
@every 5sÿ
+1
View File
@@ -0,0 +1 @@
@once
+1
View File
@@ -0,0 +1 @@
@hourly
+1
View File
@@ -0,0 +1 @@
@daily
+1
View File
@@ -0,0 +1 @@
@weekly
+1
View File
@@ -0,0 +1 @@
*/5 * * * *
+1
View File
@@ -0,0 +1 @@
0 0 * * *
+1
View File
@@ -0,0 +1 @@
every 5s
+1
View File
@@ -0,0 +1 @@
5s
+1
View File
@@ -0,0 +1 @@
@every 5
+1
View File
@@ -0,0 +1 @@
@every s
+1
View File
@@ -0,0 +1 @@
@every 9223372036854775807s skip-forward-triple payload XXXXX
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
������������������������
Binary file not shown.
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
//! The §3.2 target: the three per-engine `@every` parsers driven
//! differentially through the `fuzz-parser-exposure` doc-hidden
//! surfaces, plus each engine's `skip_forward` boundary arithmetic.
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
alkstore_fuzz_shared::every_parse::fuzz_every_parse(data);
});
+122
View File
@@ -124,8 +124,130 @@ def payload_codec_seeds():
i += 1
def every_parse_seeds():
i = 0
# Every grammar-valid unit form (s|m|h|d), single and multi-digit.
for spec in [
"@every 1s",
"@every 5s",
"@every 9s",
"@every 17s",
"@every 42m",
"@every 1m",
"@every 90m",
"@every 2h",
"@every 24h",
"@every 1d",
"@every 7d",
"@every 365d",
]:
write_seed("every_parse", i, spec.encode())
i += 1
# Leading-zero forms (parsed through, value = the digits).
for spec in ["@every 007s", "@every 007m", "@every 000000042h", "@every 007d"]:
write_seed("every_parse", i, spec.encode())
i += 1
# The n <= 0 arms: positive zero (with and without leading zeros)
# and the negative shapes (digits == 0 inside the grammar).
for spec in ["@every 0s", "@every 00m", "@every 0000d", "@every -5s", "@every -0s"]:
write_seed("every_parse", i, spec.encode())
i += 1
# The i64::MAX neighborhood: giant-n accept at scale-1, digit
# overflow past i64, and unit-multiplier overflow above it.
for spec in [
"@every 9223372036854775807s",
"@every 9223372036854775806s",
"@every 9223372036854775808s",
"@every 9223372036854775807m",
"@every 9223372036854775807h",
"@every 9223372036854775807d",
"@every 18446744073709551616s",
"@every 18446744073709551616000000m",
]:
write_seed("every_parse", i, spec.encode())
i += 1
# The digit-overflow boundary at exactly i64::MAX/86400 (and ±1).
for spec in [
"@every 106751991167300d",
"@every 106751991167301d",
"@every 106751991167299d",
]:
write_seed("every_parse", i, spec.encode())
i += 1
# d-unit multiplier blowups.
for spec in ["@every 2147483647d", "@every 4294967295d", "@every 100000000000d"]:
write_seed("every_parse", i, spec.encode())
i += 1
# Whitespace / garbage / truncation shapes.
for raw in [
b"@every",
b"@every ",
b"@every 5s",
b"@every 5s ",
b"@every\t5s",
b"@every 5 s",
b"@every 5sx",
b"@every 5S",
b"@every +5s",
b"@every .5s",
b"@every 3.5s",
b"@every 5.0s",
b"@every 5\ts",
b"@every \xc3\xa9s",
b"\xff @every \xff 5s \xfe",
b"@every 5s\xff",
]:
write_seed("every_parse", i, raw)
i += 1
# Non-grammar prefixes and shapes (cron / @once adjacency).
for raw in [
b"@once",
b"@hourly",
b"@daily",
b"@weekly",
b"*/5 * * * *",
b"0 0 * * *",
b"every 5s",
b"5s",
b"@every 5",
b"@every s",
]:
write_seed("every_parse", i, raw)
i += 1
# Byte payloads ≥ 24 bytes: the boundary-arithmetic leg folds
# adversarial (next_fire_at, now, interval) triples straight out of
# the bytes — the i64::MAX neighborhood, negative shapes, d-blowups.
for raw in [
b"@every 9223372036854775807s skip-forward-triple payload XXXXX",
b"\x00" * 24,
b"\xff" * 24,
bytes([
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, # next = i64::MAX
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, # now = 2^62
0x80, 0xC8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, # interval = 250000
]),
bytes([
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, # next = i64::MIN + 1
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, # now = i64::MAX
0x80, 0x92, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, # interval = negative
]),
]:
write_seed("every_parse", i, raw)
i += 1
def main():
payload_codec_seeds()
every_parse_seeds()
if __name__ == "__main__":
+3
View File
@@ -6,6 +6,9 @@ edition = "2021"
[dependencies]
alkstore = { path = "../../alkstore" }
alkstore-sqlite = { path = "../../alkstore-sqlite" }
alkstore-postgres = { path = "../../alkstore-postgres" }
alkstore-mem = { path = "../../alkstore-mem" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+276
View File
@@ -0,0 +1,276 @@
//! Invariant logic for the `every_parse` target (docs/research/
//! fuzzing.md §3.2): the three one-owner `@every` parsers (ADR-009 §2's
//! grammar, ADR-012 §2's one-owner rule) driven **differentially**
//! through the engines' `#[doc(hidden)]` re-exports — no-panic on any
//! string, typed rejection, three-way agreement (the Ok/Err partition
//! and the `Ok` value; error *identity* is deliberately not pinned —
//! the SQLite surface's `Result<i64, String>` vs pg/mem's
//! `Result<i64>`/`InvalidSpec` is a known, typed asymmetry), the
//! overflow posture, and the `n <= 0` / giant-`n` arms.
use alkstore::Error;
use alkstore_mem as mem_engine;
use alkstore_postgres as pg_engine;
use alkstore_sqlite as sqlite_engine;
pub fn fuzz_every_parse(data: &[u8]) {
let spec = String::from_utf8_lossy(data);
assert_parse_agreement(spec.as_ref());
assert_parse_battery();
assert_skip_battery();
assert_skip_triples(data);
}
/// pg and mem reject grammar-violating shapes with their own typed
/// error, `Error::InvalidSpec` — never a panic, never a silent `Ok`
/// on a shape the authored tests reject.
fn assert_typed_rejection(spec: &str, engine: &str) {
let result = match engine {
"pg" => pg_engine::parse_every_interval(spec),
_ => mem_engine::parse_every_interval(spec),
};
match result {
Ok(_) | Err(Error::InvalidSpec { .. }) => {}
Err(other) => panic!("{engine} rejects must be `InvalidSpec`, got {other:?} for {spec:?}"),
}
}
/// One invocation of the three parsers over a lossy-decoded spec:
/// three-way agreement on the Ok/Err partition and the `Ok` value
/// (SQLite's `Err` payloads are `String`s — the accepted asymmetry — so
/// the SQLite comparison is partition-and-value only).
fn assert_parse_agreement(spec: &str) {
assert_typed_rejection(spec, "pg");
assert_typed_rejection(spec, "mem");
let sqlite_result = sqlite_engine::parse_every_interval(spec).map_err(|_| ());
let pg_result = pg_engine::parse_every_interval(spec).map_err(|_| ());
let mem_result = mem_engine::parse_every_interval(spec).map_err(|_| ());
assert_eq!(
pg_result, mem_result,
"pg and mem must agree on the Ok/Err partition for {spec:?}"
);
match pg_result {
Ok(interval) => {
assert_eq!(
sqlite_result,
Ok(interval),
"sqlite must accept {spec:?} with pg/mem's interval"
);
}
Err(()) => {
assert_eq!(
sqlite_result,
Err(()),
"sqlite must reject what pg/mem reject for {spec:?}"
);
}
}
}
type Engine = (&'static str, fn(&str) -> Result<i64, ()>);
fn engines() -> Vec<Engine> {
vec![
("sqlite", |s: &str| {
sqlite_engine::parse_every_interval(s).map_err(|_| ())
}),
("pg", |s: &str| {
pg_engine::parse_every_interval(s).map_err(|_| ())
}),
("mem", |s: &str| {
mem_engine::parse_every_interval(s).map_err(|_| ())
}),
]
}
/// The grammar battery: shapes the authored tests already reject/accept
/// — pinned on every invocation so neither the fuzz path nor a corpus
/// replay ever observes drift (a silent `Ok` on a known-bad shape, a
/// changed interval value, a lost reject). Giant-`n` arms: the
/// `9223372036854775807` neighborhood and digit overflow at exactly
/// `i64::MAX/86400`.
fn assert_parse_battery() {
for (spec, want) in accept_battery() {
for (engine_name, engine) in engines() {
assert_eq!(
engine(spec),
Ok(want),
"{engine_name} must accept the grammar-valid {spec:?}"
);
}
}
for spec in reject_battery() {
for (engine_name, engine) in engines() {
assert!(
engine(spec).is_err(),
"{engine_name} must reject the grammar-violating {spec:?}"
);
}
}
}
fn accept_battery() -> Vec<(&'static str, i64)> {
vec![
("@every 1s", 1),
("@every 5s", 5),
("@every 9s", 9),
("@every 17s", 17),
("@every 42m", 2520),
("@every 1m", 60),
("@every 90m", 5400),
("@every 2h", 7200),
("@every 24h", 86_400),
("@every 1d", 86_400),
("@every 7d", 604_800),
("@every 007s", 7),
("@every 007m", 420),
("@every 000000042h", 151_200),
("@every 106751991167300d", 9_223_372_036_854_720_000),
("@every 9223372036854775807s", i64::MAX),
("@every 9223372036854775806s", i64::MAX - 1),
]
}
fn reject_battery() -> Vec<&'static str> {
vec![
"",
"@every",
"@every ",
"@every 0s",
"@every -5s",
"@every +5s",
"@every 5",
"@every s",
"@every 5sx",
"@every 5S",
"@every 5s",
"@every 5s ",
"@every 3.5s",
"@every 9223372036854775808s",
"@every 9223372036854775807m",
"@every 106751991167301d",
"@every 18446744073709551616s",
"@every 00m",
"every 5s",
"5s",
"*/5 * * * *",
"0 0 * * *",
"@once",
"@hourly",
"@daily",
"@weekly",
"@minute",
]
}
/// The overflow-posture battery for the next-boundary arithmetic:
/// adversarial `(next_fire_at, now, interval)` triples (the `i64::MAX`
/// neighborhood, negative shapes, `d`-unit multiplier blowups) —
/// reject-or-saturate semantics, never wrapping or panicking. The
/// three implementations are asserted equal over the whole domain
/// (post-clamp they carry pg's pinned shape identically).
fn assert_skip_battery() {
for &(next_fire_at, now, interval) in TRIPLES {
assert_skip_posture(next_fire_at, now, interval);
}
}
const TRIPLES: &[(i64, i64, i64)] = &[
(0, 0, 0),
(0, 0, 1),
(0, 1, 1),
(5, 4, 60),
(5, 3, 60),
(i64::MAX, i64::MAX, 1),
(i64::MAX - 5, i64::MAX, 1),
(i64::MAX - 100, i64::MAX, 86_400),
(i64::MAX - 100, i64::MAX, 86_400 * 30),
(i64::MAX / 2, i64::MAX, i64::MAX / 2),
(i64::MAX / 2, i64::MAX, i64::MAX),
(i64::MAX, i64::MAX, i64::MAX),
(i64::MIN, i64::MIN, 1),
(i64::MIN, i64::MIN, 86_400),
(i64::MIN, i64::MAX, 1),
(i64::MIN, i64::MAX, 86_400 * 30),
(i64::MIN, 0, 86_400),
(0, i64::MAX, 86_400 * 365),
(5, 4, 0),
(5, 4, -60),
(i64::MAX, i64::MIN, -1),
(i64::MIN, i64::MAX, i64::MIN),
(0, i64::MAX, i64::MIN),
(86_400, 86_399, 86_400 * u32::MAX as i64),
];
fn assert_skip_posture(next_fire_at: i64, now: i64, interval: i64) {
let a = sqlite_engine::skip_forward(next_fire_at, now, interval);
let b = pg_engine::skip_forward(next_fire_at, now, interval);
let c = mem_engine::skip_forward(next_fire_at, now, interval);
assert_eq!(
a, b,
"sqlite and pg skip-forward must agree for ({next_fire_at}, {now}, {interval})"
);
assert_eq!(
b, c,
"pg and mem skip-forward must agree for ({next_fire_at}, {now}, {interval})"
);
// Saturation: the result is either the `i64::MAX` clamp or a value
// strictly beyond `now` — no wrap into the past, ever.
assert!(
a == i64::MAX || a > now,
"skip-forward must land strictly beyond `now` or saturate: got {a} for ({next_fire_at}, {now}, {interval})"
);
}
/// Adversarial triples folded straight out of the input bytes — the
/// whole `(next_fire_at, now, interval)` domain through the fuzz path
/// and every ≥24-byte corpus seed.
fn assert_skip_triples(data: &[u8]) {
let (chunks, remainder) = data.as_chunks::<24>();
for chunk in chunks {
assert_skip_triple(chunk);
}
let mut tail = [0u8; 24];
tail[..remainder.len()].copy_from_slice(remainder);
assert_skip_triple(&tail);
}
fn assert_skip_triple(chunk: &[u8]) {
let next_fire_at = i64::from_le_bytes(chunk[..8].try_into().expect("slice is 8 bytes"));
let now = i64::from_le_bytes(chunk[8..16].try_into().expect("slice is 8 bytes"));
let interval = i64::from_le_bytes(chunk[16..24].try_into().expect("slice is 8 bytes"));
assert_skip_posture(next_fire_at, now, interval);
}
#[cfg(test)]
mod corpus_replay {
use super::*;
use std::path::PathBuf;
#[test]
fn committed_corpus_replays_through_the_invariants() {
let corpus = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../corpus/every_parse");
let mut count = 0usize;
for entry in std::fs::read_dir(&corpus).expect("corpus directory is committed") {
let path = entry.expect("corpus entry readable").path();
let data = std::fs::read(&path).expect("corpus entry readable");
fuzz_every_parse(&data);
count += 1;
}
assert!(
count >= 48,
"committed seed corpus is present, found {count}"
);
}
#[test]
fn invariants_hold_without_corpus() {
fuzz_every_parse(b"");
fuzz_every_parse(b"@every 5s");
fuzz_every_parse(b"@every 106751991167301d");
fuzz_every_parse(b"\xff @every \xff 5s \xfe");
fuzz_every_parse(&[0u8; 24]);
fuzz_every_parse(&(i64::MIN as i128).to_le_bytes());
}
}
+1
View File
@@ -4,4 +4,5 @@
//! CI pattern) without a nightly toolchain.
pub mod arbitrary_value;
pub mod every_parse;
pub mod payload_codec;
+113 -9
View File
@@ -1,7 +1,7 @@
---
id: fuzz-target-every-parse
name: Fuzz target — `every_parse` (the three `@every` parsers, differential)
status: pending
status: completed
depends_on: [fuzz-scaffold, fuzz-parser-exposure]
scope: moderate
risk: medium
@@ -73,17 +73,23 @@ escalate), not a task failure.
## Acceptance Criteria
- [ ] `fuzz_targets/every_parse.rs` + `shared/src/every_parse.rs`
- [x] `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
- [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)
- [ ] Seed corpus committed, deterministic-generated, gitignore
- [x] Seed corpus committed, deterministic-generated, gitignore
negation in place
- [ ] `cargo test --manifest-path fuzz/shared/Cargo.toml` green;
- [x] `cargo test --manifest-path fuzz/shared/Cargo.toml` green;
`shared` clippy/fmt gates green
- [ ] Root workspace gates still green; substrate diff still empty
- [ ] Any three-way divergence found during target development is
- [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
@@ -101,8 +107,106 @@ escalate), not a task failure.
## Notes
> To be filled by implementation agent
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
> To be filled on completion
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.