Files
vectorcraft/crates/pdf/src/streams.rs
T
Taha-Mahmoodi d3a01307ea PDF import: a string entered from many dictionaries in skipped text is read once
Review of #3924: in a flat area of ` <<(` (CMYK or RGBA 32, 60, 60, 40) in an uncompressed image, each `<<` after white space was read as a dictionary, and each read its string, which never ends, to 64 KiB past it: about 16 K bytes a pixel, so a few hundred bytes of it spent the budget and the file was refused.

Whether a byte in a literal string is escaped doesn't depend on where reading began (the run of `\` before it lies after any `(` before it), so the string from any `(` ends after the first `)` that takes the file-wide count of `(` less `)` below the count at that `(`. Those ends are found for the whole file in three linear passes the first time a string read in skipped text runs past 64 bytes: forward for the final count, back for the `(` whose string never ends, forward pairing the rest. Ends further than 64 bytes are kept (at most one for every 16 bytes of the file; a file built to nest past that is refused); nearer ones are read again. So entering a string costs at most 64 bytes, charged as one. A comment's end of line is kept for every comment read, not only the last.
2026-10-24 17:19:59 +07:00

962 lines
44 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Streams looked at before the PDF reader (hayro) decodes them. It inflates Flate and LZW data
//! with no ceiling, so a few megabytes can ask for gigabytes (#1700), and it applies a predictor
//! before checking its parameters: `Columns 0` divides by zero, a huge `Columns` allocates a row
//! that large whatever the stream holds (#1701). A file with such a stream is refused instead.
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::io::Read;
use craft_pdf::syntax::content::TypedIter;
use craft_pdf::syntax::content::ops::TypedInstruction;
use craft_pdf::syntax::object::{Array, Dict, FromBytes, MaybeRef, Name, Object, Stream};
/// The most a stream may decode to: 256 MB, the pixels of the largest image the app reads (64
/// million, 4 bytes each).
pub(crate) const MAX_DECODED: usize = 256 << 20;
/// The most an image XObject may decode to when its own size asks for more than
/// [`MAX_DECODED`]: 1 GB (an A0 sheet scanned at 300 ppi is 418 MB in RGB, 557 MB in CMYK).
const MAX_IMAGE_DECODED: usize = 1 << 30;
/// The longest row a predictor may have, in bytes.
const MAX_ROW: usize = 1 << 24;
/// A stream that decodes to more than its [`budget`].
struct TooBig;
/// The filters of the stream with dictionary `dict`, by name (`Filter` or `F`: a name or an array).
fn filters<'a>(dict: &Dict<'a>) -> Vec<Name<'a>> {
[&b"Filter"[..], b"F"]
.into_iter()
.find_map(|key| dict.get::<Name<'a>>(key).map(|n| vec![n]).or_else(|| dict.get::<Array<'a>>(key).map(|a| a.iter::<Name<'a>>().collect())))
.unwrap_or_default()
}
/// Every decode parameter dictionary of the stream with dictionary `dict`, as the reader may take
/// them (`DecodeParms` or `DP`: a dictionary, or an array with one for each filter).
fn params<'a>(dict: &Dict<'a>) -> Vec<Dict<'a>> {
let mut all = vec![];
for key in [&b"DecodeParms"[..], b"DP"] {
all.extend(dict.get::<Dict<'a>>(key));
all.extend(dict.get::<Array<'a>>(key).into_iter().flat_map(|a| a.iter::<Object<'a>>().filter_map(|o| o.into_dict()).collect::<Vec<_>>()));
}
all
}
/// Would the reader's predictor fail on `params`? It reads `Colors` and `BitsPerComponent` as
/// bytes and `Columns` as a size, multiplies the first two as bytes, divides by the length of a row
/// and allocates one before it checks anything.
fn bad_predictor(params: &Dict<'_>) -> bool {
if params.get::<u8>(b"Predictor").unwrap_or(1) == 1 {
return false;
}
let colors = usize::from(params.get::<u8>(b"Colors").unwrap_or(1));
let bits = usize::from(params.get::<u8>(b"BitsPerComponent").unwrap_or(8));
let columns = params.get::<usize>(b"Columns").unwrap_or(1);
let pixel = colors * bits;
let row = columns.checked_mul(pixel).map(|b| b.div_ceil(8));
!(matches!(bits, 1 | 2 | 4 | 8 | 16) && colors > 0 && pixel <= 255 && columns > 0 && row.is_some_and(|r| r <= MAX_ROW))
}
/// `data` inflated, up to one byte past `max` (zlib, or raw deflate as the reader also takes);
/// `None` when it is damaged before that.
fn inflate(data: &[u8], max: usize) -> Option<Vec<u8>> {
let read = |mut r: Box<dyn Read + '_>| {
let mut out = vec![];
// (Damaged data: what came out before the damage still counts.)
let done = r.by_ref().take(max as u64 + 1).read_to_end(&mut out).is_ok();
(done || out.len() > max).then_some(out)
};
read(Box::new(flate2::read::ZlibDecoder::new(data))).or_else(|| read(Box::new(flate2::read::DeflateDecoder::new(data))))
}
/// `data` decoded from LZW, up to one byte past `max`. `early`: the code length grows one code
/// early (`EarlyChange`, 1 by default).
fn lzw(data: &[u8], early: bool, max: usize) -> Option<Vec<u8>> {
use weezl::{BitOrder, LzwStatus, decode::Decoder};
let mut decoder = if early { Decoder::with_tiff_size_switch(BitOrder::Msb, 8) } else { Decoder::new(BitOrder::Msb, 8) };
let (mut out, mut chunk, mut input) = (vec![], vec![0; 1 << 16], data);
while out.len() <= max {
let r = decoder.decode_bytes(input, &mut chunk);
input = input.get(r.consumed_in..).unwrap_or_default();
out.extend_from_slice(chunk.get(..r.consumed_out).unwrap_or_default());
match r.status {
Ok(LzwStatus::Ok) => {}
Ok(LzwStatus::Done | LzwStatus::NoProgress) => break,
Err(_) => return (out.len() > max).then_some(out),
}
}
Some(out)
}
/// `data` decoded from run-length encoding, up to one byte past `max`.
fn run_length(data: &[u8], max: usize) -> Vec<u8> {
let (mut out, mut at) = (vec![], 0);
while let Some(&n) = data.get(at)
&& n != 128
&& out.len() <= max
{
if n < 128 {
let take = usize::from(n) + 1;
out.extend_from_slice(data.get(at + 1..).unwrap_or_default().get(..take).unwrap_or_default());
at += 1 + take;
} else {
out.extend(std::iter::repeat_n(data.get(at + 1).copied().unwrap_or(0), 257 - usize::from(n)));
at += 2;
}
}
out
}
/// `data` decoded from hexadecimal digits (white space skipped, up to `>`).
fn hex(data: &[u8]) -> Vec<u8> {
let digits: Vec<u8> = data.iter().take_while(|c| **c != b'>').filter_map(|c| char::from(*c).to_digit(16)).map(|d| d as u8).collect();
digits.chunks(2).map(|p| (p.first().copied().unwrap_or(0) << 4) | p.get(1).copied().unwrap_or(0)).collect()
}
/// `data` decoded from base-85 (white space skipped, `z` for four zeros, up to `~>`).
fn base85(data: &[u8]) -> Vec<u8> {
let (mut out, mut group) = (vec![], vec![]);
let flush = |group: &mut Vec<u32>, out: &mut Vec<u8>| {
let n = group.len();
if n > 1 {
group.resize(5, 84);
let v = group.iter().fold(0u32, |v, d| v.wrapping_mul(85).wrapping_add(*d));
out.extend_from_slice(v.to_be_bytes().get(..n - 1).unwrap_or_default());
}
group.clear();
};
for &c in data.iter().take_while(|c| **c != b'~') {
match c {
b'z' if group.is_empty() => out.extend([0; 4]),
b'!'..=b'u' => {
group.push(u32::from(c - b'!'));
if group.len() == 5 {
flush(&mut group, &mut out);
}
}
_ => {}
}
}
flush(&mut group, &mut out);
out
}
/// The most data of `len` bytes can grow to through `filters`: 1,032 times through Flate (its
/// largest ratio), 3,641 through LZW (4,096 bytes from a 9-bit code), 64 through run-length.
fn most_after(len: usize, filters: &[Name<'_>]) -> u128 {
filters.iter().fold(len as u128, |n, f| {
n.saturating_mul(match f.as_ref() {
b"FlateDecode" | b"Fl" => 1032,
b"LZWDecode" | b"LZW" => 3641,
b"RunLengthDecode" | b"RL" => 64,
_ => 1,
})
})
}
/// The number of colour components of the image with dictionary `dict`, from its colour space
/// (4 when it can't be told).
fn components(dict: &Dict<'_>) -> u64 {
let named = |n: &[u8]| match n {
b"DeviceGray" | b"G" | b"CalGray" | b"Indexed" | b"I" | b"Separation" => 1,
b"DeviceRGB" | b"RGB" | b"CalRGB" | b"Lab" => 3,
_ => 4,
};
if let Some(n) = dict.get::<Name<'_>>(b"ColorSpace") {
return named(n.as_ref());
}
let Some(a) = dict.get::<Array<'_>>(b"ColorSpace") else { return 4 };
let mut it = a.flex_iter();
let any = |n: Option<usize>| n.filter(|n| (1..=32).contains(n)).map_or(4, |n| n as u64);
match it.next::<Name<'_>>() {
Some(n) if n.as_ref() == b"ICCBased" => any(it.next::<Stream<'_>>().and_then(|s| s.dict().get::<usize>(b"N"))),
Some(n) if n.as_ref() == b"DeviceN" => any(it.next::<Array<'_>>().map(|names| names.iter::<Object<'_>>().count())),
Some(n) => named(n.as_ref()),
None => 4,
}
}
/// The most the stream with dictionary `dict` may decode to: [`MAX_DECODED`], or for an image
/// XObject the size its width, height, components and bits declare (each row with a predictor's
/// tag byte), up to [`MAX_IMAGE_DECODED`]: an image's size is known before it is decoded.
fn budget(dict: &Dict<'_>) -> usize {
if dict.get::<Name<'_>>(b"Subtype").is_none_or(|s| s.as_ref() != b"Image") {
return MAX_DECODED;
}
let side = |key: &[u8]| dict.get::<u32>(key).map_or(0, u64::from);
let mask = dict.get::<bool>(b"ImageMask") == Some(true);
let bits = if mask { 1 } else { dict.get::<u8>(b"BitsPerComponent").map_or(8, |b| u64::from(b.min(16))) };
let channels = if mask { 1 } else { components(dict) };
let row = side(b"Width").saturating_mul(channels).saturating_mul(bits).div_ceil(8).saturating_add(1);
let declared = usize::try_from(side(b"Height").saturating_mul(row)).unwrap_or(usize::MAX);
declared.clamp(MAX_DECODED, MAX_IMAGE_DECODED)
}
/// `data` (of the stream with dictionary `dict`) through its filters, as far as this reads them:
/// Flate, LZW, run-length and the ASCII encodings. Not decoded when it can't decode past its
/// [`budget`] and `keep` doesn't ask for it, or when an image codec or damaged data comes first
/// (an image's own decoder knows its size).
fn decode(data: &[u8], dict: &Dict<'_>, keep: bool) -> Result<Option<Vec<u8>>, TooBig> {
let filters = filters(dict);
let max = budget(dict);
if !keep && most_after(data.len(), &filters) <= max as u128 {
return Ok(None);
}
let params = params(dict);
let mut data = Cow::Borrowed(data);
for (i, f) in filters.iter().enumerate() {
let early = params.get(i).and_then(|p| p.get::<u8>(b"EarlyChange")).is_none_or(|e| e != 0);
let out = match f.as_ref() {
b"FlateDecode" | b"Fl" => inflate(&data, max),
b"LZWDecode" | b"LZW" => lzw(&data, early, max),
b"RunLengthDecode" | b"RL" => Some(run_length(&data, max)),
b"ASCIIHexDecode" | b"AHx" => Some(hex(&data)),
b"ASCII85Decode" | b"A85" => Some(base85(&data)),
_ => None,
};
match out {
Some(out) if out.len() > max => return Err(TooBig),
Some(out) => data = Cow::Owned(out),
None => return Ok(None),
}
}
Ok(Some(data.into_owned()))
}
/// May the stream with dictionary `dict` be content (of a page, form, pattern or glyph), which can
/// hold inline images? Not an image, font, profile, function, metadata or object stream.
fn maybe_content(dict: &Dict<'_>) -> bool {
let named = |key: &[u8], ok: &[&[u8]]| dict.get::<Name<'_>>(key).is_none_or(|n| ok.contains(&n.as_ref()));
named(b"Subtype", &[b"Form"])
&& named(b"Type", &[b"XObject", b"Pattern"])
&& ![&b"Length1"[..], b"Length2", b"Length3", b"FunctionType", b"N"].iter().any(|k| dict.contains_key(k))
}
/// Would the reader fail on `stream` (or on an inline image in it, when it is content): a
/// predictor it can't apply, or more than its [`budget`] decoded?
pub(crate) fn unsafe_stream(stream: &Stream<'_>) -> bool {
unsafe_data(stream, true)
}
/// [`unsafe_stream`], looking for inline images in it when `inside` (not in an inline image's own
/// data: it would be read again for each inline image a hostile one nests).
fn unsafe_data(stream: &Stream<'_>, inside: bool) -> bool {
let dict = stream.dict();
if params(dict).iter().any(bad_predictor) {
return true;
}
let content = inside && maybe_content(dict);
match decode(&stream.raw_data(), dict, content) {
Err(TooBig) => true,
Ok(Some(data)) if content && data.windows(2).any(|w| w == b"ID") => {
let mut ops = TypedIter::new(&data);
while let Some(op) = ops.next() {
if let TypedInstruction::InlineImage(image) = op
&& unsafe_data(image.0, false)
{
return true;
}
}
false
}
Ok(_) => false,
}
}
/// Would the reader fail on a stream it decodes while it opens `pdf`, before anything else can
/// look at it: an object stream or a cross-reference stream (read here from the file's text)?
/// Their decode parameters must be in their dictionary, as the reader can't be asked for others
/// yet. Encrypted object streams are looked at once the file is open (their data is ciphered
/// here).
///
/// The file is read once from the start, as the reader reads PDF: literal strings (nested
/// parentheses, `\` escaping), hex strings and comments are skipped, `<<` and `>>` pair up, and a
/// dictionary that `stream` follows (white space and comments between) is a stream's, whose data
/// is skipped. So a `stream` or `>>` in a string or a comment is no keyword (#3919 review). The
/// reader may still start at a `<<` inside what this reading skipped (a cross-reference offset or
/// its repair scan can point there), so each such `<<` it could start at is read as a dictionary
/// of its own, within a budget for the whole file: a file that spends it is refused. Linear in
/// the file (#3586), a run of `<` included (#3924 review).
pub(crate) fn unsafe_file_streams(pdf: &[u8]) -> bool {
let mut scan = FileScan {
pdf,
budget: pdf.len().saturating_mul(4).saturating_add(1 << 20),
endstream: None,
lines: BTreeMap::new(),
strings: None,
read_from: Vec::new(),
};
scan.read().is_err()
}
/// A stream the reader would fail on, or a file that spent the scan's budget.
struct Unsafe;
/// The most bytes a dictionary read from inside a string, comment or stream data may span past
/// the last `<<` in it, as far as the scan looked back from a `stream` keyword before.
const MAX_SKIPPED_DICT: usize = 1 << 16;
/// The most `<<` the scan keeps open: deeper than the reader nests dictionaries.
const MAX_OPEN: usize = 1024;
/// PDF white space (ISO 32000-1, 7.2.2).
fn white(b: u8) -> bool {
matches!(b, 0 | 9 | 10 | 12 | 13 | 32)
}
/// Where `what` is first found in `pdf` at or after `from`.
fn find(pdf: &[u8], from: usize, what: &[u8]) -> Option<usize> {
pdf.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at)
}
/// Past the literal string opening at `at` (a `(`), as the reader reads it: parentheses nest and
/// `\` escapes the byte after it. `Err` with how far it got when it doesn't close before `limit`.
fn string_end(pdf: &[u8], at: usize, limit: usize) -> Result<usize, usize> {
let (mut depth, mut i) = (0usize, at);
while let Some(&b) = pdf.get(i).filter(|_| i < limit) {
match b {
b'\\' => i += 1,
b'(' => depth += 1,
b')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Ok(i + 1);
}
}
_ => {}
}
i += 1;
}
Err(i.min(limit))
}
/// How far past its `(` a string read in skipped text may end to be read again each time it is
/// entered (bytes); one ending further is looked up in [`StringEnds`].
const NEAR_STRING: usize = 64;
/// Is the byte at `at` escaped, a `\` before it that isn't escaped itself? Reading a string from
/// any `(` before it sees the same, as the run of `\` before it lies after that `(`.
fn escaped(pdf: &[u8], at: usize) -> bool {
pdf.get(..at).unwrap_or_default().iter().rev().take_while(|b| **b == b'\\').count() % 2 == 1
}
/// Where the literal strings read from the file's `(` end, for those ending more than
/// [`NEAR_STRING`] bytes on (#3924 review). As whether a byte is escaped doesn't depend on where
/// reading started ([`escaped`]), the string from any `(` ends after the first `)` that takes the
/// count of unescaped `(` less unescaped `)` from the file's start below the count with that `(`
/// (counted even when escaped). So all are found in three passes over the file: forward for the
/// count at its end, back for the `(` whose string never ends (the count never falls below theirs
/// again), and forward pairing the others with a stack.
struct StringEnds {
/// Each `(` whose string ends more than [`NEAR_STRING`] bytes on, and where, in file order.
far: Vec<(usize, usize)>,
}
impl StringEnds {
/// The ends in `pdf`; `None` when more than one in 16 bytes of it are kept or pending (only a
/// file built for it nests strings so deep).
fn of(pdf: &[u8]) -> Option<StringEnds> {
let most = pdf.len() / 16 + 1024;
// What the byte at `i` adds to the count: an escaped `(` adds nothing but is one too.
let step = |i: usize| match pdf.get(i) {
Some(b'(') => Some(i64::from(!escaped(pdf, i))),
Some(b')') => (!escaped(pdf, i)).then_some(-1),
_ => None,
};
let total: i64 = (0..pdf.len()).filter_map(step).sum();
// The `(` whose string never ends: none after it takes the count below its own.
let mut endless = vec![0u64; pdf.len() / 64 + 1];
let (mut count, mut lowest) = (total, i64::MAX);
for i in (0..pdf.len()).rev() {
let Some(d) = step(i) else { continue };
if pdf.get(i) == Some(&b'(')
&& count <= lowest
&& let Some(word) = endless.get_mut(i / 64)
{
*word |= 1 << (i % 64);
}
lowest = lowest.min(count);
count -= d;
}
let (mut far, mut open, mut count) = (vec![], Vec::<(usize, i64)>::new(), 0);
for i in 0..pdf.len() {
let Some(d) = step(i) else { continue };
count += d;
if pdf.get(i) == Some(&b'(') {
if endless.get(i / 64).is_some_and(|word| word >> (i % 64) & 1 == 0) {
open.push((i, count));
}
} else {
while let Some(&(at, _)) = open.last().filter(|(_, level)| *level > count) {
open.pop();
if i + 1 - at > NEAR_STRING {
far.push((at, i + 1));
}
}
}
if far.len() + open.len() > most {
return None;
}
}
far.sort_unstable();
Some(StringEnds { far })
}
/// Where the string from the `(` at `at` ends, if it ends more than [`NEAR_STRING`] bytes on;
/// `None` for one that doesn't end.
fn far(&self, at: usize) -> Option<usize> {
self.far.binary_search_by_key(&at, |(open, _)| *open).ok().and_then(|n| self.far.get(n)).map(|(_, end)| *end)
}
}
/// Past the hex string opening at `at` (a `<`): hex digits and white space, then `>`. `Err` with
/// how far it got for one the reader fails on.
fn hex_end(pdf: &[u8], at: usize, limit: usize) -> Result<usize, usize> {
let mut i = at + 1;
while pdf.get(i).is_some_and(|b| i < limit && (b.is_ascii_hexdigit() || white(*b))) {
i += 1;
}
if i < limit && pdf.get(i) == Some(&b'>') { Ok(i + 1) } else { Err(i.min(limit)) }
}
/// The end of the comment starting at `at` (a `%`): its end of line, not included.
fn comment_end(pdf: &[u8], at: usize) -> usize {
pdf.get(at..).and_then(|rest| rest.iter().position(|b| matches!(b, b'\n' | b'\r'))).map_or(pdf.len(), |n| at + n)
}
/// Can the reader start a dictionary at the `<<` at `at`? Its repair scan tries one at the start
/// of the file, after white space and after a dictionary it read (`>>`); an object that a
/// cross-reference offset points at begins `N G obj`, then white space or comments. A `<<` after
/// anything else is read only as part of what is before it: in a run of `<`, only the first.
fn startable(pdf: &[u8], at: usize) -> bool {
let before = pdf.get(..at).unwrap_or_default();
before.last().is_none_or(|b| white(*b)) || before.ends_with(b">>") || before.ends_with(b"obj")
}
/// The pre-open scan of a file's streams ([`unsafe_file_streams`]).
struct FileScan<'a> {
pdf: &'a [u8],
/// The bytes left to read again: dictionaries inside skipped text, `endstream` searches and
/// the data decoded. Every byte of a well-formed file is read about once.
budget: usize,
/// Where an `endstream` was last looked for, and the first one found there (`None`: none).
endstream: Option<(usize, Option<usize>)>,
/// The comments read in skipped text: each one's end of line, and where reading it began.
lines: BTreeMap<usize, usize>,
/// The ends of strings read in skipped text far from their `(` ([`StringEnds`]), found once
/// the first such string is read.
strings: Option<StringEnds>,
/// The `<<` a dictionary in skipped text was read from, a bit a byte of the file (empty
/// until one is).
read_from: Vec<u64>,
}
impl FileScan<'_> {
fn charge(&mut self, bytes: usize) -> Result<(), Unsafe> {
self.budget = self.budget.checked_sub(bytes).ok_or(Unsafe)?;
Ok(())
}
/// The file from the start, as the reader reads it.
fn read(&mut self) -> Result<(), Unsafe> {
let pdf = self.pdf;
let mut opens = std::collections::VecDeque::new();
// The dictionary just read, while only white space and comments follow it.
let mut closed: Option<(usize, usize)> = None;
let mut i = 0;
while let Some(&b) = pdf.get(i) {
if white(b) {
i += 1;
continue;
}
if b == b'%' {
let end = comment_end(pdf, i);
self.skipped(i, end)?;
i = end;
continue;
}
let last = closed.take();
match pdf.get(i..i + 2) {
Some(b"<<") => {
if opens.len() == MAX_OPEN {
opens.pop_front();
}
opens.push_back(i);
i += 2;
}
Some(b">>") => {
closed = opens.pop_back().map(|open| (open, i + 2));
i += 2;
}
_ if b == b'(' => {
let end = string_end(pdf, i, pdf.len()).unwrap_or(pdf.len());
self.skipped(i, end)?;
i = end;
}
_ if b == b'<' => i = hex_end(pdf, i, pdf.len()).unwrap_or(i + 1),
_ => match last {
Some((open, close)) if pdf.get(i..).is_some_and(|rest| rest.starts_with(b"stream")) => {
let end = self.stream(open, close, i + 6)?;
self.skipped(i + 6, end)?;
i = end.max(i + 6);
}
_ => i += 1,
},
}
}
Ok(())
}
/// Each `<<` in `from..to`, text the reading skipped, that the reader could start at, read as
/// a dictionary of its own.
fn skipped(&mut self, from: usize, to: usize) -> Result<(), Unsafe> {
let text = self.pdf.get(..to).unwrap_or_default();
let mut i = from;
while let Some(at) = find(text, i, b"<<") {
if startable(self.pdf, at) {
self.dicts(at)?;
}
i = at + 1;
}
Ok(())
}
/// The dictionary at `at` in skipped text, read to the `>>` that closes it, as far as
/// [`MAX_SKIPPED_DICT`] past the last `<<` opened on the way; and each dictionary it holds.
/// Each `>>` closing one is looked at for a stream after it, and each `<<` opening one is
/// marked: a reading from there reads the same text as far, so a later call for it returns
/// at once and text dense with `<<` is read about once.
fn dicts(&mut self, at: usize) -> Result<(), Unsafe> {
if self.was_read(at) {
return Ok(());
}
let pdf = self.pdf;
let mut opens = std::collections::VecDeque::new();
let (mut depth, mut limit, mut i, mut work) = (0usize, at.saturating_add(MAX_SKIPPED_DICT), at, 0usize);
while let Some(&b) = pdf.get(i).filter(|_| i < limit) {
// Where to read on (`Err`: the dictionary ends there, read or unreadable), and the
// bytes read to get there.
let (next, read) = match pdf.get(i..i + 2) {
Some(b"<<") => {
self.mark(i);
limit = limit.max(i.saturating_add(MAX_SKIPPED_DICT));
if opens.len() == MAX_OPEN {
opens.pop_front();
}
opens.push_back(i);
depth += 1;
(Ok(i + 2), 2)
}
Some(b">>") => {
depth = depth.saturating_sub(1);
if let Some(open) = opens.pop_back() {
self.closed(open, i + 2)?;
}
(if depth == 0 { Err(i + 2) } else { Ok(i + 2) }, 2)
}
_ => match b {
// Strings and comments are charged as they are first read.
b'(' => (self.string_end(i)?.ok_or(pdf.len()), 0),
b'%' => (Ok(self.line_end(i)?), 0),
b'<' => {
let end = hex_end(pdf, i, limit);
let (Ok(n) | Err(n)) = end;
(end, n.saturating_sub(i))
}
_ => (Ok(i + 1), 1),
},
};
work += read;
let Ok(n) = next else { break };
i = n;
}
self.charge(work)
}
/// The dictionary `open..close` in skipped text: the stream it begins if `stream` follows it.
fn closed(&mut self, open: usize, close: usize) -> Result<(), Unsafe> {
let mut keyword = close;
while let Some(&b) = self.pdf.get(keyword) {
match b {
b'%' => keyword = self.line_end(keyword)?,
b if white(b) => keyword += 1,
_ => break,
}
}
self.charge(keyword - close)?;
if self.pdf.get(keyword..).is_some_and(|rest| rest.starts_with(b"stream")) {
self.stream(open, close, keyword + 6)?;
}
Ok(())
}
/// The end of the comment at `at` ([`comment_end`]). Comments in skipped text are read from
/// many places, so each one's end is kept: a byte is read as comment once.
fn line_end(&mut self, at: usize) -> Result<usize, Unsafe> {
// The first comment kept that ends at or after `at`.
let kept = self.lines.range(at..).next().map(|(end, from)| (*end, *from));
if let Some((end, from)) = kept
&& from <= at
{
return Ok(end);
}
// Read up to the next end of line, or to where that comment was read from.
let upto = kept.map_or(self.pdf.len(), |(_, from)| from);
let rest = self.pdf.get(at..upto).unwrap_or_default();
let end = match rest.iter().position(|b| matches!(b, b'\n' | b'\r')) {
Some(n) => at + n,
None => kept.map_or(self.pdf.len(), |(end, _)| end),
};
self.charge(rest.len().min(end - at))?;
self.lines.insert(end, at);
Ok(end)
}
/// Past the literal string at `at` (a `(`), as the reader reads it; `None` for one that
/// doesn't end. A string ending within [`NEAR_STRING`] bytes of its `(` is read again each
/// time a reading enters it; one ending further is looked up in [`StringEnds`], found for the
/// whole file in three passes (linear, so not charged) the first time one is needed. So each
/// time a string is entered costs at most [`NEAR_STRING`] bytes, charged as one.
fn string_end(&mut self, at: usize) -> Result<Option<usize>, Unsafe> {
self.charge(1)?;
if let Ok(end) = string_end(self.pdf, at, at.saturating_add(NEAR_STRING)) {
return Ok(Some(end));
}
if self.strings.is_none() {
self.strings = Some(StringEnds::of(self.pdf).ok_or(Unsafe)?);
}
Ok(self.strings.as_ref().and_then(|s| s.far(at)))
}
fn mark(&mut self, at: usize) {
if self.read_from.is_empty() {
self.read_from = vec![0; self.pdf.len() / 64 + 1];
}
if let Some(word) = self.read_from.get_mut(at / 64) {
*word |= 1 << (at % 64);
}
}
fn was_read(&self, at: usize) -> bool {
self.read_from.get(at / 64).is_some_and(|word| word >> (at % 64) & 1 == 1)
}
/// The stream whose dictionary is `open..close` and whose `stream` keyword ends at `from`:
/// `Err` when the reader would fail on it, else where its data ends.
fn stream(&mut self, open: usize, close: usize, from: usize) -> Result<usize, Unsafe> {
let pdf = self.pdf;
let cr = usize::from(pdf.get(from) == Some(&b'\r'));
let start = from + cr + usize::from(pdf.get(from + cr) == Some(&b'\n'));
self.charge(close.saturating_sub(open))?;
let dict = pdf.get(open..close).and_then(Dict::from_bytes);
let length = dict.as_ref().and_then(|d| d.get::<usize>(b"Length")).and_then(|n| start.checked_add(n)).filter(|e| *e <= pdf.len());
let end = match length {
Some(end) => end,
None => self.endstream(start)?,
};
let Some(dict) = dict.filter(|d| d.get::<Name<'_>>(b"Type").is_some_and(|t| matches!(t.as_ref(), b"ObjStm" | b"XRef"))) else {
return Ok(end);
};
let indirect = [&b"DecodeParms"[..], b"DP"].iter().any(|k| matches!(dict.get_raw::<Object<'_>>(k), Some(MaybeRef::Ref(_))));
if indirect || params(&dict).iter().any(bad_predictor) {
return Err(Unsafe);
}
if !filters(&dict).is_empty() {
self.charge(end.saturating_sub(start))?;
}
if matches!(decode(pdf.get(start..end).unwrap_or_default(), &dict, false), Err(TooBig)) {
return Err(Unsafe);
}
Ok(end)
}
/// Where the first `endstream` at or after `start` is, or the end of the file.
fn endstream(&mut self, start: usize) -> Result<usize, Unsafe> {
let len = self.pdf.len();
if let Some((from, found)) = self.endstream
&& from <= start
&& found.is_none_or(|f| f >= start)
{
return Ok(found.unwrap_or(len));
}
let found = find(self.pdf, start, b"endstream");
self.charge(found.unwrap_or(len).saturating_sub(start))?;
self.endstream = Some((start, found));
Ok(found.unwrap_or(len))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dict(text: &str) -> Dict<'_> {
Dict::from_bytes(text.as_bytes()).unwrap()
}
#[test]
fn predictors_the_reader_can_apply_pass() {
for ok in [
"<< >>",
"<< /Predictor 1 /Columns 0 >>",
"<< /Predictor 12 /Columns 5 >>",
"<< /Predictor 2 /Colors 3 /BitsPerComponent 16 /Columns 1000 >>",
] {
assert!(!bad_predictor(&dict(ok)), "{ok}");
}
for bad in [
"<< /Predictor 2 /Columns 0 >>",
"<< /Predictor 12 /Colors 0 >>",
"<< /Predictor 2 /BitsPerComponent 0 >>",
"<< /Predictor 2 /BitsPerComponent 3 >>",
"<< /Predictor 12 /Colors 32 /BitsPerComponent 16 >>",
"<< /Predictor 15 /Columns 99999999999 >>",
"<< /Predictor 10 /Columns 18446744073709551615 /Colors 4 >>",
] {
assert!(bad_predictor(&dict(bad)), "{bad}");
}
}
#[test]
fn the_decoders_read_what_the_specification_writes() {
assert_eq!(hex(b"48 65 6c6C6\n>junk"), b"Hell`");
assert_eq!(base85(b"87cURD]i,\"Ebo80~>"), b"Hello World!");
assert_eq!(base85(b"z!!~>"), [0, 0, 0, 0, 0]);
assert_eq!(run_length(&[2, b'a', b'b', b'c', 254, b'x', 128, b'q'], MAX_DECODED), b"abcxxx");
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
std::io::Write::write_all(&mut e, &[7; 1000]).unwrap();
assert_eq!(inflate(&e.finish().unwrap(), MAX_DECODED).unwrap(), [7; 1000]);
// "-----A---B" in LZW, as the specification's example writes it (7.4.4.2).
let lzw_example = [0x80, 0x0B, 0x60, 0x50, 0x22, 0x0C, 0x0C, 0x85, 0x01];
assert_eq!(lzw(&lzw_example, true, MAX_DECODED).unwrap(), b"-----A---B");
}
/// Zeros past the cap, compressed.
fn zeros_past_the_cap() -> Vec<u8> {
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
for _ in 0..(MAX_DECODED >> 20) + 1 {
std::io::Write::write_all(&mut e, &[0; 1 << 20]).unwrap();
}
e.finish().unwrap()
}
#[test]
fn a_stream_that_decodes_past_the_cap_is_found_without_holding_it() {
// Inflated in pieces: never more than one byte past the cap is held.
let bomb = zeros_past_the_cap();
assert!(bomb.len() < MAX_DECODED / 500);
assert_eq!(inflate(&bomb, MAX_DECODED).map(|d| d.len()), Some(MAX_DECODED + 1));
let d = dict("<< /Filter /FlateDecode >>");
assert!(matches!(decode(&bomb, &d, false), Err(TooBig)));
// Small enough that it can't get there: not decoded at all.
assert!(matches!(decode(&[1, 2, 3], &d, false), Ok(None)));
// An image may decode to its declared size, and no further.
let image =
|w: u32, h: u32| format!("<< /Subtype /Image /Width {w} /Height {h} /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode >>");
assert!(decode(&bomb, &dict(&image(1 << 15, 1 << 14)), false).is_ok());
assert!(matches!(decode(&bomb, &dict(&image(16, 16)), false), Err(TooBig)));
assert_eq!(budget(&dict(&image(1 << 20, 1 << 20))), MAX_IMAGE_DECODED);
}
/// A one-page PDF drawing one flat 8-bit DeviceRGB image of `w` × `h` pixels, Flate-compressed.
fn one_image_pdf(w: usize, h: usize) -> Vec<u8> {
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
let row: Vec<u8> = [200, 180, 150].repeat(w);
for _ in 0..h {
std::io::Write::write_all(&mut e, &row).unwrap();
}
let image = e.finish().unwrap();
let content = b"q 595 0 0 842 0 0 cm /Im0 Do Q";
let mut out = b"%PDF-1.6\n".to_vec();
let mut offsets = vec![];
let objects: Vec<Vec<u8>> = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /XObject << /Im0 5 0 R >> >> >>".to_vec(),
[format!("<< /Length {} >>\nstream\n", content.len()).as_bytes(), content, b"\nendstream"].concat(),
[
format!(
"<< /Type /XObject /Subtype /Image /Width {w} /Height {h} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /Length {} >>\nstream\n",
image.len()
)
.as_bytes(),
&image,
b"\nendstream",
]
.concat(),
];
for (i, o) in objects.iter().enumerate() {
offsets.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
out.extend_from_slice(o);
out.extend_from_slice(b"\nendobj\n");
}
let xref = out.len();
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
for o in offsets {
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
out
}
#[test]
fn an_image_past_the_stream_cap_within_its_declared_size_opens() {
// An A0 sheet scanned at 300 ppi: 9930 × 14040 RGB pixels, 418 MB decoded (#2005).
let pdf = one_image_pdf(9930, 14040);
let info = crate::info(&pdf, None);
assert!(info.is_ok(), "{:?}", info.err());
assert_eq!(info.map(|i| i.pages.len()).unwrap(), 1);
}
#[test]
fn object_and_cross_reference_streams_are_looked_at_before_the_file_is_read() {
let stream = |dict: &str, data: &[u8]| {
[format!("1 0 obj\n<< {dict} /Length {} >>\nstream\n", data.len()).as_bytes(), data, b"\nendstream\nendobj\n"].concat()
};
for bad in [
stream("/Type /ObjStm /N 1 /First 4 /Filter /FlateDecode /DecodeParms << /Predictor 2 /Columns 0 >>", b"x"),
stream("/Type /XRef /Filter /FlateDecode /DecodeParms 9 0 R", b"x"),
stream("/Type /ObjStm /N 1 /First 4 /Filter /FlateDecode", &zeros_past_the_cap()),
] {
assert!(unsafe_file_streams(&bad));
}
for fine in [
stream("/Type /XRef /Filter /FlateDecode /DecodeParms << /Predictor 12 /Columns 5 >>", b"x"),
// Another kind of stream is looked at once the file is open, its references followed.
stream("/Filter /FlateDecode /DecodeParms << /Predictor 2 /Columns 0 >>", b"x"),
] {
assert!(!unsafe_file_streams(&fine));
}
// A bad stream after others is still found.
let fine = stream("/Type /XRef /Filter /FlateDecode /DecodeParms << /Predictor 12 /Columns 5 >>", b"x");
let bad = stream("/Type /XRef /Filter /FlateDecode /DecodeParms 9 0 R", b"x");
assert!(unsafe_file_streams(&[fine.as_slice(), b"<< >>stream x endstream", &bad].concat()));
}
#[test]
fn a_file_dense_with_stream_keywords_is_scanned_in_linear_time() {
// #3586: each `stream` keyword walked back up to 64 KiB, through every dictionary before
// it, so 1 MiB of keywords took minutes. Now the whole scan takes milliseconds.
for unit in [&b"<</A <</B 1>> >>stream"[..], b">>stream", b"<</Type/XRef>>stream"] {
let pdf = unit.repeat((1 << 20) / unit.len());
let t = std::time::Instant::now();
assert!(!unsafe_file_streams(&pdf));
let took = t.elapsed();
assert!(took < std::time::Duration::from_secs(5), "{} took {took:?}", String::from_utf8_lossy(unit));
}
}
#[test]
fn a_stream_keyword_in_a_string_or_comment_hides_no_stream_after_it() {
// #3919 review: a `>>stream` in a literal string (one with a `<<` before it, nested
// parentheses, an escaped one) or in a comment, a `>>` in a string and a comment between
// `>>` and `stream` each left the cross-reference stream they were in or before unread.
let cases = [
&b"<< /Type /XRef /Note (<<) /Pad (>>stream) /DecodeParms 9 0 R >> stream\nx\nendstream"[..],
b"<< /Type /XRef /Pad (a (b) \\) >>stream) /DecodeParms 9 0 R >> stream\nx\nendstream",
b"<< /Type /XRef /Pad (>>) /DecodeParms 9 0 R >> stream\nx\nendstream",
b"<< /Type /XRef % >>stream\n/DecodeParms 9 0 R >> stream\nx\nendstream",
b"<< /Type /XRef /DecodeParms 9 0 R >> % a comment\nstream\nx\nendstream",
];
let unread: Vec<_> = cases.iter().filter(|bad| !unsafe_file_streams(bad)).map(|bad| String::from_utf8_lossy(bad)).collect();
assert!(unread.is_empty(), "{unread:#?}");
}
#[test]
fn a_stream_in_text_the_scan_skips_is_still_read() {
// The scan skips a stream's data, a string and a comment; the reader may still start at
// a dictionary in them (a cross-reference offset can point at `1 0 obj` anywhere, its
// repair scan tries a `<<` after white space), so they're read too.
let bad = b"<< /Type /XRef /DecodeParms 9 0 R >> stream\nx\nendstream";
// `data` as the data of a stream whose length holds all of it.
let image = |data: &[u8]| [format!("1 0 obj << /Length {} >> stream\n", data.len()).as_bytes(), data, b"\nendstream endobj"].concat();
let run = vec![b'<'; 1 << 16];
let cases = [
image(bad),
[&b"(1 0 obj "[..], bad, b")"].concat(),
[&b"(1 0 obj"[..], bad, b")"].concat(),
[&b"% "[..], bad, b"\n"].concat(),
[&b"<< /Note (1 0 obj "[..], bad, b") >>"].concat(),
// #3924 review: right after a run of `<` in an image, and as the end of one (a
// reader starting at the run's first `<<` reads it inside the others).
image(&[&run[..], b"\n", bad].concat()),
image(&[&run[..], b" /Type /XRef /DecodeParms 9 0 R >> stream\nx\nendstream"].concat()),
// Strings in it that end far from their `(` (looked up, not read again): one, one
// holding others, one from an escaped `(`, and the same after a flat area of
// ` <<(` whose strings never end.
image(&[&b"<< /Type /XRef /Pad ("[..], &[b'x'; 200], b") /DecodeParms 9 0 R >> stream\nx\nendstream"].concat()),
image(&[&b"<< /Type /XRef /Pad (a (b) "[..], &[b'x'; 200], b" (c (d)) \\) ) /DecodeParms 9 0 R >> stream\nx\nendstream"].concat()),
image(&[&b"<< /Type /XRef /Pad \\("[..], &[b'x'; 200], b") /DecodeParms 9 0 R >> stream\nx\nendstream"].concat()),
image(&[&b" <<(".repeat(1 << 14)[..], b"\n", bad].concat()),
];
let unread: Vec<_> =
cases.iter().filter(|bad| !unsafe_file_streams(bad)).map(|bad| String::from_utf8_lossy(&bad[..bad.len().min(80)])).collect();
assert!(unread.is_empty(), "{unread:#?}");
}
#[test]
fn a_run_of_lt_in_an_image_a_string_or_a_comment_opens() {
// #3924 review: every `<<` in a run of `<` (an image's flat area of grey 60) was read as a
// dictionary to past the run, so 1 MiB of it spent the budget and the file was refused.
// The reader starts a dictionary only at the run's first `<<`.
let run = vec![b'<'; 1 << 20];
let image = b"1 0 obj << /Type /XObject /Subtype /Image /Width 1024 /Height 1024 /ColorSpace /DeviceGray /BitsPerComponent 8 /Length 1048576 >> stream\n";
for (what, pdf) in [
("image", [&b"%PDF-1.7\n"[..], image, &run[..], b"\nendstream endobj\n"].concat()),
("string", [&b"%PDF-1.7\n2 0 obj ("[..], &run[..], b") endobj\n"].concat()),
("comment", [&b"%PDF-1.7\n% "[..], &run[..], b"\n"].concat()),
] {
let t = std::time::Instant::now();
assert!(!unsafe_file_streams(&pdf), "{what}");
let took = t.elapsed();
assert!(took < std::time::Duration::from_secs(5), "{what} took {took:?}");
}
}
#[test]
fn a_flat_area_that_opens_a_string_after_each_dictionary_opens() {
// #3924 review: in a flat area of ` <<(` (CMYK or RGBA 32, 60, 60, 40) each `<<` was read
// as a dictionary, and each read its never-ending string to 64 KiB past it: the budget
// was spent and the file refused. A string ending far from its `(` is now read once.
let noise: Vec<u8> = (0u32..1 << 18).map(|i| (i.wrapping_mul(2_654_435_761) >> 24) as u8).collect();
for (what, data) in [
(" <<(", b" <<(".repeat(1 << 18)),
("\\0<<(", b"\0<<(".repeat(1 << 18)),
("( <<", b"( <<".repeat(1 << 18)),
(" <<( then noise", [&b" <<(".repeat(1 << 16)[..], &noise].concat()),
] {
let pdf = [format!("1 0 obj << /Length {} >> stream\n", data.len()).as_bytes(), &data[..], b"\nendstream endobj\n"].concat();
let t = std::time::Instant::now();
assert!(!unsafe_file_streams(&pdf), "{what}");
let took = t.elapsed();
assert!(took < std::time::Duration::from_secs(5), "{what} took {took:?}");
}
}
#[test]
fn strings_and_comments_dense_with_dictionaries_are_scanned_in_bounded_time() {
// Each `<<` in a string or comment the reader could start at is read as a dictionary of
// its own, within a budget for the whole file.
for unit in [&b"<<("[..], b"(<<", b"%<<\n", b"<</A (<</B 1>>stream) >>stream\n", b"<"] {
let pdf = unit.repeat((1 << 20) / unit.len());
let t = std::time::Instant::now();
unsafe_file_streams(&pdf);
let took = t.elapsed();
assert!(took < std::time::Duration::from_secs(5), "{} took {took:?}", String::from_utf8_lossy(unit));
}
// An image's flat area of a colour whose bytes are `<<` and white space, `(` or `%` (a
// reader may start at each `<<` after white space): read about once, and it opens.
for pixel in [&b" <<"[..], b"<<(", b"<<%", b" <<%", b"\0<<\0"] {
let data = pixel.repeat((1 << 20) / pixel.len());
let pdf = [format!("1 0 obj << /Length {} >> stream\n", data.len()).as_bytes(), &data[..], b"\nendstream endobj\n"].concat();
let t = std::time::Instant::now();
assert!(!unsafe_file_streams(&pdf), "{}", String::from_utf8_lossy(pixel));
let took = t.elapsed();
assert!(took < std::time::Duration::from_secs(5), "{} took {took:?}", String::from_utf8_lossy(pixel));
}
}
}