Skip to content

Commit 0a2952b

Browse files
committed
add a compression fuzzer that includes the gz header
1 parent 1318b34 commit 0a2952b

File tree

3 files changed

+265
-0
lines changed

3 files changed

+265
-0
lines changed

fuzz/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ path = "fuzz_targets/compress.rs"
7070
test = false
7171
doc = false
7272

73+
[[bin]]
74+
name = "compress_gz"
75+
path = "fuzz_targets/compress_gz.rs"
76+
test = false
77+
doc = false
78+
7379
[[bin]]
7480
name = "end_to_end"
7581
path = "fuzz_targets/end_to_end.rs"

fuzz/fuzz_targets/compress_gz.rs

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
#![cfg_attr(not(miri), no_main)]
2+
3+
use std::{
4+
ffi::{c_int, c_uint, c_ulong, CString},
5+
mem::{size_of, MaybeUninit},
6+
};
7+
8+
use libfuzzer_sys::{
9+
arbitrary::{self, Arbitrary},
10+
fuzz_target,
11+
};
12+
13+
use zlib_rs::{deflate::DeflateConfig, DeflateFlush, ReturnCode};
14+
15+
#[derive(Debug, Arbitrary, Clone)]
16+
struct GzHeaderData {
17+
text: i32,
18+
time: c_ulong,
19+
os: i32,
20+
extra: Vec<u8>,
21+
name: CString,
22+
comment: CString,
23+
hcrc: i32,
24+
}
25+
26+
impl GzHeaderData {
27+
fn as_gz_header(&mut self) -> libz_rs_sys::gz_header {
28+
libz_rs_sys::gz_header {
29+
text: self.text,
30+
time: self.time,
31+
xflags: 0,
32+
os: self.os,
33+
extra: self.extra.as_mut_ptr(),
34+
extra_len: self.extra.len().try_into().unwrap(),
35+
extra_max: 0, // doesn't matter for writing.
36+
name: self.name.as_ptr() as *mut u8, // hack: UB if written to, but we shouldn't write during deflate.
37+
name_max: 0, // doesn't matter for writing.
38+
comment: self.comment.as_ptr() as *mut u8, // hack: UB if written to, but we shouldn't write during deflate.
39+
comm_max: 0, // doesn't atter for writing.
40+
hcrc: self.hcrc,
41+
done: 0, // doesn't matter for writing.
42+
}
43+
}
44+
}
45+
46+
#[derive(Debug, Arbitrary)]
47+
struct Input {
48+
source: String,
49+
config: DeflateConfig,
50+
flush: DeflateFlush,
51+
header: GzHeaderData,
52+
}
53+
54+
fuzz_target!(|input: Input| {
55+
let ng = compress_gz_ng(&input);
56+
let rs = compress_gz_rs(&input);
57+
58+
assert_eq!(ng, rs)
59+
});
60+
61+
fn compress_gz_rs(input: &Input) -> Option<Vec<u8>> {
62+
use libz_rs_sys::*;
63+
64+
let Input {
65+
ref source,
66+
config,
67+
flush,
68+
ref header,
69+
} = input;
70+
71+
let mut header = header.clone();
72+
73+
// Initialize stream.
74+
let mut stream = MaybeUninit::zeroed();
75+
let err = unsafe {
76+
deflateInit2_(
77+
stream.as_mut_ptr(),
78+
config.level,
79+
config.method as i32,
80+
config.window_bits,
81+
config.mem_level,
82+
config.strategy as i32,
83+
zlibVersion(),
84+
size_of::<z_stream>() as c_int,
85+
)
86+
};
87+
88+
if err != ReturnCode::Ok as i32 {
89+
return None;
90+
}
91+
92+
let streamp = unsafe { stream.assume_init_mut() };
93+
94+
// Create header.
95+
let mut header = header.as_gz_header();
96+
let err = unsafe { deflateSetHeader(streamp, &mut header as gz_headerp) };
97+
if err != ReturnCode::Ok as i32 {
98+
// Deallocate, so that we don't trigger ASAN leak detector.
99+
let err = unsafe { deflateEnd(streamp) };
100+
assert_eq!(err, ReturnCode::Ok as i32);
101+
return None;
102+
}
103+
104+
let bound = unsafe { deflateBound(streamp, source.len() as u64) };
105+
let buf_size = match flush {
106+
DeflateFlush::NoFlush | DeflateFlush::Finish => bound,
107+
// Other flush options might require more than `bound` bytes, so add a safety margin. We
108+
// _could_ catch Z_BUF_ERROR to allocate more memory, but that's not interesting right now.
109+
// (In fact, it's more interesting if NoFlush/Finish triggers UB write if we're
110+
// miscalculating)
111+
DeflateFlush::PartialFlush
112+
| DeflateFlush::SyncFlush
113+
| DeflateFlush::FullFlush
114+
| DeflateFlush::Block => bound * 2,
115+
};
116+
117+
// We deliberately use uninitialized memory as the input buffer here, to simulate how these
118+
// functions will likely be used from C, and to catch us ever reading uninitialized memory.
119+
let mut dest: Vec<u8> = Vec::with_capacity(buf_size as usize);
120+
121+
let max = c_uint::MAX as usize;
122+
123+
let mut left = dest.capacity();
124+
let mut source_len = source.len();
125+
126+
streamp.next_in = source.as_ptr().cast_mut().cast();
127+
streamp.next_out = dest.as_mut_ptr().cast();
128+
129+
loop {
130+
if streamp.avail_out == 0 {
131+
streamp.avail_out = Ord::min(left, max) as _;
132+
left -= streamp.avail_out as usize;
133+
}
134+
135+
if streamp.avail_in == 0 {
136+
streamp.avail_in = Ord::min(source_len, max) as _;
137+
source_len -= streamp.avail_in as usize;
138+
}
139+
140+
let flush = if source_len > 0 {
141+
*flush
142+
} else {
143+
DeflateFlush::Finish
144+
};
145+
146+
let err = unsafe { deflate(streamp, flush as i32) };
147+
if ReturnCode::from(err) != ReturnCode::Ok {
148+
break;
149+
}
150+
}
151+
152+
unsafe { dest.set_len(streamp.total_out as usize) }
153+
154+
let err = unsafe { deflateEnd(streamp) };
155+
assert_eq!(ReturnCode::from(err), ReturnCode::Ok);
156+
157+
Some(dest)
158+
}
159+
160+
fn compress_gz_ng(input: &Input) -> Option<Vec<u8>> {
161+
use libz_ng_sys::*;
162+
163+
let Input {
164+
ref source,
165+
config,
166+
flush,
167+
ref header,
168+
} = input;
169+
170+
let mut header = header.clone();
171+
172+
// Initialize stream.
173+
let mut stream = MaybeUninit::zeroed();
174+
let err = unsafe {
175+
deflateInit2_(
176+
stream.as_mut_ptr(),
177+
config.level,
178+
config.method as i32,
179+
config.window_bits,
180+
config.mem_level,
181+
config.strategy as i32,
182+
zlibVersion(),
183+
size_of::<z_stream>() as c_int,
184+
)
185+
};
186+
187+
if err != ReturnCode::Ok as i32 {
188+
return None;
189+
}
190+
191+
let streamp = unsafe { stream.assume_init_mut() };
192+
193+
// Create header. The layout is the same between zlib-rs and zlib-ng.
194+
let mut header = unsafe { core::mem::transmute::<libz_rs_sys::gz_header, gz_header>(header.as_gz_header()) };
195+
196+
let err = unsafe { deflateSetHeader(streamp, &mut header as gz_headerp) };
197+
if err != ReturnCode::Ok as i32 {
198+
// Deallocate, so that we don't trigger ASAN leak detector.
199+
let err = unsafe { deflateEnd(streamp) };
200+
assert_eq!(err, ReturnCode::Ok as i32);
201+
return None;
202+
}
203+
204+
let bound = unsafe { deflateBound(streamp, source.len() as u64) };
205+
let buf_size = match flush {
206+
DeflateFlush::NoFlush | DeflateFlush::Finish => bound,
207+
// Other flush options might require more than `bound` bytes, so add a safety margin. We
208+
// _could_ catch Z_BUF_ERROR to allocate more memory, but that's not interesting right now.
209+
// (In fact, it's more interesting if NoFlush/Finish triggers UB write if we're
210+
// miscalculating)
211+
DeflateFlush::PartialFlush
212+
| DeflateFlush::SyncFlush
213+
| DeflateFlush::FullFlush
214+
| DeflateFlush::Block => bound * 2,
215+
};
216+
217+
// We deliberately use uninitialized memory as the input buffer here, to simulate how these
218+
// functions will likely be used from C, and to catch us ever reading uninitialized memory.
219+
let mut dest: Vec<u8> = Vec::with_capacity(buf_size as usize);
220+
221+
let max = c_uint::MAX as usize;
222+
223+
let mut left = dest.capacity();
224+
let mut source_len = source.len();
225+
226+
streamp.next_in = source.as_ptr().cast_mut().cast();
227+
streamp.next_out = dest.as_mut_ptr().cast();
228+
229+
loop {
230+
if streamp.avail_out == 0 {
231+
streamp.avail_out = Ord::min(left, max) as _;
232+
left -= streamp.avail_out as usize;
233+
}
234+
235+
if streamp.avail_in == 0 {
236+
streamp.avail_in = Ord::min(source_len, max) as _;
237+
source_len -= streamp.avail_in as usize;
238+
}
239+
240+
let flush = if source_len > 0 {
241+
*flush
242+
} else {
243+
DeflateFlush::Finish
244+
};
245+
246+
let err = unsafe { deflate(streamp, flush as i32) };
247+
if ReturnCode::from(err) != ReturnCode::Ok {
248+
break;
249+
}
250+
}
251+
252+
unsafe { dest.set_len(streamp.total_out as _) }
253+
254+
let err = unsafe { deflateEnd(streamp) };
255+
assert_eq!(ReturnCode::from(err), ReturnCode::Ok);
256+
257+
Some(dest)
258+
}

zlib-rs/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub const MAX_WBITS: i32 = 15; // 32kb LZ77 window
5353
pub(crate) const DEF_WBITS: i32 = MAX_WBITS;
5454

5555
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56+
#[cfg_attr(feature = "__internal-fuzz", derive(arbitrary::Arbitrary))]
5657
pub enum DeflateFlush {
5758
#[default]
5859
/// if flush is set to `NoFlush`, that allows deflate to decide how much data

0 commit comments

Comments
 (0)