Skip to content

Validate transfer lists before serializing instead of silently dropping invalid entries#32809

Merged
Jarred-Sumner merged 6 commits into
mainfrom
farm/5864dcaf/structured-clone-transfer-validation
Jun 28, 2026
Merged

Validate transfer lists before serializing instead of silently dropping invalid entries#32809
Jarred-Sumner merged 6 commits into
mainfrom
farm/5864dcaf/structured-clone-transfer-validation

Conversation

@robobun

@robobun robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Problem

structuredClone, the worker-global postMessage, and the transferList worker option each hand-roll their transfer-list parsing and silently skip entries that are not objects. The surviving entries are still transferred, so a call that node and every browser reject with a TypeError instead succeeds and destroys the caller's buffer:

const b = new ArrayBuffer(8);
try { structuredClone({ b }, { transfer: [b, null] }); } catch {}
// node v26:  TypeError thrown, b.byteLength === 8
// bun 1.4:   no error,         b.byteLength === 0   (buffer silently detached)

transfer: [undefined], [42], ["x"], transfer: 5, transfer: "abc", and a non-object options argument are all silently accepted too, and the same silent-skip loop exists in self.postMessage(msg, [buf, null]) inside a worker and in new Worker(path, { workerData, transferList }).

Fix

Convert the transfer list per WebIDL before anything is serialized, using the converters the generated Worker.prototype.postMessage and MessagePort.prototype.postMessage bindings already use, so an invalid transfer list throws a TypeError with no side effects:

  • StructuredClone.cpp: structuredClone converts its options through convertDictionary<StructuredSerializeOptions> (WebIDL sequence<object>) before SerializedScriptValue::create. This also makes non-array iterables (Set, generators) usable in transfer, which the generated postMessage bindings already supported.
  • Worker.cpp (jsFunctionPostMessage, the worker-global postMessage): same conversion for both the sequence<object> and { transfer } overloads.
  • JSWorker.cpp (the transferList option of the Worker constructor): same conversion, so an invalid entry throws before workerData is serialized and nothing is detached.

Verification

7 new tests across test/js/web/workers/structured-clone.test.ts, test/js/web/workers/worker-postmessage-transfer.test.ts, and test/js/node/worker_threads/worker-transfer-list.test.ts. All fail on bun 1.4.0 and pass with this change; the surrounding worker and structured-clone suites pass.

Related PRs

The first version of this PR also made MessagePort.postMessage follow the HTML "message port post message steps" (source port in the transfer list throws before serialization, transferring the entangled peer dooms the channel instead of throwing) and changed the untransferred-MessagePort serialization error to DataCloneError. Both are already implemented in #31216 together with the rest of that MessagePort work, so they were dropped here to avoid duplicating it. The close event and transferable streams have their own PRs (#32565, #32401). #29177 adds a JS-level markAsUntransferable registry in node:worker_threads and does not validate transfer entries or touch these binding paths.

…er semantics

structuredClone, the worker-global postMessage, and the Worker transferList
option silently skipped transfer entries that are not objects, so a call that
node and browsers reject with a TypeError instead succeeded and detached the
caller's buffers. Convert the transfer list per WebIDL (sequence<object> /
StructuredSerializeOptions) before any serialization instead.

MessagePort.postMessage now checks the transfer list before serializing:
transferring the source port throws with no side effects, and transferring the
entangled peer is allowed but dooms the channel per the HTML spec.

Serializing a MessagePort that is not in the transfer list now fails with a
DataCloneError instead of a TypeError.
@robobun

robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:11 AM PT - Jun 27th, 2026

@robobun, your commit 2307c10 has some failures in Build #65329 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 32809

That installs a local version of the PR into your bun-32809 executable, so you can run:

bun-32809 --bun

@robobun

robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced against bun 1.4.0 and cross-checked against node v26 and the HTML spec before fixing. Scope was narrowed after the duplicate check: the MessagePort postMessage semantics and the untransferred-port DataCloneError are already implemented in #31216, so this PR only keeps the transfer-list validation (structuredClone, worker-global postMessage, Worker transferList option).

Verification:

  • fail-before: building the merge base and running the three test files fails exactly the 7 new tests (no throw and a detached buffer where node throws TypeError and leaves it intact).
  • pass-after: bun bd test test/js/web/workers/structured-clone.test.ts test/js/web/workers/worker-postmessage-transfer.test.ts test/js/node/worker_threads/worker-transfer-list.test.ts is green (214 pass) on the current head, which includes a merge of main.

