Skip to content

parser: enforce DepthGuard in DDL and stop trigger-body infinite loop#51

Merged
chiradip merged 1 commit into
mainfrom
fix-ddl-depthguard-and-trigger-body-loop
Jul 27, 2026
Merged

parser: enforce DepthGuard in DDL and stop trigger-body infinite loop#51
chiradip merged 1 commit into
mainfrom
fix-ddl-depthguard-and-trigger-body-loop

Conversation

@chiradip

@chiradip chiradip commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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 overflow

Every DDL entry point (12 sites in parser_ddl.cpp) and parse_transaction_stmt (parser_statements.cpp) wrote the recursion 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 → 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:

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 BEGIN..END input degraded into this hang; it is also independently triggerable by CREATE 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:

  • shallow nested triggers (both body forms) 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 (11 tests) clean under ASan/UBSan.

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
@chiradip
chiradip merged commit d0b206c into main Jul 27, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant