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

+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;