Skip to main content

max / makenotwork

3.8 KB · 106 lines History Blame Raw
1 //! Replay every committed part-geometry input through the oracle, on stable.
2 //!
3 //! The soak tier runs on astra, on nightly, when the box is idle. That is the
4 //! wrong place for the only copy of a property to live: a crash found there is
5 //! fixed here, and nothing on a developer's machine would notice it coming
6 //! back.
7 //!
8 //! So both corpora replay as an ordinary `cargo test`:
9 //!
10 //! - `fuzz/seeds/plan/` is human intent -- the boundaries where part arithmetic
11 //! goes wrong, and the off-by-ones either side of each.
12 //! - `fuzz/regressions/` is inputs that once found a bug. Anything landing
13 //! there is a permanent test by virtue of the directory, with no test
14 //! function to write and no chance of forgetting one.
15 //!
16 //! The assertions are `s3_storage::oracle::check_plan` and `check_auto`, the
17 //! same functions the libFuzzer target calls, so the two cannot check different
18 //! things.
19
20 use std::path::Path;
21
22 /// Decode one corpus file the way the fuzz target's `Arbitrary` impl does: a
23 /// `(u64, u64)` tuple, eight little-endian bytes each.
24 ///
25 /// This mirrors `libfuzzer_sys`'s decoding rather than calling it, so that the
26 /// replay does not need the fuzzing dependency on stable. The coupling is
27 /// deliberate and narrow: if the target's input type ever changes, this is the
28 /// one place that has to change with it, and a mismatch shows up as a replay
29 /// that decodes nonsense rather than as a silent pass.
30 fn decode(bytes: &[u8]) -> Option<(u64, u64)> {
31 if bytes.len() < 16 {
32 return None;
33 }
34 let total = u64::from_le_bytes(bytes[0..8].try_into().ok()?);
35 let part = u64::from_le_bytes(bytes[8..16].try_into().ok()?);
36 Some((total, part))
37 }
38
39 /// Replay one directory, returning how many inputs it held.
40 ///
41 /// A missing directory is fine and returns zero: `fuzz/regressions/` holds only
42 /// a README until the first crash.
43 fn replay(dir: &Path) -> usize {
44 let Ok(entries) = std::fs::read_dir(dir) else {
45 return 0;
46 };
47
48 let mut count = 0;
49 for entry in entries {
50 let path = entry.expect("readable dir entry").path();
51 if !path.is_file() {
52 continue;
53 }
54 // READMEs are documentation, not inputs.
55 if path.extension().is_some_and(|e| e == "md") {
56 continue;
57 }
58 let bytes = std::fs::read(&path).expect("readable input");
59 let Some((total_size, raw_part)) = decode(&bytes) else {
60 continue;
61 };
62
63 // The same three calls the target makes, in the same order, so a crash
64 // reproduces here unchanged.
65 let outcome = std::panic::catch_unwind(|| {
66 s3_storage::oracle::check_plan(total_size, raw_part as usize);
67 const MIN_PART: u64 = 5 * 1024 * 1024;
68 const MAX_PART: u64 = 5 * 1024 * 1024 * 1024;
69 let shaped = MIN_PART + (raw_part % (MAX_PART - MIN_PART + 1));
70 s3_storage::oracle::check_plan(total_size, shaped as usize);
71 s3_storage::oracle::check_auto(total_size);
72 });
73 assert!(
74 outcome.is_ok(),
75 "oracle failed on {} (total_size {total_size}, part_size {raw_part})",
76 path.display()
77 );
78 count += 1;
79 }
80 count
81 }
82
83 #[test]
84 fn plan_seeds_satisfy_the_oracle() {
85 let n = replay(
86 Path::new(env!("CARGO_MANIFEST_DIR"))
87 .join("fuzz/seeds/plan")
88 .as_path(),
89 );
90 assert!(n > 0, "no plan seeds found; the corpus is the point");
91 println!("replayed {n} seeds");
92 }
93
94 #[test]
95 fn every_past_crash_stays_fixed() {
96 // No assertion on the count. An empty regressions directory is the good
97 // state, and a test demanding a crash to have happened would fail on a
98 // healthy crate.
99 let n = replay(
100 Path::new(env!("CARGO_MANIFEST_DIR"))
101 .join("fuzz/regressions")
102 .as_path(),
103 );
104 println!("replayed {n} regression inputs");
105 }
106