From 703d057578e87941449af9996d22e696e44dec74 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 1 Oct 2026 09:20:36 -0400 Subject: [PATCH] M3.3: effective_scale floor overrode the size caps (a huge page box rendered 9M px wide); regression tests for the fuzz findings Co-Authored-By: Claude Opus 5.5 --- crates/render/src/raster.rs | 77 ++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index ef885236c..aa4090a8e 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -107,7 +107,10 @@ pub fn effective_scale(width_pt: f32, height_pt: f32, scale: f32) -> f32 { let (w, h) = (width_pt.max(1.0), height_pt.max(1.0)); let by_side = MAX_SIDE / w.max(h); let by_area = (MAX_PIXELS / (w * h)).sqrt(); - scale.min(by_side).min(by_area).max(0.01) + // The caps always win: a floor here once let a 934-million-point-wide page (a fuzzed file) + // render 9 million pixels wide. Only guard against a zero or non-finite scale. + let capped = scale.min(by_side).min(by_area); + if capped.is_finite() && capped > 0.0 { capped } else { by_side.min(by_area).max(f32::MIN_POSITIVE) } } /// Render one page with a caller-owned parser and cache. Panics inside the renderer are caught @@ -314,6 +317,17 @@ fn worker(bytes: Arc>, config: RenderConfig, queue: Queue, wake: Receive #[cfg(test)] mod tests { + #[test] + fn effective_scale_never_exceeds_the_caps() { + for (w, h, s) in [(934_775_807.0, 792.0, 0.25), (612.0, 792.0, 0.0), (1.0e9, 1.0e9, 1.0), (612.0, 792.0, 2.0)] { + let k = super::effective_scale(w, h, s); + assert!(k > 0.0 && k.is_finite(), "{w}×{h} @ {s}: {k}"); + assert!(w * k <= super::MAX_SIDE + 1.0 && h * k <= super::MAX_SIDE + 1.0, "{w}×{h} @ {s}: {k}"); + assert!(w * k * h * k <= super::MAX_PIXELS * 1.01, "{w}×{h} @ {s}: {k}"); + } + assert_eq!(super::effective_scale(612.0, 792.0, 2.0), 2.0); + } + use super::*; const ONE_PAGE: &[u8] = b"%PDF-1.4 @@ -450,6 +464,67 @@ trailer << /Root 1 0 R >> assert_eq!(px(off), vec![255, 255, 255, 255], "toggled-off layer must not draw"); } + /// From `cargo xtask fuzz`: a tiling pattern with no /Resources inherits the page's, where + /// its own name points back at it. It painted itself until the stack overflowed (vendored + /// hayro-interpret patch: `MAX_PATTERN_NESTING`). Ordinary patterns must still paint. + #[test] + fn self_referencing_tiling_pattern_terminates() { + let pdf = |pattern_body: &str| { + 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 << /Pattern << /P1 5 0 R >> >> >> endobj +4 0 obj << /Length 30 >> stream +/Pattern cs /P1 scn 0 0 40 40 re f +endstream endobj +5 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Length {} >> stream +{pattern_body} +endstream endobj +trailer << /Root 1 0 R >> +%%EOF", + pattern_body.len() + ) + }; + let render = |doc: String| { + let mut r = PageRenderer::new(Arc::new(doc.into_bytes()), RenderConfig::default()); + r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }) + }; + let looped = render(pdf("/Pattern cs /P1 scn 0 0 10 10 re f")); + assert_eq!((looped.width, looped.height), (40, 40)); + let red = render(pdf("1 0 0 rg 0 0 10 10 re f")); + assert!(red.error.is_none(), "{:?}", red.error); + assert_eq!(&red.rgba[((20 * 40 + 20) * 4)..][..4], &[255, 0, 0, 255], "a normal tiling pattern still paints"); + } + + /// From `cargo xtask fuzz`: a CID font whose /W range spans every u32 inserted billions of + /// widths (vendored hayro-interpret patch: `MAX_CID`). Must finish quickly. + #[test] + fn huge_cid_width_ranges_terminate() { + let pdf = "%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 100 40] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj +4 0 obj << /Length 35 >> stream +BT /F1 12 Tf 10 10 Td <0041> Tj ET +endstream endobj +5 0 obj << /Type /Font /Subtype /Type0 /BaseFont /Helvetica /Encoding /Identity-H /DescendantFonts [6 0 R] >> endobj +6 0 obj << /Type /Font /Subtype /CIDFontType2 /BaseFont /Helvetica /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> + /FontDescriptor 7 0 R /W [0 4294967295 500] /W2 [0 4294967295 -1000 250 880] >> endobj +7 0 obj << /Type /FontDescriptor /FontName /Helvetica /Flags 32 /FontBBox [0 -200 1000 900] /ItalicAngle 0 /Ascent 800 /Descent -200 /CapHeight 700 /StemV 80 >> endobj +trailer << /Root 1 0 R >> +%%EOF"; + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let mut r = PageRenderer::new(Arc::new(pdf.as_bytes().to_vec()), RenderConfig::default()); + let px = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + let text = r.render(RenderRequest { page: 0, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0 }); + let _ = tx.send((px.error, text.error)); + }); + let (px, text) = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("rendering a huge /W range must not hang"); + assert!(px.is_none() && text.is_none(), "{px:?} {text:?}"); + } + #[test] fn tiles_match_full_render() { let mut r = PageRenderer::new(Arc::new(ONE_PAGE.to_vec()), RenderConfig::default());