From b3793789f6b98a42bfc506f52b2266a722fb18cc Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Mon, 9 Nov 2026 05:18:11 -0500 Subject: [PATCH] fix(create): bound a PNG's frame buffer by what its bytes can hold; no truncating JP2 box lengths (#6007) --- crates/create/src/lib.rs | 16 ++++++++++++++-- crates/create/src/tests.rs | 7 +++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/create/src/lib.rs b/crates/create/src/lib.rs index 149bd99d97..ef46ccb713 100644 --- a/crates/create/src/lib.rs +++ b/crates/create/src/lib.rs @@ -281,7 +281,8 @@ fn jp2_boxes(b: &[u8]) -> Vec<([u8; 4], &[u8])> { }, n => (8, n), }; - let end = i.saturating_add(len as usize).min(b.len()); + // `len as usize` would truncate on 32-bit targets and put the end before the payload. + let end = i.saturating_add(usize::try_from(len).unwrap_or(usize::MAX)).min(b.len()); if len < head as u64 || end <= i { break; } @@ -428,7 +429,13 @@ fn png_image(name: &str, bytes: &[u8]) -> Result { // The decoded size comes from the header alone, so a file of a few bytes can claim any // size: keep the ceiling, and ask for the memory fallibly (a failed allocation would abort // the whole app, and no `catch_unwind` stops that). - let size = reader.output_buffer_size().filter(|n| *n <= MAX_DECODED_BYTES).ok_or_else(|| bad("image too large".into()))?; + // Flate expands at most 1032:1 and decoding at most 32:1 more (1-bit palette to RGBA), so a + // file cannot honestly hold more than that: refuse a header the bytes cannot back before + // zero-filling a buffer of its size. The slack keeps tiny real images out of the check. + let size = reader + .output_buffer_size() + .filter(|n| *n <= MAX_DECODED_BYTES && *n <= bytes.len().saturating_mul(PNG_MAX_EXPANSION).saturating_add(PNG_SLACK)) + .ok_or_else(|| bad("image too large".into()))?; let mut buf = Vec::new(); buf.try_reserve_exact(size).map_err(|_| bad("not enough memory for this image".into()))?; buf.resize(size, 0); @@ -646,6 +653,11 @@ const MAX_GIF_PIXELS: u64 = 1 << 24; /// twice over. const MAX_DECODED_BYTES: usize = 2 << 30; +/// Most a PNG's decoded size can exceed its file size: Flate's 1032:1 times the 32:1 of +/// expanding a 1-bit palette to RGBA, plus a fixed allowance for small files. +const PNG_MAX_EXPANSION: usize = 1032 * 32; +const PNG_SLACK: usize = 1 << 20; + /// Most decoded bytes all the pages of one TIFF may take together: its pages stay in memory until /// they are embedded, and many small directories can share one compressed strip. Saturates to the /// whole address space on 32-bit targets (wasm32), where 4 GiB doesn't fit in `usize`. diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index 9d5a70ffcd..584b013503 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -316,6 +316,13 @@ fn a_tiny_png_claiming_huge_dimensions_is_refused_before_allocating() { } } +#[test] +fn a_small_png_claiming_a_large_frame_is_refused_before_zero_filling() { + // 100 MP RGBA = 400 MB is under the 2 GiB ceiling, but 68 bytes cannot hold it. + let r = from_images(&[("big.png".into(), png_claiming(10_000, 10_000))]); + assert!(matches!(&r, Err(CreateError::Image(_, m)) if m.contains("too large"))); +} + /// Likewise a PNG: the png crate's limits don't cover the frame buffer, which its header sizes /// (60000x60000 RGBA is 14.4 GB). Refused before anything is allocated, not after. #[test]