From 9b4166ed5e1fd1b69aeaf67ce1c738b423c06076 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 9 Oct 2026 16:18:23 +0700 Subject: [PATCH] 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 --- crates/eps/src/import/ai/mod.rs | 18 +++++++++++------- crates/eps/src/import/ai/read.rs | 19 ++++++++++++++++--- crates/eps/src/import/ai/tests.rs | 19 +++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/crates/eps/src/import/ai/mod.rs b/crates/eps/src/import/ai/mod.rs index 76d6af42..2d3e46f2 100644 --- a/crates/eps/src/import/ai/mod.rs +++ b/crates/eps/src/import/ai/mod.rs @@ -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; - } } } } diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index ccdabf6f..31d1c965 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -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, + /// Where the `[` marks are on the stack, innermost last (some may have gone with operands). + marks: Vec, gs: GState, frames: Vec, 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 = self.stack.split_off(at).into_iter().skip(1).collect(); return self.push(V::Arr(items)); } diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 4aa76c07..bb4429df 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -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 { [&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()); +}