Repository navigation
Fire messageerror when a posted message fails to deserialize #39408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,6 +101,11 @@ type NodeWorkerOptions = import("node:worker_threads").WorkerOptions; | |
| // after their Worker exits | ||
| let urlRevokeRegistry: FinalizationRegistry<string> | undefined = undefined; | ||
|
|
||
| // The native `messageerror` MessageEvent carries no error object; node hands listeners an Error. | ||
| function deserializeError() { | ||
| return new TypeError("Unable to deserialize data."); | ||
| } | ||
|
|
||
| function injectFakeEmitter(Class) { | ||
| // Per-instance registry mapping each event to (user listener -> wrapper), so | ||
| // listenerCount/eventNames/removeAllListeners work over EventTarget's opaque | ||
|
|
@@ -124,6 +129,10 @@ function injectFakeEmitter(Class) { | |
| return event.error; | ||
| } | ||
|
|
||
| function messageErrorEventHandler(event: ErrorEvent | MessageEvent) { | ||
| return event instanceof MessageEvent ? deserializeError() : event.error; | ||
| } | ||
|
|
||
| function customEventHandler(event) { | ||
| return event.detail; | ||
| } | ||
|
|
@@ -136,11 +145,14 @@ function injectFakeEmitter(Class) { | |
|
|
||
| function functionForEventType(event, listener) { | ||
| switch (event) { | ||
| case "error": | ||
| case "messageerror": { | ||
| case "error": { | ||
| return wrapped(errorEventHandler, listener); | ||
| } | ||
|
|
||
| case "messageerror": { | ||
| return wrapped(messageErrorEventHandler, listener); | ||
| } | ||
|
|
||
| case "message": { | ||
| return wrapped(messageEventHandler, listener); | ||
| } | ||
|
|
@@ -1394,8 +1406,7 @@ class Worker extends EventEmitter { | |
| } | ||
|
|
||
| #onMessageError(event: MessageEvent) { | ||
| // TODO: is this right? | ||
| this.emit("messageerror", (event as any).error ?? event.data ?? event); | ||
| this.emit("messageerror", (event as any).error ?? deserializeError()); | ||
| } | ||
|
Comment on lines
1408
to
1410
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 One sibling on this same parentPort→Worker channel is missed: Extended reasoning...What the bug isThis PR fixes "a message that fails to deserialize" for the four faces that funnel through {
let entry;
while ((entry = _receiveMessageOnPort(this.#publicPort)) !== undefined) {
this.emit("message", entry.message);
}
this.#publicPort.close();
}
this.#onExitPromise = e.code;
this.emit("exit", e.code);Code path
On the no-exception failure branch, this PR's own change at SerializedScriptValue.cpp:5083 ( Why existing code doesn't prevent itThe Step-by-step proof
Impact and severitynit. (a) Reachability requires both an adversarial undeserializable payload and the exit-before-drain race — an edge case, though one the drain loop exists precisely to handle. (b) The throwing behavior of FixWrap the loop body in try/catch and route both failure modes to let entry;
for (;;) {
try {
entry = _receiveMessageOnPort(this.#publicPort);
} catch {
this.emit("messageerror", deserializeError());
continue;
}
if (entry === undefined) break;
this.emit("message", entry.message);
}
this.#publicPort.close();(Optionally also treat |
||
|
|
||
| #onOpen() { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡
event instanceof MessageEventreads a bare global at listener-invocation time and routes through user-overridableSymbol.hasInstance. REVIEW.md's built-in JS section says to avoid this, but the file already uses bareMessageEvent/ErrorEventandinstanceofelsewhere so this matches local convention — a simpler tamper-safe branch likeevent.error === undefined(ErrorEvent's.errordefaults tonull, MessageEvent has none) would sidestep it.Extended reasoning...
What the issue is
The new
messageErrorEventHandleratsrc/js/node/worker_threads.ts:132-134distinguishes a nativemessageerrorMessageEvent(dispatched by C++ when deserialization fails) from anErrorEvent(dispatched via the fake-emitter'semit()path) by testingevent instanceof MessageEvent. Two things about this are not tamper-resistant per REVIEW.md's built-in JS modules section ("globals captured at module load", "never route internal logic through user-overridable machinery (Array.isArray, neverinstanceof Array)"):MessageEventis read as a bare global at listener-invocation time, not captured at module load likeMessageChannel/BroadcastChannel/Workerare on lines 57-66.instanceofgoes through user-overridableSymbol.hasInstance.Concrete walk-through
Object.defineProperty(MessageEvent, Symbol.hasInstance, { value: () => false })(or reassignsglobalThis.MessageEvent).MessagePortfails to deserialize; native code dispatches aMessageEventwith type"messageerror"anddata === null.port.on("messageerror", listener)callsmessageErrorEventHandler(event).event instanceof MessageEventevaluates tofalse, so the handler returnsevent.error— which isundefinedon aMessageEvent.undefinedinstead of the synthesizedTypeError("Unable to deserialize data.").Why existing code doesn't prevent it
Nothing in this file captures
MessageEventat load time; the reference at line 133 is a live global lookup on every event.instanceofhas no intrinsic form here.Why this is a nit, not blocking
MessageEvent/ErrorEventas bare globals inEventClass()andemit(), and already usesinstanceof URL,instanceof Map,instanceof Set,instanceof ArrayBuffer,instanceof _MessagePortthroughout — REVIEW.md also says "Match the exact file's local conventions", and this line does.MessageEventbreaks the argument shape of their ownmessageerrorlistener. There is no security boundary crossed and no correctness issue for anyone who hasn't monkey-patched a global.How to fix
Any of these avoids the tamperable check without changing behavior:
return event.error === undefined ? deserializeError() : event.error;—ErrorEvent#errordefaults tonull(neverundefined), andMessageEventhas no.errorproperty.const { MessageChannel, BroadcastChannel, Worker: WebWorker, MessageEvent } = globalThis;and keep theinstanceof.