Skip to content

bake: keep a __proto__ export an own key of the dev server ssr manifest entry - #40828

Open
robobun wants to merge 3 commits into
mainfrom
farm/7647edaa/bake-ssr-manifest-proto-export
Open

robobun wants to merge 3 commits into
mainfrom
farm/7647edaa/bake-ssr-manifest-proto-export

Conversation

@robobun

@robobun robobun commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • In the dev server, a "use client" export named __proto__ is missing from its ssrManifest entry. When the module stops being a client component, serverManifest["<file>#__proto__"] stays behind.
  • registerUpdate (src/runtime/bake/hmr-runtime-server.ts) built the entry with client[exportName] = {...} on a plain object. For "__proto__" that calls the Object.prototype.__proto__ setter and replaces the entry prototype. The delete loop (for...in) then never sees __proto__.

Fix

  • Build the entry with Object.fromEntries over the export names. It defines own data properties, so __proto__ becomes a key like any other.
  • The entry stays a plain object, the shape the production build emits as an object literal (Keep __proto__ object keys as written when printing and synthesizing object literals #40812 keeps __proto__ a computed key there). Dev and prod entries have the same keys, prototype, and attributes.
  • The only consumer in the tree, react-server-dom-bun, reads moduleMap[id][name] and moduleMap[id]["*"]. Unchanged.
  • Verified: test/bake/dev/bundle.test.ts ("client component exported as proto ..."). The released bun fails both assertions. Also ran that whole file, esm.test.ts, react-response.test.ts, request-cookies.test.ts.

Background

  • bun:bake/server exposes two maps. serverManifest maps "<file>#<export>" to a client module id. ssrManifest maps a client file id to one entry per export, { specifier: "ssr:<file>", name }. React's Flight client uses them during SSR.
  • In dev, registerUpdate fills the maps after each bundle: it loads the server-side module and iterates Object.keys(exports). Removing the directive runs the delete branch.
  • The test re-exports imported functions. The HMR export lowering emits getters for those, so the module namespace has an own __proto__ key today, independent of Keep __proto__ object keys as written when printing and synthesizing object literals #40812.
Notes

Repro output with the released bun (v1.4.1-canary.1), page returns JSON.stringify({ exports, ssr: ssrManifest, server: Object.keys(serverManifest) }):

{"exports":["__proto__","other"],
 "ssr":{"components/Client.ts":{"other":{"specifier":"ssr:components/Client.ts","name":"other"}}},
 "server":["components/Client.ts#__proto__","components/Client.ts#other"]}

After removing "use client" from the file:

{"exports":["__proto__","other"],"ssr":{},"server":["components/Client.ts#__proto__"]}

With the fix the first response lists both __proto__ and other under ssr, and the second response has server: [].

The first revision of this PR used Object.create(null) for the entry. The self-review pointed out that this makes the dev entry differ from the production one (no Object.prototype, so entry.hasOwnProperty(...) throws in dev only). Object.fromEntries gives the same result for __proto__ and keeps the shape identical. Checked with a probe: Object.getPrototypeOf(entry) === Object.prototype, and the __proto__ property is enumerable, writable, and configurable.

serverManifest keys are "<file>#<export>" and ssrManifest keys are project-relative file paths, so neither can equal __proto__. Those two objects are left as they are.

The export-clause form export { Client as __proto__ } of a local function, and export function __proto__, are lowered to a plain { __proto__: ... } property today, which sets the prototype of the exports object itself. So those forms never reach this code with a __proto__ key until the bundler fix in #40812 lands. The test uses the re-export form so it exercises this runtime fix on its own.

Two adjacent observations, not changed here:

  • import.meta.hot.on("__proto__", cb) hits the same class of bug in eventHandlers (src/runtime/bake/hmr-module.ts:32): eventHandlers.__proto__ is Object.prototype, so ??= [] skips and .push throws.
  • DevServer.rs calls registerUpdate and drops the returned promise. A "use client" module with three or more top-level awaits can be rendered on the first request before its manifest entry exists. The second request sees it. The test module has no top-level await, so the test is not affected.

[review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bake/dev/bundle.test.ts
bun test v1.4.1 (65362b53b)

test/bake/dev/bundle.test.ts:
Dev server testing directory: /tmp/bun-dev-test-A7Fj08
�[0;30mdev|�[0m Started development server: http://localhost:38761
�[0;30mdev|�[0m �[32mBundled page in 200ms�[0m�[2m:�[0m routes/index.ts �[2m+ 1 more�[0m
�[0;30mdev|�[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
�[0;30mdev|�[0m 
�[0;30mdev|�[0m Module "db.ts" is not accepted by routes/index.ts,
�[0;30mdev|�[0m �[32mReloaded in 54ms�[0m�[2m:�[0m db.ts
�[0;30mdev|�[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
�[0;30mdev|�[0m 
�[0;30mdev|�[0m Module "routes/index.ts" is a root module that does not self-accept.
�[0;30mdev|�[0m �[36m[x2]�[0m �[32mReloaded in 53ms�[
... (truncated)

release without fix: 1 FAILED
bun test v1.4.1-canary.1 (65362b53b)

test/bake/dev/bundle.test.ts:
Dev server testing directory: /tmp/bun-dev-test-ebr2xj
�[0;30mdev|�[0m Started development server: http://localhost:44639
�[0;30mdev|�[0m �[32mBundled page in 3ms�[0m�[2m:�[0m routes/index.ts �[2m+ 1 more�[0m
�[0;30mdev|�[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
�[0;30mdev|�[0m 
�[0;30mdev|�[0m Module "db.ts" is not accepted by routes/index.ts,
�[0;30mdev|�[0m �[32mReloaded in 3ms�[0m�[2m:�[0m db.ts
�[0;30mdev|�[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
�[0;30mdev|�[0m 
�[0;30mdev|�[0m Module "routes/index.ts" is a root module that does not self-accept.
�[0;30mdev|�[0m �[36m[x2]�[0m �[32mReloaded in 2ms�[0m�[2m:�[0m routes/index.ts
(pass)  DEV:bundle-1: import identifier doesnt get renamed [205.21ms]
�[0;30mdev|�[0m Started development server: http://localhos
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bake/dev/bundle.test.ts
bun test v1.4.1 (65362b53b)

test/bake/dev/bundle.test.ts:
Dev server testing directory: /tmp/bun-dev-test-eOnEoc
�[0;30mdev|�[0m Started development server: http://localhost:36831
�[0;30mdev|�[0m �[32mBundled page in 153ms�[0m�[2m:�[0m routes/index.ts �[2m+ 1 more�[0m
�[0;30mdev|�[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
�[0;30mdev|�[0m 
�[0;30mdev|�[0m Module "db.ts" is not accepted by routes/index.ts,
�[0;30mdev|�[0m �[32mReloaded in 58ms�[0m�[2m:�[0m db.ts
�[0;30mdev|�[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
�[0;30mdev|�[0m 
�[0;30mdev|�[0m Module "routes/index.ts" is a root module that does not self-accept.
�[0;30mdev|�[0m �[36m[x2]�[0m �[32mReloaded in 52ms�[
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     e9a659fe6d
  features     baseline

23 deps, 131 codegen, 1172 objects in 697ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1244] install /workspace/bun
bun install v1.4.1-canary.1 (65362b53b)

Checked 26 installs across 63 packages (no changes) [5.00ms]
[2/1244] install /workspace/bun/packages/bun-error
bun install v1.4.1-canary.1 (65362b53b)

Checked 1 install across 2 packages (no changes) [3.00ms]
[3/1244] gen bindgenv2
[4/1244] fetch zlib
[zlib] up to date
[5/1244] install /workspace/bun/src/node-fallbacks
bun install v1.4.1-canary.1 (65362b53b)

Checked 111 installs across 104 packages (no changes) [6.00ms]
[6/1244] gen ErrorCode+*.h
[7/1244] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[8/1217] gen .bind.ts → GeneratedBindings.cpp
[9/1217] fetch tinycc
[tinycc] up to date
[10/1216] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[11/1216] gen node-fallbacks/react-refresh.js
Bundled 1 module in 6ms

  react-refresh.js  4.81 K
... (truncated)
diff hotspot
src/runtime/bake/hmr-runtime-server.ts | 13 ++++----
 test/bake/dev/bundle.test.ts           | 58 ++++++++++++++++++++++++++++++++++
 2 files changed, 64 insertions(+), 7 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                    reads  edits  tests
src/runtime/bake/hmr-runtime-server.ts      3      3      0
test/bake/dev/bundle.test.ts                2      2      0

…st entry

The HMR server runtime built the per-module ssrManifest entry with
client[exportName] = ... on a plain object. For an export named
__proto__ that assignment hit the Object.prototype setter and replaced
the entry's prototype. The export was missing from Object.keys of the
entry, and the delete loop in componentManifestDelete never removed
its serverManifest id.

Create the entry with Object.create(null) so every export name is an
own key.
@robobun

robobun commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced with the released bun (v1.4.1-canary.1) through test/bake/dev/bundle.test.ts -t __proto__: a "use client" module with export { Client as __proto__, Other as other } gives an ssrManifest entry with only other as an own key, and removing the directive leaves components/Client.ts#__proto__ in serverManifest. Both assertions pass with this branch.

CI: no test/bake failure in either run. The red lanes are unrelated to this change:

  • darwin x64 - test-bun: test/js/web/url/url.test.ts fails with TypeError: Invalid URL on the Unicode 16 IDNA case. It fails the same way on main.
  • darwin aarch64 - test-bun (build 108024): test/js/third_party/grpc-js/test-client.test.ts gets gRPC status 13 instead of 14 from a nonexistent server.

Both are reported for triage on main. The other listed failures passed on retry.

@robobun

robobun commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:18 PM PT - Aug 28th, 2026

❌ @robobun, your commit e9a659f has 2 failures in Build #108024 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 40828

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

bun-40828 --bun

@coderabbitai

coderabbitai Bot commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ebb794f4-9c40-4d3f-a6a3-8a821811fc86

📥 Commits

Reviewing files that changed from the base of the PR and between d578a8c and 45cd24b.

📒 Files selected for processing (2)
  • src/runtime/bake/hmr-runtime-server.ts
  • test/bake/dev/bundle.test.ts

Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review.


Walkthrough

Changes

Client export manifest handling

Layer / File(s) Summary
Null-prototype export map and regression coverage
src/runtime/bake/hmr-runtime-server.ts, test/bake/dev/bundle.test.ts
The SSR client export map now uses a null-prototype object. DevServer tests verify that __proto__ remains an own manifest key and that SSR and server manifest entries are removed when the module is no longer a client component.

Merge Risk: ⚪ Minimal · up to 45cd2

The change preserves proto exports in the development SSR manifest and removes them correctly when no longer client components. No actionable merge-blocking risk remains beyond normal checks.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the primary fix: preserving a proto export as an own key in the development server SSR manifest.
Description check ✅ Passed The description explains the problem, implementation, compatibility considerations, regression test, and verification results. It provides the information required by the template, although it uses Pr…
Full details: Description check

Explanation

The description explains the problem, implementation, compatibility considerations, regression test, and verification results. It provides the information required by the template, although it uses Problem and Fix headings instead of the template headings.


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

@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.

LGTM — one-line {} → Object.create(null) swap using an idiom already established in hmr-module.ts, with a test covering both the add and delete branches.

What was reviewed:

  • Confirmed client[exportName] = ... now defines an own key for "__proto__" and the delete-branch for...in at line 193 enumerates exactly the registered export names on the null-prototype entry.
  • Checked the test's expected object uses computed ["__proto__"] and the actual value round-trips through JSON.parse, so both sides carry __proto__ as an own enumerable key and .toEqual compares them correctly.
  • Verified the outer serverManifest/ssrManifest maps are keyed by <file>#<export> and project-relative paths, so leaving them as plain objects is a reasonable scope boundary.
Extended reasoning...

Overview

This PR changes a single object literal in src/runtime/bake/hmr-runtime-server.ts (registerUpdate) from {} to Object.create(null), so a client-component export named __proto__ is stored as an own key of the per-module ssrManifest entry rather than invoking the Object.prototype.__proto__ setter. It adds one devTest in test/bake/dev/bundle.test.ts that asserts both manifests contain the __proto__ export as an own key when "use client" is present, and that both manifests are fully cleared for that module when the directive is removed.

Security risks

The change is a prototype-pollution hardening, not a new risk surface. Previously, a user-controlled export name (__proto__) could mutate the prototype of the per-module manifest entry; the fix removes Object.prototype from the chain so all assignments become own data properties. The only consumer (react-server-dom-bun) reads moduleMap[id][name], which works identically on a null-prototype object. The outer serverManifest and ssrManifest maps remain plain objects, but their keys are <file>#<export> and project-relative file paths, neither of which can equal the bare string __proto__, so the PR's stated scope boundary holds.

Level of scrutiny

Low-to-moderate. The runtime edit is one line applying an idiom already present in the same subsystem (hmr-module.ts:879). The delete branch's for (const exportName in client) was the second half of the bug — with a null prototype it now enumerates exactly the registered names, so the stale serverManifest[<file>#__proto__] entry is correctly removed. The new comment is short and answers the obvious "why not {}" question, satisfying the repo's comment-discipline rule.

Other factors

The test is placed alongside the existing "use client" HMR test in the same file (per CLAUDE.md placement rules), uses devTest/minimalFramework like its neighbors, and asserts via a single .toEqual on a nested object (per test/CLAUDE.md). It deliberately uses the re-export form so the module namespace already has an own __proto__ getter, isolating this runtime fix from the separate bundler-side object-literal lowering issue referenced in the description. No CODEOWNERS entry covers these paths, there are no outstanding reviewer objections in the timeline, and the bug hunt exited on dry_streak.

Object.fromEntries defines own keys, so the entry stays a plain object
with the same shape as the production manifest entry, and an export
named __proto__ is an own key instead of the prototype.
Comment thread src/runtime/bake/hmr-runtime-server.ts Outdated

@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.

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
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.

1 participant