Conversation
|
Review requested:
|
|
Welcome to Node.js, and thank you for your first contribution! Before review, please take a moment to read:
Please make sure every commit is signed off. For a first pull request, GitHub Actions require collaborator approval and Jenkins CI must be started by a collaborator or triager, so an initial wait is normal. |
e61f146 to
ff40068
Compare
|
This seems reasonable as a mitigation, but I'm concerned that swallowing deserialization errors could hide real regressions in the internal report protocol. Could we make the frame validation stricter before deserializing instead? For example, checking for the expected inner V8 header after the length field may let us reject false positives as stdout without hiding genuine deserialization failures. Separating the report stream from user stdout still seems like the cleanest long-term fix, but stricter validation might be a smaller alternative. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #66273 +/- ##
=======================================
Coverage 90.36% 90.36%
=======================================
Files 790 790
Lines 274787 274813 +26
Branches 52631 52629 -2
=======================================
+ Hits 248323 248347 +24
- Misses 16932 16942 +10
+ Partials 9532 9524 -8
🚀 New features to boost your workflow:
|
|
Thanks for the sharp review. You are right that a blanket catch could hide a real regression in the report protocol. I pushed a commit that validates the inner v8 header before deserializing, so stray stdout that only mimics a frame is rejected as output, while a genuine frame that fails to deserialize is left to surface instead of being swallowed. I also added a test for that second case. I agree that separating the report stream from user stdout is the cleanest long-term fix. |
8120db3 to
642807f
Compare
| const reported = await collectReported([ | ||
| plausibleSizeFalseHeader, | ||
| ...chunks, | ||
| ]); |
There was a problem hiding this comment.
Does this actually verify resync before drain()?
collectReported() calls fileTest.drain() after feeding all chunks, so this would still pass if the real frame stayed buffered until the child exited.
Could we assert before drain() instead, e.g.:
fileTest.parseMessage(plausibleSizeFalseHeader);
fileTest.parseMessage(chunks[0]);
assert.deepStrictEqual(reported.at(-1), reportedDiagnosticEvent);
If the inner-header check already tells us this is not a valid frame, perhaps we could consume one byte as stdout and immediately restart the header search instead of breaking.
642807f to
2899307
Compare
|
Thanks for the review. I have addressed the remaining comment. Recovery is now live inside The test now asserts before I also rebased onto the latest main to pick up #66307. |
|
Thanks for the update. The behavior looks better now, but #processRawBuffer() has become difficult to reason about with the nested loops and mimicsFrame state. Could we simplify it into a single loop that re-checks the buffer head after each step?
I think this would make the resync behavior and edge cases much easier to reason about. |
The child test process sends framed report messages and raw user stdout over one pipe, using the bytes FF 0F to mark the start of a frame. User output can contain those same bytes, so #processRawBuffer could read a plausible size from stray stdout and hand the bytes to the v8 deserializer. The deserializer then threw. Because the call had no error handling, the exception aborted the whole test run. Read the frame before advancing the buffer and wrap the deserialize in a try/catch. When the read fails, leave the buffer untouched and stop parsing frames so #drainRawBuffer emits the stray byte as stdout and rescans for the next real header. This turns a fatal crash into recoverable stdout and preserves any real frames that follow the stray bytes. Fixes: nodejs#66164 Signed-off-by: Muhammad Faizan Uddin <faizan.uddin94@gmail.com>
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>
Recovery from stray stdout that mimics a frame happened only at shutdown, inside #drainRawBuffer, so a real frame that followed the stray bytes was reported late. Move the recovery into #processRawBuffer. When the payload does not start with the inner v8 header, emit one byte as stdout and restart the header search right away, so the next real frame is reported live. Assert before drain() in the test so it verifies live recovery instead of recovery at shutdown. Signed-off-by: Muhammad Faizan Uddin <faizan.uddin94@gmail.com>
#processRawBuffer used nested loops and a mimicsFrame flag, which made the resync path hard to follow. Rewrite it as a single loop that inspects the head of the buffer once per pass and handles one case: leading stdout, an incomplete frame, a frame that only mimics the header, or a real frame. The existing tests cover each case. Signed-off-by: Muhammad Faizan Uddin <faizan.uddin94@gmail.com>
2899307 to
d249668
Compare
|
Thanks, that reads much better. I rewrote |
|
this might break every time we upgrade v8. |
inoway46
left a comment
There was a problem hiding this comment.
Thanks for addressing. LGTM!
|
@faizanu94 It looks like the PR description is a bit outdated. Could you update it to reflect the current implementation? |
|
@inoway46 Done. I have updated the description to match the current implementation. |
|
@MoLow Would you mind landing this manually and updating the squashed commit message to reflect the current implementation? Rewriting the first commit now would require another approval and full CI run. |
Summary
node --testcan crash the parent runner when a test writes bytes to stdout that look like a v8 report frame. A singleconsole.logof the wrong bytes aborts the whole run with an internal error that does not point at any test:Fixes: #66164
Root cause
The child test process multiplexes framed report messages and raw user stdout on one pipe. A frame starts with the two magic bytes
FF 0Ffollowed by a 4 byte size.#processRawBufferscans for that magic, reads the size and hands the payload to the v8 deserializer. User output can contain those same bytes, so stray stdout with a plausible size reaches the deserializer, which throws. The call had no error handling, so the exception aborted the entire run. The earlier>>> 0fix in #64706 hardened the size read only. This case has a valid looking size and fails one step later at the deserialize.Fix
The reporter writes each frame as a v8 header, a 4 byte size, then a second v8 header, then the serialized value. So a real frame always repeats the
FF 0Fheader at the start of its payload.#processRawBuffernow uses that structure instead of a blanket catch:#processRawBufferis now one loop that inspects the head of the buffer on each pass and handles exactly one case: leading stdout, an incomplete frame, a frame that only mimics the header, or a real frame. This replaces the earlier nested loops and the mimicsFrame flag.This turns a fatal crash into recoverable stdout, keeps any real frames that follow the stray bytes and surfaces genuine deserialize failures.
Tests
Added cases to
test/parallel/test-runner-v8-deserializer.mjs:drain()Known limitation (possible follow-up, out of scope here)
For stray bytes to be misread now they would have to reproduce the full frame shape, the outer header, a matching size, the inner header, then a valid serialized payload. This is pre-existing and extremely unlikely. Fully removing the ambiguity needs an escape mechanism or a separate channel for user stdout, which is a larger change. This PR removes the crash, which is the reported bug.