From 3d112c4b8cd1bb86973c71e166d726a03237a12c Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 15 Oct 2026 18:47:52 +0700 Subject: [PATCH] #300: a colour PNG strike's size is checked before its pixels are allocated --- crates/text/src/color.rs | 12 ++++++++---- crates/text/src/color_tests.rs | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/crates/text/src/color.rs b/crates/text/src/color.rs index 16e0c759..ca927a94 100644 --- a/crates/text/src/color.rs +++ b/crates/text/src/color.rs @@ -29,6 +29,8 @@ const MAX_RASTER_BYTES: usize = 32 * 1024 * 1024; const MAX_RASTER_ENTRIES: usize = 8192; /// COLR layer lists kept per face (24 bytes a layer, so a count is enough). const MAX_LAYERS: usize = 4096; +/// Widest and tallest PNG strike decoded (strikes are at most a few hundred pixels). +const MAX_PNG_SIDE: u32 = 4096; /// Largest colour raster per em (device pixels). Past this the renderer scales one up instead. pub const MAX_PX_PER_EM: f64 = 512.0; @@ -223,13 +225,15 @@ fn decode_png(png: &[u8]) -> Option<(Vec, u32, u32)> { // Expand palettes, `tRNS` and sub-byte samples to 8-bit RGBA. decoder.set_transformations(png::Transformations::normalize_to_color8() | png::Transformations::ALPHA); let mut reader = decoder.read_info().ok()?; - let mut buf = vec![0u8; reader.output_buffer_size()?]; - let info = reader.next_frame(&mut buf).ok()?; - if info.width == 0 || info.height == 0 || info.width > 4096 || info.height > 4096 { + // The header's size is checked before anything is allocated for it: a font can claim any size. + let (w, h) = reader.info().size(); + if w == 0 || h == 0 || w > MAX_PNG_SIDE || h > MAX_PNG_SIDE { return None; } + let mut buf = vec![0u8; reader.output_buffer_size()?]; + let info = reader.next_frame(&mut buf).ok()?; let (width, height) = (info.width, info.height); - let data = &buf[..info.buffer_size()]; + let data = buf.get(..info.buffer_size())?; let rgba = match info.color_type { png::ColorType::Rgba => data.to_vec(), png::ColorType::Rgb => data.as_chunks::<3>().0.iter().flat_map(|p| [p[0], p[1], p[2], 255]).collect(), diff --git a/crates/text/src/color_tests.rs b/crates/text/src/color_tests.rs index d98f8d76..929d3112 100644 --- a/crates/text/src/color_tests.rs +++ b/crates/text/src/color_tests.rs @@ -86,6 +86,22 @@ fn png_strikes_decode_to_premultiplied_rgba() { ); } +#[test] +fn a_png_strike_claiming_a_huge_size_is_refused_before_anything_is_allocated() { + // PNG_STRAIGHT with its header saying 1 048 576 × 1 048 576 pixels (4 TiB as RGBA). + let mut png = PNG_STRAIGHT.to_vec(); + assert_eq!(crc32(&png[12..29]).to_be_bytes(), png[29..33], "the patched header passes its checksum"); + png[16..24].copy_from_slice(&[0, 0x10, 0, 0, 0, 0x10, 0, 0]); + let crc = crc32(&png[12..29]); + png[29..33].copy_from_slice(&crc.to_be_bytes()); + assert!(decode_png(&png).is_none()); +} + +/// The CRC-32 a PNG chunk ends with. +fn crc32(bytes: &[u8]) -> u32 { + !bytes.iter().fold(!0u32, |c, b| (0..8).fold(c ^ u32::from(*b), |c, _| if c & 1 == 1 { 0xedb8_8320 ^ (c >> 1) } else { c >> 1 })) +} + #[test] fn raw_bgra_strikes_are_already_premultiplied() { // BitmapData::Bgra is BGRA with premultiplied channels (the spec says so), so decoding only