fuzz campaigns + gate: wave 7 close — three 15-min detached campaigns clean (payload_codegen finding harness-model: serde_json default f64 parse drifts 1-2 ULPs; invariant pinned to 2-ULP float-leaf drift + decode determinism, crash input promoted as payload_codec/seed-900), fuzz/.gitignore retightened to seed-* tracking, corpus-replay gate joins AGENTS.md verification list, research doc §9 implementation log, README finalized
This commit is contained in:
1 parent
5043f7bcd8
commit
f7de30e57e
7 files changed
+237
-25
No files matched your search
@@ -1,7 +1,13 @@
|
||||
target
|
||||
corpus/*
|
||||
!corpus/payload_codec
|
||||
corpus/payload_codec/*
|
||||
!corpus/payload_codec/seed-*
|
||||
!corpus/every_parse
|
||||
corpus/every_parse/*
|
||||
!corpus/every_parse/seed-*
|
||||
!corpus/name_validation
|
||||
corpus/name_validation/*
|
||||
!corpus/name_validation/seed-*
|
||||
artifacts
|
||||
coverage
|
||||
+7
-6
@@ -5,8 +5,7 @@ libFuzzer targets for alkstore's parse/codec surfaces (see
|
||||
|
||||
## Layout
|
||||
|
||||
- `fuzz_targets/` — one binary per target; thin `fuzz_target!` wrappers
|
||||
(land with the wave-7 target tasks).
|
||||
- `fuzz_targets/` — one binary per target; thin `fuzz_target!` wrappers.
|
||||
- `shared/` — the invariant logic, as a plain library so normal
|
||||
`cargo test` (stable toolchain) can replay the committed corpora
|
||||
through the same invariants (`fuzz/shared/src/*.rs` `#[cfg(test)]`
|
||||
@@ -14,9 +13,11 @@ libFuzzer targets for alkstore's parse/codec surfaces (see
|
||||
the shared crate is stable-clean. It consumes alkstore's public API
|
||||
only and must never become a dependency of any workspace crate.
|
||||
- `corpus/<target>/` — committed hand-made seeds (regenerate with
|
||||
`python3 fuzz/gen_fuzz_seeds.py` from the repo root). Grown corpora
|
||||
and artifacts are gitignored; each target task adds its `!`-negation
|
||||
line when its corpus lands.
|
||||
`python3 fuzz/gen_fuzz_seeds.py` from the repo root — must be a no-op
|
||||
diff on the committed set) plus promoted campaign crash inputs
|
||||
(`seed-900`+ numbering). Only `seed-*` files are tracked; grown
|
||||
corpora (SHA-named units), artifacts, and coverage output are
|
||||
gitignored and are **not** merged back into the committed seeds.
|
||||
- `run-detached.sh` — mandatory runner for agent sessions: wraps
|
||||
`cargo fuzz run` in `setsid` + `nohup` + log redirection so an OOM
|
||||
in a target can never take down the agent host (research doc §5).
|
||||
@@ -27,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 |
|
||||
| `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 closes exactly outside `Number` float leaves, float leaves pinned to serde_json's default-parse drift bound (1–2 ULPs, see the research doc's implementation log — ADR-020 §4 pins default serde_json, not the `float_roundtrip` flavor); decode deterministic over stored bytes; typed round-trip for a representative payload set |
|
||||
| `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`) | no-panic on any byte sequence (lossy-decoded and `arbitrary`-structured strings); deterministic accept/reject (double-invocation-pinned); rejection only via the typed taxonomy — `Error::InvalidName` for empty/whitespace-only on both kinds, `Error::ReservedName` for the leading byte-exact `__alkstore_` prefix on the shared kind only (the local kind takes the non-empty rule only, so a leading reserved prefix is legal there); the reserved-prefix rule exact in every casing/boundary form (casing deviations, missing trailing underscore, embedded and whitespace-led forms accept); the rejection carries the supplied name verbatim (names are not trimmed); verdict-class length-independence across the probed neighborhoods (0↔1, 11↔12, 63↔64, 4095↔4096, 65536) — no length cap is pinned (ADR-008 §4), so no length is ever a rejection cause |
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
46741844073709.51615
|
||||
@@ -69,13 +69,23 @@ pub fn fuzz_payload_codec(data: &[u8]) {
|
||||
assert_decode_partition::<Vec<Value>>(data);
|
||||
|
||||
// A successfully-decoded `Value` re-encodes byte-stably: the Value
|
||||
// round-trip closes over the raw input, with serde_json's
|
||||
// canonicalization (duplicate-key last-write-wins, number-to-NormalizedNumber,
|
||||
// key ordering) pinned — not re-derived, just not regressed.
|
||||
// round-trip closes over the raw input up to serde_json's
|
||||
// canonicalization (duplicate-key last-write-wins, key ordering) —
|
||||
// pinned, not re-derived. serde_json's default fast-path f64
|
||||
// parser is not exactly correctly-rounded (its parse of its own
|
||||
// ryu serialization may drift 1–2 ULPs; the ADR-020 §4 posture
|
||||
// pins default serde_json, not the `float_roundtrip` flavor), so
|
||||
// float leaves are compared under the pinned drift bound; every
|
||||
// other shape must be exactly equal.
|
||||
if let Ok(v) = payload_as::<Value>(data) {
|
||||
let bytes = encode_payload(&v).expect("a decoded `Value` must re-encode");
|
||||
let back: Value = payload_as(&bytes).expect("the encoded `Value` must decode");
|
||||
assert_eq!(back, v, "the `Value` round-trip must close over raw input");
|
||||
assert_canonicalized_closure(&v, &back);
|
||||
let back_again: Value = payload_as(&bytes).expect("the encoded `Value` must decode");
|
||||
assert_eq!(
|
||||
back, back_again,
|
||||
"payload decode must be deterministic over stored bytes"
|
||||
);
|
||||
}
|
||||
|
||||
assert_deep_input_loudness();
|
||||
@@ -92,6 +102,68 @@ fn assert_decode_partition<T: serde::de::DeserializeOwned>(data: &[u8]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// serde_json's default fast-path f64 parse is not exactly
|
||||
/// correctly-rounded: parsing its own ryu serialization can drift by
|
||||
/// at most this many ULPs (one canonicalization step; measured over
|
||||
/// randomized campaigns — see the research doc's implementation log).
|
||||
const FLOAT_DRIFT_ULPS: f64 = 2.0;
|
||||
|
||||
fn assert_canonicalized_closure(v: &Value, back: &Value) {
|
||||
match (v, back) {
|
||||
(Value::Number(a), Value::Number(b)) => {
|
||||
if a.is_f64() && b.is_f64() {
|
||||
let x = a.as_f64().expect("checked is_f64");
|
||||
let y = b.as_f64().expect("checked is_f64");
|
||||
if y == x {
|
||||
return;
|
||||
}
|
||||
let drift = (y - x).abs() / canonicalization_ulp(x);
|
||||
assert!(
|
||||
drift <= FLOAT_DRIFT_ULPS,
|
||||
"float canonicalization drifted {drift} ULPs, over the \
|
||||
{FLOAT_DRIFT_ULPS}-ULP bound: {x} -> {y}"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(a, b, "an integer leaf lost exactness across the round-trip");
|
||||
}
|
||||
}
|
||||
(Value::Array(a), Value::Array(b)) => {
|
||||
assert_eq!(
|
||||
a.len(),
|
||||
b.len(),
|
||||
"array length changed across the round-trip"
|
||||
);
|
||||
for (x, y) in a.iter().zip(b.iter()) {
|
||||
assert_canonicalized_closure(x, y);
|
||||
}
|
||||
}
|
||||
(Value::Object(a), Value::Object(b)) => {
|
||||
assert_eq!(
|
||||
a.len(),
|
||||
b.len(),
|
||||
"object key set changed across the round-trip"
|
||||
);
|
||||
for (key, x) in a {
|
||||
let y = b
|
||||
.get(key)
|
||||
.unwrap_or_else(|| panic!("key {key:?} lost across the round-trip"));
|
||||
assert_canonicalized_closure(x, y);
|
||||
}
|
||||
}
|
||||
_ => assert_eq!(v, back, "the `Value` round-trip must close over raw input"),
|
||||
}
|
||||
}
|
||||
|
||||
fn canonicalization_ulp(x: f64) -> f64 {
|
||||
let a = x.abs();
|
||||
let up = a.next_up();
|
||||
if up > a {
|
||||
up - a
|
||||
} else {
|
||||
a - a.next_down()
|
||||
}
|
||||
}
|
||||
|
||||
/// Deep inputs decode loud and typed: past serde_json's recursion
|
||||
/// limit, deserialization fails with `Error::Codec` — never a stack
|
||||
/// overflow, never an untyped panic. The encode side handles deep
|
||||
|
||||
Reference in new issue
Block a user