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

Fix gif OOM when decoding #1696

Merged
merged 2 commits into from Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 26 additions & 2 deletions src/codecs/gif.rs
Expand Up @@ -43,22 +43,36 @@ use crate::error::{
UnsupportedError, UnsupportedErrorKind,
};
use crate::image::{self, AnimationDecoder, ImageDecoder, ImageFormat};
use crate::io::Limits;
use crate::traits::Pixel;
use crate::ImageBuffer;

/// GIF decoder
pub struct GifDecoder<R: Read> {
reader: gif::Decoder<R>,
limits: Limits,
}

impl<R: Read> GifDecoder<R> {
/// Creates a new decoder that decodes the input steam ```r```
/// Creates a new decoder that decodes the input steam `r`
pub fn new(r: R) -> ImageResult<GifDecoder<R>> {
let mut decoder = gif::DecodeOptions::new();
decoder.set_color_output(ColorOutput::RGBA);

Ok(GifDecoder {
reader: decoder.read_info(r).map_err(ImageError::from_decoding)?,
limits: Limits::default(),
})
}

/// Creates a new decoder that decodes the input steam `r`, using limits `limits`
pub fn with_limits(r: R, limits: Limits) -> ImageResult<GifDecoder<R>> {
let mut decoder = gif::DecodeOptions::new();
decoder.set_color_output(ColorOutput::RGBA);

Ok(GifDecoder {
reader: decoder.read_info(r).map_err(ImageError::from_decoding)?,
limits,
})
}
}
Expand Down Expand Up @@ -154,7 +168,17 @@ impl<'a, R: 'a + Read> ImageDecoder<'a> for GifDecoder<R> {
} else {
// If the frame does not match the logical screen, read into an extra buffer
// and 'insert' the frame from left/top to logical screen width/height.
let mut frame_buffer = vec![0; self.reader.buffer_size()];
let buffer_size = self.reader.buffer_size();

self.limits.reserve_usize(buffer_size)?;

let mut frame_buffer = vec![0; buffer_size];

self.limits.free(
Copy link
Contributor Author

@5225225 5225225 Apr 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the reserve/free API is good, but I needed to free earlier than this buffer is actually freed to avoid the limits not actually being reset (in the event of a EOF when reading, I want to reset the limits back to what they were when we were given it).

Maybe an alternate API like a Limits being an Allocator wrapper would be useful. Not sure! Probably best to just get more examples of code that use the limits API and then see what fits them the best.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably correct, an allocator API would be interesting but it would need to interact with all containers. Vec and Box is potentially a good start but not generic enough. But it's hard to say if it was better to wait on an the allocator interface or to solve it separately.

Given that there is no concurrency in this specific, I would say the order doesn't matter to here. The only effect of free is to allow following reserve calls to succeed.

u64::try_from(buffer_size)
.expect("if buffer_size overflows a usize, we should have returned already"),
HeroicKatora marked this conversation as resolved.
Show resolved Hide resolved
);

self.reader
.read_into_buffer(&mut frame_buffer[..])
.map_err(ImageError::from_decoding)?;
Expand Down
20 changes: 19 additions & 1 deletion src/io/mod.rs
Expand Up @@ -122,8 +122,26 @@ impl Limits {
Ok(())
}

/// This function acts identically to [`reserve`], but takes a `usize` for convenience.
pub fn reserve_usize(&mut self, amount: usize) -> ImageResult<()> {
use std::convert::TryFrom;

match u64::try_from(amount) {
Ok(n) => self.reserve(n),
Err(_) if self.max_alloc.is_some() => {
return Err(ImageError::Limits(error::LimitError::from_kind(
error::LimitErrorKind::InsufficientMemory,
)));
}
Err(_) => {
// Out of bounds, but we weren't asked to consider any limit.
Ok(())
}
}
}

/// This function increases the `max_alloc` limit with amount. Should only be used
/// togheter with [`reserve`].
/// together with [`reserve`].
///
/// [`reserve`]: #method.reserve
pub fn free(&mut self, amount: u64) {
Expand Down
20 changes: 20 additions & 0 deletions tests/regression.rs
Expand Up @@ -59,3 +59,23 @@ fn bad_bmps() {
assert!(im.is_err());
}
}

#[test]
fn bad_gif_oom() {
let data = [
71, 73, 70, 56, 55, 97, 0, 0, 0, 0, 0, 0, 0, 44, 255, 255, 219, 255, 172, 199, 199, 255,
216, 255, 255, 0, 0, 48, 230, 0, 195, 195, 195, 195, 255, 239, 0,
];

// The original code made a vec![0; very_large_number] which due to overcommit *does not* OOM.
// It then exits normally with an EOF when reading.
//
// So instead we look for a limits error (or an unsupported error, for the case that we're
// running these tests without bmp being actually supported)
let error = image::load_from_memory(&data).unwrap_err();

assert!(
matches!(error, image::ImageError::Limits(_))
| matches!(error, image::ImageError::Unsupported(_))
);
}