Browse / github.com/storytold/pdfcraft / patch
fix(redact): an image too large to count its pixels is removed, not a panic
fix(redact): an image too large to count its pixels is removed, not a panic ## What this changes Redaction clears the covered pixels of an image in place when it can decode it (`image::decode_and_clear`). Before decoding, it refuses images over `MAX_PIXELS` with `(w as u64) * (h as u64) > MAX_PIXELS`. `/Width` and `/Height` come straight from the file as `i64`, so two dimensions of 2^32 or more make that product overflow `u64`. In a debug or test build that panics with `attempt to multiply with overflow` at `crates/redact/src/image.rs:64`, reached from `apply` through the content interpreter's `Do` for any image a redaction mark overlaps. Release builds wrap instead,
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- lowtide-lab (imported from pull request #5061, open)
- Base
5af102765222c8220df0f9eaa27bfa3bcf481ead- Size
- +14 −1 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
3257698d92a4f8503b6d0d81
How it went
No other operator has reported using it yet.
Diff
raw · crates/redact/src/image.rs crates/redact/src/tests/limits.rs
@@ -61,7 +61,8 @@ fn decode_and_clear(doc: &Document, s: &Stream, ctm: &Matrix, rects: &[[f64; 4]] return None; } let (w, h) = (s.dict.int(b"Width")?, s.dict.int(b"Height")?);- if w <= 0 || h <= 0 || (w as u64) * (h as u64) > MAX_PIXELS {+ // Checked: two dimensions of 2^32 or more would overflow the product.+ if w <= 0 || h <= 0 || (w as u64).checked_mul(h as u64).is_none_or(|n| n > MAX_PIXELS) { return None; } let (w, h) = (w as usize, h as usize);@@ -24,6 +24,18 @@ fn an_image_decoding_far_past_its_raster_is_removed_without_decoding_it_all() { assert_eq!((r.images_removed, r.images_cleared), (1, 0), "{r:?}"); } +#[test]+fn an_image_whose_pixel_count_overflows_is_removed() {+ // 2^32 × 2^32 pixels is 2^64, one past the largest `u64`; 2^33 × 2^31 and 2^40 × 2^40 too.+ for (w, h) in [(1u64 << 32, 1u64 << 32), (1 << 33, 1 << 31), (1 << 40, 1 << 40)] {+ let dict = format!("/Type /XObject /Subtype /Image /Width {w} /Height {h} /ColorSpace /DeviceGray /BitsPerComponent 8");+ let mut doc = one_page(b"q 40 0 0 10 100 100 cm /Im Do Q", "/XObject << /Im 7 0 R >>", vec![stream(&dict, b"")]);+ mark(&mut doc, 0, &[[95.0, 95.0, 120.0, 115.0]], "");+ let r = apply(&mut doc, None).unwrap();+ assert_eq!((r.images_removed, r.images_cleared), (1, 0), "{w}×{h}: {r:?}");+ }+}+ /// Marked content nested `depth` deep, each level with its own MCID, around `depth` drawing /// operators. fn nested_marked_content(depth: usize) -> Vec<u8> {