From e45bb864ed426199297ed693acc697edc152f9bb Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:52:43 +0300 Subject: [PATCH] math: cap LaTeX expression nesting depth so hostile input can't overflow the stack `math.svg` and `math.insert` take an unbounded `latex` string, and the parser's arg/atom/list/list_until_right recursive descent recursed once per group: 100k nested braces, `\frac`s or `\left(`s overflowed the stack (an abort the command guard cannot catch). Past MAX_NESTING (256) groups stop descending, so the rest of the input is consumed iteratively. Regression test: 100k nested braces, `\frac`s and `\left(`s typeset to an SVG; the old code aborted with a stack overflow. --- crates/engine/src/math.rs | 51 +++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/math.rs b/crates/engine/src/math.rs index 97327ed8..71d873ae 100644 --- a/crates/engine/src/math.rs +++ b/crates/engine/src/math.rs @@ -160,9 +160,15 @@ fn big_op(name: &str) -> Option<&'static str> { }) } +/// Deepest expression nesting parsed. Recursive descent must not overflow the stack on hostile +/// input (`\frac\frac…`, `{{{{…`, `\left(\left(…`); past the cap groups stop descending. +const MAX_NESTING: usize = 256; + struct Parser<'a> { s: &'a [char], i: usize, + /// Enclosing group depth, at most [`MAX_NESTING`]. + depth: usize, } impl Parser<'_> { @@ -220,14 +226,19 @@ impl Parser<'_> { out } fn arg(&mut self) -> Node { + if self.depth >= MAX_NESTING { + return Node::Group(Vec::new()); + } + self.depth += 1; self.skip_ws(); - if self.peek() == Some('{') { + let n = if self.peek() == Some('{') { self.i += 1; - let g = self.list(Some('}')); - Node::Group(g) + Node::Group(self.list(Some('}'))) } else { self.atom().unwrap_or(Node::Group(vec![])) - } + }; + self.depth -= 1; + n } fn delimiter(&mut self) -> String { self.skip_ws(); @@ -280,7 +291,7 @@ impl Parser<'_> { } s.push(c); } - Box::new(parse(&s)) + Box::new(parse_at(&s, self.depth + 1)) }); let a = self.arg(); Node::Sqrt(idx, Box::new(a)) @@ -329,6 +340,10 @@ impl Parser<'_> { }) } fn list(&mut self, end: Option) -> Vec { + if self.depth >= MAX_NESTING { + return Vec::new(); + } + self.depth += 1; let mut out: Vec = Vec::new(); loop { self.skip_ws(); @@ -362,9 +377,13 @@ impl Parser<'_> { } } } + self.depth -= 1; out } fn list_until_right(&mut self) -> Vec { + if self.depth >= MAX_NESTING { + return Vec::new(); + } let mut out = Vec::new(); loop { self.skip_ws(); @@ -375,7 +394,7 @@ impl Parser<'_> { self.i += 6; break; } - let mut p = Parser { s: self.s, i: self.i }; + let mut p = Parser { s: self.s, i: self.i, depth: self.depth + 1 }; let one = p.list_one(); self.i = p.i; out.extend(one); @@ -410,8 +429,12 @@ impl Parser<'_> { } fn parse(src: &str) -> Node { + parse_at(src, 0) +} + +fn parse_at(src: &str, depth: usize) -> Node { let chars: Vec = src.chars().collect(); - let mut p = Parser { s: &chars, i: 0 }; + let mut p = Parser { s: &chars, i: 0, depth }; Node::Group(p.list(None)) } @@ -714,4 +737,18 @@ mod tests { let (svg, _) = to_svg(r"\sqrt{a^2+b^2} \ne \pi", 14.0); assert!(designcraft_images::svg_tree(&svg).is_some()); } + + /// Hostile input must not overflow the stack: nesting past the cap stops descending instead + /// of recursing once per group (100k levels used to abort the process). + #[test] + fn an_over_deep_expression_is_bounded() { + for latex in [ + format!("{}x{}", "{".repeat(100_000), "}".repeat(100_000)), + format!("{}x", r"\frac".repeat(100_000)), + format!("{}x", r"\left(".repeat(100_000)), + ] { + let (svg, _) = to_svg(&latex, 12.0); + assert!(!svg.is_empty()); + } + } }