Skip to content

fix(runtime): make this.super aliases resolve their receiver's wrapper at use - #570

Open
edusperoni wants to merge 2 commits into
mainfrom
fix/super-alias-receiver
Open

edusperoni wants to merge 2 commits into
mainfrom
fix/super-alias-receiver

Conversation

@edusperoni

@edusperoni edusperoni commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

What

Fixes #568.

this.super on an .extend() class returns a new object on every access. Its internal field 0 was a copy of the receiver's: the External holding the receiver's ObjCDataWrapper*. 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:

  • after the receiver was collected;
  • after __releaseNativeCounterpart(receiver);
  • when saved from a JS method run by -dealloc, whose borrowed wrapper is deleted when the call returns (fix(runtime): release wrappers made for JS methods called from native code #564);
  • at worker teardown. Teardown disposes registered objects newest first, so an alias was disposed before its receiver. Disposing the alias released the object and deleted the wrapper, and disposing the receiver then read the deleted wrapper. Aliases stay registered until a major collection finalizes them, so any worker that evaluated this.super since its last major collection could crash when terminated, including through a JS initializer that calls this.super.init().

Under AddressSanitizer these are heap-use-after-free reports in ArgConverter::Invoke and in ObjectManager::DisposeValue.

Fix

The alias holds its receiver, not a copy of the wrapper pointer:

  • Internal field 0 of the alias is the receiver object. That is a strong JS reference, so the receiver lives at least as long as the alias.
  • Internal field 1 is still the string "super", which is how the runtime recognises an alias. That check is unchanged.
  • tns::GetValue follows an alias to its receiver when it is used. It only does so when field 0 is not an External, 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::DisposeValue skips 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.
  • A Proxy receiver resolves to its target. Before, it failed a V8 internal-field check and aborted.
  • An alias receiver resolves to that alias's receiver, and the new alias gets the same prototype as the first, so an alias never points at another alias. This is only reachable by calling the getter on an alias.

tns::SuperAliasReceiver(obj) returns an alias's receiver, or empty for anything else. tns::SetValue and tns::DeleteValue do not follow aliases; a comment in Helpers.h says a write-back must resolve the alias first.

This only concerns .extend() classes. super.method() in ES and TypeScript classes doesn't use this.super.

Behaviour change: super on a receiver that is not a native instance

super is an enumerable accessor on the implementation object, so generic code reads it with a non-instance receiver: Object.assign({}, implementation), spread, or Ext.prototype.super. It now reads undefined, 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.super access 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:

  • initializers, methods and property getters dispatch to the superclass through the alias, and the objects are freed once;
  • an alias taken from a live object reads as released after __releaseNativeCounterpart(obj);
  • an alias keeps its receiver alive across collections after every other JS reference is dropped, and the object is freed exactly once when the alias goes;
  • an alias saved from a JS method run by -dealloc reads as released;
  • __releaseNativeCounterpart(alias) releases the receiver: the object, the alias and a new obj.super all read as released, with one deallocation;
  • the alias of a Proxy over the receiver resolves to the receiver;
  • the alias of an alias (and of a Proxy over 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 doesn't throw;
  • a worker holding an object and an alias of it is terminated;
  • a worker holding an object whose only alias was the one its initializer evaluated is terminated.

"Reads as released" requires the disposed-object error, and hash comparisons first assert a numeric hash. The existing this.super coverage in Inheritance/InheritanceTests.js still passes.

Results:

  • With the fix: 1900 specs, 0 failures; under AddressSanitizer, 1900 specs, 0 failures, 0 ASan reports.
  • On the runtime from before this PR, under AddressSanitizer:
    • heap-use-after-free in ArgConverter::Invoke, reading the freed wrapper, in the __releaseNativeCounterpart(obj) spec; with that spec skipped, the same report from the keep-alive spec;
    • each worker-teardown spec, run alone, aborts on the worker thread in BaseDataWrapper::IsGcProtected ← ObjectManager::DisposeValue ← DisposeAllRegistered ← ~Runtime.

Summary by CodeRabbit

  • Bug Fixes
    • Improved superclass method and property access for extended classes, including when accessed through proxies or nested aliases.
    • Fixed cases where superclass aliases could lose their receiver or interfere with native object release and cleanup.
    • Non-native receivers now return undefined when accessing the superclass accessor.
  • Tests
    • Added coverage for superclass dispatch, alias lifetimes, proxy behavior, and cleanup.

…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
@coderabbitai

coderabbitai Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The change makes this.super aliases resolve wrapper access through their receiver. It adjusts alias creation, disposal, and native-counterpart release handling. New tests cover dispatch, proxy and nested aliases, receiver lifetime, release, and worker teardown.

Changes: Super Alias Handling

Layer / File(s) Summary
Alias resolution and access
NativeScript/runtime/ClassBuilder.mm, NativeScript/runtime/Helpers.h, NativeScript/runtime/Helpers.mm, TestRunner/app/tests/SuperAliasTests.js
The accessor resolves a super alias to its receiver, and GetValue attempts wrapper lookup through that receiver. Tests cover superclass dispatch, proxy access, and accessor behavior for non-native receivers.
Alias lifetime and release
NativeScript/runtime/ObjectManager.mm, TestRunner/app/tests/SuperAliasTests.js, TestRunner/app/tests/index.js
Disposal skips "super" aliases, and native-counterpart release resolves an alias to its receiver. Tests cover release, receiver lifetime, aliases created during deallocation, and worker teardown. The test suite now loads SuperAliasTests.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant JavaScript
  participant SuperAccessorGetterCallback
  participant SuperAliasReceiver
  participant GetValue
  JavaScript->>SuperAccessorGetterCallback: access this.super
  SuperAccessorGetterCallback->>SuperAliasReceiver: resolve receiver
  SuperAliasReceiver-->>SuperAccessorGetterCallback: receiver or empty object
  SuperAccessorGetterCallback-->>JavaScript: return super object
  JavaScript->>GetValue: read through super alias
  GetValue->>SuperAliasReceiver: resolve alias receiver
  SuperAliasReceiver-->>GetValue: receiver or empty object
Loading

Suggested reviewers: nathanwalker, adrian-niculescu

Merge Risk: Minimal · up to 04890

This change makes this.super aliases resolve their receiver instead of holding a raw wrapper pointer, so released objects report the disposed-object error rather than risking use-after-free. No merge-blocking risk was identified in the supplied change.

Pre-merge checks: Passed 4

Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title names the main change: this.super aliases now resolve their receiver's wrapper when used, instead of copying a wrapper pointer. The raw summary and PR objectives both center on this change…
Linked Issues check Passed [#568] Requirement: an alias must not hold a raw wrapper pointer that can outlive the wrapper. Evidence at reviewed head: tns::SuperAliasReceiver (Helpers.mm) returns the receiver object from internal…
Out of Scope Changes check Passed Changes map to #568 or to the alias design. Helpers.mm adds SuperAliasReceiver and the GetValue alias path. ObjectManager.mm changes cover alias disposal and __releaseNativeCounterpart on aliases, bot…
Full details: Title check

Explanation

The title names the main change: this.super aliases now resolve their receiver's wrapper when used, instead of copying a wrapper pointer. The raw summary and PR objectives both center on this change. The title follows the conventional commit form and states the change in one sentence.

Full details: Linked Issues check

Explanation

[#568] Requirement: an alias must not hold a raw wrapper pointer that can outlive the wrapper. Evidence at reviewed head: tns::SuperAliasReceiver (Helpers.mm) returns the receiver object from internal field 0 when field 1 is a string and field 0 is a non-External object. tns::GetValue (Helpers.mm) follows that receiver when field 0 is not an External. This matches the PR design, where the alias holds its receiver and resolves the wrapper on use. Per the PR summary, ObjectManager::DisposeValue skips aliases on final disposal, and ReleaseNativeCounterpartCallback resolves an alias to its receiver. Per the PR summary, SuperAliasTests.js covers the three requested scenarios: release through __releaseNativeCounterpart, keep-alive across collections, and -dealloc callback. The PR reports ASan results for these tests. Limitation: the ClassBuilder.mm diff could not be read because changed blobs were missing from the checkout. The ClassBuilder behavior (receiver stored in field 0) rests on the PR summary and raw summary, not on a direct read of the diff.

Full details: Out of Scope Changes check

Explanation

Changes map to #568 or to the alias design. Helpers.mm adds SuperAliasReceiver and the GetValue alias path. ObjectManager.mm changes cover alias disposal and __releaseNativeCounterpart on aliases, both named in the issue. ClassBuilder.mm changes build the alias from the receiver. Proxy and nested-alias resolution follow from the receiver lookup. The behavior change where super returns undefined for non-native receivers is a direct result of no longer copying the wrapper, and the PR discloses it. SuperAliasTests.js and the test index registration are test coverage for the change. Limitation: the ClassBuilder.mm diff could not be read, so its exact changes are established from the PR summary only.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

A rabbit hops where aliases roam,
It finds the receiver, safe at home.
Through proxies, super paths now wind,
And wrappers follow close behind.
Tests watch release, then gather leaves,
While worker teardown quiets the eaves.

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 005237e4-b601-4ef6-87ad-546d119c289e
Commits

Reviewing files that changed from the base of the PR and between b1fc316 and 04890c9.

Files selected for processing: 6

  • NativeScript/runtime/ClassBuilder.mm
  • NativeScript/runtime/Helpers.h
  • NativeScript/runtime/Helpers.mm
  • NativeScript/runtime/ObjectManager.mm
  • TestRunner/app/tests/SuperAliasTests.js
  • TestRunner/app/tests/index.js

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

`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.
@edusperoni
edusperoni marked this pull request as ready for review November 14, 2026 23:17

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

this.super keeps a raw pointer to the receiver's wrapper and can outlive it

1 participant