Skip to content

Fix out-of-bounds crash in ObjectStorageQueueSource::FileIterator::next - #108977

Merged
alexey-milovidov merged 10 commits into
ClickHouse:masterfrom
groeneai:fix-s3queue-fileiterator-oob
Jul 31, 2026
Merged

Fix out-of-bounds crash in ObjectStorageQueueSource::FileIterator::next#108977
alexey-milovidov merged 10 commits into
ClickHouse:masterfrom
groeneai:fix-s3queue-fileiterator-oob

Conversation

@groeneai

@groeneai groeneai commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Changelog category (leave one):

  • Critical Bug Fix (crash, data loss, RBAC)

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Fix a server crash (out-of-bounds access) in S3Queue/AzureQueue with enable_hash_ring_filtering = 1 when a batch contained a non-processable file and the Keeper request to set the batch as processing failed at the same time.

Description

Reported by @ alexey-milovidov on #107740 (unrelated to that PR). Report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=107740&sha=9b2ca442ced2acecfefdd7307edee38be6a5dbf1&name_0=PR&name_1=Integration%20tests%20%28amd_asan_ubsan%2C%20db%20disk%2C%20old%20analyzer%2C%202%2F6%29

In the unordered hash-ring batch path of ObjectStorageQueueSource::FileIterator::next, file_metadatas is sized to the batch and populated per file. When the Keeper tryMulti that sets the batch as processing fails, the else branch clears file_metadatas. The following compaction block, entered whenever some file in the batch was non-processable (num_successful_objects < new_batch.size()), still did file_metadatas[i] = file_metadatas[batch_i] and file_metadatas.resize(...) on the now-empty vector, so the subscript went out of bounds and aborted the server. The trailing chassert(file_metadatas.empty() || new_batch.size() == file_metadatas.size()) already documents that an empty file_metadatas is an expected post-state.

The compaction now writes file_metadatas only when it was not cleared; new_batch is compacted as before. Downstream code already handles an empty file_metadatas (a null FileMetadataPtr is returned).

Regression test: test_batch_set_processing_failure_does_not_crash in tests/integration/test_storage_s3_queue/test_parallel_inserts.py reproduces both preconditions the way they occur when several consumers share one Keeper path. The failed Keeper multi is real - the test pre-creates a real processing node for one file of the batch (node name computed the same way as getNodeName), so the batch tryMulti fails with Node exists and clears file_metadatas. The non-processable file in the same batch is produced by the object_storage_queue_skip_one_file_in_batch failpoint, which takes the same std::nullopt path as a file already grabbed by another consumer. The test waits for the ObjectStorageQueueTrySetProcessingFailed profile event so the fixed path is guaranteed to have been exercised. Without the fix the server aborts in FileIterator::next; with it the queue keeps draining.

Follow-up (pre-existing on master, independent of this crash): after the per-file fallback trySetProcessing loses to a foreign processing node, the local FileStatus is cached as Processing and is never invalidated, so that one file can stay skipped by this table until restart or cache eviction. That is why the regression asserts >= files_to_generate - 1 rather than equality. Fixed separately in #112313.

Version info

  • Merged into: 26.8.1.562 (included in 26.8 and later)
  • Backported to: 26.7.2.54, 26.6.2.144, 26.5.6.103, 26.3.17.107, 25.8.29.49

@groeneai

