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

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