Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/script_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ class ScriptParser
private:
std::vector<Token> 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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -293,6 +313,7 @@ class ScriptParser
left = makeBinary(std::move(left), opTok.type, std::move(right));
}

--depth_;
return left;
}

Expand Down
22 changes: 22 additions & 0 deletions tests/script_parser_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(), {} };
Expand Down
Loading