Skip to content

Give File its own prototype that inherits from Blob.prototype - #30328

Open
Jarred-Sumner wants to merge 1 commit into
mainfrom
claude/fix-25422-file-instanceof-proxy
Open

Jarred-Sumner wants to merge 1 commit into
mainfrom
claude/fix-25422-file-instanceof-proxy

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented May 6, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #25422.
Closes #26899.

What does this PR do?

File.prototype was the same object as Blob.prototype, with a comment saying "This is not quite right. But we'll fix it if someone files an issue about it." To keep new Blob() instanceof File false despite that, the File constructor installed a custom [Symbol.hasInstance] that checked a native flag on the underlying cell instead of walking the prototype chain.

That broke every case where instanceof is supposed to consult the prototype chain:

new Proxy(new File([], "f"), {}) instanceof File                          // false, should be true
Object.create(File.prototype) instanceof File                              // false, should be true
new Proxy({}, { getPrototypeOf: () => File.prototype }) instanceof File    // false, should be true
Object.setPrototypeOf(new File([], "f"), null) instanceof File             // true,  should be false

It also meant new File(...).constructor === Blob, Object.prototype.toString.call(file) === "[object Blob]", and File.prototype === Blob.prototype, none of which match browsers or Node.

This gives File a real prototype chain:

  • JSDOMFilePrototype is a new prototype object whose [[Prototype]] is Blob.prototype. It carries constructor and Symbol.toStringTag; everything else is inherited from Blob.prototype.
  • m_JSDOMFileStructure is a cached Structure on the global object for File instances (same JSBlob cell type, different prototype).
  • new File(...), structuredClone(file), FormData file entries, and Bun.Archive file entries all allocate with that structure.
  • The custom hasInstance and its JSDOMFile__hasInstance Rust export are deleted. instanceof File is now ordinary OrdinaryHasInstance.
Expression Before After Node/browsers
File.prototype === Blob.prototype true false false
Object.getPrototypeOf(File.prototype) === Blob.prototype n/a true true
new File([], "x").constructor Blob File File
Object.prototype.toString.call(new File([], "x")) "[object Blob]" "[object File]" "[object File]"
new Proxy(new File([], "x"), {}) instanceof File false true true
Object.create(File.prototype) instanceof File false true true
Object.create(Blob.prototype) instanceof File false false false
new Blob() instanceof File false false false
structuredClone(file).constructor Blob File File

Bun.file() is unchanged (it is a Blob, not a File).

How did you verify your code works?

27 new tests in test/js/web/fetch/blob.test.ts covering the prototype chain, constructor, Symbol.toStringTag, Object.getPrototypeOf(File) === Blob, proxies (transparent and with traps), Object.create, Object.setPrototypeOf, subclassing, structuredClone, Worker.postMessage into a fresh realm, FormData append/set with named, empty, and omitted filenames, multipart filename="", WebSocket binaryType="blob", and file.slice() / Response(file).blob() / new Blob([file]) staying plain Blobs. Verified fail-before on the unfixed build and pass-after with bun bd test.

Also ran the existing blob, FormData, structured-clone, Archive, globals, inspect, body, and response suites locally with no regressions.

Rebased twice onto main. The second rebase moved the m_JSDOMFileClassStructure init into main's new lazyClassStructureInits table in ZigGlobalObject.cpp and dropped the stale m_JSDOMFileConstructor entry from lazyObjectInits. The .lut.txt entry follows main's renamed m_generatedLazyClasses neighbours.


no test proof · iteration 11 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/web/fetch/blob.test.ts

@robobun

robobun commented May 6, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 11:02 PM PT - Aug 23rd, 2026

✅ @robobun, your commit 9b7d1ea5a517c4165290a1a58f0ffe80798701cf passed in Build #104626! 🎉


🧪   To try this PR locally:

bunx bun-pr 30328

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

bun-30328 --bun

@github-actions github-actions Bot added the claude label May 6, 2026
@coderabbitai

coderabbitai Bot commented May 6, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

JSDOMFile::customHasInstance was rewritten to perform a prototype-chain walk (using getPrototype/global getPrototypeOf semantics) that unwraps ProxyObject targets and consults JSDOMFile__hasInstance for unwrapped Blob-derived objects; a new test suite verifies instanceof File/Blob behavior across proxy and prototype edge cases.

Changes

File instanceof Check Fix

