mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 10:39:01 -06:00
Fuzzed inputs drove one check-one process to 2-6 GB (the 16 GB CI runner running two of them was killed every night). Three causes, each with a test: - lopdf (bootstrap inspector) sized PNG predictor rows from /Columns before reading data: /Columns 4294967295 allocated two 4 GiB rows. Vendored lopdf 0.45.0 refuses rows longer than the data (and uses saturating row widths); its load-time stream decoding also gets a 256 MiB limit (it had none). - hayro sized a tiling pattern's cell pixmap from /XStep x scale, bounded only by u16: a step far beyond /BBox allocated 2 GB. The vendored patch keeps the cell within 3000 px a side, lowering the resolution instead. - cos decoded cross-reference streams up to the general 1 GiB limit; they now stop at what 8,388,607 objects' rows can hold (~200 MB at most), and the section is reconstructed (logged) instead. On the nightly's seed (41) the worst captured input fell from 6.2 GB to 6 MB, and a 300 s fuzz run had no process above 300 MB (before: many at 2-5 GB). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
136 lines
5.3 KiB
Rust
136 lines
5.3 KiB
Rust
use lopdf::{Document, Object, ObjectStream};
|
|
|
|
#[cfg(feature = "async")]
|
|
use tokio::runtime::Builder;
|
|
|
|
#[cfg(not(feature = "async"))]
|
|
fn load_document(path: &str) -> Result<Document, lopdf::Error> {
|
|
Document::load(path)
|
|
}
|
|
|
|
#[cfg(feature = "async")]
|
|
fn load_document(path: &str) -> Result<Document, lopdf::Error> {
|
|
Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async move { Document::load(path).await })
|
|
}
|
|
|
|
fn main() {
|
|
let pdf_path = "/Users/nicolasdao/Downloads/pdfs/RFQ - SDS WebApp.docx_compressed.pdf";
|
|
println!("Inspecting object stream in: {}", pdf_path);
|
|
|
|
match load_document(pdf_path) {
|
|
Ok(doc) => {
|
|
// Find object stream 511
|
|
if let Ok(Object::Stream(stream)) = doc.get_object((511, 0)) {
|
|
println!("\nObject Stream 511 0 R found!");
|
|
println!("Dictionary: {:?}", stream.dict);
|
|
|
|
// Try to parse it
|
|
match ObjectStream::new(stream) {
|
|
Ok(obj_stream) => {
|
|
println!("\nSuccessfully parsed object stream!");
|
|
println!("Contains {} objects", obj_stream.objects.len());
|
|
|
|
// List first 20 objects
|
|
println!("\nFirst 20 objects in stream:");
|
|
for (count, ((id, generation), obj)) in obj_stream.objects.iter().enumerate() {
|
|
if count >= 20 {
|
|
break;
|
|
}
|
|
println!(" {} {} R: {:?}", id, generation, obj.type_name().unwrap_or(b"Unknown"));
|
|
|
|
// If it's a dictionary, show some keys
|
|
if let Object::Dictionary(dict) = obj {
|
|
let key_count = dict.len();
|
|
println!(" Dictionary with {} keys", key_count);
|
|
}
|
|
}
|
|
|
|
// Check if any page-related objects are in there
|
|
println!("\nChecking for page-related objects:");
|
|
for ((id, generation), obj) in &obj_stream.objects {
|
|
if let Object::Dictionary(dict) = obj {
|
|
if let Ok(type_obj) = dict.get(b"Type")
|
|
&& let Ok(type_name) = type_obj.as_name()
|
|
&& type_name == b"Page"
|
|
{
|
|
println!(" WARNING: Page object {} {} R is in object stream!", id, generation);
|
|
}
|
|
|
|
// Check for font descriptors
|
|
if dict.has(b"FontDescriptor") || dict.has(b"BaseFont") {
|
|
println!(" Font-related object {} {} R", id, generation);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
println!("ERROR parsing object stream: {}", e);
|
|
}
|
|
}
|
|
} else {
|
|
println!("Object stream 511 not found or not a stream!");
|
|
}
|
|
|
|
// Also check what critical objects might be compressed
|
|
println!("\n\nChecking critical object locations:");
|
|
|
|
// Check catalog
|
|
if let Ok(root_ref) = doc.trailer.get(b"Root")
|
|
&& let Object::Reference(root_id) = root_ref
|
|
{
|
|
check_object_status(&doc, *root_id, "Catalog (Root)");
|
|
}
|
|
|
|
// Check pages tree
|
|
if let Ok(catalog) = doc.catalog()
|
|
&& let Ok(pages_ref) = catalog.get(b"Pages")
|
|
&& let Object::Reference(pages_id) = pages_ref
|
|
{
|
|
check_object_status(&doc, *pages_id, "Pages tree root");
|
|
}
|
|
|
|
// Check specific page objects
|
|
let pages = doc.get_pages();
|
|
for (num, &id) in pages.iter().take(2) {
|
|
check_object_status(&doc, id, &format!("Page {}", num));
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Failed to load PDF: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn check_object_status(doc: &Document, id: (u32, u16), name: &str) {
|
|
match doc.get_object(id) {
|
|
Ok(_) => {
|
|
// Check xref entry
|
|
if let Some(xref_entry) = doc.reference_table.get(id.0) {
|
|
match xref_entry {
|
|
lopdf::xref::XrefEntry::Normal { offset, generation: _ } => {
|
|
println!("{} ({} {} R): Normal entry at offset {}", name, id.0, id.1, offset);
|
|
}
|
|
lopdf::xref::XrefEntry::Compressed { container, index } => {
|
|
println!(
|
|
"{} ({} {} R): COMPRESSED in stream {} at index {}",
|
|
name, id.0, id.1, container, index
|
|
);
|
|
}
|
|
_ => {
|
|
println!("{} ({} {} R): Other xref type", name, id.0, id.1);
|
|
}
|
|
}
|
|
} else {
|
|
println!("{} ({} {} R): No xref entry!", name, id.0, id.1);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
println!("{} ({} {} R): ERROR - {}", name, id.0, id.1, e);
|
|
}
|
|
}
|
|
}
|