Skip to content

Fix remaining_length accounting after Huffman decode in RegressionPredictor / ComposedPredictor - #137

Open
alexey-milovidov wants to merge 1 commit into
szcompressor:masterfrom
ClickHouse:ch-fix-lorenzo-reg-remaining-length
Open

Fix remaining_length accounting after Huffman decode in RegressionPredictor / ComposedPredictor#137
alexey-milovidov wants to merge 1 commit into
szcompressor:masterfrom
ClickHouse:ch-fix-lorenzo-reg-remaining-length

Conversation

@alexey-milovidov

Copy link
Copy Markdown

Problem

RegressionPredictor::load threads a remaining_length byte counter through its sub-reads so later reads know how many bytes are left. After Huffman-decoding the regression coefficients it does:

encoder.load(c, remaining_length);
regression_coeff_quant_inds = encoder.decode(c, coeff_size);
encoder.postprocess_decode();
remaining_length -= coeff_size * sizeof(int);   // <-- wrong amount

decode advances the read pointer c by the compressed (Huffman) stream size, but remaining_length is decremented by coeff_size * sizeof(int) — the uncompressed index count. Those two are unrelated, so after this point remaining_length no longer reflects the bytes actually consumed (it typically overshoots the compressed stream and understates the remainder, and can even wrap past zero).

ComposedPredictor::load has the mirror defect: it Huffman-decodes a per-block predictor-selection stream but never decrements remaining_length for it at all.

Today the read helpers in MemoryUtil.hpp only guard the counter with assert, so a release build tolerates the wrong value and the bug is latent. But any reader that actually enforces the bound will then reject valid data. This surfaced while integrating SZ3 into ClickHouse, whose hardened build makes those reads throw instead of assert: a column compressed with ALGO_LORENZO_REG could be written but failed every subsequent read.

Fix

Decrement remaining_length by exactly the bytes decode consumed, measured as the pointer advance:

const uchar *decode_start = c;
regression_coeff_quant_inds = encoder.decode(c, coeff_size);
encoder.postprocess_decode();
remaining_length -= static_cast<size_t>(c - decode_start);

and apply the same accounting to the selection stream in ComposedPredictor::load. Behavior for valid data is unchanged; the counter now tracks the real byte cursor.

Context

Found while integrating SZ3 into ClickHouse. Companion fix on the ClickHouse fork: ClickHouse#2

…mposed predictors

RegressionPredictor::load decremented remaining_length by the uncompressed
index count (coeff_size * sizeof(int)) after Huffman decode(), but decode()
advances the read pointer only by the compressed stream size. The uncompressed
count overshoots the compressed stream, understating remaining_length for the
subsequent quantizer encoder.load(), whose bound check then spuriously rejects
valid data with "SZ3 Huffman: tree exceeds compressed buffer".

RegressionPredictor is only used by ALGO_LORENZO_REG, so a column compressed
with CODEC(SZ3('ALGO_LORENZO_REG', ...)) could be written on insert but fail
every subsequent read with CORRUPTED_DATA (effective data loss). Small element
counts are affected; large counts happen to leave enough slack to survive the
understated bound.

Account for exactly the bytes decode() consumed via pointer difference.
ComposedPredictor::load had the mirror defect (it never decremented
remaining_length for its selection stream); tightened the same way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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