Files
pdfcraft/vendor/lopdf/tests/object_stream_edge_cases_test.rs
T
Brandon ThomasandClaude Opus 5.5 1449b71a19 Never crash: bound the allocations behind the nightly fuzz runner's out-of-memory deaths
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>
2026-10-04 17:59:25 -04:00

187 lines
5.8 KiB
Rust

use lopdf::{Document, Object, ObjectStream, dictionary};
#[test]
fn test_empty_trailer() {
let doc = Document::with_version("1.5");
let obj_id = (1, 0);
let obj = Object::Dictionary(dictionary! { "Test" => "Value" });
// With empty trailer, all objects should be compressible
assert!(
ObjectStream::can_be_compressed(obj_id, &obj, &doc),
"Object should be compressible with empty trailer"
);
}
#[test]
fn test_malformed_encrypt_reference() {
let mut doc = Document::with_version("1.5");
let obj_id = doc.add_object(dictionary! {
"Type" => "Catalog"
});
// Set Encrypt to a malformed reference (wrong generation)
doc.trailer.set("Encrypt", Object::Reference((999, 99)));
// Object should still be compressible (not the encryption dict)
assert!(
ObjectStream::can_be_compressed(obj_id, doc.objects.get(&obj_id).unwrap(), &doc),
"Object should be compressible even with malformed Encrypt reference"
);
}
#[test]
fn test_self_referencing_object() {
let mut doc = Document::with_version("1.5");
let obj_id = (5, 0);
doc.objects.insert(
obj_id,
Object::Dictionary(dictionary! {
"Type" => "Test",
"Self" => Object::Reference(obj_id) // Self reference
}),
);
doc.trailer.set("Test", obj_id);
// Should be compressible (not encryption dict)
assert!(
ObjectStream::can_be_compressed(obj_id, doc.objects.get(&obj_id).unwrap(), &doc),
"Self-referencing object should be compressible"
);
}
#[test]
fn test_circular_references() {
let mut doc = Document::with_version("1.5");
let obj1_id = (1, 0);
let obj2_id = (2, 0);
doc.objects.insert(
obj1_id,
Object::Dictionary(dictionary! {
"Next" => Object::Reference(obj2_id)
}),
);
doc.objects.insert(
obj2_id,
Object::Dictionary(dictionary! {
"Next" => Object::Reference(obj1_id) // Circular reference
}),
);
doc.trailer.set("Start", obj1_id);
// Both should be compressible
assert!(
ObjectStream::can_be_compressed(obj1_id, doc.objects.get(&obj1_id).unwrap(), &doc),
"First object in circular reference should be compressible"
);
assert!(
ObjectStream::can_be_compressed(obj2_id, doc.objects.get(&obj2_id).unwrap(), &doc),
"Second object in circular reference should be compressible"
);
}
#[test]
fn test_trailer_with_all_pdf_types() {
let mut doc = Document::with_version("1.5");
let obj_id = doc.add_object(dictionary! { "Type" => "Test" });
// Add all possible PDF types to trailer
doc.trailer.set("Root", obj_id);
doc.trailer.set("Null", Object::Null);
doc.trailer.set("Bool", Object::Boolean(true));
doc.trailer.set("Int", Object::Integer(42));
doc.trailer.set("Real", Object::Real(1.25));
doc.trailer
.set("String", Object::String(b"test".to_vec(), lopdf::StringFormat::Literal));
doc.trailer.set("Name", Object::Name(b"Test".to_vec()));
doc.trailer
.set("Array", Object::Array(vec![Object::Integer(1), Object::Integer(2)]));
doc.trailer
.set("Dict", Object::Dictionary(dictionary! { "Key" => "Value" }));
// Object should still be compressible
assert!(
ObjectStream::can_be_compressed(obj_id, doc.objects.get(&obj_id).unwrap(), &doc),
"Object should be compressible with diverse trailer entries"
);
}
#[test]
fn test_unicode_trailer_keys() {
let mut doc = Document::with_version("1.5");
let obj_id = doc.add_object(dictionary! { "Test" => "Value" });
// Use Unicode/non-ASCII keys in trailer
doc.trailer.set("Root", obj_id);
doc.trailer.set(
"Ünïcödé",
Object::String(b"test".to_vec(), lopdf::StringFormat::Literal),
);
doc.trailer.set("日本語", Object::Integer(42));
doc.trailer.set("🎯", Object::Boolean(true));
// Should still work correctly
assert!(
ObjectStream::can_be_compressed(obj_id, doc.objects.get(&obj_id).unwrap(), &doc),
"Object should be compressible with Unicode trailer keys"
);
}
#[test]
fn test_very_large_trailer() {
let mut doc = Document::with_version("1.5");
let obj_id = doc.add_object(dictionary! { "Type" => "Catalog" });
// Add many entries to trailer
doc.trailer.set("Root", obj_id);
for i in 0..1000 {
doc.trailer
.set(format!("Custom{:04}", i).as_bytes(), Object::Integer(i));
}
// Should still be efficient
let start = std::time::Instant::now();
let compressible = ObjectStream::can_be_compressed(obj_id, doc.objects.get(&obj_id).unwrap(), &doc);
let duration = start.elapsed();
assert!(compressible, "Object should be compressible even with large trailer");
assert!(
duration.as_micros() < 100,
"Check should be fast even with large trailer"
);
}
#[test]
fn test_concurrent_modification_safety() {
// This test verifies the function doesn't panic with concurrent-like access patterns
let mut doc = Document::with_version("1.5");
let obj1_id = doc.add_object(dictionary! { "Type" => "Test1" });
let obj2_id = doc.add_object(dictionary! { "Type" => "Test2" });
doc.trailer.set("Ref1", obj1_id);
// Check first object
let result1 = ObjectStream::can_be_compressed(obj1_id, doc.objects.get(&obj1_id).unwrap(), &doc);
// Modify trailer between checks
doc.trailer.set("Ref2", obj2_id);
doc.trailer.set("Encrypt", obj1_id); // Now obj1 is encryption dict
// Check again - result should change
let result2 = ObjectStream::can_be_compressed(obj1_id, doc.objects.get(&obj1_id).unwrap(), &doc);
assert!(result1, "Initially should be compressible");
assert!(!result2, "Should not be compressible after becoming encryption dict");
}