From 5d5e0892de14a90ee00f7acb88d094eb3dc97c6a Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 02:43:24 +0000 Subject: [PATCH] krilla: Image::from_custom checks channel lengths in usize and returns an error --- crates/pdf/tests/print_color.rs | 11 +++ vendor/krilla/README.craft.md | 4 + vendor/krilla/SOURCE.json | 2 +- vendor/krilla/patches/11-png-decode-cap.patch | 87 +++++++++++++++---- vendor/krilla/src/graphics/image.rs | 38 ++++---- 5 files changed, 110 insertions(+), 32 deletions(-) diff --git a/crates/pdf/tests/print_color.rs b/crates/pdf/tests/print_color.rs index b07a423a..1dc419e9 100644 --- a/crates/pdf/tests/print_color.rs +++ b/crates/pdf/tests/print_color.rs @@ -754,6 +754,17 @@ fn device_n_image_writes_one_sample_per_colorant() { assert_eq!(found, 1); } +/// `Image::from_custom` refuses channel data that doesn't match the dimensions, and dimensions +/// whose channel length overflows, instead of panicking when the page is written. +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn custom_image_with_mismatched_or_huge_dimensions_is_refused() { + let cmyk = |data: Vec, size| Samples { data, size, space: ImageColorspace::Cmyk, device_n: None }; + assert!(Image::from_custom(cmyk(vec![0; 3], (1, 1)), false).is_err()); + assert!(Image::from_custom(cmyk(vec![0; 4], (u32::MAX, u32::MAX)), false).is_err()); + assert!(Image::from_custom(cmyk(vec![0; 4], (1, 1)), false).is_ok()); +} + /// A DeviceN image needs its space from `CustomImage::device_n_space`. #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] diff --git a/vendor/krilla/README.craft.md b/vendor/krilla/README.craft.md index 4778b48b..b7862261 100644 --- a/vendor/krilla/README.craft.md +++ b/vendor/krilla/README.craft.md @@ -292,3 +292,7 @@ A PNG whose header implies more than 1 GiB of decoded output is refused by fallibly, so a small file (or a bitmap glyph in a font) cannot request an allocation that ends the process. The color and alpha buffers that PNG, GIF and WebP decoding split an image into are reserved fallibly too, and a failure there is an image error. +`Image::from_custom` computes the expected channel lengths with checked `usize` arithmetic and +returns an error when they overflow or don't match the data, where upstream multiplied in +`u32` and asserted while writing (32 DeviceN components at 16 bits overflow at about 67 +megapixels). diff --git a/vendor/krilla/SOURCE.json b/vendor/krilla/SOURCE.json index efd41cd3..81bc5e1f 100644 --- a/vendor/krilla/SOURCE.json +++ b/vendor/krilla/SOURCE.json @@ -177,7 +177,7 @@ { "path": "src/graphics/image.rs", "upstream_sha256": "61324d6426ac79aa5322d0d33a95532934cd1e41797a9dad6302210a2b799d5d", - "sha256": "1cb957ac57f181957a87a462c49a4028a8023bdeacb3dc06bb10b84e115427b0" + "sha256": "58fd85c0a9490e898e49832d67fa5f352a066e9acff725e7d978a0d7058387d2" }, { "path": "src/graphics/mask.rs", diff --git a/vendor/krilla/patches/11-png-decode-cap.patch b/vendor/krilla/patches/11-png-decode-cap.patch index 6e79025d..6c31346e 100644 --- a/vendor/krilla/patches/11-png-decode-cap.patch +++ b/vendor/krilla/patches/11-png-decode-cap.patch @@ -1,8 +1,63 @@ diff --git a/src/graphics/image.rs b/src/graphics/image.rs -index d97b8b4..d3ffcad 100644 +index d97b8b4..d8fd6a4 100644 --- a/src/graphics/image.rs +++ b/src/graphics/image.rs -@@ -356,7 +356,7 @@ impl Image { +@@ -290,8 +290,8 @@ impl Image { + + /// Create a new image from a custom image. + /// +- /// Panics if the dimensions of the image and the length of the +- /// data doesn't match. ++ /// Returns an error if the dimensions of the image and the length of the ++ /// data don't match. + pub fn from_custom(image: T, interpolate: bool) -> Result { + let hash = (image.clone(), interpolate).sip_hash(); + let mut metadata = ImageMetadata { +@@ -312,22 +312,30 @@ impl Image { + ); + } + +- let num_components = metadata.num_components() as u32; ++ // Checked: 32 DeviceN components at 16 bits overflow `u32` at about 67 megapixels. ++ let bytes_per_component = usize::from(image.bits_per_component().as_u8() / 8); ++ let (width, height) = metadata.size; ++ let alpha_channel_len = usize::try_from(width) ++ .ok() ++ .zip(usize::try_from(height).ok()) ++ .and_then(|(w, h)| w.checked_mul(h)?.checked_mul(bytes_per_component)) ++ .ok_or("image dimensions are too large")?; ++ let color_channel_len = alpha_channel_len ++ .checked_mul(metadata.num_components()) ++ .ok_or("image dimensions are too large")?; ++ if image.color_channel().len() != color_channel_len { ++ return Err("color channel length doesn't match the image dimensions".to_string()); ++ } ++ if image ++ .alpha_channel() ++ .is_some_and(|alpha| alpha.len() != alpha_channel_len) ++ { ++ return Err("alpha channel length doesn't match the image dimensions".to_string()); ++ } ++ + Ok(Self(Arc::new(ImageRepr { + inner: Deferred::new(move || { +- let bytes_per_component = (image.bits_per_component().as_u8() / 8) as u32; +- let color_channel_len = bytes_per_component +- * num_components +- * metadata.size.0 +- * metadata.size.1; + let color_channel = image.color_channel(); +- assert_eq!(color_channel.len(), color_channel_len as usize); +- +- let alpha_channel_len = bytes_per_component * metadata.size.0 * metadata.size.1; +- let alpha_channel = image.alpha_channel(); +- if let Some(alpha_channel) = alpha_channel { +- assert_eq!(alpha_channel.len(), alpha_channel_len as usize); +- } + + Ok(Repr::Sampled(SampledRepr { + color_channel: deflate_encode(color_channel), +@@ -356,7 +364,7 @@ impl Image { Self(Arc::new(ImageRepr { inner: Deferred::new(move || { let (color_channel, alpha_channel, bits_per_component) = @@ -11,7 +66,7 @@ index d97b8b4..d3ffcad 100644 Ok(Repr::Sampled(SampledRepr { color_channel, -@@ -554,6 +554,13 @@ fn png_metadata(data: &[u8]) -> Result { +@@ -554,6 +562,13 @@ fn png_metadata(data: &[u8]) -> Result { let reader = decoder .read_info() .map_err(|e| e.to_string().to_ascii_lowercase())?; @@ -25,7 +80,7 @@ index d97b8b4..d3ffcad 100644 let info = reader.info(); let size = (info.width, info.height); -@@ -588,6 +595,11 @@ fn png_metadata(data: &[u8]) -> Result { +@@ -588,6 +603,11 @@ fn png_metadata(data: &[u8]) -> Result { }) } @@ -37,7 +92,7 @@ index d97b8b4..d3ffcad 100644 fn decode_png(data: &[u8]) -> Result { let cursor = Cursor::new(data); let mut decoder = png::Decoder::new(cursor); -@@ -595,7 +607,15 @@ fn decode_png(data: &[u8]) -> Result { +@@ -595,7 +615,15 @@ fn decode_png(data: &[u8]) -> Result { let mut reader = decoder .read_info() .map_err(|e| e.to_string().to_ascii_lowercase())?; @@ -54,7 +109,7 @@ index d97b8b4..d3ffcad 100644 let _ = reader .next_frame(&mut img_data) .map_err(|e| e.to_string())?; -@@ -610,8 +630,8 @@ fn decode_png(data: &[u8]) -> Result { +@@ -610,8 +638,8 @@ fn decode_png(data: &[u8]) -> Result { }; let (color_channel, alpha_channel, bits_per_component) = match bit_depth { @@ -65,7 +120,7 @@ index d97b8b4..d3ffcad 100644 _ => return Err("image has an unsupported bit-depth".to_string()), }; -@@ -697,7 +717,7 @@ fn decode_gif(data: Data) -> Result { +@@ -697,7 +725,7 @@ fn decode_gif(data: Data) -> Result { .ok_or("GIF image seems to be empty".to_string())?; let (color_channel, alpha_channel, bits_per_component) = @@ -74,7 +129,7 @@ index d97b8b4..d3ffcad 100644 Ok(Repr::Sampled(SampledRepr { color_channel, -@@ -755,7 +775,7 @@ fn decode_webp(data: Data) -> Result { +@@ -755,7 +783,7 @@ fn decode_webp(data: Data) -> Result { }; let (color_channel, alpha_channel, bits_per_component) = @@ -83,7 +138,7 @@ index d97b8b4..d3ffcad 100644 Ok(Repr::Sampled(SampledRepr { color_channel, -@@ -764,9 +784,21 @@ fn decode_webp(data: Data) -> Result { +@@ -764,9 +792,21 @@ fn decode_webp(data: Data) -> Result { })) } @@ -107,7 +162,7 @@ index d97b8b4..d3ffcad 100644 } else { Vec::new() }; -@@ -774,7 +806,7 @@ fn handle_u8_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, Bi +@@ -774,7 +814,7 @@ fn handle_u8_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, Bi let color_channel = match cs { ColorSpace::RGB => deflate_encode(data), ColorSpace::RGBA => { @@ -116,7 +171,7 @@ index d97b8b4..d3ffcad 100644 data.chunks_exact(4).for_each(|data| { buf.extend_from_slice(&data[0..3]); alphas.push(data[3]); -@@ -783,7 +815,7 @@ fn handle_u8_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, Bi +@@ -783,7 +823,7 @@ fn handle_u8_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, Bi } ColorSpace::Luma => deflate_encode(data), ColorSpace::LumaA => { @@ -125,7 +180,7 @@ index d97b8b4..d3ffcad 100644 data.chunks_exact(2).for_each(|data| { buf.push(data[0]); alphas.push(data[1]); -@@ -800,12 +832,12 @@ fn handle_u8_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, Bi +@@ -800,12 +840,12 @@ fn handle_u8_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, Bi None }; @@ -141,7 +196,7 @@ index d97b8b4..d3ffcad 100644 } else { Vec::new() }; -@@ -813,7 +845,7 @@ fn handle_u16_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, B +@@ -813,7 +853,7 @@ fn handle_u16_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, B let encoded_image = match cs { ColorSpace::RGB => deflate_encode(data), ColorSpace::RGBA => { @@ -150,7 +205,7 @@ index d97b8b4..d3ffcad 100644 data.chunks_exact(8).for_each(|data| { buf.extend_from_slice(&data[0..6]); alphas.extend_from_slice(&data[6..]); -@@ -822,7 +854,7 @@ fn handle_u16_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, B +@@ -822,7 +862,7 @@ fn handle_u16_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, B } ColorSpace::Luma => deflate_encode(data), ColorSpace::LumaA => { @@ -159,7 +214,7 @@ index d97b8b4..d3ffcad 100644 data.chunks_exact(4).for_each(|data| { buf.extend_from_slice(&data[0..2]); alphas.extend_from_slice(&data[2..]); -@@ -839,7 +871,7 @@ fn handle_u16_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, B +@@ -839,7 +879,7 @@ fn handle_u16_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, B None }; @@ -168,7 +223,7 @@ index d97b8b4..d3ffcad 100644 } fn get_icc_profile_type(data: &[u8], color_space: ImageColorspace) -> Option { -@@ -864,6 +896,54 @@ mod tests { +@@ -864,6 +904,54 @@ mod tests { assert!(e.is_err()); } diff --git a/vendor/krilla/src/graphics/image.rs b/vendor/krilla/src/graphics/image.rs index d3ffcad8..d8fd6a44 100644 --- a/vendor/krilla/src/graphics/image.rs +++ b/vendor/krilla/src/graphics/image.rs @@ -290,8 +290,8 @@ impl Image { /// Create a new image from a custom image. /// - /// Panics if the dimensions of the image and the length of the - /// data doesn't match. + /// Returns an error if the dimensions of the image and the length of the + /// data don't match. pub fn from_custom(image: T, interpolate: bool) -> Result { let hash = (image.clone(), interpolate).sip_hash(); let mut metadata = ImageMetadata { @@ -312,22 +312,30 @@ impl Image { ); } - let num_components = metadata.num_components() as u32; + // Checked: 32 DeviceN components at 16 bits overflow `u32` at about 67 megapixels. + let bytes_per_component = usize::from(image.bits_per_component().as_u8() / 8); + let (width, height) = metadata.size; + let alpha_channel_len = usize::try_from(width) + .ok() + .zip(usize::try_from(height).ok()) + .and_then(|(w, h)| w.checked_mul(h)?.checked_mul(bytes_per_component)) + .ok_or("image dimensions are too large")?; + let color_channel_len = alpha_channel_len + .checked_mul(metadata.num_components()) + .ok_or("image dimensions are too large")?; + if image.color_channel().len() != color_channel_len { + return Err("color channel length doesn't match the image dimensions".to_string()); + } + if image + .alpha_channel() + .is_some_and(|alpha| alpha.len() != alpha_channel_len) + { + return Err("alpha channel length doesn't match the image dimensions".to_string()); + } + Ok(Self(Arc::new(ImageRepr { inner: Deferred::new(move || { - let bytes_per_component = (image.bits_per_component().as_u8() / 8) as u32; - let color_channel_len = bytes_per_component - * num_components - * metadata.size.0 - * metadata.size.1; let color_channel = image.color_channel(); - assert_eq!(color_channel.len(), color_channel_len as usize); - - let alpha_channel_len = bytes_per_component * metadata.size.0 * metadata.size.1; - let alpha_channel = image.alpha_channel(); - if let Some(alpha_channel) = alpha_channel { - assert_eq!(alpha_channel.len(), alpha_channel_len as usize); - } Ok(Repr::Sampled(SampledRepr { color_channel: deflate_encode(color_channel),