Skip to content

Commit 642807f

Browse files
committed
test_runner: validate inner v8 header before deserializing
Check that a framed payload starts with the inner v8 header before handing it to the deserializer, so stray stdout that mimics a frame is rejected as output while genuine deserialize failures still surface. Signed-off-by: Muhammad Faizan Uddin <faizan.uddin94@gmail.com>
1 parent 9366795 commit 642807f

2 files changed

Lines changed: 58 additions & 22 deletions

File tree

‎lib/internal/test_runner/runner.js‎

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -508,25 +508,23 @@ class FileTest extends Test {
508508
const concatenatedBuffer = this.#rawBuffer.length === 1 ?
509509
this.#rawBuffer[0] : Buffer.concat(this.#rawBuffer, this.#rawBufferSize);
510510

511+
// A real frame repeats the v8 header at the start of its payload, right
512+
// before the serialized value. If that inner header is missing, these
513+
// are stray stdout bytes that only look like a frame, so stop here and
514+
// let #drainRawBuffer emit them as stdout and resync on the next real
515+
// header. A genuine frame that fails to deserialize is left to throw, so
516+
// real report-protocol regressions are not silently hidden.
517+
if (fullMessageSize - kSerializedSizeHeader < kV8HeaderLength ||
518+
concatenatedBuffer.indexOf(v8Header, kSerializedSizeHeader) !== kSerializedSizeHeader) {
519+
break;
520+
}
521+
511522
const deserializer = new DefaultDeserializer(
512523
TypedArrayPrototypeSubarray(concatenatedBuffer, kSerializedSizeHeader, fullMessageSize),
513524
);
525+
deserializer.readHeader();
526+
const item = deserializer.readValue();
514527

515-
let item;
516-
try {
517-
deserializer.readHeader();
518-
item = deserializer.readValue();
519-
} catch {
520-
// The bytes begin with the v8 header magic and a plausible size, but
521-
// the payload is not a real serialized message. This happens when a
522-
// test writes raw bytes to stdout that look like a frame. Leave the
523-
// buffer untouched and stop parsing frames here. #drainRawBuffer then
524-
// emits the stray byte as stdout and rescans for the next real header.
525-
break;
526-
}
527-
528-
// Only advance past the frame once it has been read successfully, so a
529-
// failed read above cannot drop real frames that follow the stray bytes.
530528
bufferHead = TypedArrayPrototypeSubarray(concatenatedBuffer, fullMessageSize);
531529
this.#rawBufferSize = TypedArrayPrototypeGetLength(bufferHead);
532530
this.#rawBuffer = this.#rawBufferSize !== 0 ? [bufferHead] : [];

‎test/parallel/test-runner-v8-deserializer.mjs‎

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,37 @@ const oversizedLengthStdout = String.fromCharCode(oversizedLengthHeader[0]) +
4040
const unsignedOversizedLengthStdout = String.fromCharCode(unsignedOversizedLengthHeader[0]) +
4141
Buffer.from(unsignedOversizedLengthHeader.subarray(1)).toString('utf-8');
4242
// FF 0F followed by a small, plausible size (8) and 8 payload bytes. Unlike the
43-
// oversized headers above, this passes the size check and reaches the
44-
// deserializer, which throws because the payload is not a real message.
43+
// oversized headers above, this passes the size check, but its payload does not
44+
// begin with the inner v8 header a real frame carries, so it is treated as
45+
// stdout instead of reaching the deserializer.
4546
// Regression fixture for https://github.com/nodejs/node/issues/66164
4647
const plausibleSizeFalseHeader = Buffer.from([
47-
0xff, 0x0f, // v8 serializer header magic
48-
0x00, 0x00, 0x00, 0x08, // payload size of 8 bytes
48+
0xff, 0x0f, // V8 serializer header magic
49+
0x00, 0x00, 0x00, 0x08, // Payload size of 8 bytes
4950
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, // "ABCDEFGH", not a real payload
5051
]);
5152
const plausibleSizeFalseHeaderStdout = String.fromCharCode(plausibleSizeFalseHeader[0]) +
5253
Buffer.from(plausibleSizeFalseHeader.subarray(1)).toString('utf-8');
54+
// FF 0F, a valid size, then the inner v8 header a real frame repeats, followed
55+
// by a byte that is not a valid serialized value. This passes the inner header
56+
// check and reaches the deserializer, which throws. This is what a genuine
57+
// report-protocol regression looks like, so the parser must let the error
58+
// surface instead of hiding it as stdout.
59+
const headeredCorruptFrame = Buffer.from([
60+
0xff, 0x0f, // Outer v8 serializer header magic
61+
0x00, 0x00, 0x00, 0x03, // Payload size of 3 bytes
62+
0xff, 0x0f, // Inner v8 header that a real frame repeats
63+
0xee, // Not a valid serialized value
64+
]);
65+
// FF 0F with a declared size of 1, then more header bytes. The payload is
66+
// shorter than the inner v8 header a real frame carries, so it can never be a
67+
// real frame. The length guard must reject it as stdout without reaching the
68+
// deserializer.
69+
const shortPayloadFalseHeader = Buffer.from([
70+
0xff, 0x0f, // Outer v8 serializer header magic
71+
0x00, 0x00, 0x00, 0x01, // Payload size of 1 byte, too short for a header
72+
0xff, 0x0f, // Trailing bytes that also look like a header
73+
]);
5374

5475
function collectStdout(reported) {
5576
return reported
@@ -182,16 +203,17 @@ describe('v8 deserializer', common.mustCall(() => {
182203

183204
it('should not crash when stdout mimics a v8 frame with a plausible size', async () => {
184205
// Regression test for https://github.com/nodejs/node/issues/66164
185-
// The bytes reach the deserializer and it throws. The parser must emit
186-
// them as stdout instead of letting the error abort the whole run.
206+
// The payload does not start with the inner v8 header that a real frame
207+
// carries, so the parser treats the bytes as stdout instead of handing
208+
// them to the deserializer and aborting the whole run.
187209
const reported = await collectReported([plausibleSizeFalseHeader]);
188210
assert(reported.every((event) => event.type === 'test:stdout'));
189211
assert.strictEqual(collectStdout(reported), plausibleSizeFalseHeaderStdout);
190212
});
191213

192214
it('should resync and parse a real message after a plausible-size false frame', async () => {
193215
// The poison bytes followed by a real serialized message. The parser must
194-
// recover from the failed deserialize and still report the real event.
216+
// reject the poison as stdout and still report the real event.
195217
const reported = await collectReported([
196218
plausibleSizeFalseHeader,
197219
...chunks,
@@ -226,6 +248,22 @@ describe('v8 deserializer', common.mustCall(() => {
226248
assert.strictEqual(collectStdout(reported), plausibleSizeFalseHeaderStdout);
227249
});
228250

251+
it('should surface a genuinely corrupt frame instead of hiding it', () => {
252+
// A frame with both v8 headers and a valid size but an invalid value is
253+
// what a real report-protocol regression looks like, not stray stdout.
254+
// The parser must let the deserialize error surface instead of silently
255+
// turning it into stdout.
256+
assert.throws(() => fileTest.parseMessage(headeredCorruptFrame), /deserialize/);
257+
});
258+
259+
it('should treat a frame whose payload is shorter than the header as stdout', async () => {
260+
// The declared size is smaller than the inner v8 header, so the length
261+
// guard must reject the bytes as stdout instead of reaching the
262+
// deserializer.
263+
const reported = await collectReported([shortPayloadFalseHeader]);
264+
assert(reported.every((event) => event.type === 'test:stdout'));
265+
});
266+
229267
const headerPosition = headerLength * 2 + 4;
230268
for (let i = 0; i < headerPosition + 5; i++) {
231269
const message = `should deserialize a serialized message split into two chunks {...${i},${i + 1}...}`;

0 commit comments

Comments
 (0)