Skip to content

util.inspect: recover constructor name for null-prototype class instances - #34755

Closed
robobun wants to merge 1 commit into
mainfrom
farm/c73052e6/inspect-null-proto-constructor-name
Closed

robobun wants to merge 1 commit into
mainfrom
farm/c73052e6/inspect-null-proto-constructor-name

Conversation

@robobun

@robobun robobun commented Jul 20, 2026 •

Copy link
Copy Markdown
Collaborator

Object.setPrototypeOf(new Foo(), null) now inspects as [Foo: null prototype] {} instead of [Object: null prototype] {}, matching Node.js.

const util = require('util');
class Foo {}
util.inspect(Object.setPrototypeOf(new Foo(), null));
// node: '[Foo: null prototype] {}'
// bun (before): '[Object: null prototype] {}'
// bun (after): '[Foo: null prototype] {}'

Cause

Node recovers the name via V8's Map::constructor back-reference, which is set at allocation time and survives prototype transitions. JSC's Structure::changePrototypeTransition pins the new Structure and clears previousID(), so neither the old prototype nor its constructor is reachable from the object afterwards. calculatedClassName() falls back to "Object".

Fix

JSC side (oven-sh/WebKit#312): changePrototypeTransition looks up the previous prototype's own constructor (VMInquiry, no JS execution) before entering DeferGC and caches the resulting name on the new structure's StructureRareData. toDictionaryTransition carries an existing cached name forward. Structure::sourceConstructorName() walks previousID() to find the cached value so property-add transitions after the prototype change can still reach it. The cache is a plain WTF::String, so it adds no GC root; the Object.prototype → null common path is skipped so Object.create(null) allocates no rare data.

Bun side: internalGetConstructorName in util.inspect consults a new jsFunctionGetSourceConstructorName binding before falling back to [object X] parsing.

Verification

The new assertions match Node.js v26.3.0 output for setPrototypeOf(new Foo(), null), subsequent property adds, chained prototype changes (Foo → Bar.prototype → null still reports Foo), and combinations with Symbol.toStringTag. Object.create(null) and { __proto__: null } still report [Object: null prototype]. Also un-TODOs the matching assertions in util-inspect.test.js and util-format.test.js.

Depends on

This PR pins WEBKIT_VERSION to the preview build of oven-sh/WebKit#312 (autobuild-preview-pr-312-0bab8a9a). CI here will fail until that preview release is published; the WebKit preview build is currently queued on GitHub Actions. Once oven-sh/WebKit#312 merges, WEBKIT_VERSION should be updated to the merged main sha before this lands.


no test proof · iteration 6 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js

@robobun

robobun commented Jul 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: diff is green; remaining CI failures are unrelated to util.inspect/setPrototypeOf. Needs a maintainer to land (WebKit dependency).

Reproduction (fails on released bun, passes on this branch):

bun bd test test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js -t "null prototype objects keep their constructor name"

The ExceptionScope::assertNoException() SIGABRT in test-http2-reset-flood.js from the previous run is fixed at d4aabd18 (gone from build #76080).

Remaining failures on #76080:

  • test-tls-connect-memleak.js (Windows aarch64, [new]): FinalizationRegistry timing assertion (collected === false after one gc() + setImmediate). The test has zero setPrototypeOf calls and bun's TLS/net paths have none either; passes 3/3 locally on Linux debug. The only plausible link is that adding String m_sourceConstructorName to StructureRareData grows the cell by one pointer and perturbs GC heap layout enough to change FinalizationRegistry callback timing on this one lane. Not a bug in this change; the test's single-gc-then-check pattern is timing-dependent.
  • html-rewriter-leak.test.ts, test-http-client-leaky-with-double-response.js, test-repl-close.js (all [flaky], all Windows x64).

Depends on oven-sh/WebKit#312 (preview autobuild-preview-pr-312-d4aabd18, one commit ahead of the current pin a0e65bf298). Once that merges, WEBKIT_VERSION here must be updated to the merged main sha before landing.

@robobun

robobun commented Jul 20, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:37 AM PT - Jul 20th, 2026

❌ @robobun, your commit a2d436f has 1 failures in Build #76080 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34755

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

bun-34755 --bun

@robobun
robobun marked this pull request as ready for review July 20, 2026 04:37
@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Constructor-name recovery

Layer / File(s) Summary
Native constructor lookup
src/jsc/bindings/UtilInspect.h, src/jsc/bindings/UtilInspect.cpp
Adds the jsFunctionGetSourceConstructorName binding to retrieve non-generic source constructor names from final objects.
Inspection integration
src/js/internal/util/inspect.js
Uses the native constructor-name lookup before the existing string-tag fallback.
Constructor-name regression coverage
test/js/node/util/node-inspect-tests/parallel/util-format.test.js, test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js
Tests preserved constructor names for null-prototype instances, subclasses, chained prototype changes, and Symbol.toStringTag.

WebKit version update

Layer / File(s) Summary
WebKit dependency identity
scripts/build/deps/webkit.ts
Changes WEBKIT_VERSION from a commit SHA to an autobuild-preview-pr-312-73c011c1 tag.

Possibly related PRs

Suggested reviewers: jarred-sumner

🚥 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 summarizes the main change: preserving constructor names for null-prototype class instances in util.inspect.
Description check ✅ Passed The description covers what the PR does and how it was verified, though it uses custom headings instead of the template's exact section names.

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

Comment thread test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js

@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 `@scripts/build/deps/webkit.ts`:
- Line 10: Update WEBKIT_VERSION to the merged WebKit main-branch SHA, replacing
the temporary autobuild-preview-pr-312-73c011c1 tag so dependency resolution and
prebuilt cache keys use the permanent revision.
🪄 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: 5cb494cd-43ae-4164-be5e-298652d060c9

📥 Commits

Reviewing files that changed from the base of the PR and between 0a17ce6 and 7a2ec00.

📒 Files selected for processing (6)
  • scripts/build/deps/webkit.ts
  • src/js/internal/util/inspect.js
  • src/jsc/bindings/UtilInspect.cpp
  • src/jsc/bindings/UtilInspect.h
  • test/js/node/util/node-inspect-tests/parallel/util-format.test.js
  • test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js

Comment thread scripts/build/deps/webkit.ts Outdated
@robobun
robobun force-pushed the farm/c73052e6/inspect-null-proto-constructor-name branch from 7a2ec00 to ef36834 Compare July 20, 2026 05:08

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

The Bun-side glue here looks correct, but this needs a maintainer to land: it depends on oven-sh/WebKit#312 (still open) and pins WEBKIT_VERSION to a preview tag that must be swapped for the merged SHA. The substantive change — caching the constructor name on StructureRareData during changePrototypeTransition — lives in the WebKit PR, not this diff, and touches JSC structure-transition internals that warrant human review.

What was reviewed:

  • jsFunctionGetSourceConstructorName: no JS execution, no throw scope needed (structure/typeInfo reads only), FinalObjectType gate matches the V8 behavior described.
  • internalGetConstructorName fallback ordering: new lookup slots between .constructor?.name and the [object X] regex, so Object.create(null) / non-final objects are unaffected.
  • Test changes un-TODO existing Node-parity assertions and add a focused test; the 30s timeout on no assertion failures 2 was discussed and resolved (pre-existing debug+ASAN slowness on main).
Extended reasoning...

Overview

This PR makes util.inspect(Object.setPrototypeOf(new Foo(), null)) print [Foo: null prototype] {} instead of [Object: null prototype] {}, matching Node.js. On the Bun side it adds a ~15-line C++ host function jsFunctionGetSourceConstructorName in UtilInspect.cpp that reads Structure::sourceConstructorName() and returns it as a JS string (or undefined for non-FinalObjectType objects, empty names, or "Object"), wires it into internalGetConstructorName in src/js/internal/util/inspect.js as a fallback before the Object.prototype.toString regex, un-TODOs four Node-parity assertions across two test files, and adds a dedicated regression test. It also bumps WEBKIT_VERSION to a preview tag for the unmerged oven-sh/WebKit#312.

Why this can't be auto-approved

The load-bearing change is not in this diff. Structure::sourceConstructorName() and the caching in changePrototypeTransition / toDictionaryTransition live in the WebKit fork PR, which touches JSC's structure-transition machinery and StructureRareData allocation. Per REVIEW.md's Dependencies & vendoring guidance, WebKit bumps and engine-internal changes need maintainer review — the perf claim (one VMInquiry lookup per setPrototypeOf, Object.prototype → null skipped so Object.create(null) allocates no rare data) and GC-safety claim (plain WTF::String, no GC root) can only be verified against that PR. Additionally, the PR is explicitly not landable as-is: WEBKIT_VERSION points at autobuild-preview-pr-312-0bab8a9a, and both the description and author state it must be updated to the merged main SHA before merge.

Security risks

None identified. The binding reads engine-internal metadata (structure type, cached string) with no user-JS execution, no coercions, and no external input parsing. util.inspect output formatting is not security-sensitive.

Level of scrutiny

High — not for the Bun-side diff (which is small and mechanical) but because it's coupled to a JSC-internals change in a separate repo and a WebKit version bump. The Bun-side host function is straightforward: getObject() null-checks, typeInfo().type() and sourceConstructorName() are non-throwing structure accessors so no ThrowScope/RETURN_IF_EXCEPTION is needed, and jsString(vm, WTF::move(name)) is the standard pattern. The JS-side insertion preserves existing fallback order.

Other factors

The prior inline nit about the 30s per-test timeout on no assertion failures 2 was answered with evidence that the block already times out on main under debug+ASAN (~10s vs 5s default) independent of this change; that thread is resolved. Test coverage is good: both entry points (util.inspect and util.format('%s', ...)), property adds after the prototype change, chained transitions, Symbol.toStringTag combination, and negative cases (Object.create(null), { __proto__: null }) are all asserted.

…nces

Object.setPrototypeOf(new Foo(), null) now inspects as
'[Foo: null prototype] {}' instead of '[Object: null prototype] {}',
matching Node.js.

Node recovers the name via V8's Map::constructor back-reference which
survives prototype transitions. JSC's changePrototypeTransition pins the
new Structure and clears previousID(), so neither the old prototype nor
its constructor is reachable from the object afterwards.

The JSC side (oven-sh/WebKit#312, preview autobuild-preview-pr-312-0bab8a9a)
caches the previous prototype's constructor name on StructureRareData
during changePrototypeTransition and carries it through
toDictionaryTransition. Structure::sourceConstructorName() walks
previousID() to find it. The cache is a plain WTF::String so it adds no
GC root.

On the Bun side, internalGetConstructorName consults a new
jsFunctionGetSourceConstructorName binding before falling back to
'[object X]' parsing. The binding only answers for FinalObjectType
instances; V8 reports the base type for Error/Array/etc. subclasses
after setPrototypeOf(null), which Object.prototype.toString already
covers.

The 'no assertion failures 2' test block (~1500 util.inspect assertions)
already exceeds the 5s default under debug+ASAN on main; gave it a 30s
ceiling so the file is runnable via bun bd.
@robobun
robobun force-pushed the farm/c73052e6/inspect-null-proto-constructor-name branch from ef36834 to a2d436f Compare July 20, 2026 06:41

@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 bugs in the Bun-side diff, but this needs a maintainer: the load-bearing change is in oven-sh/WebKit#312 (Structure::changePrototypeTransition / StructureRareData caching), which isn't reviewable here, and WEBKIT_VERSION is still pinned to a preview tag that must be swapped for the merged sha before landing.

Checked: jsFunctionGetSourceConstructorName has no throwing calls (structure reads + jsString on a WTF::String), so no exception scope needed; the FinalObjectType gate keeps built-in subclasses on the Object.prototype.toString path; internalGetConstructorName still tries val.constructor?.name first so the new lookup only fires on the null-prototype fallback.

Extended reasoning...

Overview

Adds a C++ binding jsFunctionGetSourceConstructorName that reads Structure::sourceConstructorName() (new JSC API from oven-sh/WebKit#312), wires it into internalGetConstructorName in src/js/internal/util/inspect.js as a fallback before the [object X] regex, un-TODOs matching Node compat assertions in util-inspect.test.js / util-format.test.js, adds a dedicated regression test, and pins WEBKIT_VERSION to autobuild-preview-pr-312-d4aabd18.

Security risks

None identified. The binding does pure in-memory reads on a Structure and returns a string or undefined; no user JS is executed, no allocation on untrusted sizes.

Level of scrutiny

High — for reasons outside this diff. The Bun-side changes are small and look correct, but the actual fix lives in the WebKit fork and modifies changePrototypeTransition / toDictionaryTransition to cache a constructor name on StructureRareData. That is core-engine hot-path code affecting every Object.setPrototypeOf call, and a prior iteration already tripped an ExceptionScope::assertNoException() SIGABRT in unrelated tests. A maintainer needs to review the WebKit PR itself and coordinate the merge order (WebKit first, then update WEBKIT_VERSION here to the permanent sha).

Other factors

  • The PR description and author comments explicitly state this must not land while WEBKIT_VERSION points at the preview tag; approving now would contradict that.
  • The 30s timeout on no assertion failures 2 was discussed in a prior thread; the author showed the block already times out on main under debug+ASAN, so it's a pre-existing condition — reasonable to leave for a follow-up split, but a maintainer may still prefer that split happen here.
  • Test coverage on the Bun side is good: chained prototype transitions, property adds after the transition, Symbol.toStringTag combination, and the negative cases (Object.create(null), { __proto__: null }) are all asserted.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-20, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

@robobun robobun closed this Sep 13, 2026
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