JSC napi: stop polluting global Object.prototype from napi_define_class#174
Conversation
JSObjectMakeConstructor(ctx, nullptr, ...) returns a function whose
`prototype` property points at the global Object.prototype. Any napi
caller that writes to constructor.prototype (either internally via
napi_define_class for instance members, or in user code via
ctor.Get("prototype").Set(...)) was mutating Object.prototype itself,
breaking `'X' in {}` and `for..in` across the whole runtime.
The previous workaround set the constructor's `[[Prototype]]` internal
slot to a fresh object and re-routed napi_get_prototype + CallAsConstructor
through it. That kept napi_define_class out of Object.prototype but left
the user-visible `.prototype` property still pointing at Object.prototype,
so user code (e.g. the File polyfill in JsRH#169) hit the pollution.
Fix:
- ConstructorInfo::Create installs a fresh per-class prototype object
as the constructor's `.prototype` PROPERTY (DontEnum|DontDelete) with
a non-enumerable `.constructor` back-reference, matching ECMAScript
semantics for normal constructors.
- CallAsConstructor sets the new instance's [[Prototype]] from the
constructor's `.prototype` property, not from its [[Prototype]] slot.
- napi_define_class looks up the `.prototype` property directly instead
of going through napi_get_prototype (which per spec returns the
[[Prototype]] internal slot = Function.prototype for a constructor).
Adds a regression test using Blob (which uses InstanceAccessor +
InstanceMethod) to assert Blob.prototype is a fresh object and no
Blob instance members leak onto the global Object.prototype.
Fixes BabylonJS#172.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes a JavaScriptCore-specific N-API shim bug where constructors created via napi_define_class could have .prototype aliased to the global Object.prototype, causing instance-member definitions (and user code) to pollute Object.prototype and break property lookups/iteration across the runtime.
Changes:
- Install a fresh per-class object as the constructor’s
.prototypeproperty (and a non-enumerable.constructorback-reference) instead of relying on JSC’s defaultObject.prototypealiasing. - Construct instances using the constructor’s
.prototypeproperty (not the constructor’s[[Prototype]]slot), and makenapi_define_classattach instance members to that.prototypeobject. - Add Blob-focused regression tests to ensure
Blob.prototypeis isolated and instance members do not leak ontoObject.prototype.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
Core/Node-API/Source/js_native_api_javascriptcore.cc |
Corrects JSC constructor/prototype wiring to prevent global Object.prototype pollution and align N-API behavior with JS semantics. |
Tests/UnitTests/Scripts/tests.ts |
Adds regression tests validating per-class prototype isolation and absence of leaked instance members on Object.prototype. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| JSValueRef protoValue{JSObjectGetProperty(ctx, constructor, JSString("prototype"), exception)}; | ||
| if (*exception) { | ||
| return nullptr; | ||
| } | ||
| JSObjectRef prototype{JSValueToObject(ctx, protoValue, exception)}; |
|
Closing this without merging. I confirmed by reading the WebKit source ( The naive fix would be The actually-correct fix is to switch the entire
That's bigger than what makes sense to bundle with #169, so I'm leaving #172 open and #169 will keep its JS-shim workaround for the |
Closes #172.
JSObjectMakeConstructor(ctx, nullptr, ...)returns a function whoseprototypeproperty points at the globalObject.prototype. Any napi caller that writes toconstructor.prototype(either internally vianapi_define_classfor instance members, or in user code viactor.Get("prototype").Set(...)) was mutatingObject.prototypeitself, corrupting'X' in {}/for..inacross the runtime.Previous workaround
The earlier
// BEGIN TODO ... END TODOblock inConstructorInfo::Createset the constructor's[[Prototype]]internal slot to a fresh object and re-routednapi_get_prototype+CallAsConstructorthrough it. That keptnapi_define_class's own instance-property writes out ofObject.prototype, but left the user-visible.prototypeproperty still pointing atObject.prototype. User code (#169) hit the pollution head-on.It also conflated
Object.getPrototypeOf(ctor)(per spec:Function.prototypefor a normal constructor) withctor.prototype(the per-class instance prototype). Two slots, two semantics — the previous code wrote both to the same object.Fix
ConstructorInfo::Createinstalls a fresh per-class prototype object as the constructor's.prototypeproperty (DontEnum|DontDelete) with a non-enumerable.constructorback-reference, matching ECMAScript semantics for normal constructors. The constructor's[[Prototype]]slot is left at JSC's default (Function.prototype).CallAsConstructorsets the new instance's[[Prototype]]from the constructor's.prototypeproperty, not from its[[Prototype]]slot.napi_define_classlooks up the.prototypeproperty directly instead of going throughnapi_get_prototype(which per N-API spec equalsObject.getPrototypeOf, i.e. the[[Prototype]]internal slot =Function.prototypefor a constructor — the old call was a semantic mismatch that only happened to work because of the kludge).napi_get_prototypeitself is unchanged and remains spec-compliant.Regression test
Added two tests in the existing
Blobdescribe block (covers every engine that ships Blob, which is all of them):Blob.prototypeis a fresh per-class object whose[[Prototype]]isObject.prototype(notBlob.prototype === Object.prototypeas before).size,type,arrayBuffer,text,bytes) leak onto the globalObject.prototype— i.e.'size' in {} === false.These tests would have caught the pollution before #169 surfaced it via the File polyfill.
Followup
Once this lands, #169's
JS_PROTOTYPE_CHAIN_SHIMcollapses to a single directObject.setPrototypeOf(File.prototype, Blob.prototype)in C++; theNapi::Eval/env.RunScriptdispatch goes away; and the 14-lineFileReaderdual-StaticValue/InstanceValuecomment shrinks to one line (per #173's recommended WHATWG pattern).