Skip to content

Reject unbalanced and overly deep type names - #587

Open
iliaal wants to merge 1 commit into
ClickHouse:masterfrom
iliaal:fix/type-parser-unbalanced-depth
Open

iliaal wants to merge 1 commit into
ClickHouse:masterfrom
iliaal:fix/type-parser-unbalanced-depth

Conversation

@iliaal

@iliaal iliaal commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Two server-supplied type strings crash CreateColumnByType.

A surplus ) or , makes TypeParser::Parse() in types/type_parser.cpp call open_elements_.top() and pop() on an empty std::stack, which is undefined behaviour on an empty deque. Release builds segfault on UInt8)), ), or ))).

Deep nesting overflows the stack. CreateColumnFromAst in columns/factory.cpp recurses once per nesting level, and so does the TypeAst destructor. A type string of about 700 KB (Array( repeated 100,000 times) overflows the stack whether or not it is closed.

Fix: Parse() returns false on a ) or , with no open element, and on nesting deeper than TypeParser::kMaxNestingDepth (512). That bounds both the factory recursion and AST destruction. Current servers reject column types nested deeper than 499 levels (26.9 accepts at most 497 nested Array( levels at default settings, 499 with the AST limits raised), so 512 rejects nothing a server can produce. It also keeps the worst-case stack near 250 KiB in an optimized build. A side effect: top-level commas (UInt8, String) and UInt8)( are now rejected; neither is a valid type.

Reproducer type strings: UInt8)), ),, ))), and Array( × 100000 followed by UInt8 (closed or unclosed).

Tests in ut/type_parser_ut.cpp:

  • UnbalancedClosingParenthesis covers 13 malformed strings.
  • NestingDepthLimit accepts depth 512 and rejects 513 and 100000, closed and unclosed.
  • NestingDepthLimitBoundsColumnFactory checks that a 499-level type still builds a column and deeper types return nullptr.
  • BalancedNestedControls covers Map(String, Array(Nullable(UInt8))), Tuple(a UInt8, b String) and deep Array.

The first three crash on master; the controls pass on both.

TypeParser::Parse() calls top() and pop() on its element stack for every
')' and ',', so a type name with a surplus closing parenthesis such as
"UInt8))" or ")," reads from an empty std::stack and crashes. It also has
no nesting limit, while CreateColumnFromAst() and the TypeAst destructor
recurse once per level, so a type nested 100000 levels deep, closed or
not, overflows the stack. Parse() now fails on a ')' or ',' with no open
parenthesis and on nesting deeper than 512 levels: current servers reject
column types nested deeper than 499 levels, and 512 keeps the recursive
paths to about 250 KiB of stack in an optimized build.
@iliaal
iliaal requested review from mzitnik and slabko as code owners October 18, 2026 17:12
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