-
Type:
Bug
-
Resolution: Fixed
-
Priority:
Major - P3
-
Affects Version/s: None
-
Component/s: None
-
Query Optimization
-
Fully Compatible
-
ALL
-
v9.0, v8.3, v8.0, v7.0
-
200
-
None
-
None
-
None
-
None
-
None
-
None
-
None
The $_internalApplyOplogUpdate aggregation stage holds an unowned reference to the oplogUpdate BSON. When the initial aggregate returns a cursor and its command Invocation (which owns the AggregateCommandRequest and its pipeline BSON) is destroyed, that buffer is freed. A subsequent getMore then dereferences the freed diff, producing a heap-use-after-free that aborts the node.
This was caught by AddressSanitizer on a shard secondary in the amazon-linux2023-aubsan-...-sharded-clusters variant, suite disagg_sharded_colls_jscore_passthrough_secondary_reads_with_balancer, running jstests/core/timeseries/query/agg_stage_coverage.js. The viewless-timeseries translation injects a $_internalApplyOplogUpdate stage and the secondary-read passthrough routes the aggregate to a secondary, which crashes on getMore. The client observes ConnectionClosedByPeer (515) via mongos.
Root Cause.
- The IDL field oplogUpdate in src/mongo/db/pipeline/document_source_internal_apply_oplog_update.idl is declared type: object (unowned), so spec.getOplogUpdate() points into the original aggregate command request BSON.
- UpdateModification::parseFromOplogEntry stores the diff as {{doc_diff::Diff
{diff.embeddedObject()}
}} (unowned), held by DeltaExecutor::_diff inside the stage's UpdateDriver (src/mongo/db/query/write_ops/write_ops.cpp).
- After the initial aggregate returns a cursor, AggregateCommand::Invocation::~Invocation destroys AggregateCommandRequest and frees the pipeline vector<BSONObj> buffer.
- On getMore, InternalApplyOplogUpdateStage::doGetNext() reads the freed diff.