//! Replay every committed fuzz input through the oracle, on stable. //! //! The soak tier runs on astra, on nightly, when the box is idle. That is the //! wrong place for the only copy of a property to live: a crash found there is //! fixed here, and nothing on a developer's machine would notice it coming //! back. //! //! So both corpora replay as an ordinary `cargo test`: //! //! - `fuzz/seeds/vt/` is human intent and captured real traffic, the shapes //! worth reaching first. //! - `fuzz/regressions/` is inputs that once found a bug. Anything landing //! there is a permanent test by virtue of the directory, with no test //! function to write and no chance of forgetting one. //! //! The assertion is `shop_grid::oracle::check_bytes`, the same function the //! libFuzzer target calls, so the two cannot check different things. use std::path::{Path, PathBuf}; fn dir(rel: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join(rel) } /// Replay one directory, returning how many inputs it held. /// /// A missing directory is fine and returns zero: `fuzz/regressions/` does not /// exist until the first crash does. fn replay(dir: &Path) -> usize { let Ok(entries) = std::fs::read_dir(dir) else { return 0; }; let mut count = 0; for entry in entries { let path = entry.expect("readable dir entry").path(); if !path.is_file() { continue; } // README and friends are documentation, not inputs. if path.extension().is_some_and(|e| e == "md") { continue; } let bytes = std::fs::read(&path).expect("readable input"); // Panics name the file: a bare oracle panic in a loop over a hundred // inputs says nothing about which one broke. let outcome = std::panic::catch_unwind(|| shop_grid::oracle::check_bytes(&bytes)); assert!(outcome.is_ok(), "oracle failed on {}", path.display()); count += 1; } count } #[test] fn every_seed_holds_the_oracle() { let n = replay(&dir("fuzz/seeds/vt")); // A seed directory that silently emptied would make this test pass while // checking nothing, which is the failure mode a corpus-replay test has. assert!( n >= 40, "expected the committed seed corpus, found {n} inputs" ); } #[test] fn every_regression_holds_the_oracle() { // No lower bound. This is empty until the first crash, and that is the // good state. replay(&dir("fuzz/regressions")); } /// The oracle must be reachable and must actually run, not merely not panic. #[test] fn oracle_consumes_the_stream_it_is_given() { let input = b"\x63\x1dhello\x1b[31mworld\x1b[H\x1b[2J"; assert_eq!(shop_grid::oracle::check_bytes(input), input.len() - 2); } /// The two ways the parser can grow without bound, replayed at a size the /// committed corpus cannot carry. /// /// `fuzz/regressions/` holds an 8 KiB witness for each, which is enough for the /// amplification ceiling to catch the CSI case. Neither string case amplifies: /// they accumulate 1:1, so the only thing that fails on an uncapped parser is /// feeding it more than the cap, and eight megabytes is not a file to commit. #[test] fn unbounded_parser_growth_stays_capped() { // Four million separators, each of which pushes a fresh Vec uncapped. // The oracle reads the first two bytes as the grid size, not as stream. let mut csi = b"\x4f\x17\x1b[".to_vec(); csi.extend(std::iter::repeat_n(b';', 4_000_000)); shop_grid::oracle::check_bytes(&csi); // Bodies twice the cap, opened and never terminated. let over = shop_vt::MAX_STRING_BYTES * 2; for opener in [&b"\x4f\x17\x1b]"[..], &b"\x4f\x17\x1b_"[..]] { let mut body = opener.to_vec(); body.extend(std::iter::repeat_n(b'A', over)); shop_grid::oracle::check_bytes(&body); } // And the buffers come back down afterwards rather than holding the cap // for the life of the parser, which is what makes the ceiling hold for // every input that follows a big one. let mut big = b"\x4f\x17\x1b]0;".to_vec(); big.extend(std::iter::repeat_n(b'A', over)); big.extend_from_slice(b"\x07\x1b[0m"); shop_grid::oracle::check_bytes(&big); }