Skip to content

Reject unbalanced and overly deep type names - #587

Merged
slabko merged 2 commits into
ClickHouse:masterfrom
iliaal:fix/type-parser-unbalanced-depth
Oct 29, 2026
Merged

slabko merged 2 commits 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
TypeParser::kMaxNestingDepth was declared public in types/type_parser.h, which
CMake installs, so the limit became part of the shipped API even though the only
production reference is in TypeParser::Parse. Move it to a file-local constant in
type_parser.cpp, matching the other internal constants there (kTypeCode,
kUnescapeMap), so the value stays tunable without a breaking change.

The tests referenced the constant to derive their boundaries, which made them
tautological: asserting that depth N parses and N+1 does not holds for any limit,
so lowering it to a value too small for the types real servers send would not
have failed TypeParser.NestingDepthLimit. Pin 512 and 513 by value instead.
@slabko
slabko merged commit cdc6dfc into ClickHouse:master Oct 29, 2026
46 of 47 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.

2 participants