diff --git a/src/script_parser.cpp b/src/script_parser.cpp index 0d1c63757..bc237963b 100644 --- a/src/script_parser.cpp +++ b/src/script_parser.cpp @@ -46,6 +46,14 @@ class ScriptParser private: std::vector tokens_; size_t current_ = 0; + int depth_ = 0; + + // Upper bound on the nesting of the recursive-descent grammar. Each open + // paren / unary prefix / right-hand operand adds one parseExpr frame, so a + // crafted expression like "((((...0...))))" would otherwise recurse until the + // native stack is exhausted. This mirrors the kMaxNestingDepth cap the XML + // parser already applies in VerifyXML and recursivelyCreateSubtree. + static constexpr int kMaxNestingDepth = 256; // Binding power constants. Higher value = tighter binding. static constexpr int kAssignmentBP = 2; @@ -254,6 +262,18 @@ class ScriptParser /// Main Pratt expression parser Ast::expr_ptr parseExpr(int minBP) { + // Bound the recursion so a deeply nested expression can't overflow the + // stack. Every open paren / unary prefix / right-hand operand adds one + // parseExpr frame; depth_ is decremented before the normal return below so + // it tracks the current nesting rather than the total call count. A parse + // failure throws and discards the parser, so the error paths need no reset. + if(++depth_ > kMaxNestingDepth) + { + throw RuntimeError(StrCat("Parse error at position ", std::to_string(peek().pos), + ": expression nesting is too deep (limit ", + std::to_string(kMaxNestingDepth), ")")); + } + auto left = parsePrefix(); while(true) @@ -293,6 +313,7 @@ class ScriptParser left = makeBinary(std::move(left), opTok.type, std::move(right)); } + --depth_; return left; } diff --git a/tests/script_parser_test.cpp b/tests/script_parser_test.cpp index 2e2c5559d..b9af10071 100644 --- a/tests/script_parser_test.cpp +++ b/tests/script_parser_test.cpp @@ -98,6 +98,28 @@ TEST(ParserTest, AnyTypes_Failing) EXPECT_FALSE(BT::ParseScriptAndExecute(env, "foo").has_value()); } +TEST(ParserTest, DeeplyNestedScriptIsRejected) +{ + // A script with thousands of nested parentheses or unary prefixes used to add + // one recursion level per character and overflow the stack (SIGSEGV) at parse + // time. The parser now caps the nesting depth and returns an error instead. + const std::string deep_parens = std::string(20000, '(') + "0" + std::string(20000, ')'); + EXPECT_FALSE(BT::ValidateScript(deep_parens)); + + const std::string deep_unary = std::string(20000, '!') + "0"; + EXPECT_FALSE(BT::ValidateScript(deep_unary)); + + // The depth cap must not reject ordinary scripts: a long flat expression is + // parsed iteratively (shallow recursion) and light nesting is well within it. + std::string wide = "1"; + for(int i = 0; i < 1000; ++i) + { + wide += "+1"; + } + EXPECT_TRUE(BT::ValidateScript(wide)); + EXPECT_TRUE(BT::ValidateScript("((((1))))")); +} + TEST(ParserTest, Equations) { BT::Ast::Environment environment = { BT::Blackboard::create(), {} };