Skip to content

Validate what CompressedInput decompresses - #586

Open
iliaal wants to merge 1 commit into
ClickHouse:masterfrom
iliaal:fix/lz4-empty-frame
Open

iliaal wants to merge 1 commit into
ClickHouse:masterfrom
iliaal:fix/lz4-empty-frame

Conversation

@iliaal

@iliaal iliaal commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

CompressedInput::Decompress in base/compressed.cpp trusts the declared original size in three ways:

  • An original size of 0 allocates Buffer(0), whose data() is null, and passes it to LZ4/ZSTD as the destination. LZ4 does pointer arithmetic on it before checking capacity, which is undefined behaviour (UBSan: "applying non-zero offset to null pointer" in lz4.c).
  • A well-formed empty block leaves DoNext with no data, so the stream reports end of input mid-packet.
  • Decompress ignores how many bytes LZ4/ZSTD actually produced. A block that decodes to 19 bytes while declaring 20 exposes a zero-filled tail as row data, a silent wrong value.

Fix: decompress empty blocks into a zero-capacity stack target, keep reading blocks until one yields data, and throw CompressionError when the output length differs from the declared original size, for both LZ4 and ZSTD.

Reproducer: a checksum-valid LZ4 block with original size 20 whose payload decodes to 19 bytes, or an LZ4 block with original size 0 and a non-empty payload.

Tests: CompressedInputCase.SkipsEmptyBlocks and RejectsBlockShorterThanDeclaredOriginalSize fail on master. RejectsEmptyBlockWithNonEmptyContent fails on master in a UBSan build.

A compressed block with an original size of 0 made
CompressedInput::Decompress allocate Buffer(0), whose data() is null, and pass
it as the LZ4/ZSTD destination; LZ4 does pointer arithmetic on it before it
checks the capacity, which is undefined behaviour. A well-formed empty block
also left CompressedInput::DoNext without data, so it reported end of stream,
and Decompress ignored how many bytes LZ4/ZSTD actually produced, so a block
that decoded short exposed a zero-filled tail as data. Decompress empty blocks
into a zero-capacity stack target, keep reading blocks until one yields data,
and reject output whose length differs from the declared original size.
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