commands: fix a remote invalid free in aggregate $sort
Present since at least d4c9b04, found by the new spec-test harness on its first
run. The $sort stage's materialization branch built its document list with the
reply arena and then handed it to `trees`, whose scope-exit deinit -- and the
$match branch above it -- free with the gpa. So a gpa free was handed an
arena-owned pointer. macOS malloc catches it and aborts with SIGTRAP and no
panic text, which is why the symptom read as "the connection closed":
mfm_free <- Allocator.rawFree
<- array_list.Aligned(*const bson.Document).deinit
<- commands.cmd_aggregate
Any pipeline with $sort and no preceding $group reached it, e.g.
aggregate([{$sort: {x: 1}}]) -- so a client could kill the server with one
ordinary query. With a $group first the stream is already in tree form and the
branch is skipped, which is precisely why it survived: every aggregate case in
e2e.js and e2e6.js sorts *after* grouping.
The list buffer now comes from ctx.gpa. The documents stay in the arena on
purpose -- it outlives the command, and only the ArrayList's own allocator has
to match its deinit.
Tests. The unit test uses a bare $sort pipeline, since a $group first would not
reach the branch, and leans on testing.allocator detecting the invalid free
itself rather than on the host allocator noticing -- mutation-checked by
restoring `arena` on the append, which gives `panic: Invalid free`. The e2e case
adds a second command afterwards, because the assertion that matters is not
that the sort returned rows but that the connection is still there.
This commit is contained in:
@@ -93,6 +93,23 @@ async function main() {
|
||||
const cnt = await users.aggregate([{ $match: {} }, { $count: 'n' }]).toArray();
|
||||
check('aggregate $count', cnt[0]?.n === 4, JSON.stringify(cnt));
|
||||
|
||||
// $sort with NO preceding $group. This shape used to kill the server: the
|
||||
// stage materialized its document list from the reply arena and it was then
|
||||
// freed with the general allocator. Every aggregate case above happens to
|
||||
// sort after a $group, which leaves the stream already materialized and the
|
||||
// guilty branch unreached -- so the bug survived the whole suite. The second
|
||||
// aggregate is the part that actually proves recovery: if the server died,
|
||||
// this connection is gone.
|
||||
const sorted = await users.aggregate([{ $sort: { age: -1 } }]).toArray();
|
||||
const ages = sorted.map((d) => d.age);
|
||||
check(
|
||||
'aggregate bare $sort (no $group) returns sorted docs',
|
||||
ages.length === 4 && ages.every((a, i) => i === 0 || ages[i - 1] >= a),
|
||||
JSON.stringify(ages),
|
||||
);
|
||||
const stillAlive = await users.aggregate([{ $match: {} }, { $count: 'n' }]).toArray();
|
||||
check('server survives a bare $sort pipeline', stillAlive[0]?.n === 4, JSON.stringify(stillAlive));
|
||||
|
||||
// --- duplicate key ---
|
||||
let dupErr = null;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user