diff --git a/crates/create/src/extract.rs b/crates/create/src/extract.rs index cb81fa2044..3c7a0da788 100644 --- a/crates/create/src/extract.rs +++ b/crates/create/src/extract.rs @@ -126,7 +126,9 @@ fn decode_parms(doc: &Document, s: &Stream) -> Dict { } /// [`decoded`] for a JBIG2 image. Its JBIG2 page has to be the image's width, which the caller -/// checked against the export's size limit; rows past the image's height are dropped, and too few +/// checked against the export's size limit, and within that limit itself: the decoder decodes the +/// whole page, rows past the image's height too, so a small `/Height` over a far taller page +/// would cost more than the limit allows. Rows past the image's height are dropped, and too few /// rows are refused by the caller. Globals that can't be read refuse the image, rather than /// decoding it without them. fn jbig2(doc: &Document, s: &Stream) -> Result, String> { @@ -138,6 +140,9 @@ fn jbig2(doc: &Document, s: &Stream) -> Result, String> { if s.dict.int(b"Width") != Some(i64::from(image.width())) { return Err("the JBIG2 image's width isn't the image's width".into()); } + if u64::from(image.width()).saturating_mul(u64::from(image.height())) > MAX_PIXELS { + return Err(format!("the JBIG2 page, {} × {}, is too large to export", image.width(), image.height())); + } let height = s.dict.int(b"Height").and_then(|h| usize::try_from(h).ok()).unwrap_or(0); let mut rows = Rows::new(image.width() as usize, height); image.decode(&mut rows).map_err(|e| format!("the JBIG2 image can't be decoded: {e}"))?; diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index b9e9cd5aa7..80b856db8e 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -1392,7 +1392,8 @@ fn images_whose_predictor_is_given_by_reference_export_as_their_picture() { /// a stencil `/Mask` of another. Their `/JBIG2Globals` are read, from a dictionary or the one in /// an array, so globals that can't be read skip the image rather than being ignored. Rows past the /// image's height are left out; a JBIG2 page of another width, or with fewer rows than the image, -/// or data that isn't JBIG2, skips the image. +/// or larger than an exported image may be (however small the image's `/Height`), or data that +/// isn't JBIG2, skips the image. #[test] fn jbig2_images_export_as_their_picture() { let (w, h) = (203i64, 41i64); @@ -1457,6 +1458,23 @@ fn jbig2_images_export_as_their_picture() { for (width, height, data) in [(w + 8, h, &jbig2[..]), (w - 1, h, &jbig2[..]), (w, h + 1, &jbig2[..]), (w, h, &b"not JBIG2"[..])] { assert!(skipped(&page(width, height, data, None, false)), "{width} × {height}, {} bytes", data.len()); } + // The same stream with its page information (the first segment's data, after its 11-byte + // header: width, then height) claiming a 20,000 × 10,000 page: 200 million pixels, over the + // export's 100 million, behind an image of 20,000 × 4. The decoder would decode the whole page + // (it takes pages of up to about 2^28 pixels), so it is refused first. + let mut large = jbig2.clone(); + large[11..15].copy_from_slice(&20_000u32.to_be_bytes()); + large[15..19].copy_from_slice(&10_000u32.to_be_bytes()); + assert!(skipped(&page(20_000, 4, &large, None, false)), "a page over the export's size"); + // A page of exactly the budget, 20,000 × 5,000, is decoded; one row more is refused. + for (rows, refused) in [(5_000u32, false), (5_001, true)] { + large[15..19].copy_from_slice(&rows.to_be_bytes()); + let doc = page(20_000, 4, &large, None, false); + assert_eq!(skipped(&doc), refused, "a page of 20,000 × {rows}"); + if !refused { + assert_eq!(exported_rgba(&doc).1.len(), 20_000 * 4 * 4); + } + } // Damaged data never panics. for end in [0, 1, 10, 20, 30, jbig2.len() / 2] { let _ = extract_images(&page(w, h, &jbig2[..end], None, false), &[0], 0);