-
Type:
Bug
-
Resolution: Fixed
-
Priority:
Major - P3
-
Affects Version/s: None
-
Component/s: None
-
Query Integration
-
Fully Compatible
-
ALL
-
v9.0, v8.3
-
None
-
None
-
None
-
None
-
None
-
None
-
None
A sharded aggregation crashes mongos during cursor teardown: MergeCursorsStage::doDispose() calls BlockingResultsMerger::kill() with a null OperationContext.
Invalid access at address: 0x204 Got signal: 11 (Segmentation fault). mongo::exec::agg::MergeCursorsStage::doDispose() BlockingResultsMerger::kill(mongo::OperationContext*)
Trigger
mongos fills a batch with a count-bounded loop:
for (long long objCount = 0; objCount < batchSize; ++objCount)
The crash needs the results to exactly fill the final batch (count == batchSize) with the cursor then at EOF. The loop stops on the count bound without ever pulling the trailing EOF, so the exec pipeline is never disposed while attached. The cursor is marked exhausted by a follow-up isEOF() peek, is not registered, and is destroyed at function end – its destructor disposes the pipeline after the opCtx was already detached.
Minimal example (sharded):
db.coll.aggregate([<deterministic source>, {$limit: 1}], {cursor: {batchSize: 1}})
1 result, batchSize 1 -> the loop runs once, appends the doc, exits at objCount == 1 without pulling EOF -> the isEOF() peek marks the cursor exhausted -> it is not registered -> disposed from its destructor after detach -> crash.
Safe cases: fewer results than batchSize pull the EOF (pipeline disposed while attached). More results register the cursor for a getMore. Only the exactly-full terminal batch crashes.
Root cause
establishMergingMongosCursor() (cluster_aggregation_planner.cpp) called ccc->detachFromOperationContext() unconditionally, before the exhausted check. An exhausted cursor is not registered, so its pipeline is disposed from the destructor, which needs a valid opCtx to kill the remote cursors.
Fix
- Detach only inside the "register a cursor" branch, so an exhausted cursor is disposed while still
attached. - Backstop: null-guard the opCtx in MergeCursorsStage::doDispose() (mirrors
~DocumentSourceMergeCursors), skipping the kill instead of dereferencing null.
Why now ?
Latent on master since git commit e76af41017f (2025-12-09). Not specific to any stage. $extensionMultiStream is used in the regression test only because it emits a deterministic doc count, making the count == batchSize alignment reproducible. Coverage: DisposeAfterDetachDoesNotCrash an jstests/extensions/document_results_and_metadata_sharded_teardown.js .