Copy link
Copy Markdown
Collaborator Author
Pre-PR validation gate (click to expand)
# Question Answer
a Deterministic repro? Yes. New object_storage_queue_fail_batch_set_processing failpoint forces the exact precondition (a non-processable file in the batch plus a failed Keeper multi). With the failpoint enabled the unpatched server aborts every time on the first batch.
b Root cause explained? In FileIterator::next (unordered hash-ring batch path), file_metadatas is sized to the batch. When tryMulti fails the else branch does file_metadatas.clear(). The compaction block, entered when any file was non-processable (num_successful_objects < new_batch.size()), then runs file_metadatas[i] = file_metadatas[batch_i] + file_metadatas.resize() on the empty vector, so the subscript is out of bounds and the server aborts.
c Fix matches root cause? Yes. The compaction writes file_metadatas only when it was not cleared (the chassert(file_metadatas.empty() || ...) already documents empty as a valid post-state); new_batch is compacted as before.
d Test intent preserved / new tests added? New regression test test_batch_set_processing_failure_does_not_crash: asserts all files are processed and the server stays alive through the injected failure. No existing test weakened.
e Both directions demonstrated? Yes (Build-ID-verified). Without the guard: server SIGABRT, frame 6 verbose_abort.cpp (libc++ hardened OOB) -> frame 7 FileIterator::next(). With the guard: test passes, all 10 files processed, server alive.
f Fix is general across code paths? This is the only compaction site that subscripts file_metadatas after a possible clear(). The sibling per-file path (ObjectStorageQueueUnorderedFileMetadata::trySetProcessing) does not batch-resize file_metadatas, so it is unaffected.
g Fix generalizes across inputs? N/A (not an input-type bug). The empty-vector post-state is independent of file/data types; the guard handles every batch size.
h Backward compatible? Yes. No setting default, on-disk/wire format, or behavior change. Only an out-of-bounds write is removed; the empty-file_metadatas path was already supported downstream.
i Invariants and contracts preserved? Yes. The fix upholds the invariant the chassert documents (file_metadatas is either empty or the same size as new_batch) and keeps next()'s contract of returning a null FileMetadataPtr when file_metadatas is empty.

Session id: cron:clickhouse-worker-slot-11:20260630-173800

@groeneai

Copy link
Copy Markdown
Collaborator Author

cc @kssenii for review. Out-of-bounds crash in ObjectStorageQueueSource::FileIterator::next: when the Keeper multi setting a batch as processing fails (file_metadatas cleared) and the batch also had a non-processable file, the compaction loop subscripted the now-empty file_metadatas. Guarded the file_metadatas compaction; regression test added via a new failpoint.

@clickhouse-gh

clickhouse-gh Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [6003acd]

Summary:


AI Review

Summary

This PR fixes the out-of-bounds abort in ObjectStorageQueueSource::FileIterator::next by avoiding file_metadatas compaction after a failed batch tryMulti, and the regression test now reproduces the crash through a real Keeper conflict plus the object_storage_queue_skip_one_file_in_batch failpoint. The crash fix itself looks correct and the current Praktika run is green, but the current head still carries the previously reported per-file recovery bug, so the regression still has to accept one permanently skipped file after the injected conflict is removed.

