Comparative fuzzing parallel Rust tools

Adrian Taylor
9 min readJan 22, 2023

--

I previously wrote about how we can use Rust’s “fearless concurrency”, resulting in a tool called ripunzip. (Here are some performance results).

How can we be sure that it behaves the same way as a non-parallel, regular, unzip? Even in unforeseen circumstances?

The answer: comparative fuzzing. Fuzzing is normally a technique used to find security bugs — here’s a recipe for how you can use it to look for bugs in Rust unsafe code — but you can also use it to check that two implementations behave the same way — comparative fuzzing.

It’s a good fit for the case where we’re trying to make a faster version of an existing tool, and fuzzing happens to be quite fun — especially in Rust where there’s the marvelous arbitrary crate.

Here’s the plan:

  • Describe all the inputs to ripunzip , including the zip file, using the arbitrary crate
  • Run ripunzip based on those inputs
  • Also unzip the same file using regular, single-threaded zip-rs
  • Check they both succeeded or both failed
  • Compare the unzipped directory. If it’s different, crash!

Then we simply set cargo fuzz to work. It should explore the space of all possible inputs to ripunzip and, if any of them result in a difference in behavior from zip-rs , it should tell us.

Here’s the fuzzer we ended up with. First, let’s look at the input data:


#[derive(arbitrary::Arbitrary, Eq, PartialEq, Hash, Debug, Clone)]
struct ZipMemberFilename(/* details omitted... described later */);

#[derive(Eq, PartialEq, Hash, Debug, Clone, Copy, arbitrary::Arbitrary)]
enum CompressionMethod {
Stored,
Deflated,
Bzip2,
Zstd,
}

#[derive(arbitrary::Arbitrary, Debug, Clone)]
struct Inputs {
// HashMap to ensure unique filenames in zip
zip_members: HashMap<ZipMemberFilename, Vec<u8>>,
compression_method: CompressionMethod,
single_threaded: bool,
}

The most important bit is theInputs struct. It includes a zip file, a compression method, and any parameters to be passed to ripunzip about how we want to unzip it.

It derives Arbitrary , which means code is generated to construct this structure from an opaque binary blob. Those binary blobs are generated by libfuzzer, and — crucially — they are amended and re-amended to gradually explore as much of the program as possible, based on coverage guidance.

We’ve got some pretty complex input there — HashMap , vectors, and custom enums. Arbitrary is smart enough to generate all of them.

Now, the fuzzer itself is essentially trivial — we literally just create the zip, unzip it twice, and compare the outputs.

fuzz_target!(|input: Inputs| {
let progress_reporter = ripunzip::NullProgressReporter;
let tempdir = tempfile::tempdir().unwrap();
let output_directory = tempdir.path().join("out_ripunzip");
let output_directory_unzip = tempdir.path().join("out_unzip");
std::fs::create_dir_all(&output_directory).unwrap();
let options = ripunzip::UnzipOptions {
single_threaded: input.single_threaded,
output_directory: Some(output_directory.clone()),
};
let zipfile = tempdir.path().join("file.zip");
let mut zip_data = Vec::new();
create_zip(&mut zip_data, &input.zip_members, input.compression_method);
let mut file = std::fs::File::create(&zipfile).unwrap();
file.write_all(&zip_data).unwrap();
drop(file);
let file = std::fs::File::open(&zipfile).unwrap();
let ripunzip_result: Result<(), anyhow::Error> = (|| {
let ripunzip = ripunzip::UnzipEngine::for_file(file, options, progress_reporter)?;
ripunzip.unzip()
})();
let unziprs_result = unzip_with_zip_rs(&zipfile, &output_directory_unzip);
match unziprs_result {
Err(err) => {
if ripunzip_result.is_ok() {
panic!("ripunzip succeeded; plain unzip gave {:?}", err)
}
}
Ok(_) => {
ripunzip_result.unwrap();
let ripunzip_paths = recursive_lsdir(&output_directory);
let unzip_paths = recursive_lsdir(&output_directory_unzip);
// We do not currently compare the actual zip file contents.
// It seems unlikely that this would be a failure mode where
// ripunzip would differ from zip-rs.
assert_eq!(ripunzip_paths, unzip_paths);
}
}
});

fn recursive_lsdir(dir: &Path) -> HashSet<std::path::PathBuf> {
walkdir::WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
.map(|e| e.path().strip_prefix(dir).unwrap().to_path_buf())
.collect()
}

// Create a zip file from zip_members into output,
// using the given compression mode. Details omitted.
fn create_zip(output: &mut Vec<u8>, zip_members: &HashMap<ZipMemberFilename, Vec<u8>>, compression_method: CompressionMethod) {
// ...
}

/// Errors that can occur with a regular zip-rs unzip, details omitted
#[derive(Debug)]
enum ZipRsError {
// ...
}

/// Unzip the content with standard zip-rs.
fn unzip_with_zip_rs(zipfile_path: &Path, dest_path: &Path) -> Result<(), ZipRsError> {
// ...
}

One final detail — the member filenames of the zip file. Originally I used arbitrary strings for filenames, but it turns out that both zip-rs and ripunzip fail non-deterministically if you make a sufficiently wacky filename. I therefore simplified this to be a sequence of particular strings:

#[derive(arbitrary::Arbitrary, Debug, Clone, strum::Display, Hash, Eq, PartialEq)]
enum FilenameSegment {
Fish,
A,
#[strum(serialize = "b")]
B,
#[strum(serialize = "31")]
ThirtyOne,
#[strum(serialize = "_c")]
C,
D,
#[strum(serialize = "e.txt")]
ETxt,
}

#[derive(arbitrary::Arbitrary, Eq, PartialEq, Hash, Debug, Clone)]
struct ZipMemberFilename(Vec<FilenameSegment>);

impl Into<String> for &ZipMemberFilename {
fn into(self) -> String {
self.0.iter().join("/")
}
}

(note use of some lovely crates there, strum and itertools ).

What happens when we run this? We see this delightful output as the fuzzer ferrets around our codebase.

