Fix recursive MSan report in HandledSignals::reset on shutdown#107442
Conversation
Pre-PR validation gate
Session id: cron:clickhouse-worker-slot-2:20260614-161500 |
|
cc @azat @Algunenano — could you review this? It marks |
|
Workflow [PR], commit [e602d07] Summary: ❌
AI ReviewSummaryThe current head routes the sanitizer death callback through Final Verdict
|
36f9f92 to
f057d41
Compare
Pre-PR validation gate (revised fix)
Session id: cron:clickhouse-worker-slot-3:20260614-184900 |
f057d41 to
fe7e8f8
Compare
Pre-PR validation gate (updated for head fe7e8f8)
Session id: cron:clickhouse-worker-slot-0:20260614-225000 |
`test_implicit_index_upgrade_alter_replay` creates a second replica `node2` and asserts `SELECT count() FROM test_alter_replay` equals `10001` immediately after `wait_for_active_replica`. But `wait_for_active_replica` only waits for `is_readonly = 0`, not for the freshly-joined replica to finish fetching parts. So `node2` can observe only the small `VALUES` part (1 row, `key = 99999`) before the 10000-row part is fetched, and the assertion fails with `assert '1' == '10001'`. This raced once each on several unrelated PRs over the last 30 days (e.g. ClickHouse#108084, ClickHouse#99280, ClickHouse#107566, ClickHouse#107442). Add `SYSTEM SYNC REPLICA test_alter_replay` after the replica becomes active, which blocks until all parts are fetched, before checking the row count. CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=108770&sha=bb760616d54e3d8e5d20968022085506c31b5f37&name_0=PR&name_1=Integration%20tests%20%28arm_binary%2C%20distributed%20plan%2C%201%2F4%29 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
azat
left a comment
There was a problem hiding this comment.
I don't think this is a good idea, now in sanitizer death callback it will create "HandledSignals" object which will create files, but likely we cannot run such syscalls from death callback (since we avoid "close" there), so let's instead introduce "resetHandledSignals()" that will reset signal handlers back (like "reset(false)") but will not do anything if they are already reseted back, for this introduce static variable
fe7e8f8 to
ddc4a2a
Compare
|
Done, implemented as requested (ddc4a2a).
So after the normal-shutdown reset, the death-callback path does nothing: it never re-enters Verified on a from-source MSan build ( |
Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-4:20260713-102400 |
azat
left a comment
There was a problem hiding this comment.
Ok, but the comments are too verbose, please do not document all "thoughts" that you have in mind while developing, at max this can be in the commit description. Comments in the code should include only essential details. Fix and rebase.
The HandledSignals singleton is reachable from the sanitizer death callback, which can run after static destructors at process exit. The callback called HandledSignals::instance().reset(false); under MSan-use-after-dtor the read of the destroyed handled_signals vector fired a use-of-uninitialized-value warning, whose death callback called reset() again, recursing until the original report was lost. Do not leak/reconstruct the singleton from the death callback: constructing HandledSignals there opens a pipe and creates files, syscalls we must avoid in a death callback (that is why the callback does not close the pipe either). Instead reset from a free function resetHandledSignals() guarded by a static flag. HandledSignals::reset() sets the flag before touching any state, so once the handlers were reset during normal shutdown the death-callback path is a no-op: it never re-enters reset(), never reads HandledSignals state that may be destroyed at exit, and never constructs the singleton. The flag being set before the reset loop also makes any re-entry return immediately, breaking the recursion. The singleton stays a normal function-local static (destroyed at exit as before); only the death-callback path is made idempotent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ddc4a2a to
729490d
Compare
|
Trimmed the code comments to essentials (rationale is in the commit description) and rebased on master. |
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 5/8 (62.50%) · Uncovered code |
CI finish ledger — e602d07Every failure below has an owner: a fixing PR, or a full-effort fix task whose fixing-PR link will be posted here when it opens. Only
This PR is a shutdown signal-handler MSan fix ( Session id: cron:clickhouse-worker-slot-2:20260714-032100 |
f4bd25f
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
...
Description
Related: #107425 (where this failure was triaged and the fix was requested).
Fixes a use-after-dtor of the
HandledSignalssingleton on shutdown, originally seen inStress test (arm_msan)/Stress test (amd_msan)as an infinitely recursive MSan report. CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=107425&sha=b2f68b66f7510cdaaec64fb58e7c5601b58180c0&name_0=PR&name_1=Stress%20test%20%28arm_msan%29Root cause:
HandledSignals::instance()was a function-local static, so the singleton was destroyed at process exit. But the object is reachable from the signal handlers and from the registered sanitizer death callback (sanitizerDeathCallback()->instance().reset(false)), both of which can run after static destructors. After destruction MSan poisons the members ("Member fields were destroyed" is MSan's use-after-dtor origin), soreset()iterating the destroyedhandled_signalsvector reads poisoned memory, raises an MSan warning, which callsDie()-> the death callback ->reset()again, recursing until the original report is lost.Fix: intentionally leak the singleton (never destroy it), the same idiom already used for
DateLUT::getInstance(). Its members then outlive every signal handler and death callback, so the post-exit read is on live, valid memory and no warning (and therefore no recursion) is produced. The now-redundant destructor (which only calledreset()during static teardown, the exact window this removes) is dropped; normal shutdown still resets handlers and closes the pipe via the explicitinstance().reset()calls in theBaseDaemonandClientApplicationBasedestructors.No behavior change in non-sanitizer builds.
Version info
26.7.1.930(included in26.7and later)