Sort spill merge can read freed WT cursor memory after a WriteConflictException

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Duplicate
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Component/s: None
    • None
    • Storage Execution
    • Storage Execution 2026-05-25
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      ContainerIterator::getDeferredValue() returns wrong/garbage values (or crashes) when a WriteConflictException is thrown anywhere inside the writeConflictRetry block in ContainerBasedSpiller::mergeSpills. The cached deferred-value pointer is invalidated bythe cursor reset that WiredTiger performs as part of transaction rollback.

      Symptom

      Surfaces under AUBSAN as a heap-use-after-free during sort-spill merging:

        ==ERROR: AddressSanitizer: heap-use-after-free
        READ of size 4 at 0x... in mongo::DataType::Handler<int,...>::unsafeLoad
          #10 IntWrapper::deserializeForSorter at sorter_test_utils.h:63
          #11 ContainerIterator<...>::getDeferredValue at container_based_spiller.h:142
          #12 Stream<...>::getDeferredValue at sorter.h:422
          #13 MergeIterator<...>::next at sorter_template_defs.h:350
          #14 ContainerBasedSpiller<...>::mergeSpills (lambda) at container_based_spiller.h:525
        freed by thread T0:
          __wt_cursor_copy_release_item ... <- __wt_session_reset_cursors
          <- __session_rollback_transaction
          <- WiredTigerRecoveryUnit::_txnClose(false)
          <- WriteUnitOfWork::~WriteUnitOfWork at container_based_spiller.h:532
        

      Reproducer: ContainerBasedSpillerWriteConflictTest.MergeSpillsSurvivesCursorResetUnderWCE

      Root cause

      mergeSpills constructs the MergeIterator outside the per-batch WriteUnitOfWork. Each child Stream eagerly calls
      ContainerIterator::nextWithDeferredValue(), which caches:

        boost::optional<std::span> _deferredValue;   // points into a WT cursor buffer
        

      This span aliases memory owned by WiredTiger — either a debug-mode cursor_copy=true heap buffer (what ASan flags) or a pinned page / session scratch buffer in release builds. Per WT's contract, that memory is only valid until the next operation on that cursor.

      When a WCE fires on addAlreadySorted or wuow.commit() inside the retry lambda:
      WriteUnitOfWork::~WriteUnitOfWork runs and calls WiredTigerRecoveryUnit::abortrollback_transaction_wt_session_reset_cursors.

      That reset is a "next operation" on every cursor in the session, including the read-side cursors backing the MergeIterator. The buffer _deferredValue points into is freed (debug) or reused (release).

      The writeConflictRetry loop re-enters the lambda. MergeIterator::next() calls Stream::getDeferredValue() → {ContainerIterator::getDeferredValue()}}, which deserializes through the now-dangling span.

      The WiredTiger API contract is explicit that any pointer returned by cursor->get_value() is only valid until the next operation on that cursor. rollback_transaction__wt_session_reset_cursors counts as such an operation. The dangling-pointer condition exists regardless of build flavor; cursor_copy=true just makes it observable.

            Assignee:
            Stephanie Eristoff
            Reporter:
            Stephanie Eristoff
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: