Fix call stack limits.

This commit is contained in:
Stephen Chung 2020-12-29 12:29:45 +08:00
parent 41c6f985f5
commit 13f5cec291
8 changed files with 70 additions and 55 deletions

View File

@ -16,6 +16,7 @@ Bug fixes
* Fix bug when accessing properties in closures. * Fix bug when accessing properties in closures.
* Fix bug when accessing a deep index with a function call. * Fix bug when accessing a deep index with a function call.
* Fix bug that sometimes allow assigning to an invalid l-value. * Fix bug that sometimes allow assigning to an invalid l-value.
* Fix off-by-one error with `Engine::set_max_call_levels`.
Breaking changes Breaking changes
---------------- ----------------

View File

@ -170,6 +170,7 @@ impl FromIterator<(ImmutableString, Shared<Module>)> for Imports {
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
#[cfg(not(feature = "no_function"))]
pub const MAX_CALL_STACK_DEPTH: usize = 8; pub const MAX_CALL_STACK_DEPTH: usize = 8;
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -181,6 +182,7 @@ pub const MAX_FUNCTION_EXPR_DEPTH: usize = 16;
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
#[cfg(not(feature = "no_function"))]
pub const MAX_CALL_STACK_DEPTH: usize = 128; pub const MAX_CALL_STACK_DEPTH: usize = 128;
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
@ -527,8 +529,8 @@ impl State {
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Limits { pub struct Limits {
/// Maximum levels of call-stack to prevent infinite recursion. /// Maximum levels of call-stack to prevent infinite recursion.
/// /// Not available under `no_function`.
/// Defaults to 16 for debug builds and 128 for non-debug builds. #[cfg(not(feature = "no_function"))]
pub max_call_stack_depth: usize, pub max_call_stack_depth: usize,
/// Maximum depth of statements/expressions at global level (0 = unlimited). /// Maximum depth of statements/expressions at global level (0 = unlimited).
pub max_expr_depth: usize, pub max_expr_depth: usize,
@ -809,6 +811,7 @@ impl Engine {
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
limits: Limits { limits: Limits {
#[cfg(not(feature = "no_function"))]
max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_call_stack_depth: MAX_CALL_STACK_DEPTH,
max_expr_depth: MAX_EXPR_DEPTH, max_expr_depth: MAX_EXPR_DEPTH,
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
@ -864,6 +867,7 @@ impl Engine {
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
limits: Limits { limits: Limits {
#[cfg(not(feature = "no_function"))]
max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_call_stack_depth: MAX_CALL_STACK_DEPTH,
max_expr_depth: MAX_EXPR_DEPTH, max_expr_depth: MAX_EXPR_DEPTH,
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]

View File

@ -1453,7 +1453,7 @@ impl Engine {
) -> Result<T, Box<EvalAltResult>> { ) -> Result<T, Box<EvalAltResult>> {
let mods = &mut (&self.global_sub_modules).into(); let mods = &mut (&self.global_sub_modules).into();
let result = self.eval_ast_with_scope_raw(scope, mods, ast)?; let result = self.eval_ast_with_scope_raw(scope, mods, ast, 0)?;
let typ = self.map_type_name(result.type_name()); let typ = self.map_type_name(result.type_name());
@ -1473,12 +1473,13 @@ impl Engine {
scope: &mut Scope, scope: &mut Scope,
mods: &mut Imports, mods: &mut Imports,
ast: &'a AST, ast: &'a AST,
level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> { ) -> Result<Dynamic, Box<EvalAltResult>> {
let state = &mut State { let state = &mut State {
source: ast.clone_source(), source: ast.clone_source(),
..Default::default() ..Default::default()
}; };
self.eval_statements_raw(scope, mods, state, ast.statements(), &[ast.lib()]) self.eval_statements_raw(scope, mods, state, ast.statements(), &[ast.lib()], level)
} }
/// Evaluate a file, but throw away the result and only return error (if any). /// Evaluate a file, but throw away the result and only return error (if any).
/// Useful for when you don't need the result, but still need to keep track of possible errors. /// Useful for when you don't need the result, but still need to keep track of possible errors.
@ -1542,7 +1543,7 @@ impl Engine {
source: ast.clone_source(), source: ast.clone_source(),
..Default::default() ..Default::default()
}; };
self.eval_statements_raw(scope, mods, state, ast.statements(), &[ast.lib()])?; self.eval_statements_raw(scope, mods, state, ast.statements(), &[ast.lib()], 0)?;
Ok(()) Ok(())
} }
/// Call a script function defined in an [`AST`] with multiple arguments. /// Call a script function defined in an [`AST`] with multiple arguments.

View File

@ -38,6 +38,7 @@ impl Engine {
/// Set the maximum levels of function calls allowed for a script in order to avoid /// Set the maximum levels of function calls allowed for a script in order to avoid
/// infinite recursion and stack overflows. /// infinite recursion and stack overflows.
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
#[cfg(not(feature = "no_function"))]
#[inline(always)] #[inline(always)]
pub fn set_max_call_levels(&mut self, levels: usize) -> &mut Self { pub fn set_max_call_levels(&mut self, levels: usize) -> &mut Self {
self.limits.max_call_stack_depth = levels; self.limits.max_call_stack_depth = levels;
@ -45,6 +46,7 @@ impl Engine {
} }
/// The maximum levels of function calls allowed for a script. /// The maximum levels of function calls allowed for a script.
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
#[cfg(not(feature = "no_function"))]
#[inline(always)] #[inline(always)]
pub fn max_call_levels(&self) -> usize { pub fn max_call_levels(&self) -> usize {
self.limits.max_call_stack_depth self.limits.max_call_stack_depth

View File

@ -405,11 +405,11 @@ impl Engine {
mods.extend(fn_def.mods.iter_raw().map(|(n, m)| (n.clone(), m.clone()))); mods.extend(fn_def.mods.iter_raw().map(|(n, m)| (n.clone(), m.clone())));
} }
// Evaluate the function at one higher level of call depth // Evaluate the function
let stmt = &fn_def.body; let stmt = &fn_def.body;
let result = self let result = self
.eval_stmt(scope, mods, state, unified_lib, this_ptr, stmt, level + 1) .eval_stmt(scope, mods, state, unified_lib, this_ptr, stmt, level)
.or_else(|err| match *err { .or_else(|err| match *err {
// Convert return statement to return value // Convert return statement to return value
EvalAltResult::Return(x, _) => Ok(x), EvalAltResult::Return(x, _) => Ok(x),
@ -586,6 +586,8 @@ impl Engine {
mem::swap(&mut state.source, &mut source); mem::swap(&mut state.source, &mut source);
let level = _level + 1;
let result = self.call_script_fn( let result = self.call_script_fn(
scope, scope,
mods, mods,
@ -595,7 +597,7 @@ impl Engine {
func, func,
rest, rest,
pos, pos,
_level, level,
); );
// Restore the original source // Restore the original source
@ -610,8 +612,10 @@ impl Engine {
mem::swap(&mut state.source, &mut source); mem::swap(&mut state.source, &mut source);
let level = _level + 1;
let result = self.call_script_fn( let result = self.call_script_fn(
scope, mods, state, lib, &mut None, func, args, pos, _level, scope, mods, state, lib, &mut None, func, args, pos, level,
); );
// Restore the original source // Restore the original source
@ -667,11 +671,12 @@ impl Engine {
state: &mut State, state: &mut State,
statements: impl IntoIterator<Item = &'a Stmt>, statements: impl IntoIterator<Item = &'a Stmt>,
lib: &[&Module], lib: &[&Module],
level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> { ) -> Result<Dynamic, Box<EvalAltResult>> {
statements statements
.into_iter() .into_iter()
.try_fold(().into(), |_, stmt| { .try_fold(().into(), |_, stmt| {
self.eval_stmt(scope, mods, state, lib, &mut None, stmt, 0) self.eval_stmt(scope, mods, state, lib, &mut None, stmt, level)
}) })
.or_else(|err| match *err { .or_else(|err| match *err {
EvalAltResult::Return(out, _) => Ok(out), EvalAltResult::Return(out, _) => Ok(out),
@ -691,7 +696,7 @@ impl Engine {
lib: &[&Module], lib: &[&Module],
script: &str, script: &str,
pos: Position, pos: Position,
_level: usize, level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> { ) -> Result<Dynamic, Box<EvalAltResult>> {
self.inc_operations(state, pos)?; self.inc_operations(state, pos)?;
@ -700,13 +705,6 @@ impl Engine {
return Ok(Dynamic::UNIT); return Ok(Dynamic::UNIT);
} }
// Check for stack overflow
#[cfg(not(feature = "no_function"))]
#[cfg(not(feature = "unchecked"))]
if _level > self.max_call_levels() {
return Err(Box::new(EvalAltResult::ErrorStackOverflow(pos)));
}
// Compile the script text // Compile the script text
// No optimizations because we only run it once // No optimizations because we only run it once
let ast = self.compile_with_scope_and_optimization_level( let ast = self.compile_with_scope_and_optimization_level(
@ -727,7 +725,8 @@ impl Engine {
..Default::default() ..Default::default()
}; };
let result = self.eval_statements_raw(scope, mods, &mut new_state, ast.statements(), lib); let result =
self.eval_statements_raw(scope, mods, &mut new_state, ast.statements(), lib, level);
state.operations = new_state.operations; state.operations = new_state.operations;
result result
@ -1208,6 +1207,8 @@ impl Engine {
let mut source = module.id_raw().clone(); let mut source = module.id_raw().clone();
mem::swap(&mut state.source, &mut source); mem::swap(&mut state.source, &mut source);
let level = level + 1;
let result = self.call_script_fn( let result = self.call_script_fn(
new_scope, mods, state, lib, &mut None, &fn_def, args, pos, level, new_scope, mods, state, lib, &mut None, &fn_def, args, pos, level,
); );

View File

@ -1720,7 +1720,7 @@ impl Module {
let orig_mods_len = mods.len(); let orig_mods_len = mods.len();
// Run the script // Run the script
engine.eval_ast_with_scope_raw(&mut scope, &mut mods, &ast)?; engine.eval_ast_with_scope_raw(&mut scope, &mut mods, &ast, 0)?;
// Create new module // Create new module
let mut module = Module::new(); let mut module = Module::new();

View File

@ -19,7 +19,7 @@ use crate::stdlib::{
vec::Vec, vec::Vec,
}; };
use crate::syntax::{CustomSyntax, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; use crate::syntax::{CustomSyntax, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT};
use crate::token::{is_doc_comment, is_keyword_function, is_valid_identifier, Token, TokenStream}; use crate::token::{is_keyword_function, is_valid_identifier, Token, TokenStream};
use crate::utils::{get_hasher, StraightHasherBuilder}; use crate::utils::{get_hasher, StraightHasherBuilder};
use crate::{ use crate::{
calc_script_fn_hash, Dynamic, Engine, ImmutableString, LexError, ParseError, ParseErrorType, calc_script_fn_hash, Dynamic, Engine, ImmutableString, LexError, ParseError, ParseErrorType,
@ -2534,35 +2534,38 @@ fn parse_stmt(
) -> Result<Stmt, ParseError> { ) -> Result<Stmt, ParseError> {
use AccessMode::{ReadOnly, ReadWrite}; use AccessMode::{ReadOnly, ReadWrite};
let mut comments: Vec<String> = Default::default(); let mut _comments: Vec<String> = Default::default();
let mut comments_pos = Position::NONE;
// Handle doc-comments.
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
while let (Token::Comment(ref comment), comment_pos) = input.peek().unwrap() { {
if comments_pos.is_none() { let mut comments_pos = Position::NONE;
comments_pos = *comment_pos;
}
if !is_doc_comment(comment) { // Handle doc-comments.
unreachable!("expecting doc-comment, but gets {:?}", comment); while let (Token::Comment(ref comment), pos) = input.peek().unwrap() {
} if comments_pos.is_none() {
comments_pos = *pos;
if !settings.is_global { }
return Err(PERR::WrongDocComment.into_err(comments_pos));
} if !crate::token::is_doc_comment(comment) {
unreachable!("expecting doc-comment, but gets {:?}", comment);
match input.next().unwrap().0 { }
Token::Comment(comment) => {
comments.push(comment); if !settings.is_global {
return Err(PERR::WrongDocComment.into_err(comments_pos));
match input.peek().unwrap() { }
(Token::Fn, _) | (Token::Private, _) => break,
(Token::Comment(_), _) => (), match input.next().unwrap().0 {
_ => return Err(PERR::WrongDocComment.into_err(comments_pos)), Token::Comment(comment) => {
} _comments.push(comment);
match input.peek().unwrap() {
(Token::Fn, _) | (Token::Private, _) => break,
(Token::Comment(_), _) => (),
_ => return Err(PERR::WrongDocComment.into_err(comments_pos)),
}
}
t => unreachable!("expecting Token::Comment, but gets {:?}", t),
} }
t => unreachable!("expecting Token::Comment, but gets {:?}", t),
} }
} }
@ -2618,7 +2621,7 @@ fn parse_stmt(
pos: pos, pos: pos,
}; };
let func = parse_fn(input, &mut new_state, lib, access, settings, comments)?; let func = parse_fn(input, &mut new_state, lib, access, settings, _comments)?;
lib.insert( lib.insert(
// Qualifiers (none) + function name + number of arguments. // Qualifiers (none) + function name + number of arguments.

View File

@ -10,21 +10,24 @@ fn test_stack_overflow_fn_calls() -> Result<(), Box<EvalAltResult>> {
engine.eval::<INT>( engine.eval::<INT>(
r" r"
fn foo(n) { if n <= 1 { 0 } else { n + foo(n-1) } } fn foo(n) { if n <= 1 { 0 } else { n + foo(n-1) } }
foo(7) foo(6)
", ",
)?, )?,
27 20
); );
let max = engine.max_call_levels();
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
assert!(matches!( assert!(matches!(
*engine *engine
.eval::<()>( .eval::<()>(&format!(
r" r"
fn foo(n) { if n == 0 { 0 } else { n + foo(n-1) } } fn foo(n) {{ if n == 0 {{ 0 }} else {{ n + foo(n-1) }} }}
foo(1000) foo({})
" ",
) max + 1
))
.expect_err("should error"), .expect_err("should error"),
EvalAltResult::ErrorStackOverflow(_) EvalAltResult::ErrorStackOverflow(_)
)); ));