mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 06:28:49 -06:00
M2.6: fuzz findings in the bootstrap renderer: paint-nesting cap now covers Type 3 glyphs (stack overflow), hayro skips images over 2^28 px (hang); regression tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0bf28b79d4
commit
ee254a91a2
@@ -466,7 +466,7 @@ trailer << /Root 1 0 R >>
|
||||
|
||||
/// 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.
|
||||
/// hayro-interpret patch: `MAX_PAINT_NESTING`). Ordinary patterns must still paint.
|
||||
#[test]
|
||||
fn self_referencing_tiling_pattern_terminates() {
|
||||
let pdf = |pattern_body: &str| {
|
||||
@@ -497,6 +497,69 @@ trailer << /Root 1 0 R >>
|
||||
assert_eq!(&red.rgba[((20 * 40 + 20) * 4)..][..4], &[255, 0, 0, 255], "a normal tiling pattern still paints");
|
||||
}
|
||||
|
||||
/// From `cargo xtask fuzz`: a Type 3 font without /Resources inherits the page's, where its
|
||||
/// own name is defined, and its glyph shows text in itself (vendored patch:
|
||||
/// `MAX_PAINT_NESTING`). Must terminate; a normal Type 3 glyph must still paint.
|
||||
#[test]
|
||||
fn self_referencing_type3_glyph_terminates() {
|
||||
let pdf = |proc_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 << /Font << /F1 5 0 R >> >> >> endobj
|
||||
4 0 obj << /Length 31 >> stream
|
||||
BT /F1 20 Tf 10 10 Td (A) Tj ET
|
||||
endstream endobj
|
||||
5 0 obj << /Type /Font /Subtype /Type3 /FontBBox [0 0 1 1] /FontMatrix [1 0 0 1 0 0] /FirstChar 65 /LastChar 65 /Widths [1]
|
||||
/Encoding << /Differences [65 /a] >> /CharProcs << /a 6 0 R >> >> endobj
|
||||
6 0 obj << /Length {} >> stream
|
||||
{proc_body}
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
proc_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("1 0 d0 BT /F1 1 Tf (A) Tj ET 0 0 1 1 re f"));
|
||||
assert_eq!((looped.width, looped.height), (40, 40));
|
||||
let plain = render(pdf("1 0 d0 0 0 1 1 re f"));
|
||||
assert!(plain.error.is_none(), "{:?}", plain.error);
|
||||
assert!(plain.rgba.chunks_exact(4).any(|p| p[0] < 128), "a normal Type 3 glyph still paints");
|
||||
}
|
||||
|
||||
/// From `cargo xtask fuzz`: an inline image claiming /W 4294967295 over four bytes of data
|
||||
/// hung in resampling (vendored hayro patch: `MAX_IMAGE_PIXELS`).
|
||||
#[test]
|
||||
fn absurd_image_dimensions_are_skipped() {
|
||||
let content = "q 20 0 0 20 5 5 cm BI /W 4294967295 /H 2 /BPC 8 /CS /G ID \u{0}\u{ff}\u{ff}\u{0} EI Q 1 0 0 rg 0 0 4 4 re f";
|
||||
let pdf = 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 >> endobj
|
||||
4 0 obj << /Length {} >> stream
|
||||
{content}
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
content.len()
|
||||
);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default());
|
||||
let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }));
|
||||
});
|
||||
let page = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("an absurd image must not hang the renderer");
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
// The rest of the page still draws: the red square at the bottom left (y-down: last rows).
|
||||
assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 0, 0, 255]);
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
Vendored
+2
-2
@@ -7,9 +7,9 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001).
|
||||
|
||||
| Crate | Version | Licence | Patches | Test |
|
||||
|---|---|---|---|---|
|
||||
| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) tiling-pattern nesting cap `MAX_PATTERN_NESTING` (a self-referencing pattern overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate` |
|
||||
| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate` |
|
||||
|
||||
| hayro | 0.7.1 | Apache-2.0 OR MIT | `RenderSettings::{x_offset, y_offset}` to render one tile of a large page | `raster::tests::tiles_match_full_render` |
|
||||
| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling) | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped` |
|
||||
| hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process | `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers` |
|
||||
|
||||
Temporary: hayro is the bootstrap renderer (ADR-0004); M2.6 replaces it with our own devices.
|
||||
|
||||
+4
@@ -18,6 +18,10 @@ use std::rc::Rc;
|
||||
|
||||
/// Maximum nesting depth for interpreting `XObject`'s/patterns/streams.
|
||||
pub(crate) const MAX_NESTED_INTERPRETATION_DEPTH: u32 = 50;
|
||||
/// PrintCraft patch: maximum nesting for content that is painted recursively by the device
|
||||
/// (tiling patterns, Type 3 glyphs). Either can refer back to itself through resources it
|
||||
/// inherits, which recursed until the stack overflowed (found by fuzzing).
|
||||
pub(crate) const MAX_PAINT_NESTING: u32 = 16;
|
||||
|
||||
/// A cache used by the interpreter.
|
||||
///
|
||||
|
||||
+5
@@ -123,6 +123,11 @@ impl<'a> Type3<'a> {
|
||||
paint: &Paint<'a>,
|
||||
device: &mut impl Device<'a>,
|
||||
) -> Option<()> {
|
||||
// PrintCraft patch: a glyph procedure may show text in this same font (through
|
||||
// inherited resources), recursing until the stack overflowed.
|
||||
if glyph.nesting_depth > crate::context::MAX_PAINT_NESTING {
|
||||
return None;
|
||||
}
|
||||
let mut state = glyph.state.clone();
|
||||
let root_transform =
|
||||
transform * glyph_transform * self.matrix * Affine::scale(UNITS_PER_EM as f64);
|
||||
|
||||
+1
-4
@@ -24,9 +24,6 @@ use kurbo::{Affine, BezPath, Rect, Shape};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
/// PrintCraft patch: maximum nesting of tiling patterns (and the forms around them).
|
||||
const MAX_PATTERN_NESTING: u32 = 16;
|
||||
/// A PDF pattern.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Pattern<'a> {
|
||||
@@ -234,7 +231,7 @@ impl<'a> TilingPattern<'a> {
|
||||
// PrintCraft patch: a tiling pattern can paint itself (directly, or through resources it
|
||||
// inherits when its own are missing), which recursed until the stack overflowed. Bound
|
||||
// the nesting like XObjects do, tighter because every level rasterizes a tile.
|
||||
if self.nesting_depth > MAX_PATTERN_NESTING {
|
||||
if self.nesting_depth > crate::context::MAX_PAINT_NESTING {
|
||||
warn!("tiling pattern nesting depth exceeded");
|
||||
return None;
|
||||
}
|
||||
|
||||
Vendored
+8
@@ -20,6 +20,9 @@ use vello_cpu::{
|
||||
Image, ImageSource, Mask, PaintType, Pixmap, RenderContext, RenderSettings, peniko,
|
||||
};
|
||||
|
||||
|
||||
/// PrintCraft patch: largest image drawn, in pixels (2^28 ≈ 268 MP, well above real page images).
|
||||
const MAX_IMAGE_PIXELS: u64 = 1 << 28;
|
||||
pub(crate) struct Renderer {
|
||||
pub(crate) ctx: RenderContext,
|
||||
pub(crate) inside_pattern: bool,
|
||||
@@ -213,6 +216,11 @@ impl Renderer {
|
||||
};
|
||||
let mut img_width = image_data.width();
|
||||
let mut img_height = image_data.height();
|
||||
// PrintCraft patch: skip images with absurd dimensions (a fuzzed inline image claimed
|
||||
// /W 4294967295 over four bytes of data; resampling it never finished).
|
||||
if img_width == 0 || img_height == 0 || u64::from(img_width) * u64::from(img_height) > MAX_IMAGE_PIXELS {
|
||||
return;
|
||||
}
|
||||
let interpolate = image_data.interpolate();
|
||||
|
||||
if let Some(a) = &alpha_data
|
||||
|
||||
Reference in New Issue
Block a user