mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 14:08:23 -06:00
M11: Export ▸ Images refuses a JBIG2 page larger than it exports, before decoding
The export checked an image's /Width × /Height against its 100-million- pixel budget, and the JBIG2 page's width against /Width, but not the page's height. The decoder decodes the whole page, rows past the image's height too, and takes pages of up to about 2^28 pixels: a 20,000 × 4 image over a 20,000 × 10,000 JBIG2 page cost 200 million pixels of decoding to export four rows. The page itself is now held to the budget before it is decoded. Found by CodeRabbit on #4320. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5500eb10f3
commit
e76c4f5613
@@ -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<Vec<u8>, String> {
|
||||
@@ -138,6 +140,9 @@ fn jbig2(doc: &Document, s: &Stream) -> Result<Vec<u8>, 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}"))?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user