From cf420aa51a250821120340855b2bd57a51b7877f Mon Sep 17 00:00:00 2001 From: Ryan Bahadori Date: Sun, 18 Oct 2026 20:41:46 -0700 Subject: [PATCH 1/2] cap statement nesting depth Statement parsing recurses once per nesting level, through compound blocks and the bodies of if/while/for/do/switch, with nothing bounding it. Deep input overflows the stack: 10k levels survive an 8MB stack here, 20k segfault, and dangling ifs do it as readily as nested braces. The 200 level cap matches the one expression nesting already has, and it sits at the statement entry point so brace-less nesting is covered too. Input deeper than that is now a hard error rather than a crash. --- src/ccompiler/parser.c | 33 ++++++++++++++++++++++++++ tests/data/smoke_cases.txt | 3 +++ tests/pathological_dangling_if.c | 8 +++++++ tests/pathological_statement_nesting.c | 9 +++++++ 4 files changed, 53 insertions(+) create mode 100644 tests/pathological_dangling_if.c create mode 100644 tests/pathological_statement_nesting.c diff --git a/src/ccompiler/parser.c b/src/ccompiler/parser.c index 17b6eb3..5f10389 100644 --- a/src/ccompiler/parser.c +++ b/src/ccompiler/parser.c @@ -31,6 +31,7 @@ typedef struct { size_t typedef_scope_count; size_t typedef_scope_capacity; size_t expression_depth; + size_t statement_depth; } CCParser; static const char *const cc_ast_kind_names[] = { @@ -271,6 +272,25 @@ static void cc_leave_expression_nesting(CCParser *parser) { parser->expression_depth--; } +/* Statement parsing recurses per nesting level too, through compound blocks + * and the bodies of if/while/for/do/switch. Nested braces alone are enough to + * exhaust the stack, so the same cap applies one level up. */ +#define CC_MAX_STATEMENT_NESTING 200 + +static bool cc_enter_statement_nesting(CCParser *parser) { + if (parser->statement_depth >= CC_MAX_STATEMENT_NESTING) { + cc_expected(parser, "statement nesting too deep"); + return false; + } + + parser->statement_depth++; + return true; +} + +static void cc_leave_statement_nesting(CCParser *parser) { + parser->statement_depth--; +} + static CCAstNode *cc_new_ast_node(CCAstKind kind, CCSpan span, const char *text) { CCAstNode *node; @@ -579,6 +599,7 @@ static void cc_synchronize_to_semicolon_or_brace(CCParser *parser) { static CCAstNode *cc_parse_expression(CCParser *parser); static CCAstNode *cc_parse_assignment_expression(CCParser *parser); static CCAstNode *cc_parse_statement(CCParser *parser); +static CCAstNode *cc_parse_statement_body(CCParser *parser); static CCAstNode *cc_parse_declaration(CCParser *parser); static CCParsedDeclarator cc_parse_declarator(CCParser *parser, bool allow_abstract); @@ -1809,6 +1830,18 @@ static CCAstNode *cc_parse_expression_statement(CCParser *parser) { } static CCAstNode *cc_parse_statement(CCParser *parser) { + CCAstNode *statement; + + if (!cc_enter_statement_nesting(parser)) { + return NULL; + } + + statement = cc_parse_statement_body(parser); + cc_leave_statement_nesting(parser); + return statement; +} + +static CCAstNode *cc_parse_statement_body(CCParser *parser) { if (cc_current_token(parser)->kind == CC_TOKEN_IDENTIFIER && cc_peek_token(parser, 1)->kind == CC_TOKEN_COLON) { return cc_parse_labeled_statement(parser); } diff --git a/tests/data/smoke_cases.txt b/tests/data/smoke_cases.txt index 1cd2423..2b28cd0 100644 --- a/tests/data/smoke_cases.txt +++ b/tests/data/smoke_cases.txt @@ -11,6 +11,9 @@ parse|tests/pathological_statement.c|1|expected expression parse|tests/pathological_conditional.c|1|expected expression # Regression: deep nesting is capped instead of overflowing the stack parse|tests/pathological_nesting.c|1|expression nesting too deep +# Regression: statement nesting is capped too, whether or not blocks are involved +parse|tests/pathological_statement_nesting.c|1|statement nesting too deep +parse|tests/pathological_dangling_if.c|1|statement nesting too deep # Regression: self-including headers hit the include-depth cap, not the stack preprocess|tests/include_cycle_a.c|1|include nesting too deep # Regression: const folding must reject UB shifts/overflow instead of tripping UBSan diff --git a/tests/pathological_dangling_if.c b/tests/pathological_dangling_if.c new file mode 100644 index 0000000..2386019 --- /dev/null +++ b/tests/pathological_dangling_if.c @@ -0,0 +1,8 @@ +/* Regression test: the depth cap covers brace-less nesting too. A chain of + * dangling ifs recurses through the statement parser without ever opening a + * block, so a cap on compound statements alone would not have caught it. + */ +int main(void) { + if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) if (1) return 0; + return 0; +} diff --git a/tests/pathological_statement_nesting.c b/tests/pathological_statement_nesting.c new file mode 100644 index 0000000..8dcb401 --- /dev/null +++ b/tests/pathological_statement_nesting.c @@ -0,0 +1,9 @@ +/* Regression test: statement nesting is capped the way expression nesting + * already is. Nested blocks recurse through the statement and + * compound-statement parsers with no other bound, which overflows the stack + * on deep input. The parser now reports "statement nesting too deep". + */ +int main(void) { +{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} + return 0; +} From e3a46bfbb6e9c9400195eb435e82008cb892af81 Mon Sep 17 00:00:00 2001 From: Ryan Bahadori Date: Sun, 18 Oct 2026 20:41:55 -0700 Subject: [PATCH 2/2] fold 0b literals without shifting into the sign bit The accumulator was a signed long long, so a literal whose top bit lands in the sign position shifted into it. That is undefined rather than a wrapped value, and UBSan flagged the 64 ones case at the shift. Accumulating unsigned and converting once on the way out removes the UB and leaves the folded value alone, so a 64 bit binary literal still folds to -1. --- src/ccompiler/sema_const.c | 8 +++++--- tests/binary_literal_overflow.c | 6 ++++++ tests/data/smoke_cases.txt | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 tests/binary_literal_overflow.c diff --git a/src/ccompiler/sema_const.c b/src/ccompiler/sema_const.c index 43f679b..2e7b17a 100644 --- a/src/ccompiler/sema_const.c +++ b/src/ccompiler/sema_const.c @@ -33,7 +33,7 @@ static bool cc_parse_integer_literal(const char *text, long long *value) { copy[length] = '\0'; if (length > 2 && copy[0] == '0' && (copy[1] == 'b' || copy[1] == 'B')) { - long long parsed; + unsigned long long parsed; parsed = 0; for (index = 2; index < length; index++) { @@ -41,9 +41,11 @@ static bool cc_parse_integer_literal(const char *text, long long *value) { free(copy); return false; } - parsed = (parsed << 1) | (copy[index] - '0'); + /* Accumulate unsigned: shifting a signed value up into the sign bit + * is undefined, and a 64-bit literal does exactly that. */ + parsed = (parsed << 1) | (unsigned long long)(copy[index] - '0'); } - *value = parsed; + *value = (long long)parsed; free(copy); return true; } diff --git a/tests/binary_literal_overflow.c b/tests/binary_literal_overflow.c new file mode 100644 index 0000000..7f20ddf --- /dev/null +++ b/tests/binary_literal_overflow.c @@ -0,0 +1,6 @@ +/* Regression test: the 0b fold accumulated into a signed long long, so a + * literal whose top bit lands in the sign position shifted into it. That is + * undefined behaviour, not a wrapped value, and UBSan flagged it. The fold now + * accumulates unsigned and converts once on the way out. + */ +int main(void){ switch (0) { case 0b1111111111111111111111111111111111111111111111111111111111111111: return 1; } return 0; } diff --git a/tests/data/smoke_cases.txt b/tests/data/smoke_cases.txt index 2b28cd0..ff3e637 100644 --- a/tests/data/smoke_cases.txt +++ b/tests/data/smoke_cases.txt @@ -19,3 +19,5 @@ preprocess|tests/include_cycle_a.c|1|include nesting too deep # Regression: const folding must reject UB shifts/overflow instead of tripping UBSan check|tests/shift_too_large.c|1|case label must be an integer constant expression check|tests/const_overflow.c|1|case label must be an integer constant expression +# Regression: a 0b literal wide enough to reach the sign bit must fold without UB +check|tests/binary_literal_overflow.c|0|semantic analysis succeeded