parser: enforce DepthGuard in DDL and stop trigger-body infinite loop#51
Merged
Merged
Conversation
Two related crash/DoS defects on nested CREATE TRIGGER, both in the DDL
recursion path.
1) Defeated DepthGuard (stack overflow). Every DDL entry point (12 sites
in parser_ddl.cpp) and parse_transaction_stmt (parser_statements.cpp)
wrote the guard as an if-init-statement:
if (const DepthGuard guard(this); !guard.is_valid()) return nullptr;
The guard object is destroyed at the end of the `if`, so depth is
incremented then immediately decremented and max_depth is never
enforced for the function body. Nested CREATE TRIGGER recurses
parse_create_trigger -> parse_statement -> parse_create_stmt ->
parse_create_trigger with no participant holding depth, giving
unbounded native recursion and a stack-overflow crash. Rewritten as a
function-scope local (matching the correct form already used in the
select/expression parsers):
DepthGuard guard(this);
if (!guard.is_valid()) return nullptr;
2) No-forward-progress infinite loop. The BEGIN ... END trigger-body loop
called parse_statement() and, on nullptr, only advanced when the next
token was ';'. On any token it cannot start a statement from (a stray
token such as `@`, or the depth guard now returning nullptr) it
re-called parse_statement() on the same token forever. Once fix (1)
stops the stack overflow, deep nested BEGIN..END input degraded into
this hang; it is also independently triggerable by `... BEGIN @ END`.
Added a forward-progress guard: if an iteration neither parsed a
statement nor consumed a separator, break (the parser is lenient about
the unconsumed remainder).
Both were reproduced under ASan/UBSan and now resolve to a graceful
"Maximum recursion depth exceeded" (deep nesting) or clean termination
(stray token), never a crash or hang.
Tests (tests/security/test_depth_guard.cpp): shallow nested triggers
parse; deeply nested single-statement and BEGIN..END trigger chains are
rejected with the depth error; a stray token in a trigger body terminates
and leaves the parser reusable. Falsifiable: reverting (1) restores the
ASan stack-overflow; reverting (2) restores the infinite-loop hang.
Full suite 37/37; depth-guard suite clean under ASan/UBSan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyKDAbMWGwiZq4iJx3imsg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related crash / DoS defects on nested
CREATE TRIGGER, both in the DDL recursion path. Found during the bottom-up audit (the tokenizer layer came back clean; this is the parser layer).1. Defeated
DepthGuard→ stack overflowEvery DDL entry point (12 sites in
parser_ddl.cpp) andparse_transaction_stmt(parser_statements.cpp) wrote the recursion guard as an if-init-statement:The guard object is destroyed at the end of the
if, so depth is incremented then immediately decremented andmax_depthis never enforced for the function body. NestedCREATE TRIGGERrecursesparse_create_trigger → parse_statement → parse_create_stmt → parse_create_triggerwith no participant holding depth → unbounded native recursion → stack-overflow crash (confirmed under ASan). Rewritten as a function-scope local, matching the correct form already used in the select/expression parsers:2. No-forward-progress infinite loop
The
BEGIN … ENDtrigger-body loop calledparse_statement()and, onnullptr, only advanced when the next token was;. On any token it cannot start a statement from — a stray token such as@, or the depth guard now returningnullptr— it re-calledparse_statement()on the same token forever. Once fix (1) stops the stack overflow, deepBEGIN..ENDinput degraded into this hang; it is also independently triggerable byCREATE TRIGGER x AFTER INSERT ON t BEGIN @ END. Added a forward-progress guard: if an iteration neither parsed a statement nor consumed a separator,break(the parser is lenient about the unconsumed remainder).Both defects were reproduced under ASan/UBSan and now resolve to a graceful
"Maximum recursion depth exceeded"(deep nesting) or clean termination (stray token) — never a crash or hang.Tests
tests/security/test_depth_guard.cpp:BEGIN..ENDtrigger chains are rejected with the depth error;Falsifiable: reverting (1) restores the ASan stack-overflow; reverting (2) restores the infinite-loop hang. Full suite 37/37; depth-guard suite (11 tests) clean under ASan/UBSan.