$ cargo +nightly fuzz run fuzz_ripunzip
Compiling ripunzip v0.2.2 (ripunzip)
Compiling ripunzip-fuzz v0.0.0 (ripunzip/fuzz)
Finished release [optimized] target(s) in 18.06s
Finished release [optimized] target(s) in 0.18s
Running `target/x86_64-apple-darwin/release/fuzz_ripunzip -artifact_prefix=ripunzip/fuzz/artifacts/fuzz_ripunzip/ ripunzip/fuzz/corpus/fuzz_ripunzip`
fuzz_ripunzip(13490,0x7ff85b1088c0) malloc: nano zone abandoned due to inability to preallocate reserved vm space.
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3274692728
INFO: Loaded 1 modules (720556 inline 8-bit counters): 720556 [0x107a95260, 0x107b4510c),
INFO: Loaded 1 PC tables (720556 PCs): 720556 [0x107b45110,0x108643bd0),
INFO: 0 files found in ripunzip/fuzz/corpus/fuzz_ripunzip
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2 INITED exec/s: 0 rss: 53Mb
NEW_FUNC[1/171]: 0x105266120 in _$LT$core..iter..adapters..map..Map$LT$I$C$F$GT$$u20$as$u20$core..iter..traits..iterator..Iterator$GT$::fold::h2eed7f9eb6137b14+0x0 (fuzz_ripunzip:x86_64+0x100003120) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
NEW_FUNC[2/171]: 0x10526abc0 in _$LT$ripunzip..unzip..cloneable_seekable_reader..CloneableSeekableReader$LT$R$GT$$u20$as$u20$std..io..Read$GT$::read::h7325b227d55c68e7+0x0 (fuzz_ripunzip:x86_64+0x100007bc0) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
#212 NEW cov: 1204 ft: 1177 corp: 2/7b lim: 6 exec/s: 212 rss: 59Mb L: 6/6 MS: 4 ChangeBit-ChangeBit-InsertByte-InsertRepeatedBytes-
#213 NEW cov: 1209 ft: 1182 corp: 3/13b lim: 6 exec/s: 213 rss: 59Mb L: 6/6 MS: 1 ShuffleBytes-
#217 NEW cov: 1210 ft: 1184 corp: 4/18b lim: 6 exec/s: 217 rss: 59Mb L: 5/6 MS: 4 CopyPart-ChangeByte-ShuffleBytes-InsertRepeatedBytes-
NEW_FUNC[1/90]: 0x10526a3b0 in _$LT$std..io..cursor..Cursor$LT$$RF$mut$u20$alloc..vec..Vec$LT$u8$C$A$GT$$GT$$u20$as$u20$std..io..Write$GT$::write::hb0a9a4b62999ace6+0x0 (fuzz_ripunzip:x86_64+0x1000073b0) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
NEW_FUNC[2/90]: 0x10526ca90 in _$LT$alloc..vec..Vec$LT$T$GT$$u20$as$u20$alloc..vec..spec_from_iter_nested..SpecFromIterNested$LT$T$C$I$GT$$GT$::from_iter::h62629dd53aaca6a9+0x0 (fuzz_ripunzip:x86_64+0x100009a90) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
#220 NEW cov: 2100 ft: 2235 corp: 5/23b lim: 6 exec/s: 220 rss: 60Mb L: 5/6 MS: 3 CrossOver-ShuffleBytes-InsertRepeatedBytes-
#222 NEW cov: 2101 ft: 2237 corp: 6/29b lim: 6 exec/s: 222 rss: 60Mb L: 6/6 MS: 2 CrossOver-CopyPart-
NEW_FUNC[1/1]: 0x1053ad5c0 in _$LT$alloc..vec..Vec$LT$T$GT$$u20$as$u20$alloc..vec..spec_from_iter_nested..SpecFromIterNested$LT$T$C$I$GT$$GT$::from_iter::h14e3944acd671fa2+0x0 (fuzz_ripunzip:x86_64+0x10014a5c0) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
#231 NEW cov: 2108 ft: 2245 corp: 7/35b lim: 6 exec/s: 231 rss: 60Mb L: 6/6 MS: 4 CopyPart-ShuffleBytes-ShuffleBytes-CopyPart-
NEW_FUNC[1/12]: 0x1052d2580 in core::ptr::drop_in_place$LT$fuzz_ripunzip..ZipRsError$GT$::h864830fa1576e5d5+0x0 (fuzz_ripunzip:x86_64+0x10006f580) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
NEW_FUNC[2/12]: 0x1053f7e70 in core::ptr::drop_in_place$LT$anyhow..error..ErrorImpl$LT$anyhow..error..ContextError$LT$core..mem..manually_drop..ManuallyDrop$LT$$RF$str$GT$$C$std..io..error..Error$GT$$GT$$GT$::he14691a68652570b+0x0 (fuzz_ripunzip:x86_64+0x100194e70) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
#235 NEW cov: 2193 ft: 2373 corp: 8/41b lim: 6 exec/s: 235 rss: 60Mb L: 6/6 MS: 4 InsertRepeatedBytes-CopyPart-CopyPart-ChangeByte-
#241 REDUCE cov: 2193 ft: 2373 corp: 8/40b lim: 6 exec/s: 241 rss: 61Mb L: 5/6 MS: 1 EraseBytes-
#244 NEW cov: 2193 ft: 2375 corp: 9/45b lim: 6 exec/s: 244 rss: 61Mb L: 5/6 MS: 3 ChangeByte-EraseBytes-ShuffleBytes-
#252 NEW cov: 2199 ft: 2394 corp: 10/51b lim: 6 exec/s: 252 rss: 61Mb L: 6/6 MS: 3 CrossOver-InsertByte-InsertRepeatedBytes-
#256 NEW cov: 2199 ft: 2398 corp: 11/56b lim: 6 exec/s: 256 rss: 61Mb L: 5/6 MS: 4 ChangeBit-CrossOver-CrossOver-InsertRepeatedBytes-
#273 NEW cov: 2199 ft: 2399 corp: 12/62b lim: 6 exec/s: 273 rss: 62Mb L: 6/6 MS: 2 ChangeBit-InsertByte-
#274 NEW cov: 2199 ft: 2401 corp: 13/68b lim: 6 exec/s: 274 rss: 62Mb L: 6/6 MS: 1 ChangeByte-
#282 NEW cov: 2199 ft: 2402 corp: 14/74b lim: 6 exec/s: 282 rss: 62Mb L: 6/6 MS: 3 ShuffleBytes-CrossOver-ChangeBinInt-
#285 NEW cov: 2205 ft: 2463 corp: 15/80b lim: 6 exec/s: 285 rss: 62Mb L: 6/6 MS: 3 CMP-CopyPart-CMP- DE: " \000\000\000"-"\001\000\000\005"-
#298 NEW cov: 2205 ft: 2465 corp: 16/86b lim: 6 exec/s: 298 rss: 63Mb L: 6/6 MS: 3 ChangeBit-ChangeBinInt-ChangeBinInt-
NEW_FUNC[1/1]: 0x1074d09b0 in _$LT$u8$u20$as$u20$arbitrary..Arbitrary$GT$::arbitrary::hbd4728a44a66213a+0x0 (fuzz_ripunzip:x86_64+0x10226d9b0) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
#314 NEW cov: 2219 ft: 2479 corp: 17/92b lim: 6 exec/s: 314 rss: 63Mb L: 6/6 MS: 1 ChangeByte-
#340 NEW cov: 2231 ft: 2499 corp: 18/98b lim: 6 exec/s: 340 rss: 64Mb L: 6/6 MS: 1 ChangeBinInt-
#347 NEW cov: 2232 ft: 2502 corp: 19/104b lim: 6 exec/s: 347 rss: 64Mb L: 6/6 MS: 2 ChangeBinInt-ChangeBit-
NEW_FUNC[1/27]: 0x1053a3620 in rayon_core::join::join_context::_$u7b$$u7b$closure$u7d$$u7d$::heeeee28ce9473e59+0x0 (fuzz_ripunzip:x86_64+0x100140620) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
NEW_FUNC[2/27]: 0x1053a5d20 in rayon_core::sleep::Sleep::new_jobs::hfc61bcd28f9a7e21+0x0 (fuzz_ripunzip:x86_64+0x100142d20) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
#379 NEW cov: 2447 ft: 3413 corp: 20/110b lim: 6 exec/s: 379 rss: 65Mb L: 6/6 MS: 1 ChangeBinInt-
#384 NEW cov: 2450 ft: 3427 corp: 21/116b lim: 6 exec/s: 384 rss: 65Mb L: 6/6 MS: 5 ChangeByte-CopyPart-ChangeBit-CopyPart-PersAutoDict- DE: "\001\000\000\005"-
NEW_FUNC[1/1]: 0x105285c50 in std::thread::local::fast::Key$LT$T$GT$::try_initialize::h078871c80893dca2+0x0 (fuzz_ripunzip:x86_64+0x100022c50) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
#387 NEW cov: 2469 ft: 3446 corp: 22/122b lim: 6 exec/s: 387 rss: 65Mb L: 6/6 MS: 3 CrossOver-ChangeByte-CrossOver-
#388 NEW cov: 2470 ft: 3449 corp: 23/128b lim: 6 exec/s: 388 rss: 65Mb L: 6/6 MS: 1 ChangeBinInt-
NEW_FUNC[1/3]: 0x1053b3ea0 in crossbeam_deque::deque::Worker$LT$T$GT$::pop::h027afe55d57c8358+0x0 (fuzz_ripunzip:x86_64+0x100150ea0) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
NEW_FUNC[2/3]: 0x1053b4b30 in crossbeam_deque::deque::Stealer$LT$T$GT$::steal::h20410f4e42e0f324+0x0 (fuzz_ripunzip:x86_64+0x100151b30) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
#394 NEW cov: 2497 ft: 3598 corp: 24/134b lim: 6 exec/s: 394 rss: 65Mb L: 6/6 MS: 1 ChangeBit-
#400 NEW cov: 2497 ft: 3602 corp: 25/140b lim: 6 exec/s: 400 rss: 66Mb L: 6/6 MS: 1 CopyPart-
#411 NEW cov: 2502 ft: 3618 corp: 26/146b lim: 6 exec/s: 411 rss: 66Mb L: 6/6 MS: 1 InsertByte-
#439 NEW cov: 2503 ft: 3619 corp: 27/152b lim: 6 exec/s: 439 rss: 67Mb L: 6/6 MS: 3 ChangeBit-ChangeBinInt-ShuffleBytes-
#462 NEW cov: 2504 ft: 3621 corp: 28/158b lim: 6 exec/s: 462 rss: 68Mb L: 6/6 MS: 3 ChangeBinInt-ShuffleBytes-ShuffleBytes-
#463 NEW cov: 2504 ft: 3624 corp: 29/164b lim: 6 exec/s: 463 rss: 68Mb L: 6/6 MS: 1 CrossOver-
#464 NEW cov: 2504 ft: 3630 corp: 30/170b lim: 6 exec/s: 464 rss: 68Mb L: 6/6 MS: 1 ChangeBit-
#480 NEW cov: 2505 ft: 3631 corp: 31/176b lim: 6 exec/s: 480 rss: 68Mb L: 6/6 MS: 1 CrossOver-
#486 NEW cov: 2505 ft: 3636 corp: 32/182b lim: 6 exec/s: 486 rss: 68Mb L: 6/6 MS: 1 ChangeBinInt-
#487 NEW cov: 2505 ft: 3637 corp: 33/188b lim: 6 exec/s: 487 rss: 68Mb L: 6/6 MS: 1 CopyPart-
#527 NEW cov: 2505 ft: 3641 corp: 34/194b lim: 6 exec/s: 527 rss: 69Mb L: 6/6 MS: 5 ChangeBinInt-CrossOver-CopyPart-ChangeBinInt-PersAutoDict- DE: "\001\000\000\005"-
NEW_FUNC[1/1]: 0x1052de9a0 in core::iter::traits::double_ended::DoubleEndedIterator::try_rfold::hcf0f30052624299f+0x0 (fuzz_ripunzip:x86_64+0x10007b9a0) (BuildId: d85ba98c8ed134a4845c0cff1565ef9632000000200000000100000000000d00)
#538 NEW cov: 2535 ft: 3675 corp: 35/200b lim: 6 exec/s: 538 rss: 70Mb L: 6/6 MS: 1 CopyPart-