CI state (build 65329, finished): every lane that ran tests is green, including all Windows and Linux test lanes. The only two failed jobs are macOS darwin 26 aarch64 - test-bun runners that never executed a test: buildkite-agent artifact download timed out after 120s for step 'darwin-aarch64-build-bun'. That is runner infrastructure, not this change. Ready for review.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 1 minute and 2 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ac0063ea-cd2f-49d0-a941-fdd9ff7f4cd1

📥 Commits

Reviewing files that changed from the base of the PR and between e5a7954 and 2307c10.

📒 Files selected for processing (1)
  • test/js/web/workers/structured-clone.test.ts

Walkthrough

Worker construction, structuredClone, and postMessage now convert transfer-related inputs through shared IDL helpers. Tests add coverage for invalid entries, malformed options, iterable transfers, and buffer detachment behavior.

Changes

Transfer option conversion

Layer / File(s) Summary
Worker constructor transferList conversion
src/jsc/bindings/webcore/JSWorker.cpp, test/js/node/worker_threads/worker-transfer-list.test.ts
JSWorkerDOMConstructor::construct now converts options.transferList through the IDL sequence helper, and tests cover invalid entries and valid buffer transfer.
structuredClone transfer options conversion
src/jsc/bindings/webcore/StructuredClone.cpp, test/js/web/workers/structured-clone.test.ts
jsFunctionStructuredClone converts options with convertDictionary<StructuredSerializeOptions> before serialization, and tests cover invalid values, non-object options, and iterable transfers.
postMessage options conversion
src/jsc/bindings/webcore/Worker.cpp, test/js/web/workers/worker-postmessage-transfer.test.ts
jsFunctionPostMessage converts sequence and dictionary options through shared IDL helpers, and tests cover invalid transfer entries and malformed options.

