Cap DiceServ expression size to prevent a stack-overflow crash
This commit is contained in:
parent
9806da9c28
commit
ab361e8646
1 changed files with 46 additions and 2 deletions
|
|
@ -224,6 +224,14 @@ pub fn evaluate(input: &str, rng: &mut impl Rng) -> Result<Evaluated, String> {
|
|||
if toks.is_empty() {
|
||||
return Err("nothing to roll".to_string());
|
||||
}
|
||||
// Bound the token count so a deeply-nested expression (e.g. thousands of `(`
|
||||
// or unary `-`) can't overflow the recursive-descent parser or `eval` and
|
||||
// crash the single-threaded daemon. Each nesting level needs >=1 token, so
|
||||
// this caps both recursion depths. Legit dice math is far shorter.
|
||||
const MAX_TOKENS: usize = 500;
|
||||
if toks.len() > MAX_TOKENS {
|
||||
return Err("that expression is too long".to_string());
|
||||
}
|
||||
let ast = Parser { toks, pos: 0 }.parse()?;
|
||||
let mut rolls = Vec::new();
|
||||
let mut total_dice = 0u64;
|
||||
|
|
@ -273,11 +281,18 @@ fn eval(ast: &Ast, rng: &mut impl Rng, rolls: &mut Vec<Roll>, total: &mut u64) -
|
|||
if *total > MAX_TOTAL {
|
||||
return Err("that expression rolls too many dice".to_string());
|
||||
}
|
||||
let mut values = Vec::with_capacity(num as usize);
|
||||
// Retain at most MAX_KEPT individual faces for the extended (EX)
|
||||
// listing: a million-die roll is allowed for the total, but keeping
|
||||
// (and later stringifying) every face would be a multi-MB alloc under
|
||||
// the engine lock. The sum stays exact regardless.
|
||||
const MAX_KEPT: usize = 1000;
|
||||
let mut values = Vec::with_capacity((num as usize).min(MAX_KEPT));
|
||||
let mut sum = 0u64;
|
||||
for _ in 0..num {
|
||||
let face = rng.gen_range(1..=sides);
|
||||
if values.len() < MAX_KEPT {
|
||||
values.push(face);
|
||||
}
|
||||
sum += face;
|
||||
}
|
||||
rolls.push(Roll { spec: format!("{num}d{sides}"), values, total: sum });
|
||||
|
|
@ -347,3 +362,32 @@ fn call(name: &str, args: &[Ast], rng: &mut impl Rng, rolls: &mut Vec<Roll>, tot
|
|||
_ => return Err(format!("unknown function '{name}'")),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deep_nesting_is_rejected_not_a_stack_overflow() {
|
||||
let mut rng = rand::thread_rng();
|
||||
// Pathological deeply-nested / unary-heavy input must be refused by the
|
||||
// token cap, never overflow the recursive parser/eval and crash the daemon.
|
||||
assert!(evaluate(&"(".repeat(5000), &mut rng).is_err(), "deep parens rejected");
|
||||
assert!(evaluate(&"-".repeat(5000), &mut rng).is_err(), "deep unary rejected");
|
||||
// Ordinary expressions (including modest nesting) still evaluate.
|
||||
assert!(evaluate("2+3*4", &mut rng).is_ok());
|
||||
let nested = format!("{}1{}", "(".repeat(50), ")".repeat(50));
|
||||
assert!(evaluate(&nested, &mut rng).is_ok(), "modest nesting still parses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extended_roll_retains_bounded_faces() {
|
||||
let mut rng = rand::thread_rng();
|
||||
// A huge roll is allowed for the total, but only a bounded number of faces
|
||||
// is kept for the extended listing (no multi-MB alloc under the lock).
|
||||
let ev = evaluate("50000d6", &mut rng).expect("large roll ok");
|
||||
assert_eq!(ev.rolls.len(), 1);
|
||||
assert!(ev.rolls[0].values.len() <= 1000, "faces retained are capped");
|
||||
assert!(ev.rolls[0].total >= 50000, "the sum stays exact");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue