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:
glm-5.3-flash committed 2026-10-10 22:38:34 +00:00
1 parent 5043f7bcd8
commit f7de30e57e
7 files changed
+237 -25

No files matched your search

+76 -4
View File
@@ -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