-
Type:
Bug
-
Resolution: Duplicate
-
Priority:
Major - P3
-
None
-
Affects Version/s: None
-
Component/s: None
-
None
-
Query Execution
-
ALL
-
v9.0
-
QE 2026-07-20
-
200
-
None
-
None
-
None
-
None
-
None
-
None
-
None
Overview
ReshardingTxnCloner::run can crash with a null pointer dereference (SIGSEGV) when a transient/retryable error (e.g. InterruptedDueToReplStateChange from a stepdown) is thrown in the narrow window between chainCtx->pipeline being assigned and chainCtx->execPipeline being built:
chainCtx->pipeline = _restartPipeline(opCtx.get(), ...); // (1) set chainCtx->execPipeline = exec::agg::buildPipeline(chainCtx->pipeline->freeze()); // (2) set
Both the onTransientError and onCompletion handlers in
src/mongo/db/s/resharding/resharding_txn_cloner.cpp guard their cleanup on chainCtx->pipeline but dereference chainCtx->execPipeline:
if (chainCtx->pipeline) {
...
chainCtx->execPipeline->reattachToOperationContext(opCtx.get()); // null deref if (2) hasn't run
If an interruption is thrown after (1) but before (2) completes, chainCtx->execPipeline is still null when the handler runs, causing a crash inside
mongo::exec::agg::Pipeline::reattachToOperationContext (fault address matches a null-`this`member access).
Found by
BF-44665 (resharding_fuzzer, enterprise-rhel8-debug-tsan), where the crash killed a resharding recipient node mid-operation, ultimately timing out the whole task.
Fix
Rather than patch the onTransientError/onCompletion guards in place, restructure the retry loop's body (ReshardingTxnCloner::run) to build pipeline and execPipeline as local variables first, and only assign both into chainCtx only once both have been fully built without throwing (the assignments themselves are simple non-throwing unique_ptr moves, so nothing can interrupt between them):
auto pipeline = _restartPipeline(opCtx.get(), ...); auto execPipeline = exec::agg::buildPipeline(pipeline->freeze()); execPipeline->detachFromOperationContext(); pipeline->detachFromOperationContext(); chainCtx->pipeline = std::move(pipeline); chainCtx->execPipeline = std::move(execPipeline);
This guarantees chainCtx->pipeline and chainCtx->execPipeline are always either both set or both unset, so the existing cleanup guards in onTransientError and onCompletion (which only check chainCtx->pipeline) remain correct without needing to change what they dereference.
Testing
Add regression test (RecoversFromInterruptionBetweenBuildingPipelineAndExecPipeline in
resharding_txn_cloner_test.cpp) using a new failpoint, reshardingTxnClonerFailAfterSettingPipelineBeforeExecPipeline, placed exactly at the vulnerable window between building the pipeline and the execPipeline, so the race reproduces deterministically instead of depending on fuzzer timing.
- is related to
-
SERVER-131598 ReshardingTxnCloner::run can end up accessing uninitialized variable
-
- Closed
-