mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
The header changes when V8 is updated. Do not hardcode it to avoid breaking V8 updates. The update should only surface in test/parallel/test-v8-serdes.js Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: https://github.com/nodejs/node/pull/66442 Refs: https://github.com/nodejs/node/pull/65161 Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com> Reviewed-By: Richard Lau <richard.lau@ibm.com>
314 lines
14 KiB
JavaScript
314 lines
14 KiB
JavaScript
// Flags: --expose-internals --no-warnings
|
|
|
|
import * as common from '../common/index.mjs';
|
|
import { describe, it, beforeEach } from 'node:test';
|
|
import assert from 'node:assert';
|
|
import { finished } from 'node:stream/promises';
|
|
import { DefaultSerializer } from 'node:v8';
|
|
import { resolve } from 'node:path';
|
|
import serializer from 'internal/test_runner/reporter/v8-serializer';
|
|
import runner from 'internal/test_runner/runner';
|
|
|
|
async function toArray(chunks) {
|
|
const arr = [];
|
|
for await (const i of chunks) arr.push(i);
|
|
return arr;
|
|
}
|
|
|
|
const entryFile = resolve('filetest');
|
|
const diagnosticEvent = {
|
|
type: 'test:diagnostic',
|
|
data: { nesting: 0, details: {}, message: 'diagnostic' },
|
|
};
|
|
const reportedDiagnosticEvent = {
|
|
type: 'test:diagnostic',
|
|
data: { ...diagnosticEvent.data, entryFile },
|
|
};
|
|
const chunks = await toArray(serializer([diagnosticEvent]));
|
|
const defaultSerializer = new DefaultSerializer();
|
|
defaultSerializer.writeHeader();
|
|
const headerOnly = Buffer.from(defaultSerializer.releaseBuffer());
|
|
const headerLength = headerOnly.length;
|
|
const oversizedLengthHeader = Buffer.concat([headerOnly, Buffer.from([0x7f, 0xff, 0xff, 0xff])]);
|
|
const unsignedOversizedLengthHeader = Buffer.concat([headerOnly, Buffer.from([0x80, 0x00, 0x00, 0x00])]);
|
|
const truncatedLengthHeader = Buffer.concat([headerOnly, Buffer.from([0x00, 0x01, 0x00, 0x00])]);
|
|
// Expected stdout for oversizedLengthHeader: first byte is emitted via
|
|
// String.fromCharCode (byte-by-byte fallback in #drainRawBuffer), remaining
|
|
// bytes go through the nonSerialized UTF-8 decode path in #processRawBuffer.
|
|
const oversizedLengthStdout = String.fromCharCode(oversizedLengthHeader[0]) +
|
|
Buffer.from(oversizedLengthHeader.subarray(1)).toString('utf-8');
|
|
const unsignedOversizedLengthStdout = String.fromCharCode(unsignedOversizedLengthHeader[0]) +
|
|
Buffer.from(unsignedOversizedLengthHeader.subarray(1)).toString('utf-8');
|
|
function payloadSize(size) {
|
|
const buffer = Buffer.alloc(4);
|
|
buffer.writeUInt32BE(size);
|
|
return buffer;
|
|
}
|
|
// The v8 header followed by a small, plausible size (8) and 8 payload bytes. Unlike the
|
|
// oversized headers above, this passes the size check, but its payload does not
|
|
// begin with the inner v8 header a real frame carries, so it is treated as
|
|
// stdout instead of reaching the deserializer.
|
|
// Regression fixture for https://github.com/nodejs/node/issues/66164
|
|
const plausibleSizeFalseHeader = Buffer.concat([
|
|
headerOnly,
|
|
payloadSize(8),
|
|
Buffer.from('ABCDEFGH'), // Not a real payload
|
|
]);
|
|
const plausibleSizeFalseHeaderStdout = String.fromCharCode(plausibleSizeFalseHeader[0]) +
|
|
Buffer.from(plausibleSizeFalseHeader.subarray(1)).toString('utf-8');
|
|
// The v8 header, a valid size, then the inner v8 header a real frame repeats, followed
|
|
// by a byte that is not a valid serialized value. This passes the inner header
|
|
// check and reaches the deserializer, which throws. This is what a genuine
|
|
// report-protocol regression looks like, so the parser must let the error
|
|
// surface instead of hiding it as stdout.
|
|
const headeredCorruptFrame = Buffer.concat([
|
|
headerOnly,
|
|
payloadSize(headerLength + 1),
|
|
headerOnly,
|
|
Buffer.from([0xee]), // Not a valid serialized value
|
|
]);
|
|
// The v8 header with a declared size of 1, then more header bytes. The payload is
|
|
// shorter than the inner v8 header a real frame carries, so it can never be a
|
|
// real frame. The length guard must reject it as stdout without reaching the
|
|
// deserializer.
|
|
const shortPayloadFalseHeader = Buffer.concat([
|
|
headerOnly,
|
|
payloadSize(1),
|
|
headerOnly, // Trailing bytes that also look like a header
|
|
]);
|
|
|
|
function collectStdout(reported) {
|
|
return reported
|
|
.filter((event) => event.type === 'test:stdout')
|
|
.map((event) => event.data.message)
|
|
.join('');
|
|
}
|
|
|
|
describe('v8 deserializer', common.mustCall(() => {
|
|
let fileTest;
|
|
let reported;
|
|
beforeEach(common.mustCallAtLeast(() => {
|
|
reported = [];
|
|
fileTest = new runner.FileTest({
|
|
name: 'filetest',
|
|
harness: { config: {} },
|
|
});
|
|
fileTest.reporter.on('data', (data) => reported.push(data));
|
|
assert(fileTest.isClearToSend());
|
|
}));
|
|
|
|
async function collectReported(chunks) {
|
|
chunks.forEach((chunk) => fileTest.parseMessage(chunk));
|
|
fileTest.drain();
|
|
fileTest.reporter.end();
|
|
await finished(fileTest.reporter);
|
|
return reported;
|
|
}
|
|
|
|
it('should do nothing when no chunks', async () => {
|
|
const reported = await collectReported([]);
|
|
assert.deepStrictEqual(reported, []);
|
|
});
|
|
|
|
it('should deserialize a chunk with no serialization', async () => {
|
|
const reported = await collectReported([Buffer.from('unknown')]);
|
|
assert.deepStrictEqual(reported, [
|
|
{ data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
|
|
]);
|
|
});
|
|
|
|
it('should deserialize a serialized chunk', async () => {
|
|
const reported = await collectReported(chunks);
|
|
assert.deepStrictEqual(reported, [reportedDiagnosticEvent]);
|
|
});
|
|
|
|
it('should serialize a repeated object as independent messages', async () => {
|
|
const repeatedChunks = await toArray(serializer([diagnosticEvent, diagnosticEvent]));
|
|
const reported = await collectReported(repeatedChunks);
|
|
assert.deepStrictEqual(reported, [
|
|
reportedDiagnosticEvent,
|
|
reportedDiagnosticEvent,
|
|
]);
|
|
});
|
|
|
|
it('should deserialize a serialized chunk after non-serialized chunk', async () => {
|
|
const reported = await collectReported([Buffer.concat([Buffer.from('unknown'), ...chunks])]);
|
|
assert.deepStrictEqual(reported, [
|
|
{ data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
|
|
reportedDiagnosticEvent,
|
|
]);
|
|
});
|
|
|
|
it('should deserialize a serialized chunk before non-serialized output', async () => {
|
|
const reported = await collectReported([Buffer.concat([ ...chunks, Buffer.from('unknown')])]);
|
|
assert.deepStrictEqual(reported, [
|
|
reportedDiagnosticEvent,
|
|
{ data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
|
|
]);
|
|
});
|
|
|
|
it('should not hang when buffer starts with v8Header followed by oversized length', async () => {
|
|
// Regression test for https://github.com/nodejs/node/issues/62693
|
|
// The v8 serializer header is followed by 4 bytes read as a big-endian
|
|
// message size. 0x7FFFFFFF far exceeds any actual buffer size, causing
|
|
// #processRawBuffer to make no progress and #drainRawBuffer to loop
|
|
// forever without the no-progress guard.
|
|
const reported = await collectReported([oversizedLengthHeader]);
|
|
assert.partialDeepStrictEqual(
|
|
reported,
|
|
Array.from({ length: reported.length }, () => ({ type: 'test:stdout' })),
|
|
);
|
|
assert.strictEqual(collectStdout(reported), oversizedLengthStdout);
|
|
});
|
|
|
|
it('should read an oversized length as an unsigned integer', async () => {
|
|
const reported = await collectReported([unsignedOversizedLengthHeader]);
|
|
assert(reported.every((event) => event.type === 'test:stdout'));
|
|
assert.strictEqual(collectStdout(reported), unsignedOversizedLengthStdout);
|
|
});
|
|
|
|
it('should flush incomplete v8 frame as stdout and keep prior valid data', async () => {
|
|
// A valid non-serialized message followed by bytes that look like
|
|
// a v8 header with a truncated/oversized length.
|
|
const reported = await collectReported([
|
|
Buffer.from('hello'),
|
|
truncatedLengthHeader,
|
|
]);
|
|
assert.strictEqual(collectStdout(reported), `hello${truncatedLengthHeader.toString('latin1')}`);
|
|
});
|
|
|
|
it('should flush v8Header-only bytes as stdout when stream ends', async () => {
|
|
// Just the v8 header bytes with no size field at all.
|
|
const reported = await collectReported([headerOnly]);
|
|
assert(reported.every((event) => event.type === 'test:stdout'));
|
|
assert.strictEqual(collectStdout(reported), headerOnly.toString('latin1'));
|
|
});
|
|
|
|
it('should resync and parse valid messages after false v8 header', async () => {
|
|
// A false v8 header (header bytes + oversized length) followed by a
|
|
// legitimate serialized message. The parser must skip the corrupt
|
|
// bytes and still deserialize the real message.
|
|
const reported = await collectReported([
|
|
oversizedLengthHeader,
|
|
...chunks,
|
|
]);
|
|
assert.deepStrictEqual(reported.at(-1), reportedDiagnosticEvent);
|
|
assert.strictEqual(reported.filter((event) => event.type === 'test:diagnostic').length, 1);
|
|
assert.strictEqual(collectStdout(reported), oversizedLengthStdout);
|
|
});
|
|
|
|
it('should preserve a false v8 header split across chunks', async () => {
|
|
const reported = await collectReported([
|
|
oversizedLengthHeader.subarray(0, 1),
|
|
oversizedLengthHeader.subarray(1),
|
|
]);
|
|
assert(reported.every((event) => event.type === 'test:stdout'));
|
|
assert.strictEqual(collectStdout(reported), oversizedLengthStdout);
|
|
});
|
|
|
|
it('should not crash when stdout mimics a v8 frame with a plausible size', async () => {
|
|
// Regression test for https://github.com/nodejs/node/issues/66164
|
|
// The payload does not start with the inner v8 header that a real frame
|
|
// carries, so the parser treats the bytes as stdout instead of handing
|
|
// them to the deserializer and aborting the whole run.
|
|
const reported = await collectReported([plausibleSizeFalseHeader]);
|
|
assert(reported.every((event) => event.type === 'test:stdout'));
|
|
assert.strictEqual(collectStdout(reported), plausibleSizeFalseHeaderStdout);
|
|
});
|
|
|
|
it('should resync live and report a real message after a false frame', async () => {
|
|
// Feed the poison bytes then a real message but never call drain(). Recovery
|
|
// must happen live, so the real event is reported right away. If resync only
|
|
// ran at shutdown, the diagnostic would still be buffered and missing here.
|
|
// The reporter is a stream, so flush it with end() and finished() before
|
|
// asserting, rather than reading it synchronously.
|
|
fileTest.parseMessage(plausibleSizeFalseHeader);
|
|
chunks.forEach((chunk) => fileTest.parseMessage(chunk));
|
|
fileTest.reporter.end();
|
|
await finished(fileTest.reporter);
|
|
assert.deepStrictEqual(reported.at(-1), reportedDiagnosticEvent);
|
|
assert.strictEqual(reported.filter((event) => event.type === 'test:diagnostic').length, 1);
|
|
assert.strictEqual(collectStdout(reported), plausibleSizeFalseHeaderStdout);
|
|
});
|
|
|
|
it('should preserve real messages on both sides of a plausible-size false frame', async () => {
|
|
// A real message, then the poison bytes, then another real message. Both
|
|
// real messages must survive and the poison bytes must become stdout.
|
|
const reported = await collectReported([
|
|
...chunks,
|
|
plausibleSizeFalseHeader,
|
|
...chunks,
|
|
]);
|
|
const diagnostics = reported.filter((event) => event.type === 'test:diagnostic');
|
|
assert.strictEqual(diagnostics.length, 2);
|
|
diagnostics.forEach((event) => assert.deepStrictEqual(event, reportedDiagnosticEvent));
|
|
assert.strictEqual(collectStdout(reported), plausibleSizeFalseHeaderStdout);
|
|
});
|
|
|
|
it('should recover from a plausible-size false frame split across chunks', async () => {
|
|
// The same poison bytes arriving in two chunks must still be treated as
|
|
// stdout without crashing.
|
|
const reported = await collectReported([
|
|
plausibleSizeFalseHeader.subarray(0, 3),
|
|
plausibleSizeFalseHeader.subarray(3),
|
|
]);
|
|
assert(reported.every((event) => event.type === 'test:stdout'));
|
|
assert.strictEqual(collectStdout(reported), plausibleSizeFalseHeaderStdout);
|
|
});
|
|
|
|
it('should resync through several stray frames in a row', async () => {
|
|
// Two false frames back to back in one read, then a real one. The parser
|
|
// must peel each stray frame off as stdout and still report the real event.
|
|
const reported = await collectReported([
|
|
Buffer.concat([plausibleSizeFalseHeader, plausibleSizeFalseHeader, ...chunks]),
|
|
]);
|
|
assert.deepStrictEqual(reported.at(-1), reportedDiagnosticEvent);
|
|
assert.strictEqual(reported.filter((event) => event.type === 'test:diagnostic').length, 1);
|
|
assert.strictEqual(collectStdout(reported),
|
|
plausibleSizeFalseHeaderStdout + plausibleSizeFalseHeaderStdout);
|
|
});
|
|
|
|
it('should surface a genuinely corrupt frame instead of hiding it', () => {
|
|
// A frame with both v8 headers and a valid size but an invalid value is
|
|
// what a real report-protocol regression looks like, not stray stdout.
|
|
// The parser must let the deserialize error surface instead of silently
|
|
// turning it into stdout.
|
|
assert.throws(() => fileTest.parseMessage(headeredCorruptFrame), /deserialize/);
|
|
});
|
|
|
|
it('should treat a frame whose payload is shorter than the header as stdout', async () => {
|
|
// The declared size is smaller than the inner v8 header, so the length
|
|
// guard must reject the bytes as stdout instead of reaching the
|
|
// deserializer.
|
|
const reported = await collectReported([shortPayloadFalseHeader]);
|
|
assert(reported.every((event) => event.type === 'test:stdout'));
|
|
});
|
|
|
|
const headerPosition = headerLength * 2 + 4;
|
|
for (let i = 0; i < headerPosition + 5; i++) {
|
|
const message = `should deserialize a serialized message split into two chunks {...${i},${i + 1}...}`;
|
|
it(message, async () => {
|
|
const data = chunks[0];
|
|
const reported = await collectReported([data.subarray(0, i), data.subarray(i)]);
|
|
assert.deepStrictEqual(reported, [
|
|
reportedDiagnosticEvent,
|
|
]);
|
|
});
|
|
|
|
it(`${message} wrapped by non-serialized data`, async () => {
|
|
const data = chunks[0];
|
|
const reported = await collectReported([
|
|
Buffer.concat([Buffer.from('unknown'), data.subarray(0, i)]),
|
|
Buffer.concat([data.subarray(i), Buffer.from('unknown')]),
|
|
]);
|
|
assert.deepStrictEqual(reported, [
|
|
{ data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
|
|
reportedDiagnosticEvent,
|
|
{ data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
|
|
]);
|
|
}
|
|
);
|
|
}
|
|
|
|
}));
|