Fix QueryConditionCache OOB write on part-name/layout reuse (#2342) - #2355
Open
coreground wants to merge 5 commits into
Open
Fix QueryConditionCache OOB write on part-name/layout reuse (#2342)#2355coreground wants to merge 5 commits into
coreground wants to merge 5 commits into
Conversation
…#2342) The real v25.8.28.10001.altinitystable tag did NOT have the Key fix upstream (qcc-package's investigation snapshots were captured from a later revision that already had it — see docs/superpowers/plans/ 2026-09-12-qcc-2342-findings.md). This commit adds it: Key now includes marks_count/has_final_mark so a layout mismatch on getOrSet() is structurally impossible, updates read()'s signature and its call site(s) accordingly, and layers the defense-in-depth patch on top: the three size-mismatch checks in QueryConditionCache::write()/read() become LOG_ERROR + skip instead of throw, so a residual mismatch degrades to a cache miss instead of failing the user's query, and a new QueryConditionCacheLayoutMismatch ProfileEvent surfaces if that check ever fires. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NAYye5ozisLN7WBqzYfAn
…-depth (Altinity#2342) Adds two gtest files under src/Interpreters/Cache/tests/ (new directory, picked up automatically by the existing GLOB_RECURSE "gtest*.cpp" rule in src/CMakeLists.txt, no CMake changes needed): - gtest_query_condition_cache.cpp: baseline regression tests for the Key layout-mismatch fix (2 tests). - gtest_query_condition_cache_defense_in_depth.cpp: additional coverage for reused part names and concurrent writes (3 tests). Added <base/UUID.h>, <base/unit.h>, <thread>, and <vector> includes proactively since the literals/types used (1_MiB, UUID(1), std::thread, std::vector) are not guaranteed to be visible transitively. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NAYye5ozisLN7WBqzYfAn
The stale TODO comment above `extern const Event QueryConditionCacheLayoutMismatch;` (copied verbatim from qcc-package's patched .cpp per Task 2's Step 2) had a trailing " \" at the end of its last `///` line. A trailing backslash performs line splicing at the preprocessing/translation-phase level even inside line comments, before tokenization — so that spliced the next physical line (the extern declaration itself) into the same comment, silently deleting it from compilation and breaking the build wherever this event is referenced. Found by Task 5 while building the checkout. Removes the whole 5-line stale TODO block (it was dead text anyway: the ProfileEvent it describes was already registered in src/Common/ProfileEvents.cpp in the original Task 2 commit), restoring the extern declaration as live code. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NAYye5ozisLN7WBqzYfAn
Addresses all findings from the final whole-branch code review of the QueryConditionCache marks_count/has_final_mark key fix (Altinity#2342): 1. Move the file-scope `namespace ErrorCodes` block inside `namespace DB`, matching the convention used everywhere else in the codebase. 2. Convert the two throws added by this branch in write() (final-mark/zero-marks check, and mark-range bounds validation) to the same log-and-skip pattern used by the other defense-in-depth checks in this file, and delete the inaccurate "unchanged from upstream" comment that justified the throw. 3. Add back `chassert(entry->matching_marks.size() == marks_count)` immediately above each of the three log-and-skip mismatch checks, so debug/CI builds still abort on a regression while release builds degrade gracefully. 4. Move the QueryConditionCacheHits counter increment in read() to just before the final successful return, so a layout mismatch no longer double-counts as both a hit and a miss. 5. In FilterTransform::writeIntoQueryConditionCache, also compare marks_count and has_final_mark (not just table_uuid/part_name) when deciding whether to append to the buffered MarkRangesInfo or flush it, since a part_name can be reused with a different mark layout. 6. Add marks_count and has_final_mark columns to system.query_condition_cache so entries that differ only in these key fields are distinguishable. 7. Remove the stale/inaccurate header comment in gtest_query_condition_cache_defense_in_depth.cpp and delete its verbatim duplicate of KeepsEntriesWithDifferentMarkLayoutsSeparate (already covered by gtest_query_condition_cache.cpp and linked into the same test binary). 8. chmod 644 both gtest_query_condition_cache*.cpp files to match every other gtest source file in the tree. 9. Reword the QueryConditionCacheLayoutMismatch ProfileEvents description to describe the observable behavior instead of reading as an internal PR note. 10. Strengthen ReusedPartNameWithFinalMarkDoesNotThrowOrCorrupt to also assert on the small layout's actual mark values, not just its size. 11. Reword completed-work comments from TODO(Altinity#2342) to NOTE(Altinity#2342), and update QueryConditionCache::read()'s doc comment to describe the marks_count/ has_final_mark parameters added when its signature grew from 3 to 5 args. Also updated the RejectsRangesOutsideEntryMarkLayout test to match the write() throw -> log-and-skip conversion from finding 2 (EXPECT_NO_THROW + read-miss assertion instead of EXPECT_THROW). Verified via `unit_tests_dbms --gtest_filter="QueryConditionCache.*"`: 4/4 tests pass (5 tests minus the duplicate removed in finding 7). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NAYye5ozisLN7WBqzYfAn
Both throws that used it were converted to log-and-skip in the previous commit (15846cb), leaving this extern declaration and the Common/Exception.h include unused. ClickHouse's style-check CI job flags unused ErrorCodes declarations, so this would otherwise fail CI on this PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NAYye5ozisLN7WBqzYfAn
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root-cause fix, validated:
QueryConditionCachekey missing mark layoutFollowing up on the crash trace package (708 rows across a 14-node production cluster) and the direct
QueryConditionCache::writefaults reported in #2342.Mechanism
QueryConditionCache::Keywas{table_id, part_name, condition_hash}. A part name can be reused after the underlying part's mark layout changes (exact lifecycle path not confirmed — see Open Items).write()'scache.getOrSet(key, load_func)can then return a pre-existingEntrysized for the old layout, which the code proceeds to index using the current task'smarks_count/mark_ranges:On
std::vector<bool>'s bit-packed storage, this is an out-of-bounds write, not a read.Note: this branch was built starting from the actual
v25.8.28.10001.altinitystableproduction tag, not a later revision — the Key fix below was not yet present there and had to be derived from scratch as part of this work, alongside the defense-in-depth hardening.Fix
Keynow includesmarks_countandhas_final_mark— two different layouts are structurally two different cache entries, sogetOrSet()can no longer return a mismatchedEntry.LOG_ERROR+ skip rather thanthrow, so a residual mismatch degrades to "don't use the cache for this entry" instead of failing the user's query — withchassertretained alongside each, so debug/CI builds still catch a regression. A newQueryConditionCacheLayoutMismatchProfileEvent surfaces if any of these ever fire.FilterTransform's buffered-write flush comparison now also checksmarks_count/has_final_mark(not justtable_uuid/part_name) — this was a second, independent path where a differently-shaped part could otherwise be merged into a stale buffer before ever reachingwrite().system.query_condition_cachenow exposesmarks_count/has_final_markso entries that previously looked like indistinguishable duplicates are distinguishable.Validation performed
marks_count/has_final_markderivation vs. read path — confirmed matching. All 4 realwrite()call sites (FilterTransform.cppWHERE path,MergeTreeSelectProcessor.cppPREWHERE path) derive these from the same livedata_part->index_granularityobject the read path (MergeTreeDataSelectExecutor.cpp) uses — not a stale copy. Confirmed by static tracing and by a live smoke test:QueryConditionCacheHits> 0 on repeated queries, confirming the fix doesn't silently zero out the cache hit rate.apt.llvm.orgcurrently serves — a local build-environment fix, not part of this patch).ALTER ... DELETE+ALTER ... UPDATEwas inconclusive (no crash, but the mutation produced a new part name rather than reusing the original in place, so this likely didn't exercise the actual original trigger).namespace ErrorCodes, two throws this branch had added that should have been log-and-skip like the rest, missingchasserts, a metric double-counting bug, theFilterTransformgap in item 3 above, stale test comments/duplicate test, file permissions, wording, and doc-comment updates).Newly discovered issue — separate from this fix, reported for visibility
While verifying that the write path's cache-population exclusions mirror the read path's guard (
!isFinal() && !mutations_snapshot->hasDataMutations()/hasPatchParts() && !vector_search), a real, independently-verified gap was found: the write path does not fully mirror the read path's guard.MergeTreeSelectProcessor.cpp) have zero parity with the read-path guard —isFinal(), in-flight mutations/patch parts, and vector search are all unchecked before writing.FilterTransform.cpp) correctly guardisFinal(), but not mutations/patch parts or vector search.Consequence: a query the read path would refuse to consult the cache for can still populate a cache entry via these write paths, and a later "plain" query can read back results computed under different semantics — a silent wrong-results correctness bug, not a crash, and orthogonal to the Key fix above. This is a distinct issue from #2342 and is not fixed by this PR — flagging for a separate follow-up.
Still not resolved, flagged honestly
prefer_localhost_replica = 0independently reduced the crash rate ~19x before the cache was touched. Possible secondary factor: local-read concurrency increasing concurrentwrite()calls against the sameEntry— out of scope for this PR.Confidence: high that this closes a concrete cache-owned OOB-write path. Not a claim this was the first or only corruption site in the original crash traces, nor that the
prefer_localhost_replicaquestion or the newly-discovered write-path exclusion gap are resolved.🤖 Generated with Claude Code
https://claude.ai/code/session_013NAYye5ozisLN7WBqzYfAn