Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cap buffer sizes via ZlibStream::set_max_total_output. #429

Merged
merged 2 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ include = [
[dependencies]
bitflags = "1.0"
crc32fast = "1.2.0"
fdeflate = "0.3.1"
fdeflate = "0.3.3"
flate2 = "1.0"
miniz_oxide = { version = "0.7.1", features = ["simd"] }

Expand Down
41 changes: 40 additions & 1 deletion src/decoder/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1313,6 +1313,16 @@ impl StreamingDecoder {
}
};

if let Some(mut raw_row_len) = color_type.checked_raw_row_length(bit_depth, width) {
if interlaced {
// This overshoots, but overestimating should be fine.
// TODO: Calculate **exact** IDAT size for interlaced images.
raw_row_len = raw_row_len.saturating_mul(2);
fintelia marked this conversation as resolved.
Show resolved Hide resolved
}
self.inflater
.set_max_total_output((height as usize).saturating_mul(raw_row_len));
}

self.info = Some(Info {
width,
height,
Expand Down Expand Up @@ -1841,7 +1851,7 @@ mod tests {
let png = {
let mut png = Vec::new();
write_fdat_prefix(&mut png, 2, 8);
let image_data = generate_rgba8_with_width(8);
let image_data = generate_rgba8_with_width_and_height(8, 8);
write_fdat(&mut png, 2, &image_data[..30]);
write_fdat(&mut png, 3, &image_data[30..]);
write_iend(&mut png);
Expand Down Expand Up @@ -1892,4 +1902,33 @@ mod tests {

assert_eq!(first_frame, second_frame);
}

#[test]
fn test_idat_bigger_than_image_size_from_ihdr() {
let png = {
let mut png = Vec::new();
write_png_sig(&mut png);
write_rgba8_ihdr_with_width(&mut png, 8);

// Here we want to test an invalid image where the `IDAT` chunk contains more data
// (data for 8x256 image) than declared in the `IHDR` chunk (which only describes an
// 8x8 image).
write_chunk(
&mut png,
b"IDAT",
&generate_rgba8_with_width_and_height(8, 256),
);

write_iend(&mut png);
png
};
let decoder = Decoder::new(png.as_slice());
let mut reader = decoder.read_info().unwrap();
let mut buf = vec![0; reader.output_buffer_size()];

// TODO: Should this return an error instead? For now let's just have test assertions for
// the current behavior.
reader.next_frame(&mut buf).unwrap();
assert_eq!(3093270825, crc32fast::hash(&buf));
}
}
48 changes: 37 additions & 11 deletions src/decoder/zlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ pub(super) struct ZlibStream {
out_buffer: Vec<u8>,
/// The cursor position in the output stream as a buffer index.
out_pos: usize,
/// Limit on how many bytes can be decompressed in total. This field is mostly used for
/// performance optimizations (e.g. to avoid allocating and zeroing out large buffers when only
/// a small image is being decoded).
max_total_output: usize,
/// Ignore and do not calculate the Adler-32 checksum. Defaults to `true`.
///
/// This flag overrides `TINFL_FLAG_COMPUTE_ADLER32`.
Expand All @@ -27,8 +31,9 @@ impl ZlibStream {
ZlibStream {
state: Box::new(Decompressor::new()),
started: false,
out_buffer: vec![0; 2 * CHUNCK_BUFFER_SIZE],
out_buffer: Vec::new(),
out_pos: 0,
max_total_output: usize::MAX,
ignore_adler32: true,
}
}
Expand All @@ -37,9 +42,14 @@ impl ZlibStream {
self.started = false;
self.out_buffer.clear();
self.out_pos = 0;
self.max_total_output = usize::MAX;
*self.state = Decompressor::new();
}

pub(crate) fn set_max_total_output(&mut self, n: usize) {
self.max_total_output = n;
}

/// Set the `ignore_adler32` flag and return `true` if the flag was
/// successfully set.
///
Expand Down Expand Up @@ -101,10 +111,9 @@ impl ZlibStream {
return Ok(());
}

loop {
while !self.state.is_done() {
anforowicz marked this conversation as resolved.
Show resolved Hide resolved
self.prepare_vec_for_appending();

let (in_consumed, out_consumed) = self
fintelia marked this conversation as resolved.
Show resolved Hide resolved
let (_in_consumed, out_consumed) = self
.state
.read(&[], self.out_buffer.as_mut_slice(), self.out_pos, true)
.map_err(|err| {
Expand All @@ -113,23 +122,38 @@ impl ZlibStream {

self.out_pos += out_consumed;

if self.state.is_done() {
self.out_buffer.truncate(self.out_pos);
image_data.append(&mut self.out_buffer);
return Ok(());
} else {
if !self.state.is_done() {
let transferred = self.transfer_finished_data(image_data);
assert!(
transferred > 0 || in_consumed > 0 || out_consumed > 0,
transferred > 0 || out_consumed > 0,
"No more forward progress made in stream decoding."
);
}
}

self.out_buffer.truncate(self.out_pos);
image_data.append(&mut self.out_buffer);
Ok(())
}

/// Resize the vector to allow allocation of more data.
fn prepare_vec_for_appending(&mut self) {
if self.out_buffer.len().saturating_sub(self.out_pos) >= CHUNCK_BUFFER_SIZE {
// The `debug_assert` below explains why we can use `>=` instead of `>` in the condition
// that compares `self.out_post >= self.max_total_output` in the next `if` statement.
debug_assert!(!self.state.is_done());
if self.out_pos >= self.max_total_output {
// This can happen when the `max_total_output` was miscalculated (e.g.
// because the `IHDR` chunk was malformed and didn't match the `IDAT` chunk). In
// this case, let's reset `self.max_total_output` before further calculations.
self.max_total_output = usize::MAX;
}

let current_len = self.out_buffer.len();
let desired_len = self
.out_pos
.saturating_add(CHUNCK_BUFFER_SIZE)
.min(self.max_total_output);
if current_len >= desired_len {
return;
}

Expand All @@ -150,6 +174,8 @@ impl ZlibStream {
// Ensure the allocation request is valid.
// TODO: maximum allocation limits?
.min(isize::max_value() as usize)
// Don't unnecessarily allocate more than `max_total_output`.
.min(self.max_total_output)
}

fn transfer_finished_data(&mut self, image_data: &mut Vec<u8>) -> usize {
Expand Down
8 changes: 4 additions & 4 deletions src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ pub fn write_rgba8_ihdr_with_width(w: &mut impl Write, width: u32) {
write_chunk(w, b"IHDR", &data);
}

/// Generates RGBA8 `width` x `width` image and wraps it in a store-only zlib container.
pub fn generate_rgba8_with_width(width: u32) -> Vec<u8> {
/// Generates RGBA8 `width` x `height` image and wraps it in a store-only zlib container.
pub fn generate_rgba8_with_width_and_height(width: u32, height: u32) -> Vec<u8> {
// Generate arbitrary test pixels.
let image_pixels = {
let mut row = Vec::new();
Expand All @@ -88,7 +88,7 @@ pub fn generate_rgba8_with_width(width: u32) -> Vec<u8> {
row.extend(row_pixels);

std::iter::repeat(row)
.take(width as usize)
.take(height as usize)
.flatten()
.collect::<Vec<_>>()
};
Expand All @@ -104,7 +104,7 @@ pub fn generate_rgba8_with_width(width: u32) -> Vec<u8> {

/// Writes an IDAT chunk.
pub fn write_rgba8_idats(w: &mut impl Write, size: u32, idat_bytes: usize) {
let data = generate_rgba8_with_width(size);
let data = generate_rgba8_with_width_and_height(size, size);

for chunk in data.chunks(idat_bytes) {
write_chunk(w, b"IDAT", chunk);
Expand Down
Loading