From be81d5db0e458998e18eeb64a635b41a4471f0a2 Mon Sep 17 00:00:00 2001 From: VoiceLessQ Date: Fri, 30 Oct 2026 00:40:00 -0200 Subject: [PATCH] Images: check the RGBA budget before GIF tone and CMYK TIFF decodes (#1586) tone() decoded a GIF to RGBA and decode_cmyk_tiff() called read_image() without the header check decode_rgba() runs. Both now call too_large_to_decode() first. The decoders' own limits (image 512 MiB, tiff 256 MiB) already refused these files; the guard refuses them from the header instead. --- crates/images/src/lib.rs | 3 +++ crates/images/src/tests.rs | 54 ++++++++++++++++++++++++++++++++++++++ crates/images/src/tone.rs | 3 +++ 3 files changed, 60 insertions(+) diff --git a/crates/images/src/lib.rs b/crates/images/src/lib.rs index dfe4166b7..97067ac41 100644 --- a/crates/images/src/lib.rs +++ b/crates/images/src/lib.rs @@ -532,6 +532,9 @@ pub fn is_cmyk_tiff(bytes: &[u8]) -> bool { /// over the decoder's 256 MiB limit; [`is_cmyk_tiff`] tells those apart from RGB files. pub fn decode_cmyk_tiff(bytes: &[u8]) -> Option { use tiff::tags::Tag; + if too_large_to_decode(bytes) { + return None; + } let mut d = tiff_decoder(bytes)?; let has_alpha = match d.colortype().ok()? { tiff::ColorType::CMYK(8 | 16) => false, diff --git a/crates/images/src/tests.rs b/crates/images/src/tests.rs index 9ca9fe14f..dd09f2577 100644 --- a/crates/images/src/tests.rs +++ b/crates/images/src/tests.rs @@ -441,3 +441,57 @@ fn rasters_past_the_rgba_budget_are_refused_before_decoding() { let small = decode_rgba(&blank_png(9)).expect("decodes"); assert_eq!(small.get_pixel(8, 8).0, [0, 0, 0, 255]); } + +/// A GIF whose logical screen is `side` x `side`, holding one black 1x1 frame. +fn gif_screen(side: u16) -> Vec { + let mut gif = b"GIF89a".to_vec(); + gif.extend(side.to_le_bytes()); + gif.extend(side.to_le_bytes()); + // Two-colour global table: black, white. + gif.extend([0x80, 0, 0, 0, 0, 0, 255, 255, 255]); + // Frame at the origin; LZW minimum code size 2, codes clear, 0, end; trailer. + gif.extend([0x2C, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 0x44, 0x01, 0, 0x3B]); + gif +} + +/// The header of an uncompressed 8-bit CMYK TIFF of `side` x `side` pixels, without the pixels. +fn cmyk_tiff_header(side: u32) -> Vec { + let entries: [(u16, u16, u32, u32); 9] = [ + (256, 4, 1, side), // width + (257, 4, 1, side), // height + (258, 3, 4, 122), // bits per sample, stored after the IFD + (259, 3, 1, 1), // no compression + (262, 3, 1, 5), // separated (CMYK) + (273, 4, 1, 130), // strip offset + (277, 3, 1, 4), // samples per pixel + (278, 4, 1, side), // rows per strip + (279, 4, 1, side * side * 4), // strip byte count + ]; + let mut t = b"II*\0".to_vec(); + t.extend(8u32.to_le_bytes()); + t.extend(9u16.to_le_bytes()); + for (tag, kind, count, value) in entries { + t.extend(tag.to_le_bytes()); + t.extend(kind.to_le_bytes()); + t.extend(count.to_le_bytes()); + t.extend(value.to_le_bytes()); + } + t.extend(0u32.to_le_bytes()); + assert_eq!(t.len(), 122); + t.extend([8u16; 4].iter().flat_map(|b| b.to_le_bytes())); + t +} + +/// GIF tones and CMYK inks check the RGBA budget too (#1586). +#[test] +fn gif_tone_and_cmyk_inks_check_the_rgba_budget() { + let gif = gif_screen(16_385); + assert!(too_large_to_decode(&gif)); + assert_eq!(tone(&gif), None); + assert_eq!(tone(&gif_screen(2)), Some(Tone::Bitmap)); + let tiff = cmyk_tiff_header(16_385); + assert!(is_cmyk_tiff(&tiff)); + assert_eq!(pixel_size(&tiff), Some((16_385, 16_385))); + assert!(too_large_to_decode(&tiff)); + assert_eq!(decode_cmyk_tiff(&tiff), None); +} diff --git a/crates/images/src/tone.rs b/crates/images/src/tone.rs index 10be68445..3d0b945ce 100644 --- a/crates/images/src/tone.rs +++ b/crates/images/src/tone.rs @@ -39,6 +39,9 @@ pub fn tone(bytes: &[u8]) -> Option { }, // Indexed: what the palette holds decides. Transparent pixels are left out of a bitmap. F::Gif => { + if crate::too_large_to_decode(bytes) { + return None; + } let img = image::load_from_memory_with_format(bytes, F::Gif).ok()?.into_rgba8(); let mut opaque = true; let mut two_level = true;