Part of #637: the editing data's readers stay linear on hostile input

Zstandard frames: the trailing zero padding is trimmed once, not looked
for again before each frame, and at most 4096 frames are read (more is
damaged data). A `]` finds its `[` from a list of where they are rather
than by scanning the operand stack back. Images are read up to 32
million pixels. Tests: thousands of tiny frames, and hundreds of
thousands of unmatched brackets, read in one pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-09 16:18:23 +07:00
1 parent 0be7640088
commit 9b4166ed5e
3 files changed
+46 -10

No files matched your search

+11 -7
View File
@@ -37,6 +37,8 @@ mod tests;
pub const MAX_DATA: u64 = 256 << 20;
/// Largest Zstandard window accepted (a frame asking for more is damaged data).
const MAX_ZSTD_WINDOW: u64 = 64 << 20;
/// Most Zstandard frames read (the app writes one).
const MAX_FRAMES: usize = 4096;
const EPS_BEGIN: &[u8] = b"%AI9_PrivateDataBegin";
const EPS_END: &[u8] = b"%AI9_PrivateDataEnd";
@@ -126,20 +128,22 @@ impl Codec {
flate2::read::ZlibDecoder::new(data).take(max + 1).read_to_end(&mut out).map_err(|_| damaged("its zlib compression"))?;
}
Codec::Zstd => {
let mut src = data;
// The data may be several frames one after the other.
while src.iter().any(|b| *b != 0) {
// The data may be several frames one after the other, padded with zeros.
let end = data.iter().rposition(|b| *b != 0).map_or(0, |i| i + 1);
let mut src = data.get(..end).unwrap_or_default();
let mut frames = 0usize;
while !src.is_empty() {
frames += 1;
if frames > MAX_FRAMES {
return Err(damaged("its Zstandard compression"));
}
let left = (max + 1).saturating_sub(out.len() as u64);
if left == 0 {
break;
}
let d = ruzstd::decoding::StreamingDecoder::new_with_max_window_size(&mut src, MAX_ZSTD_WINDOW)
.map_err(|_| damaged("its Zstandard compression"))?;
let before = out.len();
d.take(left).read_to_end(&mut out).map_err(|_| damaged("its Zstandard compression"))?;
if out.len() == before {
break;
}
}
}
}
+16 -3
View File
@@ -47,7 +47,7 @@ const MAX_NODES: usize = 4_000_000;
/// Most points in one path.
const MAX_POINTS: usize = 4_000_000;
/// Largest image read (pixels).
const MAX_PIXELS: u64 = 1 << 28;
const MAX_PIXELS: u64 = 1 << 25;
/// Largest side of the document and its artboards (points).
const MAX_SIDE: f64 = 1e6;
@@ -200,6 +200,8 @@ struct Raster {
struct Reader<'a> {
lex: Lexer<'a>,
stack: Vec<V>,
/// Where the `[` marks are on the stack, innermost last (some may have gone with operands).
marks: Vec<usize>,
gs: GState,
frames: Vec<Frame>,
path: BezPath,
@@ -323,6 +325,7 @@ impl<'a> Reader<'a> {
Self {
lex: Lexer::new(data),
stack: Vec::new(),
marks: Vec::new(),
gs: GState::default(),
frames: Vec::new(),
path: BezPath::new(),
@@ -622,9 +625,19 @@ impl<'a> Reader<'a> {
fn op(&mut self, w: &str, hidden: bool) -> Result<(), String> {
match w {
"[" => return self.push(V::Mark),
"[" => {
self.marks.push(self.stack.len());
return self.push(V::Mark);
}
"]" => {
let at = self.stack.iter().rposition(|v| matches!(v, V::Mark)).unwrap_or(self.stack.len());
// The innermost `[` still on the stack (the stack is cut back by operators).
let mut at = self.stack.len();
while let Some(m) = self.marks.pop() {
if matches!(self.stack.get(m), Some(V::Mark)) {
at = m;
break;
}
}
let items: Vec<V> = self.stack.split_off(at).into_iter().skip(1).collect();
return self.push(V::Arr(items));
}
+19
View File
@@ -407,3 +407,22 @@ fn ai_private_data() {
}
assert!(decode_private(b"%AI12_CompressedDataxx").is_err());
}
#[test]
fn many_zstandard_frames_are_read_in_one_pass_and_capped() {
let frame = zstd(b" ");
let frames = |n: usize| -> Vec<u8> { [&b"%AI24_ZStandard_Data"[..], &frame.repeat(n), &[0; 64]].concat() };
let started = std::time::Instant::now();
assert_eq!(decode_private(&frames(4000)).unwrap().len(), 4000);
assert!(decode_private(&frames(50_000)).unwrap_err().contains("Zstandard"));
assert!(started.elapsed() < std::time::Duration::from_secs(20), "{:?}", started.elapsed());
}
#[test]
fn closing_brackets_without_their_opening_dont_rescan_the_stack() {
let n = 200_000;
let body = format!("{}\n{}\n", "1 ".repeat(n), "] ".repeat(n));
let started = std::time::Instant::now();
let _ = read(stream(&layer(&body)).as_bytes());
assert!(started.elapsed() < std::time::Duration::from_secs(20), "{:?}", started.elapsed());
}