diff --git a/crates/gif/src/frames.rs b/crates/gif/src/frames.rs index fdaec81b7..3b511c404 100644 --- a/crates/gif/src/frames.rs +++ b/crates/gif/src/frames.rs @@ -22,13 +22,17 @@ pub struct FrameLimits { pub max_bytes: u64, /// Largest canvas (logical screen) in bytes at 4 bytes a pixel. Default 512 MiB. pub max_canvas_bytes: u64, + /// Largest single decoder allocation. `None` uses twice `max_canvas_bytes`, matching the + /// decoder's working-buffer allowance. Set this when an application has a tighter allocator + /// budget. Default `None`. + pub max_decode_bytes: Option, /// Merge a frame identical to the one before into it (its delay added). Default off. pub merge_identical: bool, } impl Default for FrameLimits { fn default() -> Self { - FrameLimits { max_frames: 100_000, max_bytes: 1 << 30, max_canvas_bytes: 512 << 20, merge_identical: false } + FrameLimits { max_frames: 100_000, max_bytes: 1 << 30, max_canvas_bytes: 512 << 20, max_decode_bytes: None, merge_identical: false } } } @@ -93,6 +97,10 @@ pub fn read_frames(bytes: &[u8], limits: &FrameLimits) -> Result limits.max_canvas_bytes { return Err(FrameError::LimitExceeded(format!("a {width}x{height} canvas is over {} bytes", limits.max_canvas_bytes))); } + let max_decode_bytes = limits.max_decode_bytes.unwrap_or_else(|| limits.max_canvas_bytes.saturating_mul(2)); + if canvas > max_decode_bytes { + return Err(FrameError::LimitExceeded(format!("a {width}x{height} canvas is over the {max_decode_bytes} byte decode-allocation limit"))); + } // The decoder sets up the whole logical screen before the first frame: the file must back it. if !crate::plausible(info.bytes_to_back(4, 1 << 28), bytes.len(), 1 << 14) { return Err(FrameError::LimitExceeded(format!("a {} byte file cannot hold a {width}x{height} picture", bytes.len()))); @@ -100,7 +108,7 @@ pub fn read_frames(bytes: &[u8], limits: &FrameLimits) -> Result, /// Merge a frame identical to the one before into it (its delay added). Default off. pub merge_identical: bool, } impl Default for FrameLimits { fn default() -> Self { - FrameLimits { max_frames: 100_000, max_bytes: 1 << 30, max_canvas_bytes: 512 << 20, merge_identical: false } + FrameLimits { max_frames: 100_000, max_bytes: 1 << 30, max_canvas_bytes: 512 << 20, max_decode_bytes: None, merge_identical: false } } } @@ -97,10 +101,17 @@ pub fn read_frames(bytes: &[u8], limits: &FrameLimits) -> Result limits.max_canvas_bytes { return Err(FrameError::LimitExceeded(format!("a {}x{} canvas is over {} bytes", info.width, info.height, limits.max_canvas_bytes))); } + let max_decode_bytes = limits.max_decode_bytes.unwrap_or_else(|| limits.max_canvas_bytes.saturating_mul(2)); + if canvas > max_decode_bytes { + return Err(FrameError::LimitExceeded(format!( + "a {}x{} canvas is over the {max_decode_bytes} byte decode-allocation limit", + info.width, info.height + ))); + } let malformed = |e: image::ImageError| FrameError::Malformed(e.to_string()); let mut decoder = WebPDecoder::new(Cursor::new(bytes)).map_err(malformed)?; let mut il = image::Limits::default(); - il.max_alloc = Some(limits.max_canvas_bytes.saturating_mul(2)); + il.max_alloc = Some(max_decode_bytes); decoder.set_limits(il).map_err(malformed)?; let (width, height) = decoder.dimensions(); if !decoder.has_animation() { @@ -243,6 +254,7 @@ mod tests { assert_eq!((a.frames.len(), a.stop), (2, Stop::ByteLimit)); assert!(matches!(read_frames(&bytes, &FrameLimits { max_canvas_bytes: 23, ..FrameLimits::default() }), Err(FrameError::LimitExceeded(_)))); assert!(read_frames(&bytes, &FrameLimits { max_canvas_bytes: 24, ..FrameLimits::default() }).is_ok()); + assert!(read_frames(&bytes, &FrameLimits { max_decode_bytes: Some(1), ..FrameLimits::default() }).is_err()); } #[test]