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.
This commit is contained in:
bflatastic committed 2026-10-14 08:47:10 +07:00
1 parent 0cb55bc0aa
commit 277e893e82
2 files changed
+21 -3

No files matched your search

+16 -3
View File
@@ -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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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' => {
+5
View File
@@ -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()));