That cov figure it the total number of code blocks or edges explored. libfuzzer will mutate the input to try to explore more and more. Every so often, it finds a way to craft the input to hit a new function, and tells us of its success, like a proud little squirrel unearthing a nut.

After typing the above paragraph, it’s now explored 3795 code blocks.

Eventually it plateaus. It’s weirdly fascinating.

So, did it work? Did it find any problems?

Yes! Immediately. For example:

$ RUST_BACKTRACE=1 cargo +nightly fuzz run fuzz_ripunzip
Finished release [optimized] target(s) in 0.21s
Finished release [optimized] target(s) in 0.20s
Running `target/x86_64-apple-darwin/release/fuzz_ripunzip -artifact_prefix=ripunzip/fuzz/artifacts/fuzz_ripunzip/ ripunzip/fuzz/corpus/fuzz_ripunzip`
fuzz_ripunzip(14468,0x7ff85b1088c0) malloc: nano zone abandoned due to inability to preallocate reserved vm space.
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3659845534
INFO: Loaded 1 modules (719987 inline 8-bit counters): 719987 [0x10c8e5fe0, 0x10c995c53),
INFO: Loaded 1 PC tables (719987 PCs): 719987 [0x10c995c58,0x10d492388),
INFO: 0 files found in ripunzip/fuzz/corpus/fuzz_ripunzip
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2 INITED exec/s: 0 rss: 53Mb
WARNING: no interesting inputs were found so far. Is the code instrumented for coverage?
This may also happen if the target rejected all inputs we tried so far
thread '<unnamed>' panicked at 'attempt to subtract with overflow', ripunzip/src/unzip/cloneable_seekable_reader.rs:93:17
stack backtrace:
0: rust_begin_unwind
at /rustc/ec56537c4325ce5b798fc3628cbdd48ba4949ae5/library/std/src/panicking.rs:575:5
1: core::panicking::panic_fmt
at /rustc/ec56537c4325ce5b798fc3628cbdd48ba4949ae5/library/core/src/panicking.rs:64:14
2: core::panicking::panic
at /rustc/ec56537c4325ce5b798fc3628cbdd48ba4949ae5/library/core/src/panicking.rs:111:5
3: <ripunzip::unzip::cloneable_seekable_reader::CloneableSeekableReader<R> as std::io::Seek>::seek
4: zip::read::<impl zip::read::zip_archive::ZipArchive<R>>::get_directory_counts
5: zip::read::<impl zip::read::zip_archive::ZipArchive<R>>::new
6: ripunzip::unzip::UnzipEngine<P>::for_file
7: fuzz_ripunzip::_::run
8: _rust_fuzzer_test_input
9: std::panicking::try::do_call
10: ___rust_try
11: _LLVMFuzzerTestOneInput
12: __ZN6fuzzer6Fuzzer15ExecuteCallbackEPKhm
13: __ZN6fuzzer6Fuzzer6RunOneEPKhmbPNS_9InputInfoEbPb
14: __ZN6fuzzer6Fuzzer16MutateAndTestOneEv
15: __ZN6fuzzer6Fuzzer4LoopERNSt3__16vectorINS_9SizedFileENS1_9allocatorIS3_EEEE
16: __ZN6fuzzer12FuzzerDriverEPiPPPcPFiPKhmE
17: _main
18: <unknown>
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
==14468== ERROR: libFuzzer: deadly signal
#0 0x10e2a6185 in __sanitizer_print_stack_trace+0x35 (librustc-nightly_rt.asan.dylib:x86_64h+0x53185) (BuildId: d21cf6fbe4db36c3a2772a5990be16c5240000001000000000070a0000030c00)
#1 0x10c2c1d1a in fuzzer::PrintStackTrace()+0x2a (fuzz_ripunzip:x86_64+0x102205d1a) (BuildId: 52587ab92ee03de78cc50b187a499a5332000000200000000100000000000d00)
#2 0x10c2b49e3 in fuzzer::Fuzzer::CrashCallback()+0x43 (fuzz_ripunzip:x86_64+0x1021f89e3) (BuildId: 52587ab92ee03de78cc50b187a499a5332000000200000000100000000000d00)
#3 0x7ff817802c1c in _sigtramp+0x1c (libsystem_platform.dylib:x86_64+0x3c1c) (BuildId: f314b62b98f43a7c82968739f8b6855a240000001000000000010d0000010d00)