Layer / File(s) Summary
Data / Semantics
src/jsc/bindings/JSDOMFile.cpp
customHasInstance no longer delegates immediately; it walks the candidate's prototype chain via getPrototype(globalObject), honoring proxy getPrototypeOf traps and unwrapping ProxyObject targets.
Core Implementation
src/jsc/bindings/JSDOMFile.cpp
During traversal, unwrapped targets that inherit from WebCore::JSBlob are validated with JSDOMFile__hasInstance and cause rejection if that check fails; otherwise match succeeds only when the constructor's prototype is reached.
Tests
test/js/web/fetch/blob.test.ts
Added describe("File \instanceof` checks")covering proxygetPrototypeOftraps, nested/transparent proxies,Object.create(File.prototype), real FilevsBlob instances, prototype stripping, explicit non-Fileprototypes, primitives, andFile` subclass behavior.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The description links issues that directly match the prototype-chain and instanceof behavior addressed by the changes.
Out of Scope Changes check ✅ Passed The implementation and tests directly support the stated File prototype and instanceof objectives.
Title check ✅ Passed The title clearly summarizes the main change: giving File.prototype its own prototype that inherits from Blob.prototype.
Description check ✅ Passed The description includes both required sections and provides detailed implementation context, expected behavior, and verification results.

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

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fix(web): give File a distinct prototype from Blob #26900 - Both PRs modify JSDOMFile.cpp to fix instanceof File behavior. fix(web): give File a distinct prototype from Blob #26900 takes a more comprehensive approach by giving File a distinct prototype from Blob (creating JSDOMFilePrototype), which would also fix the Proxy/Object.create instanceof cases addressed here, since the standard OrdinaryHasInstance walk would work correctly once File has its own prototype. Both PRs would conflict in JSDOMFile.cpp.

🤖 Generated with Claude Code

Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated

@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: 2

🤖 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/JSDOMFile.cpp`:
- Around line 62-79: The current code unwraps proxies and then rejects any
unwrapped object that inherits<WebCore::JSBlob>(), but JSDOMFile__hasInstance is
only called on the original (possibly proxy) value, causing real File instances
wrapped in proxies to be rejected; fix by unwrapping proxies first (using
asObject + dynamicDowncast<ProxyObject> loop) before calling
JSDOMFile__hasInstance (or call JSDOMFile__hasInstance on both the original
value and the unwrapped object) so that proxy-wrapped real File objects are
recognized as instances, and keep the inherits<WebCore::JSBlob>() rejection only
for true Blob instances that are not File according to JSDOMFile__hasInstance.

In `@test/js/web/fetch/blob.test.ts`:
- Around line 358-371: Add a new test in the same suite that constructs a real
File (e.g., new File(["hi"], "a.txt")) wrapped in a transparent Proxy and
asserts the proxy is not instanceof File but is instanceof Blob; mirror the
existing pattern used for Blob (variables like blob/proxy/nested) so you cover
Proxy(new File(...)) and nested proxies to catch regressions in the native guard
for proxied File instances.
🪄 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: b5d8e986-d6e4-437a-948a-ebf3d56f8689

📥 Commits

Reviewing files that changed from the base of the PR and between 9fe5f12 and 9a1045d.

📒 Files selected for processing (2)
  • src/jsc/bindings/JSDOMFile.cpp
  • test/js/web/fetch/blob.test.ts

Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated
Comment thread test/js/web/fetch/blob.test.ts Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This isn't a great fix. @robobun rewrite this PR so that it does the class inheritance correctly instead.

@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

Rewritten per review: File has its own prototype that inherits from Blob.prototype, backed by a LazyClassStructure. The customHasInstance hack is removed. Object.getPrototypeOf(File) === Blob. file.slice(), new Response(file).blob(), new Blob([file]), and WebSocket binaryType="blob" frames return plain Blobs; new File(...), structuredClone(file), Worker.postMessage(file), and FormData file entries (including empty/omitted filename and multipart filename="") are Files.

27 new tests in blob.test.ts; existing blob/FormData/structured-clone/Archive/globals/body/response/inspect suites pass.

Diff is green at 7ff518e. CI build 85009 shows only unrelated parallel-batch flakes (spawn/socket/TLS timing, all passed on retry). Ready for review.

