Skip to main content

max / shop

4.2 KB · 110 lines History Blame Raw
1 //! Replay every committed fuzz 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/vt/` is human intent and captured real traffic, the shapes
11 //! worth reaching first.
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 assertion is `shop_grid::oracle::check_bytes`, the same function the
17 //! libFuzzer target calls, so the two cannot check different things.
18
19 use std::path::{Path, PathBuf};
20
21 fn dir(rel: &str) -> PathBuf {
22 Path::new(env!("CARGO_MANIFEST_DIR")).join(rel)
23 }
24
25 /// Replay one directory, returning how many inputs it held.
26 ///
27 /// A missing directory is fine and returns zero: `fuzz/regressions/` does not
28 /// exist until the first crash does.
29 fn replay(dir: &Path) -> usize {
30 let Ok(entries) = std::fs::read_dir(dir) else {
31 return 0;
32 };
33
34 let mut count = 0;
35 for entry in entries {
36 let path = entry.expect("readable dir entry").path();
37 if !path.is_file() {
38 continue;
39 }
40 // README and friends are documentation, not inputs.
41 if path.extension().is_some_and(|e| e == "md") {
42 continue;
43 }
44 let bytes = std::fs::read(&path).expect("readable input");
45 // Panics name the file: a bare oracle panic in a loop over a hundred
46 // inputs says nothing about which one broke.
47 let outcome = std::panic::catch_unwind(|| shop_grid::oracle::check_bytes(&bytes));
48 assert!(outcome.is_ok(), "oracle failed on {}", path.display());
49 count += 1;
50 }
51 count
52 }
53
54 #[test]
55 fn every_seed_holds_the_oracle() {
56 let n = replay(&dir("fuzz/seeds/vt"));
57 // A seed directory that silently emptied would make this test pass while
58 // checking nothing, which is the failure mode a corpus-replay test has.
59 assert!(
60 n >= 40,
61 "expected the committed seed corpus, found {n} inputs"
62 );
63 }
64
65 #[test]
66 fn every_regression_holds_the_oracle() {
67 // No lower bound. This is empty until the first crash, and that is the
68 // good state.
69 replay(&dir("fuzz/regressions"));
70 }
71
72 /// The oracle must be reachable and must actually run, not merely not panic.
73 #[test]
74 fn oracle_consumes_the_stream_it_is_given() {
75 let input = b"\x63\x1dhello\x1b[31mworld\x1b[H\x1b[2J";
76 assert_eq!(shop_grid::oracle::check_bytes(input), input.len() - 2);
77 }
78
79 /// The two ways the parser can grow without bound, replayed at a size the
80 /// committed corpus cannot carry.
81 ///
82 /// `fuzz/regressions/` holds an 8 KiB witness for each, which is enough for the
83 /// amplification ceiling to catch the CSI case. Neither string case amplifies:
84 /// they accumulate 1:1, so the only thing that fails on an uncapped parser is
85 /// feeding it more than the cap, and eight megabytes is not a file to commit.
86 #[test]
87 fn unbounded_parser_growth_stays_capped() {
88 // Four million separators, each of which pushes a fresh Vec<u16> uncapped.
89 // The oracle reads the first two bytes as the grid size, not as stream.
90 let mut csi = b"\x4f\x17\x1b[".to_vec();
91 csi.extend(std::iter::repeat_n(b';', 4_000_000));
92 shop_grid::oracle::check_bytes(&csi);
93
94 // Bodies twice the cap, opened and never terminated.
95 let over = shop_vt::MAX_STRING_BYTES * 2;
96 for opener in [&b"\x4f\x17\x1b]"[..], &b"\x4f\x17\x1b_"[..]] {
97 let mut body = opener.to_vec();
98 body.extend(std::iter::repeat_n(b'A', over));
99 shop_grid::oracle::check_bytes(&body);
100 }
101
102 // And the buffers come back down afterwards rather than holding the cap
103 // for the life of the parser, which is what makes the ceiling hold for
104 // every input that follows a big one.
105 let mut big = b"\x4f\x17\x1b]0;".to_vec();
106 big.extend(std::iter::repeat_n(b'A', over));
107 big.extend_from_slice(b"\x07\x1b[0m");
108 shop_grid::oracle::check_bytes(&big);
109 }
110