Suggested reviewers

  • Jarred-Sumner
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the core change: validating transfer lists before serialization instead of silently skipping invalid entries.
Description check ✅ Passed The description covers the problem, fix, verification, and scope, though it uses custom headings instead of the exact template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/worker_threads/worker_threads.test.ts`:
- Around line 547-560: Update the worker_threads test to await worker cleanup
instead of calling terminate() without waiting, so the fallback cleanup in the
failure path fully completes before the test ends. Keep the existing try/finally
around the Worker creation in test("an invalid transferList entry throws before
anything is detached"), but make the cleanup path async-aware by awaiting the
Worker termination result. Also remove the inline comment in that test that
restates the regression context, since the test name and assertions already
describe the intent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0fc3180e-4892-48f6-b5e7-90438efd787c

📥 Commits

Reviewing files that changed from the base of the PR and between c2b5cef and f9d1c65.

📒 Files selected for processing (9)
  • src/jsc/bindings/webcore/JSWorker.cpp
  • src/jsc/bindings/webcore/MessagePort.cpp
  • src/jsc/bindings/webcore/SerializedScriptValue.cpp
  • src/jsc/bindings/webcore/StructuredClone.cpp
  • src/jsc/bindings/webcore/Worker.cpp
  • test/js/node/worker_threads/worker_threads.test.ts
  • test/js/web/workers/message-channel.test.ts
  • test/js/web/workers/structured-clone.test.ts
  • test/js/web/workers/worker-postmessage-transfer.test.ts

Comment thread test/js/node/worker_threads/worker_threads.test.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. feat(worker_threads): implement markAsUntransferable and validate transfer lists #29177 - Also validates transfer lists and rejects invalid entries with DataCloneError in MessagePort and Worker paths
  2. node:worker_threads: +48 Node.js tests passing — MessagePort, stdio, SHARE_ENV, exit codes, transfer semantics, postMessageToThread + inspector #31216 - Superset PR that includes the same transfer-list validation, self-transfer DataCloneError, and entangled-peer "doomed port" semantics fixes

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any bugs, but this changes user-facing behavior across several spec-sensitive transfer/serialization paths (including flipping the entangled-peer-transfer case from throw to doomed-channel and rewriting existing test assertions), so it's worth a human look.

Extended reasoning...

Overview

This PR replaces hand-rolled transfer-list parsing in structuredClone, the worker-global postMessage, and the Worker constructor's transferList option with the WebIDL convertDictionary<StructuredSerializeOptions> / convert<IDLSequence<IDLObject>> machinery, so invalid entries throw TypeError before any buffer is detached. It also reorders the source-port / entangled-peer checks in MessagePort::postMessage to run before serialization (per the HTML spec's message-port-post-message-steps), changes the entangled-peer case from throwing DataCloneError to silently dooming the channel, and reclassifies an untransferred MessagePort from ValidationError to DataCloneError in SerializedScriptValue.cpp. Five C++ binding files and four test files are touched; two pre-existing assertions in message-channel.test.ts are inverted.

Security risks

None apparent. The change tightens validation (rejecting more inputs earlier) rather than loosening it, and does not touch auth, crypto, or permission boundaries. The doomed-channel path drops the message and disentangles the peer rather than delivering anything new.

Level of scrutiny

Moderate-to-high. These are core, spec-defined Web/Node compatibility surfaces where subtle ordering matters (e.g., the new pre-serialization loop in MessagePort::postMessage compares port->pipe() against m_pipe.ptr() to detect both this port and its peer, then defers the doomed drop until after disentanglePorts). The change also intentionally flips observable behavior — calls that used to succeed now throw, and one that used to throw now succeeds — and rewrites existing test expectations to match. That combination merits a human reviewer confirming the spec/Node alignment and that no other call sites depended on the old lenient parsing.

Other factors

Test coverage is good (11 new tests, fail-before/pass-after verified per the PR description), and the bug-hunting system found nothing. The implementation reuses existing WebIDL converters rather than adding new logic, which limits risk. Still, the breadth (five C++ files across structured clone, Worker, and MessagePort) and the deliberate behavioral inversions push this past the threshold for auto-approval.

The doomed-port semantics and the DataCloneError for an untransferred
MessagePort are already implemented in #31216 together with the rest of
that MessagePort work, so this PR keeps only the transfer-list
validation fixes.
@robobun robobun changed the title Validate transfer lists before serializing and fix MessagePort transfer semantics Validate transfer lists before serializing instead of silently dropping invalid entries Jun 27, 2026
@robobun

robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author

Checked both:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/bindings/webcore/SerializedScriptValue.cpp`:
- Around line 1781-1783: The MessagePort validation path in
SerializedScriptValue.cpp is mapping an untransferred MessagePort to
ValidationError, which later turns into TypeError; update the failure handling
in the serialization check so this case reports the DataCloneError-style
serialization failure instead. Adjust the code around the MessagePort
lookup/validation in SerializedScriptValue::serialize to use the correct return
code so maybeThrowExceptionIfSerializationFailed() and
exceptionForSerializationFailure() surface DataCloneError for an unserializable
MessagePort.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cff1cc94-da00-42a4-8674-11ee14b414ea

📥 Commits

Reviewing files that changed from the base of the PR and between f9d1c65 and 713c8a1.

📒 Files selected for processing (3)
  • src/jsc/bindings/webcore/SerializedScriptValue.cpp
  • test/js/node/worker_threads/worker_threads.test.ts
  • test/js/web/workers/structured-clone.test.ts
💤 Files with no reviewable changes (1)
  • test/js/web/workers/structured-clone.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/bindings/webcore/SerializedScriptValue.cpp`:
- Around line 1781-1783: The MessagePort validation path in
SerializedScriptValue.cpp is mapping an untransferred MessagePort to
ValidationError, which later turns into TypeError; update the failure handling
in the serialization check so this case reports the DataCloneError-style
serialization failure instead. Adjust the code around the MessagePort
lookup/validation in SerializedScriptValue::serialize to use the correct return
code so maybeThrowExceptionIfSerializationFailed() and
exceptionForSerializationFailure() surface DataCloneError for an unserializable
MessagePort.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cff1cc94-da00-42a4-8674-11ee14b414ea

📥 Commits

Reviewing files that changed from the base of the PR and between f9d1c65 and 713c8a1.

📒 Files selected for processing (3)
  • src/jsc/bindings/webcore/SerializedScriptValue.cpp
  • test/js/node/worker_threads/worker_threads.test.ts
  • test/js/web/workers/structured-clone.test.ts
💤 Files with no reviewable changes (1)
  • test/js/web/workers/structured-clone.test.ts
🛑 Comments failed to post (1)
src/jsc/bindings/webcore/SerializedScriptValue.cpp (1)

1781-1783: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return DataCloneError for untransferred MessagePort.

Line 1782 still reports ValidationError, which later becomes TypeError in maybeThrowExceptionIfSerializationFailed() / exceptionForSerializationFailure(). An unserializable MessagePort here should surface DataCloneError, not TypeError.

Suggested fix
-                code = SerializationReturnCode::ValidationError;
+                code = SerializationReturnCode::DataCloneError;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

                // MessagePort object could not be found in transferred message ports
                code = SerializationReturnCode::DataCloneError;
                return true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jsc/bindings/webcore/SerializedScriptValue.cpp` around lines 1781 - 1783,
The MessagePort validation path in SerializedScriptValue.cpp is mapping an
untransferred MessagePort to ValidationError, which later turns into TypeError;
update the failure handling in the serialization check so this case reports the
DataCloneError-style serialization failure instead. Adjust the code around the
MessagePort lookup/validation in SerializedScriptValue::serialize to use the
correct return code so maybeThrowExceptionIfSerializationFailed() and
exceptionForSerializationFailure() surface DataCloneError for an unserializable
MessagePort.

The previous commit accidentally picked up the detached-ArrayBuffer hunk
that already landed on main as #32799. This PR does not change
SerializedScriptValue.cpp.
Comment thread src/jsc/bindings/webcore/StructuredClone.cpp
robobun added 3 commits June 27, 2026 02:43
worker_threads.test.ts has unrelated tests that hit their timeout under
debug builds, so the transferList regression test gets a dedicated file
next to it (plus a positive control that a valid transferList still
detaches).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tightening this up — the metadata and stray SerializedScriptValue.cpp hunk are gone and the diff now matches the description. I didn't find any issues, but since this swaps the argument-conversion path for structuredClone, the worker-global postMessage, and the Worker constructor (a user-visible strictness change in core serialization), I'd like a human to sign off.

Extended reasoning...

Overview

Three hand-rolled transfer-list parsers are replaced with calls to the existing generated WebIDL converters:

  • StructuredClone.cpp / jsFunctionStructuredClone: now goes through convertDictionary<StructuredSerializeOptions> (the same generated converter JSStructuredSerializeOptions.cpp already provides).
  • Worker.cpp / jsFunctionPostMessage (the worker-global self.postMessage): the sequence overload now uses convert<IDLSequence<IDLObject>> and the dictionary overload uses convertDictionary<StructuredSerializeOptions> — exactly what the generated jsWorkerPrototypeFunction_postMessage{1,2}Body in JSWorker.cpp already do for the parent-side Worker#postMessage.
  • JSWorker.cpp / JSWorkerDOMConstructor::construct: options.transferList now goes through convert<IDLSequence<IDLObject>>.

Net effect is ~40 lines of C++ removed and ~15 added, plus 7 new tests across three files. Since my last comment the author dropped the unrelated SerializedScriptValue.cpp hunk (already merged in #32799), moved the worker_threads test into its own file with await worker?.terminate(), and updated the title/description.

Security risks

None identified. The change tightens input validation (rejects more inputs) and reuses converters that are already on the hot path for Worker.prototype.postMessage and MessagePort.prototype.postMessage. No new parsing of untrusted data; RETURN_IF_EXCEPTION is present after every converter call.

Level of scrutiny

Medium-high. The change itself is mechanical and follows the established pattern in the same files, but structuredClone, postMessage, and the Worker constructor are core runtime entry points, and this is a deliberate behavior change: calls that previously succeeded (with silent data loss) now throw TypeError. That's the spec-correct, Node- and browser-matching behavior, but a maintainer should confirm they're comfortable with the strictness bump (e.g. { transfer: null } and primitive options now throw).

Other factors

  • CI is green except test-docker-build-debian.ts on macOS aarch64, which is unrelated to this diff.
  • The bug-hunting system found nothing.
  • The CodeRabbit nit about awaiting worker.terminate() was addressed.
  • I verified convertDictionary<StructuredSerializeOptions> (JSStructuredSerializeOptions.cpp:32-56) handles null/undefined options, non-object options, and undefined transfer the way the new tests expect.

@Jarred-Sumner Jarred-Sumner merged commit 18e6d8d into main Jun 28, 2026
75 of 77 checks passed
@Jarred-Sumner Jarred-Sumner deleted the farm/5864dcaf/structured-clone-transfer-validation branch June 28, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants