Adding expression evaluator against stack-overflow DoS

This commit is contained in:
Sebastian 2026-06-28 09:59:38 +02:00 • committed by Hakan Seven
commit bfb81f6658

View file

@ -90,6 +90,10 @@ struct Parser {
pos: usize, pos: usize,
} }
/// Maximum recursion depth for the recursive-descent parser. Deeply nested
/// expressions (e.g. thousands of parentheses) otherwise overflow the stack.
const MAX_RECURSION: usize = 256;
impl Parser { impl Parser {
fn new(tokens: Vec<Token>) -> Self { fn new(tokens: Vec<Token>) -> Self {
Self { tokens, pos: 0 } Self { tokens, pos: 0 }
@ -120,13 +124,19 @@ impl Parser {
} }
// expr → sum // expr → sum
fn parse_expr(&mut self) -> Result<f64, ()> { fn parse_expr(&mut self, depth: usize) -> Result<f64, ()> {
self.parse_sum() if depth > MAX_RECURSION {
return Err(());
}
self.parse_sum(depth + 1)
} }
// sum → product (('+' | '-') product)* // sum → product (('+' | '-') product)*
fn parse_sum(&mut self) -> Result<f64, ()> { fn parse_sum(&mut self, depth: usize) -> Result<f64, ()> {
let mut left = self.parse_product()?; if depth > MAX_RECURSION {
return Err(());
}
let mut left = self.parse_product(depth + 1)?;
loop { loop {
let op = match self.peek().cloned() { let op = match self.peek().cloned() {
Some(Token::Plus) => Some("+"), Some(Token::Plus) => Some("+"),
@ -136,12 +146,12 @@ impl Parser {
match op { match op {
Some("+") => { Some("+") => {
self.advance(); self.advance();
let right = self.parse_product()?; let right = self.parse_product(depth + 1)?;
left = left + right; left = left + right;
} }
Some("-") => { Some("-") => {
self.advance(); self.advance();
let right = self.parse_product()?; let right = self.parse_product(depth + 1)?;
left = left - right; left = left - right;
} }
_ => break, _ => break,
@ -151,8 +161,11 @@ impl Parser {
} }
// product → power (('*' | '/' | '%') power)* // product → power (('*' | '/' | '%') power)*
fn parse_product(&mut self) -> Result<f64, ()> { fn parse_product(&mut self, depth: usize) -> Result<f64, ()> {
let mut left = self.parse_power()?; if depth > MAX_RECURSION {
return Err(());
}
let mut left = self.parse_power(depth + 1)?;
loop { loop {
let op = match self.peek().cloned() { let op = match self.peek().cloned() {
Some(Token::Star) => Some("*"), Some(Token::Star) => Some("*"),
@ -163,12 +176,12 @@ impl Parser {
match op { match op {
Some("*") => { Some("*") => {
self.advance(); self.advance();
let right = self.parse_power()?; let right = self.parse_power(depth + 1)?;
left = left * right; left = left * right;
} }
Some("/") => { Some("/") => {
self.advance(); self.advance();
let right = self.parse_power()?; let right = self.parse_power(depth + 1)?;
if right == 0.0 { if right == 0.0 {
return Ok(if left > 0.0 { f64::INFINITY } else if left < 0.0 { f64::NEG_INFINITY } else { f64::NAN }); return Ok(if left > 0.0 { f64::INFINITY } else if left < 0.0 { f64::NEG_INFINITY } else { f64::NAN });
} }
@ -176,7 +189,7 @@ impl Parser {
} }
Some("%") => { Some("%") => {
self.advance(); self.advance();
let right = self.parse_power()?; let right = self.parse_power(depth + 1)?;
left = left % right; left = left % right;
} }
_ => break, _ => break,
@ -186,31 +199,40 @@ impl Parser {
} }
// power → unary ('^' unary)? // power → unary ('^' unary)?
fn parse_power(&mut self) -> Result<f64, ()> { fn parse_power(&mut self, depth: usize) -> Result<f64, ()> {
let base = self.parse_unary()?; if depth > MAX_RECURSION {
return Err(());
}
let base = self.parse_unary(depth + 1)?;
if let Some(Token::Caret) = self.peek() { if let Some(Token::Caret) = self.peek() {
self.advance(); self.advance();
let exp = self.parse_unary()?; let exp = self.parse_unary(depth + 1)?;
return Ok(base.powf(exp)); return Ok(base.powf(exp));
} }
Ok(base) Ok(base)
} }
// unary → ('-' | '+') unary | atom // unary → ('-' | '+') unary | atom
fn parse_unary(&mut self) -> Result<f64, ()> { fn parse_unary(&mut self, depth: usize) -> Result<f64, ()> {
if depth > MAX_RECURSION {
return Err(());
}
if let Some(Token::Minus) = self.peek() { if let Some(Token::Minus) = self.peek() {
self.advance(); self.advance();
return Ok(-self.parse_unary()?); return Ok(-self.parse_unary(depth + 1)?);
} }
if let Some(Token::Plus) = self.peek() { if let Some(Token::Plus) = self.peek() {
self.advance(); self.advance();
return Ok(self.parse_unary()?); return Ok(self.parse_unary(depth + 1)?);
} }
self.parse_atom() self.parse_atom(depth + 1)
} }
// atom → NUMBER | NAME | NAME '(' args ')' | 'pi' // atom → NUMBER | NAME | NAME '(' args ')' | 'pi'
fn parse_atom(&mut self) -> Result<f64, ()> { fn parse_atom(&mut self, depth: usize) -> Result<f64, ()> {
if depth > MAX_RECURSION {
return Err(());
}
let next = match self.peek() { let next = match self.peek() {
Some(t) => t.clone(), Some(t) => t.clone(),
None => return Err(()), None => return Err(()),
@ -218,7 +240,7 @@ impl Parser {
match next { match next {
Token::LParen => { Token::LParen => {
self.advance(); self.advance();
let result = self.parse_expr()?; let result = self.parse_expr(depth + 1)?;
self.expect_rparen()?; self.expect_rparen()?;
Ok(result) Ok(result)
} }
@ -231,7 +253,7 @@ impl Parser {
// Check for function call // Check for function call
if let Some(Token::LParen) = self.peek() { if let Some(Token::LParen) = self.peek() {
self.expect_paren()?; self.expect_paren()?;
let args = self.parse_args()?; let args = self.parse_args(depth + 1)?;
self.expect_rparen()?; self.expect_rparen()?;
return Self::call_function(&name, &args); return Self::call_function(&name, &args);
} }
@ -246,19 +268,22 @@ impl Parser {
} }
} }
fn parse_args(&mut self) -> Result<Vec<f64>, ()> { fn parse_args(&mut self, depth: usize) -> Result<Vec<f64>, ()> {
if depth > MAX_RECURSION {
return Err(());
}
let mut args = Vec::new(); let mut args = Vec::new();
if matches!(self.peek(), Some(Token::RParen)) { if matches!(self.peek(), Some(Token::RParen)) {
return Ok(args); return Ok(args);
} }
args.push(self.parse_expr()?); args.push(self.parse_expr(depth + 1)?);
loop { loop {
if matches!(self.peek(), Some(Token::RParen)) { if matches!(self.peek(), Some(Token::RParen)) {
break; break;
} }
if let Some(Token::Comma) = self.peek() { if let Some(Token::Comma) = self.peek() {
self.advance(); self.advance();
args.push(self.parse_expr()?); args.push(self.parse_expr(depth + 1)?);
} else { } else {
break; break;
} }
@ -343,7 +368,7 @@ impl Parser {
pub fn eval_number(input: &str) -> Option<f64> { pub fn eval_number(input: &str) -> Option<f64> {
let tokens = tokenize(input.trim()).ok()?; let tokens = tokenize(input.trim()).ok()?;
let mut parser = Parser::new(tokens); let mut parser = Parser::new(tokens);
let result = parser.parse_expr().ok()?; let result = parser.parse_expr(0).ok()?;
if parser.pos != parser.tokens.len() { if parser.pos != parser.tokens.len() {
return None; return None;
} }
@ -595,4 +620,16 @@ mod tests {
assert_eq!(eval_to_string("#10+10,#5+5"), "#20,#10"); assert_eq!(eval_to_string("#10+10,#5+5"), "#20,#10");
assert_eq!(eval_to_string("sqrt(9)+1,cos(0)+1"), "4,2"); assert_eq!(eval_to_string("sqrt(9)+1,cos(0)+1"), "4,2");
} }
#[test]
fn deeply_nested_parens_returns_none() {
let expr = "(".repeat(1000) + "1" + &")".repeat(1000);
assert_eq!(eval_number(&expr), None, "deeply nested expression should be rejected");
}
#[test]
fn moderately_nested_parens_still_evaluate() {
let expr = "(".repeat(30) + "1" + &")".repeat(30);
assert_eq!(eval_number(&expr), Some(1.0), "moderately nested expression should evaluate");
}
} }