mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 18:47:07 -06:00
PDF: bound what a FlateDecode stream may inflate to
inflate used the unbounded miniz_oxide functions, and File::parse decodes every object stream eagerly. Use the limit variants (1 GiB per stream, 256 MiB for object streams, a quarter of that on 32-bit targets) and bound the truncated-stream salvage buffer too. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
813c7c4650
commit
8c665a19e2
+44
-10
@@ -304,9 +304,28 @@ pub fn parse_num(w: &[u8]) -> Option<f64> {
|
||||
|
||||
// ------------------------------------------------------------------ filters
|
||||
|
||||
/// The most bytes one FlateDecode filter may inflate to. Deflate expands up to about 1000:1, so
|
||||
/// unbounded, a few MB of PDF ask for gigabytes and a failed allocation aborts the process. The
|
||||
/// largest image `image::decode` accepts (64 Mi pixels) of four 16-bit components is 512 MiB;
|
||||
/// 1 GiB leaves room for more components and is PdfCraft's ceiling for one stream. 32-bit
|
||||
/// targets (the web build) get a quarter: their whole address space is 4 GiB, and the inflater's
|
||||
/// growing buffer can reserve about twice the limit.
|
||||
const MAX_INFLATED: usize = if cfg!(target_pointer_width = "32") { 256 << 20 } else { 1 << 30 };
|
||||
|
||||
/// The tighter bound for object streams, which [`File::parse`] decodes eagerly (all of them,
|
||||
/// before any page is chosen). They hold only other objects' dictionaries, arrays, numbers and
|
||||
/// strings, so real ones are far smaller; PdfCraft uses 256 MiB for the object and
|
||||
/// cross-reference streams it decodes while loading. A quarter on 32-bit targets, as above.
|
||||
const MAX_OBJECT_STREAM: usize = if cfg!(target_pointer_width = "32") { 64 << 20 } else { 256 << 20 };
|
||||
|
||||
/// Decode a stream's data through its `/Filter` chain. Unknown filters (images: DCT, JPX, CCITT,
|
||||
/// JBIG2) give `None`.
|
||||
pub fn decode_stream(file: &File, d: &Dict, raw: &[u8]) -> Option<Vec<u8>> {
|
||||
decode_stream_within(file, d, raw, MAX_INFLATED)
|
||||
}
|
||||
|
||||
/// [`decode_stream`] with each FlateDecode filter's output bounded by `max` bytes.
|
||||
pub fn decode_stream_within(file: &File, d: &Dict, raw: &[u8], max: usize) -> Option<Vec<u8>> {
|
||||
let filters: Vec<String> = match d.get("Filter").map(|f| file.resolve(f)) {
|
||||
None => vec![],
|
||||
Some(Obj::Name(n)) => vec![n.clone()],
|
||||
@@ -321,7 +340,7 @@ pub fn decode_stream(file: &File, d: &Dict, raw: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut data = raw.to_vec();
|
||||
for (i, f) in filters.iter().enumerate() {
|
||||
data = match f.as_str() {
|
||||
"FlateDecode" | "Fl" => inflate(&data)?,
|
||||
"FlateDecode" | "Fl" => inflate(&data, max)?,
|
||||
"ASCIIHexDecode" | "AHx" => ascii_hex(&data),
|
||||
"ASCII85Decode" | "A85" => ascii85(&data),
|
||||
"LZWDecode" | "LZW" => {
|
||||
@@ -358,14 +377,29 @@ fn ccitt_params(file: &File, d: Option<&Dict>) -> crate::ccitt::Params {
|
||||
p
|
||||
}
|
||||
|
||||
pub fn inflate(data: &[u8]) -> Option<Vec<u8>> {
|
||||
miniz_oxide::inflate::decompress_to_vec_zlib(data).ok().or_else(|| miniz_oxide::inflate::decompress_to_vec(data).ok()).or_else(|| {
|
||||
// Truncated streams: keep what inflates.
|
||||
let mut d = miniz_oxide::inflate::stream::InflateState::new_boxed(miniz_oxide::DataFormat::Zlib);
|
||||
let mut out = vec![0u8; data.len() * 8 + 1024];
|
||||
let r = miniz_oxide::inflate::stream::inflate(&mut d, data, &mut out, miniz_oxide::MZFlush::Finish);
|
||||
(r.bytes_written > 0).then(|| out[..r.bytes_written].to_vec())
|
||||
})
|
||||
/// Inflate zlib data (or raw deflate) to at most `max` bytes. A stream that inflates to more is
|
||||
/// undecodable (`None`), not salvaged as a truncated one.
|
||||
pub fn inflate(data: &[u8], max: usize) -> Option<Vec<u8>> {
|
||||
use miniz_oxide::inflate::{TINFLStatus, decompress_to_vec_with_limit, decompress_to_vec_zlib_with_limit};
|
||||
for attempt in [decompress_to_vec_zlib_with_limit, decompress_to_vec_with_limit] {
|
||||
match attempt(data, max) {
|
||||
Ok(v) => return Some(v),
|
||||
Err(e) if e.status == TINFLStatus::HasMoreOutput => return None,
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
// Truncated streams: keep what inflates.
|
||||
let mut d = miniz_oxide::inflate::stream::InflateState::new_boxed(miniz_oxide::DataFormat::Zlib);
|
||||
let mut out = vec![0u8; salvage_len(data.len(), max)];
|
||||
let r = miniz_oxide::inflate::stream::inflate(&mut d, data, &mut out, miniz_oxide::MZFlush::Finish);
|
||||
(r.bytes_written > 0).then(|| out[..r.bytes_written].to_vec())
|
||||
}
|
||||
|
||||
/// The buffer [`inflate`] salvages a truncated stream of `data_len` bytes into: eight times that
|
||||
/// and 1 KiB, at most `max`. The zlib attempt already stopped within `max`, so the bound loses
|
||||
/// nothing, while without it a Flate filter after another could ask for eight times `max`.
|
||||
pub fn salvage_len(data_len: usize, max: usize) -> usize {
|
||||
data_len.saturating_mul(8).saturating_add(1024).min(max)
|
||||
}
|
||||
|
||||
fn ascii_hex(data: &[u8]) -> Vec<u8> {
|
||||
@@ -655,7 +689,7 @@ impl File {
|
||||
})
|
||||
.collect();
|
||||
for (d, raw) in streams {
|
||||
let Some(dec) = decode_stream(&f, &d, &raw) else { continue };
|
||||
let Some(dec) = decode_stream_within(&f, &d, &raw, MAX_OBJECT_STREAM) else { continue };
|
||||
let n = d.get("N").and_then(Obj::num).unwrap_or(0.0) as usize;
|
||||
let first = d.get("First").and_then(Obj::num).unwrap_or(0.0) as usize;
|
||||
let mut lx = Lexer::new(&dec, 0);
|
||||
|
||||
+55
-1
@@ -147,7 +147,61 @@ fn eps_postscript_subset() {
|
||||
fn filters_decode() {
|
||||
assert_eq!(object::ascii85(b"<~87cURD]i,\"Ebo80~>"), b"Hello World!".to_vec());
|
||||
let z = miniz_oxide::deflate::compress_to_vec_zlib(b"abc", 6);
|
||||
assert_eq!(object::inflate(&z).unwrap(), b"abc");
|
||||
assert_eq!(object::inflate(&z, 1 << 20).unwrap(), b"abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flate_bomb_is_bounded() {
|
||||
// A decompression bomb in miniature: 16 MiB of zeros deflate to about 16 KB.
|
||||
let zeros = vec![0u8; 16 << 20];
|
||||
for bomb in [miniz_oxide::deflate::compress_to_vec_zlib(&zeros, 6), miniz_oxide::deflate::compress_to_vec(&zeros, 6)] {
|
||||
assert!(bomb.len() < 64 << 10);
|
||||
// Past the limit, as zlib and as raw deflate, it is undecodable: not inflated in full
|
||||
// and not salvaged as a truncated stream.
|
||||
assert_eq!(object::inflate(&bomb, 1 << 20).map(|v| v.len()), None);
|
||||
assert_eq!(object::inflate(&bomb, 32 << 20).map(|v| v.len()), Some(16 << 20));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_flate_keeps_what_inflates() {
|
||||
let mut x = 1u32;
|
||||
let data: Vec<u8> = (0..20_000)
|
||||
.map(|_| {
|
||||
x = x.wrapping_mul(1_103_515_245).wrapping_add(12_345);
|
||||
(x >> 16) as u8
|
||||
})
|
||||
.collect();
|
||||
let z = miniz_oxide::deflate::compress_to_vec_zlib(&data, 6);
|
||||
let part = object::inflate(&z[..z.len() / 2], 1 << 20).unwrap();
|
||||
assert!(!part.is_empty() && part.len() < data.len());
|
||||
assert_eq!(part, data[..part.len()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn salvage_buffer_is_bounded_by_the_limit() {
|
||||
// Small inputs keep eight times their size and 1 KiB.
|
||||
assert_eq!(object::salvage_len(0, 1 << 20), 1024);
|
||||
assert_eq!(object::salvage_len(10_000, 1 << 20), 81_024);
|
||||
// Large ones stop at the limit (`1 << 28` fits a 32-bit `usize` too), without overflowing.
|
||||
assert_eq!(object::salvage_len(1 << 28, 1 << 20), 1 << 20);
|
||||
assert_eq!(object::salvage_len(usize::MAX, 1 << 20), 1 << 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_bomb_in_the_second_flate_filter_is_refused() {
|
||||
// `/Filter [/FlateDecode /FlateDecode]`: the first filter inflates, within the limit, to a zlib
|
||||
// bomb (padded to just under the limit) that the second would inflate to 16 MiB. The second
|
||||
// stops at the limit, so the stream is undecodable.
|
||||
const MAX: usize = 1 << 20;
|
||||
let flate = object::Obj::Name("FlateDecode".into());
|
||||
let chain: object::Dict = [("Filter".to_string(), object::Obj::Array(vec![flate.clone(), flate]))].into_iter().collect();
|
||||
let file = object::File::parse(b"");
|
||||
let mut nested = miniz_oxide::deflate::compress_to_vec_zlib(&vec![0u8; 16 << 20], 6);
|
||||
nested.resize(MAX - 1, 0);
|
||||
let raw = miniz_oxide::deflate::compress_to_vec_zlib(&nested, 6);
|
||||
assert_eq!(object::inflate(&raw, MAX).map(|v| v.len()), Some(MAX - 1));
|
||||
assert_eq!(object::decode_stream_within(&file, &chain, &raw, MAX).map(|v| v.len()), None);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ M13.6: text, images,
|
||||
|
||||
Reference in New Issue
Block a user