Skip to content

Commit 805dfe6

Browse files
committed
fix(NODE-6735): prevent serializer and stringify from creating invalid float32 vectors
1 parent e0ac32b commit 805dfe6

File tree

5 files changed

+123
-67
lines changed

5 files changed

+123
-67
lines changed

src/binary.ts

+6
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,12 @@ export function validateBinaryVector(vector: Binary): void {
517517
throw new BSONError('Invalid Vector: padding must be zero for int8 and float32 vectors');
518518
}
519519

520+
if (datatype === Binary.VECTOR_TYPE.Float32) {
521+
if (size !== 0 && size - 2 !== 0 && (size - 2) % 4 !== 0) {
522+
throw new BSONError('Invalid Vector: Float32 vector must contain a multiple of 4 bytes');
523+
}
524+
}
525+
520526
if (datatype === Binary.VECTOR_TYPE.PackedBit && padding !== 0 && size === 2) {
521527
throw new BSONError(
522528
'Invalid Vector: padding must be zero for packed bit vectors that are empty'

test/node/bson_binary_vector.spec.test.ts

+102-44
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import * as util from 'util';
12
import * as fs from 'fs';
23
import * as path from 'path';
3-
import { BSON, BSONError, Binary } from '../register-bson';
4+
import { BSON, BSONError, Binary, EJSON } from '../register-bson';
45
import { expect } from 'chai';
56

67
const { toHex, fromHex } = BSON.onDemand.ByteUtils;
78

89
type VectorHexType = '0x03' | '0x27' | '0x10';
910
type VectorTest = {
1011
description: string;
11-
vector: (number | string)[];
12+
vector?: (number | string)[];
1213
valid: boolean;
1314
dtype_hex: VectorHexType;
1415
padding?: number;
@@ -87,6 +88,10 @@ const invalidTestExpectedError = new Map()
8788
'Invalid Vector: padding must be a value between 0 and 7'
8889
)
8990
.set('Negative padding PACKED_BIT', 'Invalid Vector: padding must be a value between 0 and 7')
91+
.set(
92+
'Insufficient vector data FLOAT32',
93+
'Invalid Vector: Float32 vector must contain a multiple of 4 bytes'
94+
)
9095
// skipped
9196
.set('Overflow Vector PACKED_BIT', false)
9297
.set('Underflow Vector PACKED_BIT', false)
@@ -97,6 +102,84 @@ const invalidTestExpectedError = new Map()
97102
.set('Vector with float values PACKED_BIT', false)
98103
.set('Vector with float values PACKED_BIT', false);
99104

105+
function testVectorBuilding(test: VectorTest, expectedErrorMessage: string) {
106+
describe('creating an instance of a Binary class using parameters', () => {
107+
it(`bson: ${test.description}`, function () {
108+
let thrownError: Error | undefined;
109+
try {
110+
const bin = make(test.vector!, test.dtype_hex, test.padding);
111+
BSON.serialize({ bin });
112+
} catch (error) {
113+
thrownError = error;
114+
}
115+
116+
if (thrownError?.message.startsWith('unsupported_error')) {
117+
expect(
118+
expectedErrorMessage,
119+
'We expect a certain error message but got an unsupported error'
120+
).to.be.false;
121+
this.skip();
122+
}
123+
124+
expect(thrownError, thrownError?.stack).to.be.instanceOf(BSONError);
125+
expect(thrownError?.message).to.match(new RegExp(expectedErrorMessage));
126+
});
127+
128+
it(`ejson: ${test.description}`, function () {
129+
let thrownError: Error | undefined;
130+
try {
131+
const bin = make(test.vector!, test.dtype_hex, test.padding);
132+
BSON.EJSON.stringify({ bin });
133+
} catch (error) {
134+
thrownError = error;
135+
}
136+
137+
if (thrownError?.message.startsWith('unsupported_error')) {
138+
expect(
139+
expectedErrorMessage,
140+
'We expect a certain error message but got an unsupported error'
141+
).to.be.false;
142+
this.skip();
143+
}
144+
145+
expect(thrownError, thrownError?.stack).to.be.instanceOf(BSONError);
146+
expect(thrownError?.message).to.match(new RegExp(expectedErrorMessage));
147+
});
148+
});
149+
}
150+
151+
function testVectorReserializing(test: VectorTest, expectedErrorMessage: string) {
152+
describe('creating an instance of a Binary class using canonical_bson', () => {
153+
it(`bson deserialize: ${test.description}`, function () {
154+
let thrownError: Error | undefined;
155+
const bin = BSON.deserialize(Buffer.from(test.canonical_bson!, 'hex'));
156+
157+
try {
158+
BSON.serialize(bin);
159+
} catch (error) {
160+
thrownError = error;
161+
}
162+
163+
expect(thrownError, thrownError?.stack).to.be.instanceOf(BSONError);
164+
expect(thrownError?.message).to.match(new RegExp(expectedErrorMessage));
165+
});
166+
167+
it(`ejson stringify: ${test.description}`, function () {
168+
let thrownError: Error | undefined;
169+
const bin = BSON.deserialize(Buffer.from(test.canonical_bson!, 'hex'));
170+
171+
try {
172+
EJSON.stringify(bin);
173+
} catch (error) {
174+
thrownError = error;
175+
}
176+
177+
expect(thrownError, thrownError?.stack).to.be.instanceOf(BSONError);
178+
expect(thrownError?.message).to.match(new RegExp(expectedErrorMessage));
179+
});
180+
});
181+
}
182+
100183
describe('BSON Binary Vector spec tests', () => {
101184
const tests: Record<string, VectorSuite> = Object.create(null);
102185

@@ -121,7 +204,7 @@ describe('BSON Binary Vector spec tests', () => {
121204
*/
122205
for (const test of valid) {
123206
it(`encode ${test.description}`, function () {
124-
const bin = make(test.vector, test.dtype_hex, test.padding);
207+
const bin = make(test.vector!, test.dtype_hex, test.padding);
125208

126209
const buffer = BSON.serialize({ [suite.test_key]: bin });
127210
expect(toHex(buffer)).to.equal(test.canonical_bson!.toLowerCase());
@@ -147,47 +230,22 @@ describe('BSON Binary Vector spec tests', () => {
147230
for (const test of invalid) {
148231
const expectedErrorMessage = invalidTestExpectedError.get(test.description);
149232

150-
it(`bson: ${test.description}`, function () {
151-
let thrownError: Error | undefined;
152-
try {
153-
const bin = make(test.vector, test.dtype_hex, test.padding);
154-
BSON.serialize({ bin });
155-
} catch (error) {
156-
thrownError = error;
157-
}
158-
159-
if (thrownError?.message.startsWith('unsupported_error')) {
160-
expect(
161-
expectedErrorMessage,
162-
'We expect a certain error message but got an unsupported error'
163-
).to.be.false;
164-
this.skip();
165-
}
166-
167-
expect(thrownError).to.be.instanceOf(BSONError);
168-
expect(thrownError?.message).to.match(new RegExp(expectedErrorMessage));
169-
});
170-
171-
it(`extended json: ${test.description}`, function () {
172-
let thrownError: Error | undefined;
173-
try {
174-
const bin = make(test.vector, test.dtype_hex, test.padding);
175-
BSON.EJSON.stringify({ bin });
176-
} catch (error) {
177-
thrownError = error;
178-
}
179-
180-
if (thrownError?.message.startsWith('unsupported_error')) {
181-
expect(
182-
expectedErrorMessage,
183-
'We expect a certain error message but got an unsupported error'
184-
).to.be.false;
185-
this.skip();
186-
}
187-
188-
expect(thrownError).to.be.instanceOf(BSONError);
189-
expect(thrownError?.message).to.match(new RegExp(expectedErrorMessage));
190-
});
233+
if (test.vector != null && test.canonical_bson != null) {
234+
describe('both manual vector building and re-serializing', () => {
235+
testVectorBuilding(test, expectedErrorMessage);
236+
testVectorReserializing(test, expectedErrorMessage);
237+
});
238+
} else if (test.canonical_bson != null) {
239+
describe('vector re-serializing', () => {
240+
testVectorReserializing(test, expectedErrorMessage);
241+
});
242+
} else if (test.vector != null) {
243+
describe('manual vector building', () => {
244+
testVectorBuilding(test, expectedErrorMessage);
245+
});
246+
} else {
247+
throw new Error('not testing anything for: ' + util.inspect(test));
248+
}
191249
}
192250
});
193251
});

test/node/specs/bson-binary-vector/float32.json

+9-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,15 @@
4444
"vector": [127.0, 7.0],
4545
"dtype_hex": "0x27",
4646
"dtype_alias": "FLOAT32",
47-
"padding": 3
47+
"padding": 3,
48+
"canonical_bson": "1C00000005766563746F72000A0000000927030000FE420000E04000"
49+
},
50+
{
51+
"description": "Insufficient vector data FLOAT32",
52+
"valid": false,
53+
"dtype_hex": "0x27",
54+
"dtype_alias": "FLOAT32",
55+
"canonical_bson": "1700000005766563746F7200050000000927002A2A2A00"
4856
}
4957
]
5058
}
51-

test/node/specs/bson-binary-vector/int8.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
"vector": [127, 7],
4343
"dtype_hex": "0x03",
4444
"dtype_alias": "INT8",
45-
"padding": 3
45+
"padding": 3,
46+
"canonical_bson": "1600000005766563746F7200040000000903037F0700"
4647
},
4748
{
4849
"description": "INT8 with float inputs",
@@ -54,4 +55,3 @@
5455
}
5556
]
5657
}
57-

test/node/specs/bson-binary-vector/packed_bit.json

+4-19
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"vector": [],
99
"dtype_hex": "0x10",
1010
"dtype_alias": "PACKED_BIT",
11-
"padding": 1
11+
"padding": 1,
12+
"canonical_bson": "1400000005766563746F72000200000009100100"
1213
},
1314
{
1415
"description": "Simple Vector PACKED_BIT",
@@ -61,21 +62,14 @@
6162
"dtype_alias": "PACKED_BIT",
6263
"padding": 0
6364
},
64-
{
65-
"description": "Padding specified with no vector data PACKED_BIT",
66-
"valid": false,
67-
"vector": [],
68-
"dtype_hex": "0x10",
69-
"dtype_alias": "PACKED_BIT",
70-
"padding": 1
71-
},
7265
{
7366
"description": "Exceeding maximum padding PACKED_BIT",
7467
"valid": false,
7568
"vector": [1],
7669
"dtype_hex": "0x10",
7770
"dtype_alias": "PACKED_BIT",
78-
"padding": 8
71+
"padding": 8,
72+
"canonical_bson": "1500000005766563746F7200030000000910080100"
7973
},
8074
{
8175
"description": "Negative padding PACKED_BIT",
@@ -84,15 +78,6 @@
8478
"dtype_hex": "0x10",
8579
"dtype_alias": "PACKED_BIT",
8680
"padding": -1
87-
},
88-
{
89-
"description": "Vector with float values PACKED_BIT",
90-
"valid": false,
91-
"vector": [127.5],
92-
"dtype_hex": "0x10",
93-
"dtype_alias": "PACKED_BIT",
94-
"padding": 0
9581
}
9682
]
9783
}
98-

0 commit comments

Comments
 (0)