diff --git a/vendor/README.craft-reader.md b/vendor/README.craft-reader.md index 45662035..2c0093cb 100644 --- a/vendor/README.craft-reader.md +++ b/vendor/README.craft-reader.md @@ -175,6 +175,12 @@ comma. The unit tests of all these items come with them; the CCITT, Type 3 and c raster tests are adapted in `tests/upstream_raster.rs` and the text contracts in `tests/upstream_text.rs`. (P8)'s PdfCraft test needs a Japanese substitute font and is not ported. +- A CCITT image with `/Rows 0` (unknown height) decodes at most the rows that fit the stream's + output limit (counted in 1-bit rows) and 2^28 pixels; a known height is clamped the same way. + Before, it decoded to the end of the data, and a bit of 2D code can repeat a whole row. +- A JPEG 2000 image over 2^28 pixels, or over its output limit (at least 64 MiB), by the + codestream's own dimensions is refused before decoding, and its buffer is reserved fallibly: + the image dictionary's smaller size no longer stands in for the size the decoder allocates. Paint fixes from the [PdfCraft revision 0b670caf4a090e9d83a7f41a84b4cb2144a2d33e ledger](https://github.com/storytold/pdfcraft/blob/0b670caf4a090e9d83a7f41a84b4cb2144a2d33e/vendor/README.md), each marked `PdfCraft patch` in the source: hayro-interpret (46) with its hayro counterpart diff --git a/vendor/hayro-syntax/SOURCE.json b/vendor/hayro-syntax/SOURCE.json index fbfe83ec..5e9fb9b1 100644 --- a/vendor/hayro-syntax/SOURCE.json +++ b/vendor/hayro-syntax/SOURCE.json @@ -146,7 +146,7 @@ "path": "src/filter/ccitt.rs", "source_sha256": "45d7eb89934d28cff97a00c7b264010bccfd14f3d511e1307ad50a9fef3b53c8", "source_git_blob": "685d680d2fbadce608a9bd3ca925c2ccc6c5d641", - "sha256": "b0fef2271417c3009a1de569c9573b05dd464ff76c1a5f7220f634bb5feb44e2" + "sha256": "67d17a339639eb0fef4cc6c745b579c6ba9ae3f2f67fd3bacb1bf1389aa5ffab" }, { "path": "src/filter/dct.rs", @@ -162,9 +162,9 @@ }, { "path": "src/filter/jpx.rs", - "source_sha256": "f0a2b01c692c086d30a26562d7548f9e837af166be737d2c543de7c886a45f82", + "source_sha256": "c9f068629c9cbe79af6793c3c125df4631ca1455691cb1335b44ff43cb1b4056", "source_git_blob": "74efd8b31e2b2935ff2bbceda2c1ffd36ea9c20d", - "sha256": "5364837caa8ed2abc33a1977b5c91f6895be17c899ae0cc6fe1ebc0723a62448" + "sha256": "dc3742454cfde9a994ce48984e8fcb7b18851efb36ce2d62e31629efea063a1b" }, { "path": "src/filter/lzw_flate.rs", @@ -176,7 +176,7 @@ "path": "src/filter/mod.rs", "source_sha256": "0af7145ab16b15b7a60c4b57efecb9bd5fd0cfdc9bb5f9388abb695386e63e49", "source_git_blob": "06bda29487d62c25dc39c1ee109091cb17e3867f", - "sha256": "e33e74ee55dcc850db9eb64811c56b4ffe1932908ab9684dfba07f4277b1eea9" + "sha256": "e43d7d309a4993f4a1f698208997122431b4a8b0db838bb07d4358ccbc00f02f" }, { "path": "src/filter/run_length.rs", diff --git a/vendor/hayro-syntax/src/filter/ccitt.rs b/vendor/hayro-syntax/src/filter/ccitt.rs index b83e2c41..a7921a5d 100644 --- a/vendor/hayro-syntax/src/filter/ccitt.rs +++ b/vendor/hayro-syntax/src/filter/ccitt.rs @@ -8,10 +8,12 @@ use alloc::vec::Vec; use core::iter; use hayro_ccitt::{DecodeSettings, Decoder, DecoderContext, EncodingMode}; +/// PdfCraft patch: `limit` is the most bytes the filter may produce (see `decode_limit`). pub(crate) fn decode( data: &[u8], params: &Dict<'_>, image_params: &ImageDecodeParams, + limit: usize, ) -> Option> { let k = params.get::(K).unwrap_or(0); @@ -52,6 +54,16 @@ pub(crate) fn decode( if !super::ccitt_size_ok(settings.columns, settings.rows) { return None; } + // PdfCraft patch: the decoder stops after `rows` rows; with `/Rows 0` (unknown) it ran to the + // end of the data, and a bit of 2D code can repeat a whole row, so a small stream produced + // gigabytes. Decode at most the rows that fit the output limit and the pixel cap. The limit + // counts the stream's own 1-bit rows (`decode_limit`), whatever depth the rows are widened to. + let mut settings = settings; + let fit = super::ccitt_max_rows(settings.columns, settings.columns.div_ceil(8), limit); + settings.rows = if settings.rows == 0 { fit } else { settings.rows.min(fit) }; + if settings.rows == 0 { + return None; + } // Whenever possible (if we don't have an indexed color space), we convert // the data as 8-bit instead of 1-bit, so that it can be easier converted @@ -181,7 +193,7 @@ mod tests { #[test] fn columns_past_u32_max_are_refused_not_wrapped() { let params = ImageDecodeParams { width: 8, height: 4, ..Default::default() }; - let decoded = |dict: &[u8]| decode(&ALL_BLACK_8X4, &Dict::from_bytes(dict).unwrap(), ¶ms); + let decoded = |dict: &[u8]| decode(&ALL_BLACK_8X4, &Dict::from_bytes(dict).unwrap(), ¶ms, 1 << 20); assert!(decoded(b"<< /K -1 /Columns 8 >>").is_some()); // 2^32 + 8 would wrap to 8 columns. assert!(decoded(b"<< /K -1 /Columns 4294967304 >>").is_none()); diff --git a/vendor/hayro-syntax/src/filter/jpx.rs b/vendor/hayro-syntax/src/filter/jpx.rs index 2db5b427..d04f3990 100644 --- a/vendor/hayro-syntax/src/filter/jpx.rs +++ b/vendor/hayro-syntax/src/filter/jpx.rs @@ -17,7 +17,15 @@ impl ImageColorSpace { } } -pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option> { +/// PdfCraft patch: the most pixels a JPEG 2000 image may decode to, as for JBIG2. +const MAX_PIXELS: u64 = 1 << 28; + +/// PdfCraft patch: the least output `limit` a JPEG 2000 image is held to: the limit comes from the +/// image dictionary's size, which a decoder may round differently from the codestream's. +const MIN_LIMIT: usize = 64 << 20; + +/// PdfCraft patch: `limit` is the most bytes the filter may produce (see `decode_limit`). +pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams, limit: usize) -> Option> { use crate::object::stream::ImageColorSpace; let settings = DecodeSettings { @@ -74,7 +82,23 @@ pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option MAX_PIXELS { + return None; + } + let channels = usize::from(cs.num_components()) + usize::from(has_alpha); + let len = (width as usize).checked_mul(height as usize)?.checked_mul(channels)?; + let output_limit = limit.max(MIN_LIMIT); + if len > output_limit { + return None; + } + let mut bitmap = Vec::new(); + bitmap.try_reserve_exact(len).ok()?; + bitmap.resize(len, 0); + let mut context = hayro_jpeg2000::DecoderContext::default(); + image.decode_into(&mut bitmap, &mut context).ok()?; let (mut data, mut alpha) = if !has_alpha { (bitmap, None) @@ -98,6 +122,18 @@ pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option output_limit { + return None; + } data = scale(&data, bpc, cs.num_components(), width, height)?; alpha = alpha.and_then(|alpha| scale(&alpha, bpc, cs.num_components(), width, height)); } @@ -223,7 +259,9 @@ fn scale( .checked_mul(num_components as usize)? .checked_mul(bit_per_component as usize)?; let input_len = bits_per_row.div_ceil(8).checked_mul(height as usize)?; - let mut input = vec![0; input_len]; + let mut input = Vec::new(); + input.try_reserve_exact(input_len).ok()?; + input.resize(input_len, 0); let mut writer = BitWriter::new(&mut input, bit_per_component)?; let components_per_row = (num_components as usize).checked_mul(width as usize)?; @@ -417,7 +455,7 @@ mod tests { height: 2, ..ImageDecodeParams::default() }; - let result = decode(PALETTE_JP2, ¶ms).unwrap(); + let result = decode(PALETTE_JP2, ¶ms, usize::MAX).unwrap(); let image = result.image_data.unwrap(); assert!(matches!(image.color_space, Some(ImageColorSpace::Rgb))); let red = [255, 0, 0]; @@ -437,7 +475,7 @@ mod tests { height: 2, ..ImageDecodeParams::default() }; - let result = decode(PALETTE_JP2, ¶ms).unwrap(); + let result = decode(PALETTE_JP2, ¶ms, usize::MAX).unwrap(); assert_eq!(&*result.data, &[0, 1, 0, 1, 1, 0, 1, 0][..]); } @@ -479,7 +517,7 @@ mod tests { ..ImageDecodeParams::default() }; let bpc = |data: &[u8]| { - decode(data, ¶ms) + decode(data, ¶ms, usize::MAX) .and_then(|r| r.image_data) .map(|d| d.bits_per_component) }; @@ -501,7 +539,7 @@ mod tests { bpc: Some(1), ..params.clone() }; - let palette_bpc = decode(&one_bit_jp2(&palette), &one_bit) + let palette_bpc = decode(&one_bit_jp2(&palette), &one_bit, usize::MAX) .and_then(|r| r.image_data) .map(|d| d.bits_per_component); assert_eq!(palette_bpc, Some(8), "/BitsPerComponent 1 with a palette"); diff --git a/vendor/hayro-syntax/src/filter/mod.rs b/vendor/hayro-syntax/src/filter/mod.rs index e3622795..c6ee6ed2 100644 --- a/vendor/hayro-syntax/src/filter/mod.rs +++ b/vendor/hayro-syntax/src/filter/mod.rs @@ -110,11 +110,11 @@ impl Filter { #[cfg(feature = "images")] Self::DctDecode => dct::decode(data, params, image_params).ok_or(DecodeFailure::ImageDecode), #[cfg(feature = "images")] - Self::CcittFaxDecode => ccitt::decode(data, params, image_params).ok_or(DecodeFailure::ImageDecode), + Self::CcittFaxDecode => ccitt::decode(data, params, image_params, limit).ok_or(DecodeFailure::ImageDecode), #[cfg(feature = "images")] Self::Jbig2Decode => jbig2::decode(data, params, image_params).ok_or(DecodeFailure::ImageDecode), #[cfg(feature = "images")] - Self::JpxDecode => jpx::decode(data, image_params).ok_or(DecodeFailure::ImageDecode), + Self::JpxDecode => jpx::decode(data, image_params, limit).ok_or(DecodeFailure::ImageDecode), #[cfg(not(feature = "images"))] Self::DctDecode | Self::CcittFaxDecode | Self::Jbig2Decode | Self::JpxDecode => { warn!("image decoding is not supported (enable the `images` feature)"); @@ -168,6 +168,30 @@ pub(crate) fn decode_limit(params: &ImageDecodeParams, last: bool, limits: crate /// real fax lines are a few thousand pixels wide. pub fn ccitt_size_ok(columns: u32, rows: u32) -> bool { const MAX_COLUMNS: u32 = 1 << 20; - const MAX_PIXELS: u64 = 1 << 28; - columns > 0 && columns <= MAX_COLUMNS && u64::from(columns) * u64::from(rows.max(1)) <= MAX_PIXELS + columns > 0 && columns <= MAX_COLUMNS && u64::from(columns) * u64::from(rows.max(1)) <= MAX_CCITT_PIXELS +} + +/// PdfCraft patch: the most pixels a CCITT image may decode to. +const MAX_CCITT_PIXELS: u64 = 1 << 28; + +/// PdfCraft patch: the most rows of `columns` pixels, `row_bytes` bytes each once decoded, that fit +/// both `limit` bytes and the pixel cap; at least one when one row fits. +pub(crate) fn ccitt_max_rows(columns: u32, row_bytes: u32, limit: usize) -> u32 { + let by_pixels = MAX_CCITT_PIXELS / u64::from(columns.max(1)); + let by_bytes = u64::try_from(limit).unwrap_or(u64::MAX) / u64::from(row_bytes.max(1)); + u32::try_from(by_pixels.min(by_bytes)).unwrap_or(u32::MAX) +} + +#[cfg(test)] +mod ccitt_tests { + use super::ccitt_max_rows; + + /// PdfCraft patch: an image of unknown height decodes at most the rows that fit both the output + /// limit (in 1-bit rows) and the 2^28-pixel cap. + #[test] + fn unknown_ccitt_rows_fit_the_limit_and_the_pixel_cap() { + assert_eq!(ccitt_max_rows(1728, 216, 216 * 100), 100); + assert_eq!(ccitt_max_rows(1 << 20, 1 << 17, usize::MAX), 1 << 8); + assert_eq!(ccitt_max_rows(1728, 216, 100), 0); + } }