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.
This commit is contained in:
VoiceLessQ committed 2026-10-30 00:40:00 -02:00
1 parent 033fc2e17e
commit be81d5db0e
3 files changed
+60

No files matched your search

+3
View File
@@ -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<CmykRaster> {
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,
+54
View File
@@ -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<u8> {
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<u8> {
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);
}
+3
View File
@@ -39,6 +39,9 @@ pub fn tone(bytes: &[u8]) -> Option<Tone> {
},
// 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;