Skip to content

Commit a2887db

Browse files
authored
fix(NODE-1502): command monitoring objects hold internal state references (#2832)
1 parent 6bd506b commit a2887db

File tree

4 files changed

+124
-9
lines changed

4 files changed

+124
-9
lines changed

src/cmap/command_monitoring_events.ts

+20-8
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ const OP_QUERY_KEYS = [
181181
function extractCommand(command: WriteProtocolMessageType): Document {
182182
if (command instanceof GetMore) {
183183
return {
184-
getMore: command.cursorId,
184+
getMore: deepCopy(command.cursorId),
185185
collection: collectionName(command),
186186
batchSize: command.numberToReturn
187187
};
@@ -190,15 +190,15 @@ function extractCommand(command: WriteProtocolMessageType): Document {
190190
if (command instanceof KillCursor) {
191191
return {
192192
killCursors: collectionName(command),
193-
cursors: command.cursorIds
193+
cursors: deepCopy(command.cursorIds)
194194
};
195195
}
196196

197197
if (command instanceof Msg) {
198-
return command.command;
198+
return deepCopy(command.command);
199199
}
200200

201-
if (command.query && command.query.$query) {
201+
if (command.query?.$query) {
202202
let result: Document;
203203
if (command.ns === 'admin.$cmd') {
204204
// up-convert legacy command
@@ -208,15 +208,15 @@ function extractCommand(command: WriteProtocolMessageType): Document {
208208
result = { find: collectionName(command) };
209209
Object.keys(LEGACY_FIND_QUERY_MAP).forEach(key => {
210210
if (typeof command.query[key] !== 'undefined') {
211-
result[LEGACY_FIND_QUERY_MAP[key]] = command.query[key];
211+
result[LEGACY_FIND_QUERY_MAP[key]] = deepCopy(command.query[key]);
212212
}
213213
});
214214
}
215215

216216
Object.keys(LEGACY_FIND_OPTIONS_MAP).forEach(key => {
217217
const legacyKey = key as keyof typeof LEGACY_FIND_OPTIONS_MAP;
218218
if (typeof command[legacyKey] !== 'undefined') {
219-
result[LEGACY_FIND_OPTIONS_MAP[legacyKey]] = command[legacyKey];
219+
result[LEGACY_FIND_OPTIONS_MAP[legacyKey]] = deepCopy(command[legacyKey]);
220220
}
221221
});
222222

@@ -234,11 +234,23 @@ function extractCommand(command: WriteProtocolMessageType): Document {
234234
if (command.query.$explain) {
235235
return { explain: result };
236236
}
237-
238237
return result;
239238
}
240239

241-
return command.query ? command.query : command;
240+
const clonedQuery: Record<string, unknown> = {};
241+
const clonedCommand: Record<string, unknown> = {};
242+
if (command.query) {
243+
for (const k in command.query) {
244+
clonedQuery[k] = deepCopy(command.query[k]);
245+
}
246+
clonedCommand.query = clonedQuery;
247+
}
248+
249+
for (const k in command) {
250+
if (k === 'query') continue;
251+
clonedCommand[k] = deepCopy(((command as unknown) as Record<string, unknown>)[k]);
252+
}
253+
return command.query ? clonedQuery : clonedCommand;
242254
}
243255

244256
function extractReply(command: WriteProtocolMessageType, reply?: Document) {

test/functional/apm.test.js

+37
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,43 @@ describe('APM', function () {
658658
}
659659
});
660660

661+
// NODE-1502
662+
it('should not allow mutation of internal state from commands returned by event monitoring', function () {
663+
const started = [];
664+
const succeeded = [];
665+
const client = this.configuration.newClient(
666+
{ writeConcern: { w: 1 } },
667+
{ maxPoolSize: 1, monitorCommands: true }
668+
);
669+
client.on('commandStarted', filterForCommands('insert', started));
670+
client.on('commandSucceeded', filterForCommands('insert', succeeded));
671+
let documentToInsert = { a: { b: 1 } };
672+
return client
673+
.connect()
674+
.then(client => {
675+
const db = client.db(this.configuration.db);
676+
return db.collection('apm_test').insertOne(documentToInsert);
677+
})
678+
.then(r => {
679+
expect(r).to.have.property('insertedId').that.is.an('object');
680+
expect(started).to.have.lengthOf(1);
681+
// Check if contents of returned document are equal to document inserted (by value)
682+
expect(documentToInsert).to.deep.equal(started[0].command.documents[0]);
683+
// Check if the returned document is a clone of the original. This confirms that the
684+
// reference is not the same.
685+
expect(documentToInsert !== started[0].command.documents[0]).to.equal(true);
686+
expect(documentToInsert.a !== started[0].command.documents[0].a).to.equal(true);
687+
688+
started[0].command.documents[0].a.b = 2;
689+
expect(documentToInsert.a.b).to.equal(1);
690+
691+
expect(started[0].commandName).to.equal('insert');
692+
expect(started[0].command.insert).to.equal('apm_test');
693+
expect(succeeded).to.have.lengthOf(1);
694+
return client.close();
695+
});
696+
});
697+
661698
describe('spec tests', function () {
662699
before(function () {
663700
return setupDatabase(this.configuration);

test/functional/multiple_db.test.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ describe('Multiple Databases', function () {
8585
{ returnDocument: ReturnDocument.AFTER },
8686
function (err) {
8787
expect(err).to.not.exist;
88-
client.close(done);
88+
collection.drop(() => {
89+
client.close(done);
90+
});
8991
}
9092
);
9193
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict';
2+
3+
const { Msg, Query, GetMore, KillCursor } = require('../../../src/cmap/commands');
4+
const { CommandStartedEvent } = require('../../../src/cmap/command_monitoring_events');
5+
const { expect } = require('chai');
6+
const { Long } = require('bson');
7+
8+
describe('Command Monitoring Events - unit/cmap', function () {
9+
const commands = [
10+
new Query('admin.$cmd', { a: { b: 10 }, $query: { b: 10 } }, {}),
11+
new Query('hello', { a: { b: 10 }, $query: { b: 10 } }, {}),
12+
new Msg('admin.$cmd', { b: { c: 20 } }, {}),
13+
new Msg('hello', { b: { c: 20 } }, {}),
14+
new GetMore('admin.$cmd', Long.fromNumber(10)),
15+
new GetMore('hello', Long.fromNumber(10)),
16+
new KillCursor('admin.$cmd', [Long.fromNumber(100), Long.fromNumber(200)]),
17+
new KillCursor('hello', [Long.fromNumber(100), Long.fromNumber(200)]),
18+
{ ns: 'admin.$cmd', query: { $query: { a: 16 } } },
19+
{ ns: 'hello there', f1: { h: { a: 52, b: { c: 10, d: [1, 2, 3, 5] } } } }
20+
];
21+
22+
for (const command of commands) {
23+
it(`should make a deep copy of object of type: ${command.constructor.name}`, () => {
24+
const ev = new CommandStartedEvent({ id: 'someId', address: 'someHost' }, command);
25+
if (command instanceof Query) {
26+
if (command.ns === 'admin.$cmd') {
27+
expect(ev.command !== command.query.$query).to.equal(true);
28+
for (const k in command.query.$query) {
29+
expect(ev.command[k]).to.deep.equal(command.query.$query[k]);
30+
}
31+
} else {
32+
expect(ev.command.filter !== command.query.$query).to.equal(true);
33+
for (const k in command.query.$query) {
34+
expect(ev.command.filter[k]).to.deep.equal(command.query.$query[k]);
35+
}
36+
}
37+
} else if (command instanceof Msg) {
38+
expect(ev.command !== command.command).to.equal(true);
39+
expect(ev.command).to.deep.equal(command.command);
40+
} else if (command instanceof GetMore) {
41+
// NOTE: BSON Longs pass strict equality when their internal values are equal
42+
// i.e.
43+
// let l1 = Long(10);
44+
// let l2 = Long(10);
45+
// l1 === l2 // returns true
46+
// expect(ev.command.getMore !== command.cursorId).to.equal(true);
47+
expect(ev.command.getMore).to.deep.equal(command.cursorId);
48+
49+
ev.command.getMore = Long.fromNumber(50128);
50+
expect(command.cursorId).to.not.deep.equal(ev.command.getMore);
51+
} else if (command instanceof KillCursor) {
52+
expect(ev.command.cursors !== command.cursorIds).to.equal(true);
53+
expect(ev.command.cursors).to.deep.equal(command.cursorIds);
54+
} else if (typeof command === 'object') {
55+
if (command.ns === 'admin.$cmd') {
56+
expect(ev.command !== command.query.$query).to.equal(true);
57+
for (const k in command.query.$query) {
58+
expect(ev.command[k]).to.deep.equal(command.query.$query[k]);
59+
}
60+
}
61+
}
62+
});
63+
}
64+
});

0 commit comments

Comments
 (0)