NOTE: libFuzzer has rudimentary signal handlers.
Combine libFuzzer with AddressSanitizer or similar for better crash reports.
SUMMARY: libFuzzer: deadly signal
MS: 1 InsertRepeatedBytes-; base unit: adc83b19e793491b1c6ea0fd8b46cd9f32e592fc
0xa,0x0,0x0,0x0,0x0,0x0,
\012\000\000\000\000\000
artifact_prefix='ripunzip/fuzz/artifacts/fuzz_ripunzip/'; Test unit written to ripunzip/fuzz/artifacts/fuzz_ripunzip/crash-fd324ba7ec17e3df5e8735319dc70009a81c1b6c
Base64: CgAAAAAA

────────────────────────────────────────────────────────────────────────────────

Failing input:

artifacts/fuzz_ripunzip/crash-fd324ba7ec17e3df5e8735319dc70009a81c1b6c

Output of `std::fmt::Debug`:

Inputs {
zip_members: {},
compression_method: Stored,
single_threaded: false,
}

Reproduce with:

cargo fuzz run fuzz_ripunzip artifacts/fuzz_ripunzip/crash-fd324ba7ec17e3df5e8735319dc70009a81c1b6c

Minimize test case with:

cargo fuzz tmin fuzz_ripunzip artifacts/fuzz_ripunzip/crash-fd324ba7ec17e3df5e8735319dc70009a81c1b6c

