Skip to content

Reject zero, oversized, and malformed FixedString sizes - #577

Open
iliaal wants to merge 2 commits into
ClickHouse:masterfrom
iliaal:fix/fixedstring-zero-width
Open

iliaal wants to merge 2 commits into
ClickHouse:masterfrom
iliaal:fix/fixedstring-zero-width

Conversation

@iliaal

@iliaal iliaal commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

A server that sends a column typed FixedString(0) crashes the client with SIGFPE, even in an empty result. ColumnFixedString::Size() divides by the width, and Block::AppendColumn() calls it on the zero-row header block.

A malformed width is worse because it poisons the type cache. For FixedString(-), std::stol throws out of TypeParser::Parse(). ParseTypeName() in types/type_parser.cpp inserts the cache entry before parsing and only erases it when Parse() returns false, so the half-built AST stays cached. The first query throws stol; the second identical query hits the zero division.

Fix:

  • Number tokens parse with std::from_chars. Invalid or out-of-range values fail the parse instead of throwing, so the existing erase path runs. It also removes the 32-bit long limit on Windows, where std::stol threw for widths above 2^31.
  • ParseTypeName() parses into a local and caches only on success, so an exception can't leave an entry behind.
  • With low_cardinality_as_wrapped_column set, CreateColumnFromAst() built LowCardinality(FixedString(N)) straight from the AST value and skipped the width bound, so the first load allocated whatever width the server declared. The check now lives in GetFixedStringSize(), which both paths call.
  • The factory rejects widths outside 1..16777215, the server's own MAX_FIXEDSTRING_SIZE, with ValidationError. The ColumnFixedString constructor rejects 0.

The server never accepts FixedString(0), so the existing tests that built one now use FixedString(1).

Reproducer: CreateColumnByType("FixedString(0)")->Size(), or a server Data block whose column type is FixedString(0). For the cache case, call CreateColumnByType("FixedString(-)") twice.

Tests: TypeParser.InvalidNumber, ParseTypeName.MalformedNumberReturnsNull, CreateColumnByType.FixedStringSize, CreateColumnByType.MalformedFixedStringSize, CreateColumnByType.FixedStringSizeLimit, CreateColumnByType.LowCardinalityFixedStringSizeLimit and ColumnsCase.FixedString_Size_Eq0_Throws. On master, FixedStringSize dies with SIGFPE and the others fail.

A server-supplied FixedString(0) creates a ColumnFixedString with a
zero width, and ColumnFixedString::Size() divides by it as soon as
Block::AppendColumn() sees the zero-row header block.

A malformed width such as FixedString(-) makes std::stol throw out of
TypeParser::Parse(). ParseTypeName() inserts the cache entry before
parsing and erases it only when Parse() returns false, so the throw
leaves a half-built AST cached and the next query with the same type
hits the same division.

Number tokens now parse with std::from_chars, and invalid or
out-of-range values fail the parse instead of throwing, which lets the
existing erase path run. ParseTypeName() also parses into a local and
caches only on success, so an exception from the parser can no longer
leave an entry behind.

The factory rejects sizes outside 1..0xFFFFFF, the server's own
DataTypeFixedString limit (MAX_FIXEDSTRING_SIZE), and the
ColumnFixedString constructor rejects 0. The server never accepts
FixedString(0), so the tests that built one now use FixedString(1).
The 1..0xFFFFFF FixedString check only ran in CreateTerminalColumn().
With low_cardinality_as_wrapped_column set, CreateColumnFromAst() builds
LowCardinality(FixedString(N)) straight from the AST value, so a server
could declare any width and the client would allocate it on the first
load.

I moved the check into GetFixedStringSize() and call it from both
paths.
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