mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 11:08:48 -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>
174 lines
5.7 KiB
Rust
174 lines
5.7 KiB
Rust
use lopdf::{Document, Object};
|
|
use std::env;
|
|
|
|
#[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 args: Vec<String> = env::args().collect();
|
|
if args.len() < 2 {
|
|
eprintln!("Usage: {} <pdf_file>", args[0]);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let pdf_path = &args[1];
|
|
println!("Analyzing page contents in: {}", pdf_path);
|
|
println!("{}", "=".repeat(80));
|
|
|
|
match load_document(pdf_path) {
|
|
Ok(doc) => {
|
|
analyze_pages(&doc);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Failed to load PDF: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn analyze_pages(doc: &Document) {
|
|
let pages = doc.get_pages();
|
|
println!("Total pages: {}", pages.len());
|
|
|
|
// Check if we have object streams
|
|
let mut obj_streams = Vec::new();
|
|
for (id, obj) in &doc.objects {
|
|
if let Object::Stream(stream) = obj
|
|
&& let Ok(type_obj) = stream.dict.get(b"Type")
|
|
&& let Ok(type_name) = type_obj.as_name()
|
|
&& type_name == b"ObjStm"
|
|
{
|
|
obj_streams.push(id.0);
|
|
}
|
|
}
|
|
|
|
if !obj_streams.is_empty() {
|
|
println!("\nObject streams found: {:?}", obj_streams);
|
|
}
|
|
|
|
println!("\nAnalyzing pages:");
|
|
for (page_num, &page_id) in pages.iter() {
|
|
println!("\n{}", "-".repeat(60));
|
|
println!("Page {}: Object {} 0 R", page_num, page_id.0);
|
|
|
|
match doc.get_object(page_id) {
|
|
Ok(Object::Dictionary(page_dict)) => {
|
|
// Check page type
|
|
if let Ok(type_obj) = page_dict.get(b"Type") {
|
|
println!(" Type: {:?}", type_obj);
|
|
}
|
|
|
|
// Check Contents
|
|
match page_dict.get(b"Contents") {
|
|
Ok(Object::Reference(content_id)) => {
|
|
println!(" Contents: {} {} R", content_id.0, content_id.1);
|
|
check_content_stream(doc, *content_id);
|
|
}
|
|
Ok(Object::Array(contents)) => {
|
|
println!(" Contents array with {} elements:", contents.len());
|
|
for (i, content_ref) in contents.iter().enumerate() {
|
|
if let Object::Reference(content_id) = content_ref {
|
|
println!(" [{}] {} {} R", i, content_id.0, content_id.1);
|
|
check_content_stream(doc, *content_id);
|
|
}
|
|
}
|
|
}
|
|
Ok(other) => {
|
|
println!(" Contents: Unexpected type: {:?}", other);
|
|
}
|
|
Err(e) => {
|
|
println!(" Contents: ERROR - {}", e);
|
|
}
|
|
}
|
|
|
|
// Check Resources
|
|
match page_dict.get(b"Resources") {
|
|
Ok(Object::Reference(res_id)) => {
|
|
println!(" Resources: {} {} R", res_id.0, res_id.1);
|
|
check_object_location(doc, *res_id);
|
|
}
|
|
Ok(Object::Dictionary(_)) => {
|
|
println!(" Resources: Inline dictionary");
|
|
}
|
|
Ok(other) => {
|
|
println!(" Resources: Unexpected type: {:?}", other);
|
|
}
|
|
Err(e) => {
|
|
println!(" Resources: ERROR - {}", e);
|
|
}
|
|
}
|
|
}
|
|
Ok(other) => {
|
|
println!(" ERROR: Page is not a dictionary! Type: {:?}", other);
|
|
}
|
|
Err(e) => {
|
|
println!(" ERROR: Cannot get page object: {}", e);
|
|
check_object_location(doc, page_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn check_content_stream(doc: &Document, content_id: (u32, u16)) {
|
|
match doc.get_object(content_id) {
|
|
Ok(Object::Stream(stream)) => {
|
|
println!(" ✓ Stream exists");
|
|
println!(" Length: {}", stream.content.len());
|
|
if let Ok(filter) = stream.dict.get(b"Filter") {
|
|
println!(" Filter: {:?}", filter);
|
|
}
|
|
|
|
// Try to get decompressed content
|
|
match stream.decompressed_content() {
|
|
Ok(content) => {
|
|
let preview = String::from_utf8_lossy(&content[..content.len().min(100)]);
|
|
println!(" Preview: {:?}", preview);
|
|
}
|
|
Err(e) => {
|
|
println!(" ERROR decompressing: {}", e);
|
|
}
|
|
}
|
|
}
|
|
Ok(other) => {
|
|
println!(" ERROR: Content is not a stream! Type: {:?}", other);
|
|
}
|
|
Err(e) => {
|
|
println!(" ERROR: Cannot get content stream: {}", e);
|
|
check_object_location(doc, content_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn check_object_location(doc: &Document, obj_id: (u32, u16)) {
|
|
// Check if object exists
|
|
if doc.objects.contains_key(&obj_id) {
|
|
println!(" Note: Object exists in document");
|
|
} else {
|
|
println!(
|
|
" WARNING: Object {} {} R not found in document!",
|
|
obj_id.0, obj_id.1
|
|
);
|
|
|
|
// Check xref
|
|
if let Some(xref_entry) = doc.reference_table.get(obj_id.0) {
|
|
println!(" Xref entry: {:?}", xref_entry);
|
|
} else {
|
|
println!(" No xref entry found!");
|
|
}
|
|
}
|
|
}
|