────────────────────────────────────────────────────────────────────────────────

Error: Fuzz target exited with exit status: 77

Stack backtrace:
0: std::backtrace::Backtrace::create
1: std::backtrace::Backtrace::capture
2: anyhow::error::<impl anyhow::Error>::msg
3: cargo_fuzz::project::FuzzProject::exec_fuzz
4: <cargo_fuzz::options::run::Run as cargo_fuzz::RunCommand>::run_command
5: cargo_fuzz::main
6: std::sys_common::backtrace::__rust_begin_short_backtrace
7: std::rt::lang_start::{{closure}}
8: std::rt::lang_start_internal
9: _main
10: <unknown>

I’d only tested with larger zip files. As soon as I tried a tiny zip file, seeks went into negative territory, and we panicked instead of returning an error code. Oops. Note the ridiculously good output, with not just a stack trace, but the precise input that generated the problem.

It also spotted:

  • sometimes multiple threads raced to create parent directories of the members they were unzipping.
  • we took different decisions regarding filenames ending in / — I aligned to zip-rs behavior here

These were literally cases I hadn’t thought of, so weren’t in my test suite. It’s rather cool to use a technology which can uncover your Unknown Assumptions. Comparative fuzzing FTW!

PS it’s now explored 4359 code blocks

--

--

Adrian Taylor

Ade works on Chrome at Google, and likes mountain biking, climbing, snowboarding, and usually his kids. All opinions are my own.