render: don't let a hostile page-tree /Count abort inspection (#956)

* render: don't let a hostile page-tree /Count abort inspection

Found by cargo xtask fuzz on Windows (seed 42): a 355-byte PDF whose
/Type /Pages node carries /Count 4294967295 aborted pdfcraft-cli
check-one with a 48 GiB allocation (4294967295 x 12 bytes) - an abort,
which the engine's catch_unwind guard cannot catch. AGENTS.md §4
requires a malformed PDF to produce an actable error, never a crash.

The lopdf Structure impl delegated get_pages to lopdf's own page-tree
walk, which trusts /Count and pre-allocates by it. The trait's default
walk (depth cap, seen-set, checked page numbering) handles the same
input fine, so the override is simply dropped and the bounded walk
counts the real pages.

Comes with a synthetic regression test (/Count 4294967295, one real
page); cargo test -p pdfcraft-render, clippy -D warnings, fmt --check
and an end-to-end check-one run all pass.

* Drop the duplicated use super::* in the structure tests

---------

Co-authored-by: pechnikov2026-star <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-11 13:33:02 -04:00
1 parent bed9a48ccd
commit 6ed0e66848
1 file changed
+26 -3
+26 -3
View File
@@ -115,9 +115,9 @@ impl Structure for lopdf::Document {
fn encrypted(&self) -> bool {
self.is_encrypted() || self.was_encrypted() || self.trailer.has(b"Encrypt")
}
fn get_pages(&self) -> BTreeMap<u32, ObjectId> {
self.get_pages()
}
// `get_pages` is not overridden: lopdf's own page-tree walk trusts /Count and pre-allocates
// by it, so a hostile /Count (a fuzz finding: 4294967295) aborted the process with a 48 GiB
// allocation. The trait's bounded default walk counts the real pages instead.
fn get_page_fonts(&self, page: ObjectId) -> Result<BTreeMap<Vec<u8>, &Dictionary>> {
self.get_page_fonts(page)
}
@@ -207,6 +207,29 @@ fn convert(object: &CosObject, depth: usize) -> Result<Object> {
mod tests {
use super::*;
/// A hostile page-tree /Count must not abort the process: lopdf's own `get_pages`
/// pre-allocates by it (fuzz finding: /Count 4294967295 -> a 48 GiB allocation),
/// the bounded default walk counts the real pages instead.
#[test]
fn a_hostile_page_count_does_not_abort_inspection() {
let pdf = b"%PDF-1.7
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 4294967295 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>
endobj
trailer
<< /Size 4 /Root 1 0 R >>
%%EOF";
let doc = lopdf::Document::load_mem(&pdf[..]).unwrap();
let pages = Structure::get_pages(&doc);
assert_eq!(pages.len(), 1, "the walk counts the one real page, not /Count");
}
#[test]
fn inspection_loads_only_requested_objects_in_a_compressed_document() {
let mut source = lopdf::Document::with_version("1.7");