-
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::abort → rollback_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.
- depends on
-
SERVER-124271 Sort spill merge can read freed WT cursor memory after a WriteConflictException
-
- Closed
-