fix(create): refuse a GIF screen past 4096x4096 and convert its canvas in place

A one-pixel GIF on a huge logical screen decodes to a canvas that size,
which the conversion then copied before splitting it into colour and alpha,
so a few bytes could hold over a gigabyte. Screens past 16 Mpx are refused
before decoding, and the decoded canvas is converted without a copy.
This commit is contained in:
aiongg
2026-10-22 21:17:15 +00:00
parent 7e9f5a4435
commit cad0fe4526
2 changed files with 31 additions and 1 deletions
+12 -1
View File
@@ -393,11 +393,22 @@ fn rgba_image(rgba: &[u8], (w, h): (u32, u32), dpi: (f64, f64)) -> Embedded {
/// BMP and GIF (the first frame), through the `image` decoders. Their resolution is not read:
/// 72 dpi, as for images without one.
fn decoded(name: &str, bytes: &[u8], format: image::ImageFormat) -> Result<Embedded, CreateError> {
// A GIF's first frame is painted on a canvas the size of its logical screen, however small
// the frame: a few bytes can ask for hundreds of MB.
if format == image::ImageFormat::Gif
&& let Some(&[w0, w1, h0, h1]) = bytes.get(6..10)
&& u64::from(u16::from_le_bytes([w0, w1])) * u64::from(u16::from_le_bytes([h0, h1])) > MAX_GIF_PIXELS
{
return Err(CreateError::Image(name.into(), "the GIF's screen is larger than 4096×4096 pixels".into()));
}
let img = image::load_from_memory_with_format(bytes, format).map_err(|e| CreateError::Image(name.into(), e.to_string()))?;
let rgba = img.to_rgba8();
let rgba = img.into_rgba8();
Ok(rgba_image(rgba.as_raw(), rgba.dimensions(), (72.0, 72.0)))
}
/// Largest GIF logical screen, in pixels (4096×4096).
const MAX_GIF_PIXELS: u64 = 1 << 24;
/// Most decoded bytes one TIFF page or PNG may take: room for the 768 MiB CMYK poster in #665,
/// twice over.
const MAX_DECODED_BYTES: usize = 2 << 30;
+19
View File
@@ -1222,3 +1222,22 @@ fn tiff_pages_are_decoded_within_a_total_budget() {
let mut doc = Document::new_empty();
assert!(image_xobject(&mut doc, "t.tif", &tif).is_ok(), "the unreadable last page is never decoded");
}
/// A one-pixel GIF on a `w`×`h` logical screen: the decoder paints it on a canvas that size.
fn gif_on_screen(w: u16, h: u16) -> Vec<u8> {
let mut gif = b"GIF89a".to_vec();
gif.extend_from_slice(&w.to_le_bytes());
gif.extend_from_slice(&h.to_le_bytes());
gif.extend_from_slice(&[0x80, 0, 0, 0xFF, 0xFF, 0xFF, 0, 0, 0]);
gif.extend_from_slice(&[0x2C, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 0x44, 1, 0, 0x3B]);
gif
}
/// The decoder's own limit lets a 74-byte GIF ask for a canvas of hundreds of MB, which the
/// conversion then copies; a screen past [`MAX_GIF_PIXELS`] is refused before decoding.
#[test]
fn a_tiny_gif_on_a_huge_screen_is_refused() {
assert!(matches!(from_images(&[("big.gif".into(), gif_on_screen(4200, 4100))]), Err(CreateError::Image(..))));
let doc = reopen(&from_images(&[("small.gif".into(), gif_on_screen(40, 30))]).unwrap());
assert_eq!(media(&pages(&doc)[0]), [0.0, 0.0, 40.0, 30.0]);
}