From 6df001f72d161dc4dfd246dc91b6b578c40ec6d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=A4llberg?= Date: Fri, 22 Sep 2023 18:24:35 +0200 Subject: [PATCH] Prevent cubic time on nested parentheses (#10467) # Description When parse_range get an item like ((((1..2)))) it would try to parse "((((1" with a long chain of recursive parsers, namely: - parse_value - parse_paren_expr - parse_full_cell_path - parse_block - parse_pipeline - parse_builtin_commands - parse_expression - parse_math_expression - parse_value - ... where `parse_paren_expr` calls `parse_range` in turn. Because at any time in the chain `parse_paren_expr` can call `parse_range`, which will then continue the chain, we get quadratic number of function calls, each linear on the size of the input By checking with the lexer that the parens are matched, we prevent the long chain from being called on unmatched braces. Now, this is still more quadratic than it needs to be, to fix that, we should process parens only once, instead of on each recursive call # User-Facing Changes Speed improvements in some edge cases # Tests + Formatting Not sure how to test this, maybe I could add a benchmark # After Submitting # Other notes Found using the fuzzer, by setting a timeout on max run-time. It also found a stack-overflow on too many parentheses, which this doesn't fix. --- crates/nu-parser/src/parser.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index d12670cb2..2bf1b650d 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1425,6 +1425,21 @@ pub fn parse_range(working_set: &mut StateWorkingSet, span: Span) -> Expression return garbage(span); } }; + // Avoid calling sub-parsers on unmatched parens, to prevent quadratic time on things like ((((1..2)))) + // No need to call the expensive parse_value on "((((1" + if dotdot_pos[0] > 0 { + let (_tokens, err) = lex( + &contents[..dotdot_pos[0]], + span.start, + &[], + &[b'.', b'?'], + true, + ); + if let Some(_err) = err { + working_set.error(ParseError::Expected("Valid expression before ..", span)); + return garbage(span); + } + } let (inclusion, range_op_str, range_op_span) = if let Some(pos) = token.find("..<") { if pos == range_op_pos {