@robobun
robobun force-pushed the claude/fix-25422-file-instanceof-proxy branch from 90b01e8 to 1e6ee27 Compare July 29, 2026 01:12
@robobun robobun changed the title Fix instanceof File for proxies and prototype-chain objects Give File its own prototype that inherits from Blob.prototype Jul 29, 2026
Comment thread src/runtime/webcore/Blob.rs
Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp
Comment thread src/runtime/webcore/Blob.rs
Comment thread src/jsc/bindings/blob.cpp Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated
Comment thread src/jsc/webcore_types.rs Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated
Comment thread test/js/web/fetch/blob.test.ts
Comment thread src/jsc/bindings/blob.cpp
Comment thread test/js/web/fetch/blob.test.ts Outdated
Comment thread src/jsc/bindings/blob.cpp
Comment thread src/jsc/bindings/JSDOMFile.cpp
Comment thread test/js/web/fetch/blob.test.ts

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

No new issues found; all earlier threads (isNull vs isEmpty, the third DOMFormData filename producer, reportExtraMemoryAllocated, test onerror/onclose wiring, fd.set coverage, dead include) are addressed in 7b99d88. Deferring to a human because this rewires File's prototype/structure on ZigGlobalObject and changes which wrapper every WebCore::Blob toJS path allocates — worth a maintainer glance.

Checked: LazyClassStructure is in FOR_EACH_GLOBALOBJECT_GC_MEMBER so it's visited; reifyStaticProperties passes JSBlob::info() as classForThis, which matches the instance cell type; dupe_with_content_type/the sibling clone reset is_jsdom_file so file.slice() / new Blob([file]) / response.blob() stay plain Blobs (all covered by tests); toJS now calls Blob__setAsFile on the dupe rather than the shared impl, which is strictly safer.

Extended reasoning...

Overview

Gives File a real prototype chain: adds JSDOMFilePrototype (inheriting from Blob.prototype), replaces m_JSDOMFileConstructor (LazyProperty<JSObject>) with m_JSDOMFileClassStructure (LazyClassStructure) on ZigGlobalObject, deletes the custom [Symbol.hasInstance] and its Rust export, and routes every File-producing path (new File, structuredClone, FormData entries, toJS/toJSNewlyCreated, BlobExt::to_js when is_jsdom_file) through the new JSDOMFileStructure. blob.cpp toJS now branches on fileName().isNull() to keep WebSocket binaryType='blob' frames as plain Blobs, and DOMFormData::append/set normalize a null filename to emptyString() so all three FormData filename producers agree. dupe_with_content_type and the response-body clone site reset is_jsdom_file to false so slices/blob() results don't inherit File-ness. ~280 lines of new tests.

Security risks

None identified. No untrusted-input parsing changes; the DOMFormData edit only normalizes null → empty on an already-validated filename.

Level of scrutiny

High. This touches ZigGlobalObject.{h,cpp,lut.txt} (a header included nearly everywhere, with an explicit "be very cautious" warning), adds a GC-visited LazyClassStructure, hand-writes a JSC prototype class, and changes the wrapper-structure selection for every C++-side Blob that reaches JS. Several subtle bugs were found and fixed across seven review rounds (proxy unwrapping in the abandoned first approach, isEmpty vs isNull, the multipart filename="" producer, missing reportExtraMemoryAllocated), which is evidence the change is not mechanical.

Other factors

All 33 inline threads are resolved. A maintainer (Jarred) was engaged through the May iterations but has not signed off on the July refresh. Test coverage is thorough (prototype chain, proxies, Object.create, subclassing, structuredClone, FormData.append/set × empty/omitted filename, multipart parse, file.slice(), response.blob(), WebSocket blob frames, cross-Worker postMessage). The LazyClassStructure addition is exactly the kind of member the header comment says is acceptable, and it's placed in the visitor macro, so GC rooting looks correct — but given the scope and the number of subtle iterations it took to converge, I'd rather a human confirm the final shape than shadow-approve.

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

No new issues found on this pass — all earlier threads (empty-vs-null filename, reportExtraMemoryAllocated, dead include, fd.set coverage, test error-wiring) look addressed. Deferring to a human for final sign-off since this is a non-trivial JSC binding change: new JSDOMFilePrototype class + LazyClassStructure wiring, a new BUN__createJSDOMFile FFI entry, and the fileName().isNull() File-vs-Blob discrimination in blob.cpp that took a few rounds to settle.

What was reviewed:

  • isNull() gate in blob.cpp against all three filename producers (JS append/set, multipart parser via DOMFormData::append) — the null→empty normalization now covers each.
  • dupe_with_content_type / the new Blob([file]) fast-path resetting is_jsdom_file so slices and re-wraps stay plain Blobs; structuredClone and Worker postMessage round-trips.
  • reportExtraMemoryAllocated present at both BUN__createJSDOMFile and the constructor path; LazyClassStructure initialization order (Blob prototype/constructor are forced before File's).
Extended reasoning...

Overview

The PR gives File its own prototype object that inherits from Blob.prototype, replacing the previous hack where File.prototype === Blob.prototype and a custom [Symbol.hasInstance] was installed on the File constructor. Ten files touched: a new JSDOMFilePrototype C++ class with name/lastModified accessors and Symbol.toStringTag, m_JSDOMFileClassStructure on ZigGlobalObject (replacing the old m_JSDOMFileConstructor lazy property + custom getter/setter and .lut.txt CellProperty entry), a new BUN__createJSDOMFile extern-C allocator, blob.cpp's toJS/toJSNewlyCreated branching on fileName().isNull() to pick the File vs Blob structure, DOMFormData::append/set normalizing a null filename to emptyString(), Rust-side is_jsdom_file reset in dupe_with_content_type and the new Blob([file]) fast-path, removal of the JSDOMFile__hasInstance Rust export, and ~280 lines of new tests.

Security risks

None identified. No auth/crypto/permissions surface. The change is prototype-chain shape and wrapper-structure selection; user input reaches it only as filenames already flowing through existing FormData/multipart paths.

Level of scrutiny

Medium-high. This is hand-written JSC binding C++: a new prototype class, LazyClassStructure initialization that cross-references the Blob class structure, a new FFI entry point, and a semantic gate (isNull()) whose correctness depends on every filename producer agreeing on null-vs-empty. Two of those producers were found wrong in earlier review rounds and fixed, which is exactly why a maintainer should confirm the final shape. It also changes user-visible Web API behavior (instanceof, constructor, Symbol.toStringTag, Object.getPrototypeOf(File)), so compat sign-off matters.

Other factors

Test coverage is thorough (25+ new cases spanning proxies, subclassing, structuredClone, FormData append/set with empty/omitted filenames, multipart filename="", WebSocket binaryType='blob', Worker postMessage in a fresh realm, file.slice()/Response(file).blob() staying plain Blobs). Every prior inline thread — including several from a maintainer during the May iteration and the July null/empty and extra-memory-reporting rounds — is resolved, and the last two commits directly addressed the final nits (dead include, fd.set coverage, name setter). Given the scope and the GC-adjacent C++ involved, I'd rather a human give the final approval than auto-approve.

@robobun

robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

Rewritten per review: File has its own prototype that inherits from Blob.prototype, backed by a LazyClassStructure. The customHasInstance hack is removed. Object.getPrototypeOf(File) === Blob. file.slice(), new Response(file).blob(), new Blob([file]), and WebSocket binaryType="blob" frames return plain Blobs; new File(...), structuredClone(file), Worker.postMessage(file), and FormData file entries (including empty/omitted filename and multipart filename="") are Files.

27 new tests in blob.test.ts; existing blob/FormData/structured-clone/Archive/globals/inspect suites pass.

Rebased onto current main (9b7d1ea). Ready for review.

@robobun
robobun force-pushed the claude/fix-25422-file-instanceof-proxy branch 2 times, most recently from 47a93f3 to daee436 Compare August 21, 2026 00:39

@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 reviewed this PR again after the 08-12 rebase and didn't find any new issues. All prior feedback is addressed — the dead FunctionPrototype.h include is gone, BUN__createJSDOMFile and the constructor both call reportExtraMemoryAllocated, blob.cpp gates on .isNull() with DOMFormData::append/set normalizing null→empty, and the WebSocket/Worker tests wire onclose/onerror and use try/finally. Because this restructures the File/Blob prototype chain and structure caching across the JSC bindings, Rust FFI, and the global lut, a human sign-off is still worthwhile.

What was reviewed:

  • JSDOMFilePrototype/JSDOMFileConstructor setup and LazyClassStructure wiring — prototype inherits from Blob.prototype, constructor's [[Prototype]] is Blob, instance structure reuses JSBlob.
  • blob.cpp toJS/toJSNewlyCreated routing on fileName().isNull() and the three FormData filename producers agreeing on null vs empty.
  • dupe_with_content_type and the single-Blob-part fast path now clear is_jsdom_file so file.slice() / new Blob([file]) / Response(file).blob() stay plain Blobs.
  • Deleted JSDOMFile__hasInstance export has no remaining callers; BUN__createJSDOMFile FFI signature matches on both sides.
Extended reasoning...

Overview

This PR gives File its own prototype object that inherits from Blob.prototype, replacing the previous hack where File.prototype === Blob.prototype and a custom [Symbol.hasInstance] on the File constructor faked the distinction. Changes span 10 files: a new JSDOMFilePrototype/JSDOMFileConstructor pair backed by a LazyClassStructure on ZigGlobalObject, a new JSDOMFileStructure() accessor and BUN__createJSDOMFile extern, routing in blob.cpp's toJS/toJSNewlyCreated to pick File vs Blob prototype based on whether m_fileName is null, DOMFormData::append/set normalizing a null filename to the empty string, two Rust-side sites (dupe_with_content_type and the single-Blob-part fast path in Blob.rs) clearing is_jsdom_file on copies, a new is_jsdom_file branch in BlobExt::to_js, removal of the JSDOMFile__hasInstance export and the old CellProperty lut entry, and 27 new tests in blob.test.ts.

Security risks

None identified. The change is entirely about prototype/constructor identity and instance-structure selection for File wrappers. No user input parsing, no auth/crypto/permissions surface. The one place that touches untrusted-ish input — FormData multipart filenames — only changes null-vs-empty normalization, which affects which prototype the wrapper gets, not what bytes are read or written.

Level of scrutiny

Medium-high. This is core JSC binding machinery: adding a new LazyClassStructure, a new prototype class with its own ClassInfo and reifyStaticProperties (reusing the generated BlobPrototype__nameGetterWrap/lastModifiedGetterWrap with JSBlob::info() as the DOMAttribute class so the generated getter's jsDynamicCast<JSBlob*> still matches), and rerouting several allocation sites through a new extern. Mistakes here could manifest as GC issues, wrong prototype on cross-realm/Worker instances, or subtle FormData/WebSocket behavior changes. The PR has already been through several review rounds that caught real bugs (the isEmpty vs isNull conflation, the missing reportExtraMemoryAllocated, the third filename producer via WebCore__DOMFormData__appendBlob), which is a signal that the surface is subtle enough to warrant a maintainer look.

Other factors

  • All 12+ inline findings from prior automated runs are marked resolved and I confirmed each fix is present in the current diff.
  • Test coverage is thorough: prototype-chain identity, Proxy (transparent and with traps), Object.create, setPrototypeOf(null), subclassing, structuredClone, Worker.postMessage in a fresh realm, FormData.get for append AND set with empty/omitted filename, multipart filename="", and negative cases (file.slice(), new Response(file).blob(), new Blob([file]), WebSocket binaryType='blob' all stay plain Blobs).
  • I checked that reifyStaticProperties(vm, WebCore::JSBlob::info(), ...) is intentional — the DOMAttribute getters are the generated Blob wrappers, whose jsDynamicCast targets JSBlob, and File instances are JSBlob cells, so passing JSBlob::info() keeps the receiver check consistent.
  • The is_jsdom_file: Cell::new(false) change in dupe_with_content_type looks correct for slice()/Response.blob()/new Blob([file]), and structuredClone(file) still preserves File-ness because deserialization goes through BlobExt::to_js on a Blob whose is_jsdom_file was set from the serialized flag, not through dupe.
  • No outstanding human-reviewer requests; the last human-authored comments were the author's own resolution replies.

Given the breadth (JSC prototype/structure setup, global-object lut changes, cross-language FFI signature) and the number of subtle issues already found during review, I'm deferring rather than approving so a maintainer can give it a final pass.

File.prototype was aliased to Blob.prototype, which forced a custom
[Symbol.hasInstance] on the File constructor to keep
new Blob() instanceof File false. That override bypassed the normal
prototype-chain walk, so Proxy getPrototypeOf traps,
Object.create(File.prototype), and Object.setPrototypeOf(file, null)
all gave the wrong answer.

This adds a JSDOMFilePrototype object whose [[Prototype]] is
Blob.prototype (with own name/lastModified accessors), a
LazyClassStructure on the global object so prototype, structure and
constructor initialize together, and Object.getPrototypeOf(File) is
Blob. new File(...), structuredClone, Worker.postMessage, Archive
entries and FormData file entries allocate with the File structure.
Blob dupes (slice, Response.blob(), new Blob([file])) and WebSocket
binaryType=blob frames stay plain Blobs. The custom hasInstance and
its Rust helper are removed; OrdinaryHasInstance now gives the
correct results.

Fixes #25422
Fixes #26899
@robobun
robobun force-pushed the claude/fix-25422-file-instanceof-proxy branch from daee436 to 9b7d1ea Compare August 24, 2026 04:58

@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 reviewed this PR and didn't find any bugs. All prior review threads (including the maintainer's) are resolved and every earlier finding — the isNull() vs isEmpty() filename gate, the missing reportExtraMemoryAllocated, the fd.set coverage gap, the dead FunctionPrototype.h include, and the test error-wiring nits — has been addressed in the current diff. Because this is a non-trivial JSC bindings change (new LazyClassStructure, per-global cached Structure for File instances, and rerouted toJS paths for FormData/WebSocket/structuredClone), a maintainer look would still be worthwhile.

What was reviewed:

  • JSDOMFilePrototype/JSDOMFileConstructor shape, setConstructor's implicit prototype.constructor wiring, and Object.getPrototypeOf(File) === Blob.
  • blob.cpp toJS/toJSNewlyCreated isNull() gate against all three filename producers (append, set, multipart parser via DOMFormData::append's new normalization).
  • BUN__createJSDOMFile and the constructor both call reportExtraMemoryAllocated; dupe_with_content_type and the single-Blob-part fast path now clear is_jsdom_file so file.slice() / new Blob([file]) stay plain Blobs.
  • Ruled out the finder claim that jsDOMFilePrototypeNameSetter duplicates the generated setter — it's a distinct wrapper reifying name on File.prototype while reusing the generated getter.
Extended reasoning...

Overview

This PR gives File a real prototype (JSDOMFilePrototype) whose [[Prototype]] is Blob.prototype, backed by a new LazyClassStructure (m_JSDOMFileClassStructure) on ZigGlobalObject. The old customHasInstance hack and its JSDOMFile__hasInstance Rust export are deleted; instanceof File is now ordinary OrdinaryHasInstance. It touches: JSDOMFile.cpp/.h (new prototype/constructor classes, BUN__createJSDOMFile extern), ZigGlobalObject.{cpp,h,lut.txt} (LazyClassStructure init/visitation, global File binding moved from CellProperty to ClassStructure), blob.cpp (toJS/toJSNewlyCreated now branch on fileName().isNull() to pick Blob vs File structure), DOMFormData.cpp (normalize null filename → empty so all FormData blob entries wrap as Files), webcore_types.rs/Blob.rs (dupe clears is_jsdom_file; BlobExt::to_js routes File-flagged blobs through BUN__createJSDOMFile; the old JSDOMFile__hasInstance export removed), plus 27 new tests in blob.test.ts.

Security risks

None. This is prototype-chain / structure-selection plumbing for a Web API type; no auth, crypto, filesystem, or network parsing changes. The FormData filename normalization is a null→empty coercion at a layer that already sanitizes surrogates.

Level of scrutiny

High. New JSC cell classes with hand-written HashTableValue tables and reifyStaticProperties, a new cached per-global Structure, GC memory-reporting calls, and rerouted allocation paths across C++↔Rust FFI are exactly the class of change REVIEW.md flags as most-blocked (memory safety, GC rooting, structure caching). It has already been through many iterations with a maintainer and prior automated review, and every raised issue is resolved in the current diff — but the surface area (structuredClone, Worker postMessage into a fresh realm, FormData, WebSocket binaryType='blob', Archive entries, subclassing) is broad enough that this is not a mechanical change I can approve without a human.

Other factors

  • All six Jarred-Sumner inline threads and every prior claude[bot] finding are marked resolved; I verified each fix landed in the current diff (dead include removed, fd.set test cases added, w.onerror/ws.onclose wired, reportExtraMemoryAllocated added at both allocation sites, isNull() gate with DOMFormData::append/set normalizing null→empty).
  • The two is_jsdom_file: Cell::new(false) changes in dupe_with_content_type and the single-Blob-part constructor path are what keep file.slice() / new Blob([file]) / new Response(file).blob() as plain Blobs — covered by tests.
  • Finder agents raised (and verifiers refuted) that jsDOMFilePrototypeNameSetter duplicates the generated BlobPrototype__nameSetterWrap; it does not — it's a separate DOMAttribute setter on File.prototype that forwards to BlobPrototype__setName while reusing the generated getters, so name/lastModified are own accessors on File.prototype per the File API spec.
  • 27 new tests cover the prototype chain, proxies, subclassing, structuredClone, cross-realm Worker, FormData append/set with empty/omitted filenames, multipart filename="", WebSocket blob frames, and the negative cases (slice/Response.blob/new Blob([file]) stay plain Blobs).

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.

instanceof builtin checks fail for proxies using getPrototypeOf trap inconsistensy fileInstance.constructor.name between nodejs and browsers

2 participants