Repository navigation
fix(runtime): make this.super aliases resolve their receiver's wrapper at use - #570
edusperoni wants to merge 2 commits into
Conversation
…r at use `this.super` on an extend() class returned an object whose internal field 0 was a copy of the receiver's External, a raw ObjCDataWrapper pointer. The alias held no reference to its receiver and nothing told it when that wrapper was deleted, so an alias JS kept longer than the wrapper read freed memory: after the receiver was collected, after __releaseNativeCounterpart(receiver), or when saved from a JS method run by -dealloc (whose borrowed wrapper is deleted when the call returns). Under AddressSanitizer this is a heap-use-after-free in ArgConverter::Invoke. The alias now holds the receiver itself in internal field 0 (a strong JS reference, so the receiver lives at least as long as the alias), and internal field 1 is still the string "super" that identifies an alias. GetValue follows an alias to its receiver only when field 0 is not an External, so the common path does no extra work. A receiver whose wrapper was released or detached reads as released through the alias, with the existing released-object error. - The accessor resolves a Proxy receiver to its target, and an alias receiver to that alias's receiver, so an alias never points at another alias. A receiver that is not a native instance throws a TypeError instead of failing a V8 internal-field check. - __releaseNativeCounterpart(alias) releases the receiver, exactly as __releaseNativeCounterpart(receiver) does; the alias is never written to. - ObjectManager::DisposeValue skips aliases on final disposal too: an alias owns no wrapper, and disposing it at isolate teardown freed the receiver's. Fixes #568
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
`super` is an enumerable accessor on an extend() implementation object,
so generic code that copies the properties of that object or of the
class prototype (Object.assign, spread, clone helpers) calls the getter
with a receiver that is not a native instance. Throwing there broke
those helpers; the runtime's other native accessors read undefined for
such a receiver, and the `super` getter now does the same.
The alias's prototype is now taken from the resolved receiver's chain.
Called on an alias (getter.call(alias), or through a Proxy over one),
the getter walked the alias's own chain, which already starts at the
base class prototype, so the alias of an alias got a prototype three
levels above the receiver's `super`. Each step of the walk is checked,
so a receiver whose chain is shorter than an extended instance's reads
undefined instead of dereferencing a null prototype.
Helpers.h now states that GetValue follows an alias to its receiver
while SetValue and DeleteValue do not, so a write-back to an object
reached through GetValue must resolve the alias first.
Specs:
- worker teardown with an object and a kept alias, and with only the
initializer's alias: both abort under AddressSanitizer in
BaseDataWrapper::IsGcProtected <- ObjectManager::DisposeValue <-
DisposeAllRegistered <- ~Runtime on the worker thread with the
runtime sources from before the alias fix;
- the alias of an alias resolves to the original receiver, and a second
__releaseNativeCounterpart reports "not a native wrapper";
- `super` reads undefined on {}, the implementation object and the
class prototype, and Object.assign over the implementation copies it;
- reads through a released alias must throw the disposed-object error,
and hash comparisons first assert a numeric hash.
What
Fixes #568.
this.superon an.extend()class returns a new object on every access. Its internal field 0 was a copy of the receiver's: theExternalholding the receiver'sObjCDataWrapper*. The alias held no reference to its receiver, and nothing told it when that wrapper was deleted. An alias that outlived the wrapper read freed memory:__releaseNativeCounterpart(receiver);-dealloc, whose borrowed wrapper is deleted when the call returns (fix(runtime): release wrappers made for JS methods called from native code #564);this.supersince its last major collection could crash when terminated, including through a JS initializer that callsthis.super.init().Under AddressSanitizer these are heap-use-after-free reports in
ArgConverter::Invokeand inObjectManager::DisposeValue.Fix
The alias holds its receiver, not a copy of the wrapper pointer:
"super", which is how the runtime recognises an alias. That check is unchanged.tns::GetValuefollows an alias to its receiver when it is used. It only does so when field 0 is not anExternal, which used to return null, so the common path does the same work as before.A receiver whose wrapper was released or detached now reads as released through the alias. In Debug that is the existing "disposed native object" error; Release builds assert, as for any released object.
Other changes that follow from the alias no longer carrying a wrapper:
ObjectManager::DisposeValueskips aliases on final disposal too, which is what fixes the worker-teardown crash.__releaseNativeCounterpart(obj.super)releases the receiver, exactly as__releaseNativeCounterpart(obj)does. The alias is never written to. Before, it cleared only the alias and left the receiver pointing at a deleted wrapper.tns::SuperAliasReceiver(obj)returns an alias's receiver, or empty for anything else.tns::SetValueandtns::DeleteValuedo not follow aliases; a comment inHelpers.hsays a write-back must resolve the alias first.This only concerns
.extend()classes.super.method()in ES and TypeScript classes doesn't usethis.super.Behaviour change:
superon a receiver that is not a native instancesuperis an enumerable accessor on the implementation object, so generic code reads it with a non-instance receiver:Object.assign({}, implementation), spread, orExt.prototype.super. It now readsundefined, like the runtime's other native accessors. Before, it returned an object that read as released; the runtime is built without V8's API checks, so the internal-field read on a plain object went unchecked.Not changed
Each
this.superaccess still registers a weak handle and finalizer for the alias, whose disposal does nothing. That cost was already there.Tests
New
SuperAliasTests.js, 10 specs:__releaseNativeCounterpart(obj);-deallocreads as released;__releaseNativeCounterpart(alias)releases the receiver: the object, the alias and a newobj.superall read as released, with one deallocation;__releaseNativeCounterpartreports "not a native wrapper";superreadsundefinedon{}, the implementation object and the class prototype, andObject.assignover the implementation doesn't throw;"Reads as released" requires the disposed-object error, and hash comparisons first assert a numeric hash. The existing
this.supercoverage inInheritance/InheritanceTests.jsstill passes.Results:
ArgConverter::Invoke, reading the freed wrapper, in the__releaseNativeCounterpart(obj)spec; with that spec skipped, the same report from the keep-alive spec;BaseDataWrapper::IsGcProtected←ObjectManager::DisposeValue←DisposeAllRegistered←~Runtime.Summary by CodeRabbit
undefinedwhen accessing the superclass accessor.