Fix null pointer dereference in ReshardingTxnCloner when a transient error lands between building the pipeline and execPipeline

XMLWordPrintableJSON

    • 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.

            Assignee:
            Daniel Tabacaru
            Reporter:
            Daniel Tabacaru
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated:
              Resolved: