Skip to content

Commit df183d7

Browse files
thisalihassanaduh95
authored andcommitted
test_runner: avoid hanging on incomplete v8 frames
Signed-off-by: Ali Hassan <ali-hassan27@outlook.com> PR-URL: #62704 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
1 parent 6d7cd50 commit df183d7

2 files changed

Lines changed: 124 additions & 18 deletions

File tree

‎lib/internal/test_runner/runner.js‎

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
SafePromiseAllSettledReturnVoid,
2626
SafeSet,
2727
String,
28+
StringFromCharCode,
2829
StringPrototypeIndexOf,
2930
StringPrototypeSlice,
3031
StringPrototypeStartsWith,
@@ -248,6 +249,7 @@ class FileTest extends Test {
248249
#rawBuffer =[];// Raw data waiting to be parsed
249250
#rawBufferSize =0;
250251
#reportedChildren =0;
252+
#pendingPartialV8Header =false;
251253
failedSubtests=false;
252254

253255
constructor(options){
@@ -339,6 +341,12 @@ class FileTest extends Test {
339341
}
340342
parseMessage(readData){
341343
letdataLength=TypedArrayPrototypeGetLength(readData);
344+
if(this.#pendingPartialV8Header){
345+
readData=Buffer.concat([TypedArrayPrototypeSubarray(v8Header,0,1),readData]);
346+
dataLength=TypedArrayPrototypeGetLength(readData);
347+
this.#pendingPartialV8Header =false;
348+
}
349+
342350
if(dataLength===0)return;
343351
constpartialV8Header=readData[dataLength-1]===v8Header[0];
344352

@@ -349,22 +357,52 @@ class FileTest extends Test {
349357
dataLength--;
350358
}
351359

352-
if(this.#rawBuffer[0]&&TypedArrayPrototypeGetLength(this.#rawBuffer[0])<kSerializedSizeHeader){
353-
this.#rawBuffer[0]=Buffer.concat([this.#rawBuffer[0],readData]);
354-
}else{
355-
ArrayPrototypePush(this.#rawBuffer,readData);
360+
if(dataLength>0){
361+
if(this.#rawBuffer[0]&&TypedArrayPrototypeGetLength(this.#rawBuffer[0])<kSerializedSizeHeader){
362+
this.#rawBuffer[0]=Buffer.concat([this.#rawBuffer[0],readData]);
363+
}else{
364+
ArrayPrototypePush(this.#rawBuffer,readData);
365+
}
366+
this.#rawBufferSize +=dataLength;
367+
this.#processRawBuffer();
356368
}
357-
this.#rawBufferSize +=dataLength;
358-
this.#processRawBuffer();
359369

360370
if(partialV8Header){
361-
ArrayPrototypePush(this.#rawBuffer,TypedArrayPrototypeSubarray(v8Header,0,1));
362-
this.#rawBufferSize++;
371+
this.#pendingPartialV8Header =true;
363372
}
364373
}
365374
#drainRawBuffer(){
375+
if(this.#pendingPartialV8Header){
376+
ArrayPrototypePush(this.#rawBuffer,TypedArrayPrototypeSubarray(v8Header,0,1));
377+
this.#rawBufferSize++;
378+
this.#pendingPartialV8Header =false;
379+
}
380+
366381
while(this.#rawBuffer.length>0){
382+
constprevBufferLength=this.#rawBuffer.length;
383+
constprevBufferSize=this.#rawBufferSize;
367384
this.#processRawBuffer();
385+
386+
if(this.#rawBuffer.length===prevBufferLength&&
387+
this.#rawBufferSize ===prevBufferSize){
388+
constbufferHead=this.#rawBuffer[0];
389+
this.addToReport({
390+
__proto__: null,
391+
type: 'test:stdout',
392+
data: {
393+
__proto__: null,
394+
file: this.name,
395+
message: StringFromCharCode(bufferHead[0]),
396+
},
397+
});
398+
399+
if(TypedArrayPrototypeGetLength(bufferHead)===1){
400+
ArrayPrototypeShift(this.#rawBuffer);
401+
}else{
402+
this.#rawBuffer[0]=TypedArrayPrototypeSubarray(bufferHead,1);
403+
}
404+
this.#rawBufferSize--;
405+
}
368406
}
369407
}
370408
#processRawBuffer(){

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

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,29 @@ async function toArray(chunks) {
1414
returnarr;
1515
}
1616

17-
constchunks=awaittoArray(serializer([
18-
{type: 'test:diagnostic',data: {nesting: 0,details: {},message: 'diagnostic'}},
19-
]));
17+
constdiagnosticEvent={
18+
type: 'test:diagnostic',
19+
data: {nesting: 0,details: {},message: 'diagnostic'},
20+
};
21+
constchunks=awaittoArray(serializer([diagnosticEvent]));
2022
constdefaultSerializer=newDefaultSerializer();
2123
defaultSerializer.writeHeader();
2224
constheaderLength=defaultSerializer.releaseBuffer().length;
25+
constheaderOnly=Buffer.from([0xff,0x0f]);
26+
constoversizedLengthHeader=Buffer.from([0xff,0x0f,0x7f,0xff,0xff,0xff]);
27+
consttruncatedLengthHeader=Buffer.from([0xff,0x0f,0x00,0x01,0x00,0x00]);
28+
// Expected stdout for oversizedLengthHeader: first byte is emitted via
29+
// String.fromCharCode (byte-by-byte fallback in #drainRawBuffer), remaining
30+
// bytes go through the nonSerialized UTF-8 decode path in #processRawBuffer.
31+
constoversizedLengthStdout=String.fromCharCode(oversizedLengthHeader[0])+
32+
Buffer.from(oversizedLengthHeader.subarray(1)).toString('utf-8');
33+
34+
functioncollectStdout(reported){
35+
returnreported
36+
.filter((event)=>event.type==='test:stdout')
37+
.map((event)=>event.data.message)
38+
.join('');
39+
}
2340

2441
describe('v8 deserializer',common.mustCall(()=>{
2542
letfileTest;
@@ -56,35 +73,86 @@ describe('v8 deserializer', common.mustCall(() => {
5673

5774
it('should deserialize a serialized chunk',async()=>{
5875
constreported=awaitcollectReported(chunks);
59-
assert.deepStrictEqual(reported,[
60-
{data: {nesting: 0,details: {},message: 'diagnostic'},type: 'test:diagnostic'},
61-
]);
76+
assert.deepStrictEqual(reported,[diagnosticEvent]);
6277
});
6378

6479
it('should deserialize a serialized chunk after non-serialized chunk',async()=>{
6580
constreported=awaitcollectReported([Buffer.concat([Buffer.from('unknown'), ...chunks])]);
6681
assert.deepStrictEqual(reported,[
6782
{data: {__proto__: null,file: 'filetest',message: 'unknown'},type: 'test:stdout'},
68-
{data: {nesting: 0,details: {},message: 'diagnostic'},type: 'test:diagnostic'},
83+
diagnosticEvent,
6984
]);
7085
});
7186

7287
it('should deserialize a serialized chunk before non-serialized output',async()=>{
7388
constreported=awaitcollectReported([Buffer.concat([ ...chunks,Buffer.from('unknown')])]);
7489
assert.deepStrictEqual(reported,[
75-
{data: {nesting: 0,details: {},message: 'diagnostic'},type: 'test:diagnostic'},
90+
diagnosticEvent,
7691
{data: {__proto__: null,file: 'filetest',message: 'unknown'},type: 'test:stdout'},
7792
]);
7893
});
7994

95+
it('should not hang when buffer starts with v8Header followed by oversized length',async()=>{
96+
// Regression test for https://github.com/nodejs/node/issues/62693
97+
// FF 0F is the v8 serializer header; the next 4 bytes are read as a
98+
// big-endian message size. 0x7FFFFFFF far exceeds any actual buffer
99+
// size, causing #processRawBuffer to make no progress and
100+
// #drainRawBuffer to loop forever without the no-progress guard.
101+
constreported=awaitcollectReported([oversizedLengthHeader]);
102+
assert.partialDeepStrictEqual(
103+
reported,
104+
Array.from({length: reported.length},()=>({type: 'test:stdout'})),
105+
);
106+
assert.strictEqual(collectStdout(reported),oversizedLengthStdout);
107+
});
108+
109+
it('should flush incomplete v8 frame as stdout and keep prior valid data',async()=>{
110+
// A valid non-serialized message followed by bytes that look like
111+
// a v8 header with a truncated/oversized length.
112+
constreported=awaitcollectReported([
113+
Buffer.from('hello'),
114+
truncatedLengthHeader,
115+
]);
116+
assert.strictEqual(collectStdout(reported),`hello${truncatedLengthHeader.toString('latin1')}`);
117+
});
118+
119+
it('should flush v8Header-only bytes as stdout when stream ends',async()=>{
120+
// Just the two-byte v8 header with no size field at all.
121+
constreported=awaitcollectReported([headerOnly]);
122+
assert(reported.every((event)=>event.type==='test:stdout'));
123+
assert.strictEqual(collectStdout(reported),headerOnly.toString('latin1'));
124+
});
125+
126+
it('should resync and parse valid messages after false v8 header',async()=>{
127+
// A false v8 header (FF 0F + oversized length) followed by a
128+
// legitimate serialized message. The parser must skip the corrupt
129+
// bytes and still deserialize the real message.
130+
constreported=awaitcollectReported([
131+
oversizedLengthHeader,
132+
...chunks,
133+
]);
134+
assert.deepStrictEqual(reported.at(-1),diagnosticEvent);
135+
assert.strictEqual(reported.filter((event)=>event.type==='test:diagnostic').length,1);
136+
assert.strictEqual(collectStdout(reported),oversizedLengthStdout);
137+
});
138+
139+
it('should preserve a false v8 header split across chunks',async()=>{
140+
constreported=awaitcollectReported([
141+
oversizedLengthHeader.subarray(0,1),
142+
oversizedLengthHeader.subarray(1),
143+
]);
144+
assert(reported.every((event)=>event.type==='test:stdout'));
145+
assert.strictEqual(collectStdout(reported),oversizedLengthStdout);
146+
});
147+
80148
constheaderPosition=headerLength*2+4;
81149
for(leti=0;i<headerPosition+5;i++){
82150
constmessage=`should deserialize a serialized message split into two chunks {...${i},${i+1}...}`;
83151
it(message,async()=>{
84152
constdata=chunks[0];
85153
constreported=awaitcollectReported([data.subarray(0,i),data.subarray(i)]);
86154
assert.deepStrictEqual(reported,[
87-
{data: {nesting: 0,details: {},message: 'diagnostic'},type: 'test:diagnostic'},
155+
diagnosticEvent,
88156
]);
89157
});
90158

@@ -96,7 +164,7 @@ describe('v8 deserializer', common.mustCall(() => {
96164
]);
97165
assert.deepStrictEqual(reported,[
98166
{data: {__proto__: null,file: 'filetest',message: 'unknown'},type: 'test:stdout'},
99-
{data: {nesting: 0,details: {},message: 'diagnostic'},type: 'test:diagnostic'},
167+
diagnosticEvent,
100168
{data: {__proto__: null,file: 'filetest',message: 'unknown'},type: 'test:stdout'},
101169
]);
102170
}

0 commit comments

Comments
 (0)