Findings
  • ⚠️ Majors
    • [src/Storages/ObjectStorageQueue/ObjectStorageQueueIFileMetadata.cpp:298-320,365-385; tests/integration/test_storage_s3_queue/test_parallel_inserts.py:364-366] [dismissed by author -- https://github.com/ClickHouse/ClickHouse/pull/108977#discussion_r3623471640] The per-file fallback that runs after file_metadatas.clear() still caches a foreign processing-node conflict as terminal FileStatus::State::Processing: afterSetProcessing writes that state on failure, and trySetProcessing then short-circuits on it. After the test deletes the foreign Keeper node, that file remains locally unprocessable until restart or cache eviction, so the queue does not actually drain and the regression has to accept >= files_to_generate - 1.
    • Suggested fix: merge the follow-up that makes "processing by another processor" a retryable hint instead of terminal Processing, or otherwise clear/reclassify that state on the per-file fallback path before tightening the regression back to equality.
Final Verdict
  • Status: ⚠️ Request changes
  • Minimum required actions: bring the foreign-processing-node recovery fix into this branch (or an equivalent local fix) so the replayed file becomes retryable again, then tighten the regression to prove the queue fully drains after the injected conflict is removed.

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label Jun 30, 2026
@groeneai

groeneai commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

CI finish ledger — 3ba3147

Every failure below has an owner: a fixing PR (ours or external), or a full-effort fix task whose fixing-PR link will be posted here when it opens. Only CH Inc sync is exempt.

Check / test Reason Owner / fixing PR
Bugfix validation (integration, amd64+aarch64) / test_parallel_inserts.py::test_parallel_inserts_* (8 rows) expected bugfix-validation signal (job-level PASS: this PR's own regression test must FAIL on master to prove it catches the OOB) PR-caused N/A (this PR @ 3ba3147)
Stateless tests (arm_binary, parallel) / 02985_dialects_with_distributed_tables flaky (connection-pool desync; 10 unrelated PRs + 1 master / 30d) #108854 (external, open)
Stress test (amd_asan_ubsan) / Hung check deadlock (chronic stress-shutdown; not affected by an s3_queue OOB guard) #108212 (ours, merged) / #105905 (ours, open)
Stress test (amd_tsan) / Hung check deadlock #108212 (ours, merged) / #105905 (ours, open)
Stress test (arm_tsan) / Hung check deadlock #108212 (ours, merged) / #105905 (ours, open)
Performance Comparison (arm_release, master_head, 5/6) perf-infra: "Errors while building the report" (host-OOM at report build; 31 unrelated PRs / 7d; all perf tests themselves PASS) #108804 (ours, merged)
Mergeable Check / PR derivative aggregators (fail only due to the above)
CH Inc sync private fork-sync mirror CH Inc sync (private, not actionable by us)

No PR-caused failures: the s3_queue OOB fix is a 1-guard change in ObjectStorageQueueSource::FileIterator::next; the Bugfix-validation per-test FAILs are the expected signal (job-level PASS), and every other red check is a pre-existing trunk flake owned above.

Session id: cron:our-pr-ci-monitor:20260701-003000

@kssenii kssenii self-assigned this Jul 21, 2026
Comment on lines +304 to +306
/// Reproduce the crash precondition: a non-processable file in the
/// batch together with a failed keeper multi that clears file_metadatas.
if (new_batch.size() > 1 && new_batch.back() && num_successful_objects > 0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we reproduce the same in a more realistic way?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworked the regression test to reproduce this through real S3Queue operation instead of a fabricated keeper failure.

The two crash preconditions are now produced the way they happen when several consumers share one keeper path:

  • The failed keeper multi is real: the test pre-creates a real processing node in keeper for one batch file (node name = sipHash64(path), same as getNodeName), so the engine's own tryMulti fails against it with a real Node exists (real code, real responses, real getFailedOpIndex). No faked Coordination::Responses.
  • The non-processable file is produced by the engine's own std::nullopt reject path (the same branch taken when another table on the path already grabbed the file). The only test-only hook is a small failpoint that flips one batch entry onto that existing path; it changes no keeper or vector state itself.

To keep it deterministic on slow CI the test waits for the ObjectStorageQueueFailedToBatchSetProcessing profile event before continuing, so the failing batch is always exercised.

Verified both directions locally: without the fix the server aborts in FileIterator::next (libc++ ... index out of bounds); with the fix it survives and the queue drains.

In the unordered hash-ring batch path of FileIterator::next, file_metadatas is
resized to the batch size and populated per file. When a file in the batch is
non-processable (num_successful_objects < new_batch.size()) and the keeper
tryMulti that sets the batch as processing then fails, the else branch clears
file_metadatas. The subsequent compaction block still wrote
file_metadatas[i] = file_metadatas[batch_i] and called file_metadatas.resize()
on the now-empty vector, going out of bounds and aborting the server. The
trailing chassert(file_metadatas.empty() || ...) already documents that an empty
file_metadatas is an expected post-state.

Guard the file_metadatas compaction so it only runs when file_metadatas was not
cleared; new_batch is still compacted as before. Downstream code already handles
an empty file_metadatas (next() returns a null FileMetadataPtr).

The regression test reproduces both crash preconditions the way they occur when
several consumers share one keeper path: a real processing node is pre-created in
keeper for one batch file so the engine's own keeper multi fails against it (real
Node exists, no faked keeper response), and a small failpoint marks another file
non-processable through the same std::nullopt path the engine takes when a file
is already being processed elsewhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@groeneai
groeneai force-pushed the fix-s3queue-fileiterator-oob branch from 3ba3147 to c5f10ed Compare July 21, 2026 15:07
@groeneai

Copy link
Copy Markdown
Collaborator Author
Internal second-model review — adjudication log (click to expand)

Pre-publication review by an independent model (engine: codex; 3 findings; bounded: 1 full pass + 1 fix round + 1 delta recheck).

# Sev Finding Verdict Evidence / action
1 ⚠️ Diff advances 4 unrelated dependency submodules (mongo-c-driver, msgpack-c, orc, wasmedge) DISAGREE git show --stat HEAD on the pushed commit contains 0 contrib/ lines; the submodule drift was unstaged working-tree noise from provisioning, never staged (files added explicitly, never git add -A). The PR carries exactly 3 files.
2 ⚠️ Test can delete the keeper conflict before proving the failing batch was attempted (false negative on a slow worker) AGREE — fixed @ c5f10ed Replaced the fixed-time liveness loop with a poll on system.events ObjectStorageQueueFailedToBatchSetProcessing > 0 (incremented exactly when the multi fails); each poll also runs SELECT 1 so a crash aborts loudly. Re-verified both directions.
3 💡 Failpoint comment embeds regression narrative in production code AGREE — fixed @ c5f10ed Trimmed the comment from 5 lines to 2 (states only the local invariant); narrative kept in the commit message.

Severity: ❌ blocker / ⚠️ major / 💡 nit. DISAGREE verdicts carry recorded evidence and are terminal per finding. The delta recheck raised 0 new findings.

Session id: cron:clickhouse-author-slot-1:20260721-122500

@groeneai

Copy link
Copy Markdown
Collaborator Author

Test reworked per @kssenii's request to reproduce the crash realistically (real keeper conflict, no fabricated responses). Re-validated both directions.

Pre-PR validation gate (click to expand)
# Question Answer
a Deterministic repro? Yes. pytest test_storage_s3_queue/test_parallel_inserts.py::test_batch_set_processing_failure_does_not_crash. Deterministic (waits on the ObjectStorageQueueFailedToBatchSetProcessing profile event so the failing batch is always exercised); 3/3 stable.
b Root cause explained? Yes. In FileIterator::next unordered hash-ring path: a non-processable file makes num_successful_objects < new_batch.size(); the keeper tryMulti then fails and the else branch does file_metadatas.clear(); the compaction block still subscripts + resizes the now-empty file_metadatas -> OOB abort.
c Fix matches root cause? Yes. Guards the file_metadatas compaction (compact_file_metadatas = !file_metadatas.empty()) so it runs only when the vector was not cleared; new_batch compaction unchanged. Upholds the existing `chassert(file_metadatas.empty()
d Test intent preserved / new tests added? Yes. New integration regression test asserts the server survives the exact two-condition batch. Reworked to a realistic mechanism (real Node exists keeper conflict + engine's own std::nullopt reject path); still fails without the fix.
e Both directions demonstrated? Yes, Build-ID-verified. Without fix (build e3123a63): server SIGABRT, libc++ ... index out of bounds in FileIterator::next. With fix (build 8be2b706): passes, server alive, real conflict fired.
f Fix is general across code paths? Yes. This is the only site that writes file_metadatas after the possible clear(); new_batch path is correct and unchanged. No sibling path has the same empty-after-clear compaction.
g Fix generalizes across inputs (params/datatypes/wrappers)? N/A (no datatype/wrapper dimension). The empty-file_metadatas post-state is a single invariant; the guard holds for any batch size, any number of non-processable files, and any keeper failure.
h Backward compatible? Yes. Internal crash guard only; no setting/format/protocol/behavior change.
i Invariants and contracts preserved? Yes. Upholds the documented invariant that an empty file_metadatas is a legal post-state, on all paths incl. the cleared path; downstream next() already returns a null FileMetadataPtr for empty file_metadatas.

Session id: cron:clickhouse-author-slot-1:20260721-122500

Comment on lines +352 to +354
# All files except the one left in an in-memory Processing state by the aborted
# multi are processed; the point is that the server survived and the queue drains.
run_with_retry(lambda x: x >= files_to_generate - 1, get_count)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion is hiding a real recovery bug in the fallback path that this PR exercises. After the batch tryMulti fails and file_metadatas is cleared, the conflicted file is replayed through next(size_t) with file_metadata == nullptr, so it falls back to trySetProcessing (ObjectStorageQueueSource.cpp:551-555). If the foreign processing node still exists, ObjectStorageQueueUnorderedFileMetadata::setProcessingImpl returns {false, Processing} and afterSetProcessing stores Processing in the shared FileStatus cache (ObjectStorageQueueIFileMetadata.cpp:313-372).

From that point on, subsequent attempts short-circuit on state == Processing and never go back to Keeper, even after this test deletes the conflict node. In other words, the queue can leave one file permanently stuck in the local metadata cache instead of draining once the conflict disappears. I think the fix needs to keep that file retryable (or clear the cached Processing state on the individual fallback failure) rather than teaching the regression test to accept >= files_to_generate - 1.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against the code, the mechanism is correct. After the batch tryMulti fails, file_metadatas is cleared (ObjectStorageQueueSource.cpp:400); the conflicted file is replayed through next(size_t) with file_metadata == nullptr -> trySetProcessing (551-555); with the foreign processing node present, ObjectStorageQueueUnorderedFileMetadata::setProcessingImpl hits the create_processing_node_idx failure branch and returns {false, Processing}, so afterSetProcessing caches Processing via updateState (ObjectStorageQueueIFileMetadata.cpp:369-372). Subsequent trySetProcessing then short-circuits on state == Processing (294-304), and reset() runs only from resetProcessing(), which needs created_processing_node == true (never set on a foreign-node conflict). So the file can stay in in-memory Processing after the conflict node is gone. Confirmed.

Two scope points:

  1. It is pre-existing and independent of this PR. It needs only the failed batch multi (cond2), not the out-of-bounds compaction path this PR fixes (cond1). With a failed multi and no skipped file, file_metadatas.clear() still runs, num_successful_objects == new_batch.size() so the compaction block is skipped, and every file including the conflicted one is replayed with null metadata (454-457). The afterSetProcessing / trySetProcessing / updateState logic that produces the stuck state is untouched here, so it already reproduces on master whenever a batch multi loses a race against a foreign processing node.

  2. In normal operation the stale cache is harmless: the owning consumer creates a processed node when it finishes, and filterOutProcessedAndFailed then drops the file from future listings so it never returns to this consumer. The stuck case needs the processing node to vanish without the file being processed (a crashed/abandoned consumer, or the manual zk.delete here), a real but narrow recovery gap.

So this PR stays scoped to the crash: it asserts that the abort is gone and the queue survives and drains, and >= files_to_generate - 1 states that faithfully. Clearing or retrying the cached Processing on an individual fallback failure changes shared FileStatus cache semantics and deserves its own review, so I am tracking it as a separate follow-up rather than folding it into this crash fix.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a separate pull request: #112313

The mechanism you described is exactly right, and it is pre-existing on master and independent of this crash: after trySetProcessing loses the race for the processing node, afterSetProcessing caches the local FileStatus as Processing, the two non-processable checks treat Processing as terminal, and only the processed/failed node cleanup evicts local_file_statuses - never the stale processing node cleanup.

#112313 makes the cached Processing state a hint instead of a terminal state (FileStatus::onProcessingByAnotherProcessor), so the file is picked up again once the foreign processing node is gone, with an integration test that emulates a foreign processor releasing a file without committing it. It is kept out of this pull request so that the out-of-bounds fix here stays a minimal crash fix; the >= files_to_generate - 1 assertion can be tightened to equality once #112313 is merged.

The Changelog category is also changed to Critical Bug Fix as requested.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closing the loop on the follow-up I committed to track above: this recovery gap is now being fixed by #112313, so I am not opening a PR of my own for it.

That fix keeps the observation of a foreign processing node in the cache but records it as a timestamped hint with a TTL, and relaxes the two non-processable checks to Processing && !isProcessingRetryable(). That is a superset of what I had planned: it also avoids probing Keeper for the file on every polling pass, and it keeps the case where the Processing state belongs to a concurrent local processor distinguishable from a foreign one, which a plain "do not cache it" approach collapses. Since it changes the same block of afterSetProcessing, a second PR on those lines would only conflict with it.

One thing that survives it and is unrelated to the behaviour: the status column of system.s3queue_metadata_cache and system.azure_queue_metadata_cache documents Processed, Processing, Failed, but the value is printed straight from FileStatus::State, which also has None (the initial value of a cache entry, and what reset writes). I am tracking that one-line description fix separately, after #112313 lands.

@groeneai

Copy link
Copy Markdown
Collaborator Author

CI finish ledger — c5f10ed

Our new regression test test_batch_set_processing_failure_does_not_crash is OK on every runner (asan_ubsan, tsan, msan, llvm_coverage, arm_binary, bugfix-validation amd64+aarch64). No PR-caused failure.

Check / test Reason Owner / fixing PR
Integration tests (amd_msan, 7/8) / test_replicated_database::test_replicated_table_structure_alter flaky (unrelated Replicated-DB structure-alter race; 112 master hits + 25+ unrelated PRs/30d; non-required job "do not block pipeline") #111029 (external, open)
Integration tests (amd_tsan, 3/6) / test_replicated_database::test_replicated_table_structure_alter flaky (same test, same race) #111029 (external, open)
PR derivative rollup aggregator (derivative)

test_replicated_table_structure_alter is the master-wide regression tracked by issue #110036; the root-cause write-side fix is #111029 (flips the pre-lock metadata read to a fresh lock-free load). Unrelated to this PR's s3_queue FileIterator::next out-of-bounds guard.

Comment on lines 278 to 288

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code path is still unrealistic, need inject a thrown error in needed place if you say this happens in case of error

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The crash needs two conditions in one batch, and both are non-throwing paths, which is exactly why the pre-fix compaction mishandled them:

  • cond2, the real error, already injected: the keeper multi at ObjectStorageQueueSource.cpp:373 returns a non-ZOK code (not a thrown exception), which triggers file_metadatas.clear() at :399. The test makes this real by pre-creating an actual processing node in keeper for one batch file, so the engine's own tryMulti fails against it with a real code and real responses.
  • cond1, not an error: another file in the same batch is non-processable, so num_successful_objects < new_batch.size() and the compaction at :402 runs over the cleared vector. In production this is the benign std::nullopt skip from prepareSetProcessingRequests when another consumer on the same server already holds the file (lock at ObjectStorageQueueIFileMetadata.cpp:324, or state already Processing/Processed/exhausted-Failed at :334). It never throws.

A literal throw at either site would not reproduce this: there is no try/catch between the batch loop and the compaction (the nearest is generate() at :1015), so a throw escapes next() before the compaction runs, while the out-of-bounds is inside that compaction.

The only artificial part left is how cond1's file is made non-processable (the force_skip failpoint). I can make that realistic by having a genuine second consumer mark the file Processing first and dropping the failpoint, but that turns the non-processable window into a race, so the test would need retries and risks being flaky. Which do you prefer: the two-consumer version, or keeping the failpoint for cond1 together with the real keeper error for cond2?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unrealistic bit is gone now: the failpoint no longer sits in FileIterator::next forcing a std::nullopt. It was moved into ObjectStorageQueueIFileMetadata::prepareSetProcessingRequests, where it makes file_status->processing_lock.try_lock report failure — i.e. it takes exactly the production path of a file already grabbed by another consumer on the same server, and returns std::nullopt from the real code, not from a test-only branch in the iterator.

So both batch preconditions are now produced by real code paths:

  • one file returns std::nullopt from the genuine processing-lock conflict branch (failpoint only flips the try_lock result, ONCE);
  • another file makes the keeper tryMulti fail for real, against a processing node that the test pre-creates in keeper (real code, real responses, real getFailedOpIndex), so file_metadatas.clear() happens through the engine's own error handling.

No exception is thrown on either path, which is why the pre-fix compaction indexed past the end of the cleared vector.

@alexey-milovidov

Copy link
Copy Markdown
Member

@groeneai, the test_replicated_database was fixed, please update the branch.

Comment thread src/Storages/ObjectStorageQueue/ObjectStorageQueueSource.cpp Outdated

@alexey-milovidov alexey-milovidov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test might be not realistic, but the fix itself is trivial, so it is good for me.

@alexey-milovidov alexey-milovidov self-assigned this Jul 26, 2026
@alexey-milovidov
alexey-milovidov requested a review from kssenii July 26, 2026 15:45
pull Bot pushed a commit to SINHASantos/ClickHouse that referenced this pull request Jul 31, 2026
…rigin` of `toStartOfInterval`

The three-argument overload of `toStartOfInterval` computed the result as `origin + offset`
without an overflow check. For an origin near the lower bound of `Int64` in a time zone east of
UTC the offset is negative - the interval boundary in local time lies before the origin - so the
addition wrapped around and produced a bogus value instead of an error. The UBSan build reported
it as `signed integer overflow: -9223372036854775807 + -7200000000000 cannot be represented in
type 'Int64'` at `src/Functions/toStartOfInterval.cpp:293`, found by the AST fuzzer in the
`Stress test (arm_asan_ubsan)` job.

Use `common::addOverflow` and throw `DECIMAL_OVERFLOW`, matching the check already applied to
the `time_arg - origin` subtraction a few lines above.

Verified with an A/B build: before the fix
`toStartOfInterval(reinterpret(toInt64(-9223372036854775807), 'DateTime64(9, \'Asia/Istanbul\')'), toIntervalHour(3), <same origin>)`
returns the wrapped `2262-04-12 00:47:16.854775809`; after the fix it throws `DECIMAL_OVERFLOW`.

CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=108977&sha=7dbcb13520f1a4671a9453f2fdd71a51e4f3a45f&name_0=PR&name_1=Stress%20test%20%28arm_asan_ubsan%29
Related: ClickHouse#108977
@alexey-milovidov
alexey-milovidov added this pull request to the merge queue Jul 31, 2026
Merged via the queue into ClickHouse:master with commit 2f335d3 Jul 31, 2026
179 checks passed
@robot-ch-test-poll4 robot-ch-test-poll4 added the pr-must-backport-synced The `*-must-backport` labels are synced into the cloud Sync PR label Aug 1, 2026
@robot-ch-test-poll2 robot-ch-test-poll2 added the pr-synced-to-cloud The PR is synced to the cloud repo label Aug 1, 2026
alexey-milovidov added a commit that referenced this pull request Aug 1, 2026
…-strip-uaf

Picks up the fix for the s3_queue server abort that failed the integration
tests on this branch: an out-of-bounds `vector` access in
`ObjectStorageQueueSource::FileIterator::next` (PR #108977, merged as
2f335d3).

CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=111192&sha=fe3e297d38e5b6974711ee5cd522baa3a90d6d99&name_0=PR&name_1=Integration%20tests%20%28arm_binary%2C%20distributed%20plan%2C%202%2F4%29
Related: #111192
Related: #108977
Related: #111659

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
clickhouse-gh Bot pushed a commit that referenced this pull request Aug 1, 2026
clickhouse-gh Bot added a commit that referenced this pull request Aug 1, 2026
Backport #108977 to 26.7: Fix out-of-bounds crash in ObjectStorageQueueSource::FileIterator::next
alexey-milovidov added a commit that referenced this pull request Aug 1, 2026
Backport #108977 to 26.5: Fix out-of-bounds crash in ObjectStorageQueueSource::FileIterator::next
robot-clickhouse-ci-1 added a commit that referenced this pull request Aug 2, 2026
Cherry pick #108977 to 25.8: Fix out-of-bounds crash in ObjectStorageQueueSource::FileIterator::next
robot-clickhouse added a commit that referenced this pull request Aug 2, 2026
robot-clickhouse-ci-1 added a commit that referenced this pull request Aug 2, 2026
Cherry pick #108977 to 26.3: Fix out-of-bounds crash in ObjectStorageQueueSource::FileIterator::next
robot-clickhouse added a commit that referenced this pull request Aug 2, 2026
@robot-ch-test-poll robot-ch-test-poll added the pr-backports-created Backport PRs are successfully created, it won't be processed by CI script anymore label Aug 2, 2026
kssenii added a commit that referenced this pull request Aug 3, 2026
Backport #108977 to 26.6: Fix out-of-bounds crash in ObjectStorageQueueSource::FileIterator::next
kssenii added a commit that referenced this pull request Aug 4, 2026
Backport #108977 to 25.8: Fix out-of-bounds crash in ObjectStorageQueueSource::FileIterator::next
kssenii added a commit that referenced this pull request Aug 4, 2026
Backport #108977 to 26.3: Fix out-of-bounds crash in ObjectStorageQueueSource::FileIterator::next
UnamedRus pushed a commit to UnamedRus/ClickHouse that referenced this pull request Aug 12, 2026
kewin-robetti pushed a commit to viasoftkorp/ClickHouse that referenced this pull request Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

can be tested Allows running workflows for external contributors pr-backports-created Backport PRs are successfully created, it won't be processed by CI script anymore pr-critical-bugfix pr-must-backport Pull request should be backported intentionally. Use this label with great care! pr-must-backport-synced The `*-must-backport` labels are synced into the cloud Sync PR pr-synced-to-cloud The PR is synced to the cloud repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants