From 277e893e828fc500dfd26073014515948ba134d9 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 14 Oct 2026 08:32:48 +0700 Subject: [PATCH] Fix #1715: ASCII85 decoding is bounded while it decodes A `z` is four bytes for one character, so the text's length didn't bound the result, and the filter's and the interpreter's budgets only saw it once it was whole. The decoder now stops at 256 MB, the most one filter or data section may hold. --- crates/eps/src/ps.rs | 19 ++++++++++++++++--- crates/eps/src/tests.rs | 5 +++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/eps/src/ps.rs b/crates/eps/src/ps.rs index e00d813a..fb27425e 100644 --- a/crates/eps/src/ps.rs +++ b/crates/eps/src/ps.rs @@ -181,13 +181,26 @@ fn push85(s: &mut String, mut v: u32, n: usize) { } } -/// ASCII85 `text` (whitespace ignored, up to `~>`) decoded; `None` when it is malformed. +/// Most bytes [`ascii85_decode`] makes: what one filter or data section may hold. +pub(crate) const MAX_ASCII85: usize = 1 << 28; + +/// ASCII85 `text` (whitespace ignored, up to `~>`) decoded; `None` when it is malformed or longer +/// than [`MAX_ASCII85`]. pub(crate) fn ascii85_decode(text: &str) -> Option> { - let mut out = Vec::with_capacity(text.len() * 4 / 5); + ascii85_decode_within(text, MAX_ASCII85) +} + +/// [`ascii85_decode`] of at most `max` bytes: `z` is four bytes for one, so the length of the +/// text doesn't bound what it becomes (#1715). +pub(crate) fn ascii85_decode_within(text: &str, max: usize) -> Option> { + let mut out = Vec::with_capacity((text.len() / 5 * 4).min(max)); let mut group = [0u8; 5]; let mut n = 0; let mut chars = text.bytes().filter(|b| !b.is_ascii_whitespace()).peekable(); while let Some(b) = chars.next() { + if out.len() > max { + return None; + } match b { b'~' => { if chars.next() != Some(b'>') { @@ -201,7 +214,7 @@ pub(crate) fn ascii85_decode(text: &str) -> Option> { let v = value85(&padded)?; out.extend(v.to_be_bytes().iter().take(n - 1)); } - return Some(out); + return (out.len() <= max).then_some(out); } b'z' if n == 0 => out.extend([0; 4]), b'!'..=b'u' => { diff --git a/crates/eps/src/tests.rs b/crates/eps/src/tests.rs index b4e5fa1c..4cab4628 100644 --- a/crates/eps/src/tests.rs +++ b/crates/eps/src/tests.rs @@ -345,6 +345,11 @@ fn run_length_and_ascii85_round_trip() { assert_eq!(ps::ascii85(b"Man "), "9jqo^~>\n"); assert_eq!(ps::ascii85(&[0, 0, 0, 0]), "z~>\n"); assert!(ps::ascii85_decode("9jqo^").is_none(), "no end marker"); + // #1715: the result is bounded while it is made, not by the length of the text. + assert_eq!(ps::ascii85_decode_within("zz~>", 8), Some(vec![0; 8])); + assert!(ps::ascii85_decode_within("zzz~>", 8).is_none(), "three `z` are 12 bytes"); + assert!(ps::ascii85_decode_within("zz9jqo^~>", 8).is_none()); + assert!(ps::ascii85_decode_within(&format!("{}~>", "z".repeat(1 << 16)), 1 << 10).is_none()); assert!(ps::ascii85_decode("9jqo^v~>").is_none(), "not base 85"); assert_eq!(ps::num(1.0 / 3.0), "0.3333"); assert_eq!((ps::num(-0.00001), ps::num(f64::NAN), ps::num(2.5)), ("0".into(), "0".into(), "2.5".into()));