pdf: an inline image with absurd dimensions falls back to the EI scan

inline_len computed (w * n * bpc).div_ceil(8) * h with Width/Height taken
straight from the content stream and no size validation — unlike decode()
and mask_image, which bound their geometry first. /W 5000000000000000000
overflows the multiply in debug and test builds; in release it wraps to a
small length and the caller slices the wrong bytes, misparsing the image
and shifting the EI resync.

Chain the arithmetic through checked_mul and return None, which the
caller already routes to the EI-scan fallback. Regression test:
inline_image_with_absurd_dimensions_falls_back_to_ei_scan — panics at
crates/pdf/src/image.rs:85 on main, passes with the chain.
This commit is contained in:
midasmatterhorn
2026-10-15 16:10:10 +03:00
parent 0311bdc34d
commit 5e18a7fd92
2 changed files with 19 additions and 1 deletions
+4 -1
View File
@@ -82,7 +82,10 @@ pub(crate) fn inline_len(file: &File, d: &Dict, res: Option<&Dict>) -> Option<us
let mask = matches!(file.get(d, "ImageMask"), Some(Obj::Bool(true)));
let bpc = if mask { 1 } else { file.get_num(d, "BitsPerComponent").unwrap_or(8.0) as usize };
let n = if mask { 1 } else { file.get(d, "ColorSpace").map(|c| color_space(file, c, res).components()).unwrap_or(1) };
Some((w * n * bpc).div_ceil(8) * h)
// `Width`/`Height` come straight from the file and can be enormous: fall back to the
// `EI` scan (the caller's `None` branch) instead of overflowing the arithmetic.
let row = w.checked_mul(n)?.checked_mul(bpc)?.div_ceil(8);
Some(row.checked_mul(h)?)
}
/// Decode an image's samples (all filters, including DCT). Returns (bytes, components from
+15
View File
@@ -263,6 +263,21 @@ fn hostile_structures_return_errors_or_draw_nothing() {
assert!(!doc.skipped.is_empty());
}
#[test]
fn inline_image_with_absurd_dimensions_falls_back_to_ei_scan() {
// `/W 5000000000000000000` overflows the row math (`* bpc` alone exceeds usize::MAX):
// `inline_len` must return `None` (scan for `EI`) instead of overflowing; the oversized
// image itself is then refused by size.
let bytes = page_pdf(
"q 10 0 0 10 0 0 cm BI /W 5000000000000000000 /H 100 /CS /G /BPC 8 ID \0 EI Q",
"",
vec![],
);
let doc = parse(&bytes).unwrap();
let _ = render(&doc);
assert!(!doc.skipped.is_empty());
}
#[test]
fn truncated_and_corrupt_eps_never_panic() {
let eps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n\