Skip to content

Commit 043efbb

Browse files
clearly describe how to perform fuzzing, and make a single workspace
- add publish = false to fuzz subcrates
1 parent 72cce40 commit 043efbb

File tree

10 files changed

+195
-56
lines changed

10 files changed

+195
-56
lines changed

Cargo.toml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,34 @@ keywords = ["zip", "archive", "compression"]
1313
# Any change to rust-version must be reflected also in `README.md` and `.github/workflows/ci.yaml`.
1414
# The MSRV policy is documented in `README.md`.
1515
rust-version = "1.83.0"
16+
categories = ["compression", "filesystem", "parser-implementations"]
1617
description = """
1718
Library to support the reading and writing of zip files.
1819
"""
19-
edition = "2021"
20+
edition.workspace = true
2021
exclude = ["tests/**", "examples/**", ".github/**", "fuzz_read/**", "fuzz_write/**"]
2122

2223
[package.metadata.docs.rs]
2324
all-features = true
2425
rustdoc-args = ["--cfg", "docsrs"]
2526

27+
[workspace]
28+
members = [".", "fuzz_read", "fuzz_write"]
29+
default-members = ["."]
30+
resolver = "2"
31+
32+
[workspace.package]
33+
edition = "2021"
34+
2635
[workspace.dependencies]
36+
afl = "0.15"
37+
arbitrary = { version = "1.4.1" }
38+
tikv-jemallocator = "0.6.0"
2739
time = { version = "0.3.37", default-features = false }
40+
zip = { path = ".", default-features = false }
2841

2942
[dependencies]
43+
arbitrary = { workspace = true, features = ["derive"], optional = true }
3044
aes = { version = "0.8", optional = true }
3145
bzip2 = { version = "0.6.0", optional = true }
3246
chrono = { version = "^0.4.27", optional = true }
@@ -52,9 +66,6 @@ deflate64 = { version = "0.1.9", optional = true }
5266
lzma-rust2 = { version = "0.13", optional = true, default-features = false, features = ["std", "encoder", "optimization", "xz"] }
5367
bitstream-io = { version = "4.5.0", optional = true }
5468

55-
[target.'cfg(fuzzing)'.dependencies]
56-
arbitrary = { version = "1.4.1", features = ["derive"] }
57-
5869
[dev-dependencies]
5970
bencher = "0.1.5"
6071
getrandom = { version = "0.3.1", features = ["wasm_js", "std"] }
@@ -65,6 +76,7 @@ clap = { version = "=4.4.18", features = ["derive"] }
6576
tempfile = "3.15"
6677

6778
[features]
79+
arbitrary = ["dep:arbitrary"]
6880
aes-crypto = ["dep:aes", "dep:constant_time_eq", "hmac", "pbkdf2", "sha1", "getrandom", "zeroize"]
6981
chrono = ["dep:chrono"]
7082
_deflate-any = []

README.md

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
zip
2-
========
2+
===
33

44
[![Build Status](https://github.com/zip-rs/zip2/actions/workflows/ci.yaml/badge.svg)](https://github.com/Pr0methean/zip/actions?query=branch%3Amaster+workflow%3ACI)
55
[![Crates.io version](https://img.shields.io/crates/v/zip.svg)](https://crates.io/crates/zip)
66

77
[Documentation](https://docs.rs/zip/latest/zip/)
88

9-
Info
10-
----
11-
9+
# Info
1210

1311
A zip library for rust which supports reading and writing of simple ZIP files. Formerly hosted at
1412
https://github.com/zip-rs/zip2.
@@ -28,8 +26,7 @@ Currently unsupported zip extensions:
2826

2927
* Multi-disk
3028

31-
Features
32-
--------
29+
# Features
3330

3431
The features available are:
3532

@@ -53,17 +50,15 @@ The features available are:
5350

5451
By default `aes-crypto`, `bzip2`, `deflate`, `deflate64`, `lzma`, `ppmd`, `time`, `xz` and `zstd` are enabled.
5552

56-
MSRV
57-
----
53+
# MSRV
5854

5955
Our current Minimum Supported Rust Version is **1.83**. When adding features,
6056
we will follow these guidelines:
6157

6258
- We will always support a minor Rust version that has been stable for at least 6 months.
6359
- Any change to the MSRV will be accompanied with a **minor** version bump.
6460

65-
Examples
66-
--------
61+
# Examples
6762

6863
See the [examples directory](examples) for:
6964

@@ -74,10 +69,9 @@ See the [examples directory](examples) for:
7469
* How to read a zip from the standard input.
7570
* How to append a directory to an existing archive
7671

77-
Fuzzing
78-
-------
72+
# Fuzzing
7973

80-
Fuzzing support is through [cargo afl](https://rust-fuzz.github.io/book/afl.html). To install cargo afl:
74+
Fuzzing support is through [`cargo afl`](https://rust-fuzz.github.io/book/afl/tutorial.html). To install `cargo afl`:
8175

8276
```bash
8377
cargo install cargo-afl
@@ -86,13 +80,65 @@ cargo install cargo-afl
8680
To start fuzzing zip extraction:
8781

8882
```bash
89-
cargo +nightly afl build --all-features --manifest-path fuzz_read/Cargo.toml
90-
cargo +nightly afl run fuzz_read/target/debug/fuzz_read
83+
mkdir -vp fuzz-read-out
84+
cargo afl build --all-features -p fuzz_read
85+
# Curated input corpus:
86+
cargo afl fuzz -i fuzz_read/in -o fuzz-read-out target/debug/fuzz_read
87+
# Test data files:
88+
cargo afl fuzz -i tests/data -e zip -o fuzz-read-out target/debug/fuzz_read
9189
```
9290

9391
To start fuzzing zip creation:
9492

9593
```bash
96-
cargo +nightly afl build --all-features --manifest-path fuzz_write/Cargo.toml
97-
cargo +nightly afl run fuzz_write/target/debug/fuzz_write
94+
mkdir -vp fuzz-write-out
95+
cargo afl build --all-features -p fuzz_write
96+
# Curated input corpus and dictionary schema:
97+
cargo afl fuzz -x fuzz_write/fuzz.dict -i fuzz_write/in -o fuzz-write-out target/debug/fuzz_write
98+
```
99+
100+
## Fuzzing stdio
101+
102+
The read and write fuzzers can also receive input over stdin for one-off validation. Note here that the fuzzers can be configured to build in support for DEFLATE, or not:
103+
```bash
104+
# Success, no output:
105+
cargo run --quiet --all-features -p fuzz_read <tests/data/deflate64.zip
106+
# Error, without deflate64 support:
107+
cargo run --quiet -p fuzz_read <tests/data/deflate64.zip
108+
109+
thread 'main' (537304) panicked at fuzz_read/src/main.rs:40:36:
110+
called `Result::unwrap()` on an `Err` value: UnsupportedArchive("Compression method not supported")
111+
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
112+
```
113+
114+
The zip creation fuzzer will try to print out a description of the kind of input it translated the input bytes into:
115+
```bash
116+
# This is an empty input case:
117+
<fuzz_write/in/id-000000,time-0,execs-0,orig-0011743621118ab6c5278ffbb8fd14bddd8369ee.min \
118+
cargo run --quiet --all-features -p fuzz_write
119+
# This input was translated into one or more test cases:
120+
<fuzz_write/in/id-000000,time-0,execs-0,orig-0011743621118ab6c5278ffbb8fd14bddd8369ee.min \
121+
cargo run --quiet -p fuzz_write
122+
writer.start_file_from_path("", FileOptions { compression_method: Stored, compression_level: None, last_modified_time: DateTime::from_date_and_time(2048, 1, 1, 0, 0, 0)?, permissions: None, large_file: false, encrypt_with: None, extended_options: ExtendedFileOptions {extra_data: vec![].into(), central_extra_data: vec![].into()}, alignment: 0 })?;
123+
writer.write_all(&[])?;
124+
writer
125+
let _ = writer.finish_into_readable()?;
126+
```
127+
128+
The zip creation fuzzer uses [`arbitrary::Unstructured`](https://docs.rs/arbitrary/latest/arbitrary/struct.Unstructured.html) to convert bytes over stdin to random inputs, so it can be triggered with other sources of random input:
129+
```bash
130+
# Usually, the random input is translated into zero test cases:
131+
head -c50 /dev/random | cargo run --quiet --all-features -p fuzz_write
132+
# Sometimes, one or more test cases are generated and successfully evaluated:
133+
head -c50 /dev/random | cargo run --quiet --all-features -p fuzz_write
134+
writer.set_raw_comment([20, 202])?;
135+
let mut writer = ZipWriter::new_append(writer.finish()?)?;
136+
let sub_writer = {
137+
let mut initial_junk = Cursor::new(vec![106]);
138+
initial_junk.seek(SeekFrom::End(0))?;
139+
let mut writer = ZipWriter::new(initial_junk);
140+
writer
141+
};
142+
writer.merge_archive(sub_writer.finish_into_readable()?)?;
143+
let mut writer = ZipWriter::new_append(writer.finish()?)?;
98144
```

fuzz_read/Cargo.toml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
[package]
22
name = "fuzz_read"
33
version = "0.1.0"
4-
edition = "2021"
4+
edition.workspace = true
5+
publish = false
56

67
[dependencies]
7-
afl = "0.15"
8-
tikv-jemallocator = "0.6.0"
8+
afl.workspace = true
9+
zip.workspace = true
910

10-
[dependencies.zip]
11-
path = ".."
12-
default-features = false
11+
[target.'cfg(fuzzing)'.dependencies]
12+
tikv-jemallocator.workspace = true
1313

1414
[features]
1515
zip-default = ["zip/default"]
16-
17-
[workspace]

fuzz_read/src/main.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1+
#![allow(unexpected_cfgs)] // Needed for cfg(fuzzing) on nightly as of 2024-05-06
2+
3+
#[cfg(fuzzing)]
14
use afl::fuzz;
25
use std::io::{Read, Seek, SeekFrom};
6+
#[cfg(fuzzing)]
37
use tikv_jemallocator::Jemalloc;
48
use zip::read::read_zipfile_from_stream;
59

610
const MAX_BYTES_TO_READ: u64 = 1 << 24;
711

12+
#[cfg(fuzzing)]
813
#[global_allocator]
914
static GLOBAL: Jemalloc = Jemalloc;
1015

11-
fn decompress_all(data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
12-
let reader = std::io::Cursor::new(data);
16+
fn decompress_all(reader: impl Read + Seek) -> Result<(), Box<dyn std::error::Error>> {
1317
let mut zip = zip::ZipArchive::new(reader)?;
1418

1519
for i in 0..zip.len() {
@@ -25,7 +29,17 @@ fn decompress_all(data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
2529
}
2630

2731
fn main() {
32+
#[cfg(fuzzing)]
2833
fuzz!(|data: &[u8]| {
29-
let _ = decompress_all(data);
34+
let reader = std::io::Cursor::new(data);
35+
let _ = decompress_all(reader);
3036
});
37+
38+
#[cfg(not(fuzzing))]
39+
{
40+
let mut v = Vec::new();
41+
std::io::stdin().read_to_end(&mut v).unwrap();
42+
let reader = std::io::Cursor::new(v);
43+
decompress_all(reader).unwrap();
44+
}
3145
}

fuzz_write/Cargo.toml

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
[package]
22
name = "fuzz_write"
33
version = "0.1.0"
4-
edition = "2021"
4+
edition.workspace = true
5+
publish = false
56

67
[dependencies]
7-
afl = "0.15"
8-
arbitrary = { version = "1.3.2", features = ["derive"] }
8+
afl.workspace = true
9+
arbitrary = { workspace = true, features = ["derive"] }
910
replace_with = "0.1.7"
10-
tikv-jemallocator = "0.6.0"
11+
zip = { workspace = true, features = ["arbitrary"] }
1112

12-
[dependencies.zip]
13-
path = ".."
14-
default-features = false
13+
[target.'cfg(fuzzing)'.dependencies]
14+
tikv-jemallocator.workspace = true
1515

1616
[features]
1717
zip-default = ["zip/default"]
18-
19-
[workspace]

0 commit comments

Comments
 (0)