fuzz: W3-2 — serde_json parse-side one-ulp float drift pinned with slack

The restarted validate_pair campaign found a second crash: materialize
produces f64 0x5bffffffffffffff, serde_json emits the shortest repr
1.4536774485912136e+135, and the non-`float_roundtrip` parse side
(lexical concise-float over re-parsed digits) lands one ulp low —
probe-verified upstream of this crate (ryu's own float parser accepts
the same digits exactly; the std parser is exact; only serde_json's
concise reparse drifts). The harness's structural serde round-trip
assertion assumed Value equality holds for every finite f64 — upstream
parse-side drift breaks that assumption on adversarial magnitudes.

- assert_values_agree_with_ulp_slack replaces the bare Value equality:
  keys/shapes exact, numbers equal-or-within-one-ulp (bit diff ≤ 1)
- the exact artifact bytes pinned as corpus seed-047, plus
  deterministic minimal forms as seed-045/seed-046 (45→48 seeds)
- upstream note: enabling serde_json's float_roundtrip feature would
  remove the drift; the crate pins serde_json default features +
  preserve_order by design, so the slack is the honest pin

Verification: corpus replay 30/30 green (48 seeds), fuzz build clean,
clippy -D warnings clean.
This commit is contained in:
glm-5.3-flash committed 2026-09-30 07:43:02 +00:00
1 parent 9ca9922fd4
commit b7ead99724
5 files changed
+76 -3

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
+15
View File
@@ -640,6 +640,21 @@ def validate_pair_seeds():
# the W3-1 invariant pin documents.
w(pair(menu_lane(2), True, shape("padded"), b""))
# W3-2 (the second campaign crash): serde_json's non-`float_
# roundtrip` parser drifts one ulp re-parsing its own emission of
# adversarial f64 bits. Menu 255 % 10 = 5 (u8 tag | f64), aligned,
# padded body with the f64 bits 0x5bffffffffffffff. The harness
# pins the round-trip with one-ulp slack (upstream property).
w(pair(menu_lane(255), True, shape("padded"),
b"\xff" * 7 + b"\x5b"))
w(bytes.fromhex(
"00230000" + "ff" * 25 + "5b" + "ff" * 2 + b"encoding".hex()
+ "ff" * 36 + b"string".hex() + "ff" * 16
+ "0260010081000100" + "3d0021000030010401"))
# Deterministic minimal form of the same shape for the record:
w(pair(menu_lane(5), True, shape("raw"),
b"\x07" + b"\xff" * 7 + b"\x5c"))
# Aligned truncation sweep over the fixed-shape menu 7 body.
for n in range(1, len(be)):
w(pair(menu_lane(7), True, shape("trunc", n), be))
+61 -3
View File
@@ -249,6 +249,53 @@ fn assert_engine_shape(engine: &AlkTypeEngine) {
}
}
/// Structural serde round-trip equality with the W3-2 f64 slack:
/// numbers agree either exactly or within one ulp of each other's
/// f64 bit patterns (serde_json's non-`float_roundtrip` parse of its
/// own re-emitted shortest repr can drift one ulp on adversarial
/// magnitudes — upstream, not an alktype contract). Keys and value
/// shapes must match exactly.
fn assert_values_agree_with_ulp_slack(a: &Value, b: &Value) {
match (a, b) {
(Value::Number(x), Value::Number(y)) => {
let (xf, yf) = (x.as_f64(), y.as_f64());
match (xf, yf) {
(Some(xf), Some(yf)) if xf.is_finite() && yf.is_finite() => {
if x == y {
return;
}
let (xb, yb) = (xf.to_bits(), yf.to_bits());
let drift = xb.abs_diff(yb);
assert!(
drift <= 1,
"number drift beyond one ulp: {x} vs {y} (bit diff {drift})"
);
}
_ => assert_eq!(x, y, "number mismatch"),
}
}
(Value::Array(x), Value::Array(y)) => {
assert_eq!(x.len(), y.len(), "array length mismatch");
for (i, (xa, ya)) in x.iter().zip(y.iter()).enumerate() {
assert_values_agree_with_ulp_slack(xa, ya);
let _ = i;
}
}
(Value::Object(x), Value::Object(y)) => {
assert_eq!(x.len(), y.len(), "object size mismatch");
let mut xk: Vec<&String> = x.keys().collect();
let mut yk: Vec<&String> = y.keys().collect();
xk.sort();
yk.sort();
assert_eq!(xk, yk, "object keys mismatch");
for k in xk {
assert_values_agree_with_ulp_slack(&x[k], &y[k]);
}
}
(x, y) => assert_eq!(x, y, "value shape mismatch"),
}
}
/// Unknown paths must always error, echo the path, and never panic.
fn junk_paths(engine: &AlkTypeEngine, _root: &str) {
for junk in JUNK_PATHS {
@@ -396,13 +443,24 @@ fn drive_pair(engine: &AlkTypeEngine, doc: &Value, root: &str, buffer: &[u8]) {
}
}
// serde-safety: the output survives to_vec + parse back,
// structurally equal (preserve_order keeps key order).
// serde-safe encode: the output survives to_vec and parse
// back. W3-2: the comparison carries a documented f64
// slack — the wire→f64 decode is exact, but serde_json's
// non-`float_roundtrip` parser (lexical concise-float on
// the re-emitted shortest repr) can drift one ulp on
// adversarial magnitudes (probe: 0x5bffffffffffffff emits
// 1.4536774485912136e+135 and parses back one ulp low;
// ryu's own float parse of the same digits is exact, so
// this is the parse side, upstream of this crate). Without
// slack the fuzzer fires this on adversarial f64 bits — an
// upstream serde_json property, not an alktype contract.
// Structural round-trip is asserted field-shape-wise with
// per-number comparison at the f64 bits ± 1 ulp.
let bytes = serde_json::to_vec(&value)
.expect("a materialized Value is serde-serializable");
let back = serde_json::from_slice::<Value>(&bytes)
.expect("a materialized Value survives its own JSON encoding");
assert_eq!(value, back, "materialize output serde round-trips");
assert_values_agree_with_ulp_slack(&value, &back);
}
}
}