diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index 8d7d86707..c5c24485f 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -780,6 +780,47 @@ trailer << /Root 1 0 R >> assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 0, 0, 255]); } + /// From the nightly `cargo xtask fuzz` (CI caps each child at 4 GiB): a stencil mask claiming + /// /W 4294967295 and a CCITT image claiming /Columns 4294967295 each allocated 4 GiB while + /// decoding (locally: 9.6 GB and 4.3 GB). Vendored hayro patches `image_size_ok` and + /// `ccitt_size_ok` refuse such sizes before decoding; the rest of the page still draws. + #[test] + fn absurd_mask_and_fax_sizes_are_refused_before_decoding() { + assert!(hayro::hayro_interpret::image_size_ok(8000, 8000)); + assert!(!hayro::hayro_interpret::image_size_ok(4_294_967_295, 2)); + assert!(!hayro::hayro_interpret::image_size_ok(0, 10)); + assert!(hayro::hayro_syntax::ccitt_size_ok(1728, 2200), "a fax page"); + assert!(!hayro::hayro_syntax::ccitt_size_ok(4_294_967_295, 26)); + assert!(!hayro::hayro_syntax::ccitt_size_ok(1 << 19, 1 << 12)); + let page = |content: &str, xobject: &str| { + let pdf = format!( + "%PDF-1.7 +1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj +2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents 4 0 R /Resources << /XObject << /Im1 5 0 R >> >> >> endobj +4 0 obj << /Length {} >> stream +{content} +endstream endobj +5 0 obj {xobject} endobj +trailer << /Root 1 0 R >> +%%EOF", + content.len() + ); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 })); + }); + let page = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("an absurd image must not stall the renderer"); + assert!(page.error.is_none(), "{:?}", page.error); + assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 0, 0, 255], "the rest of the page draws"); + }; + let red = "1 0 0 rg 0 0 4 4 re f"; + page(&format!("q 20 0 0 20 5 5 cm BI /W 4294967295 /H 2 /IM true /BPC 1 ID \u{0}\u{ff}\u{ff}\u{0} EI Q {red}"), "<< >>"); + let fax = "<< /Type /XObject /Subtype /Image /Width 81 /Height 26 /ColorSpace /DeviceGray /BitsPerComponent 1 /Filter /CCITTFaxDecode /DecodeParms << /Columns 4294967295 /Rows 26 /K -1 >> /Length 4 >> stream\n\u{0}\u{0}\u{0}\u{0}\nendstream"; + page(&format!("q 20 0 0 20 5 5 cm /Im1 Do Q {red}"), fax); + } + /// From `cargo xtask fuzz`: an inline image claiming /W 4294967295 over four bytes of data /// hung in resampling (vendored hayro patch: `MAX_IMAGE_PIXELS`). #[test] diff --git a/vendor/README.md b/vendor/README.md index d50098cbc..0bfc68fbb 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -7,10 +7,10 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001). | Crate | Version | Licence | Patches | Test | |---|---|---|---|---| -| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields` | +| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding` | | hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB) | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step` | | lopdf | 0.45.0 | MIT | (1) PNG predictor rows longer than the data are refused before their buffers are allocated (a fuzzed `/Columns 4294967295` allocated two 4 GiB rows); (2) predictor row widths use saturating arithmetic | (1) `inspect::tests::png_predictor_rows_longer_than_the_data_are_refused_before_allocating` | -| hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process | `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers` | +| hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB) | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding` | Temporary: hayro is the bootstrap renderer and lopdf the bootstrap inspector (ADR-0004); M2.6 replaces them with our own devices and `cos`/`model`. diff --git a/vendor/hayro-interpret/src/lib.rs b/vendor/hayro-interpret/src/lib.rs index b8ee48b8b..159932f5e 100644 --- a/vendor/hayro-interpret/src/lib.rs +++ b/vendor/hayro-interpret/src/lib.rs @@ -41,6 +41,8 @@ mod ocg; mod soft_mask; mod types; mod x_object; +/// PrintCraft patch: exported for its regression test. +pub use x_object::image_size_ok; pub mod color; pub mod encode; diff --git a/vendor/hayro-interpret/src/x_object.rs b/vendor/hayro-interpret/src/x_object.rs index 058f786eb..42bfaf321 100644 --- a/vendor/hayro-interpret/src/x_object.rs +++ b/vendor/hayro-interpret/src/x_object.rs @@ -399,6 +399,12 @@ fn decode_context<'a>( obj: &ImageXObject<'a>, target_dimension: Option<(u32, u32)>, ) -> Option> { + // PrintCraft patch: images and masks over `MAX_IMAGE_PIXELS` are not decoded at all (the + // renderer skips them anyway): a fuzzed `/W 4294967295` stencil mask allocated gigabytes here. + if !image_size_ok(obj.width, obj.height) { + (obj.warning_sink)(InterpreterWarning::ImageDecodeFailure); + return None; + } let dict = obj.stream.dict(); let dict_bpc = dict .get::(BPC) @@ -968,3 +974,9 @@ fn apply_decode_array( Some(decoded_arr) } + +/// PrintCraft patch: whether an image or mask of `width` × `height` may be decoded (at most +/// 2^28 pixels, the renderer's `MAX_IMAGE_PIXELS`). +pub fn image_size_ok(width: u32, height: u32) -> bool { + width > 0 && height > 0 && u64::from(width) * u64::from(height) <= 1 << 28 +} diff --git a/vendor/hayro-syntax/src/filter/ccitt.rs b/vendor/hayro-syntax/src/filter/ccitt.rs index b96fc4950..3c49e53ff 100644 --- a/vendor/hayro-syntax/src/filter/ccitt.rs +++ b/vendor/hayro-syntax/src/filter/ccitt.rs @@ -34,6 +34,11 @@ pub(crate) fn decode( invert_black: params.get::(BLACK_IS_1).unwrap_or(false), }; + // PrintCraft patch: refuse absurd sizes before the decoder allocates for them. + if !super::ccitt_size_ok(settings.columns, settings.rows) { + 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 // into an RGBA8 image. diff --git a/vendor/hayro-syntax/src/filter/mod.rs b/vendor/hayro-syntax/src/filter/mod.rs index 31e795e95..b45646e1a 100644 --- a/vendor/hayro-syntax/src/filter/mod.rs +++ b/vendor/hayro-syntax/src/filter/mod.rs @@ -131,3 +131,12 @@ impl Filter { res } } + +/// PrintCraft patch: whether a CCITT image of `columns` × `rows` may be decoded. The decoder sizes +/// its line buffers from `/Columns`, which a fuzzed file set to 4294967295 (a 4 GiB allocation); +/// 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 +}