Files
pdfcraft/vendor/lopdf/src/load_options.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

92 lines
3.2 KiB
Rust

use crate::Object;
/// Type alias for the filter function used during PDF loading.
///
/// The function receives an object ID and a mutable reference to the object,
/// and returns `Some((id, object))` to keep it or `None` to discard it.
pub type FilterFunc = fn((u32, u16), &mut Object) -> Option<((u32, u16), Object)>;
/// Options for loading PDF documents.
///
/// Use this struct to configure password, object filtering, and strictness
/// when loading a PDF. The default is lenient parsing with no password or filter.
///
/// # Examples
///
/// ```no_run
/// use lopdf::{Document, LoadOptions};
///
/// // Load with a password
/// let doc = Document::load_with_options(
/// "encrypted.pdf",
/// LoadOptions::with_password("secret"),
/// );
///
/// // Load with strict parsing
/// let doc = Document::load_with_options(
/// "document.pdf",
/// LoadOptions { strict: true, ..Default::default() },
/// );
/// ```
#[derive(Clone, Default)]
pub struct LoadOptions {
/// Password for encrypted PDFs.
pub password: Option<String>,
/// Object filter applied during loading.
pub filter: Option<FilterFunc>,
/// When `true`, reject non-conforming PDFs instead of silently accepting them.
/// Defaults to `false` (lenient parsing).
pub strict: bool,
/// Maximum number of bytes any single stream may decompress to during
/// loading (object streams and cross-reference streams).
///
/// Compression filters can inflate a tiny input into an enormous output (a
/// "decompression bomb"). Because object and xref streams are decoded eagerly
/// while the document is loaded, an unbounded stream can exhaust memory
/// before any of your code runs. Set this to bound that per-stream cost when
/// loading untrusted PDFs; a stream that would exceed it fails with
/// [`crate::DecompressError::MemoryLimitExceeded`].
///
/// `None` (the default) applies no limit.
pub max_decompressed_size: Option<usize>,
}
impl std::fmt::Debug for LoadOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LoadOptions")
.field("password", &self.password.as_ref().map(|_| "***"))
.field("filter", &self.filter.map(|_| "fn(..)"))
.field("strict", &self.strict)
.field("max_decompressed_size", &self.max_decompressed_size)
.finish()
}
}
impl LoadOptions {
/// Create options with a password for encrypted PDFs.
pub fn with_password(password: &str) -> Self {
Self {
password: Some(password.to_string()),
..Default::default()
}
}
/// Create options with an object filter.
pub fn with_filter(filter: FilterFunc) -> Self {
Self {
filter: Some(filter),
..Default::default()
}
}
/// Create options that bound how large any single stream may decompress to
/// during loading, to defend against decompression bombs in untrusted PDFs.
/// See [`LoadOptions::max_decompressed_size`].
pub fn with_max_decompressed_size(max_decompressed_size: usize) -> Self {
Self {
max_decompressed_size: Some(max_decompressed_size),
..Default::default()
}
}
}