Skip to content

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

Open

Description

@edusperoni

Summary

this.super on an .extend() class returns a new object on every access (ClassBuilder::SuperAccessorGetterCallback). Its internal field is a copy of the receiver's: the External that holds the receiver's ObjCDataWrapper*. The alias does not keep the receiver alive, and nothing tells it when that wrapper is deleted.

If JS keeps the alias longer than the receiver's wrapper lives, a call through it reads a deleted wrapper.

How the wrapper goes away under a live alias

  1. The receiver is collected. The alias holds no reference to it:

    let s = obj.super;
    obj = null;
    // after a collection, obj's finalizer deletes the wrapper
    s.description; // reads the deleted wrapper

    ObjectManager::DisposeValue skips the alias itself ("Do not dispose the ObjCWrapper contained in a super instance"), so the alias survives with the stale pointer.

  2. __releaseNativeCounterpart(obj) deletes the wrapper right away, with the same result for an alias taken earlier.

  3. A JS method run by -dealloc (after fix(runtime): release wrappers made for JS methods called from native code #564). The receiver gets a borrowed wrapper that is detached and deleted when the call returns. this then reads as released, but an alias saved from that call still points at the deleted wrapper:

    const Keeper = TNSApi.extend({
      methodCalledInDealloc() { saved = this.super; },
    });

    Before fix(runtime): release wrappers made for JS methods called from native code #564 the wrapper stayed in this case but the object was already freed, so the alias was unsafe there too.

Suggested fix

The alias should find the wrapper through its receiver when it is used, not copy the pointer when it is created. For example, it can hold the receiver and have the wrapper lookup follow it. Then:

  • a receiver that was released or detached reads as released through the alias, with the existing released-object error;
  • the alias keeps its receiver alive for as long as JS can reach the alias.

Tests to add

  • An alias taken from a live object, used after the object's wrapper is released with __releaseNativeCounterpart: it reports the object as released.
  • An alias kept after its receiver is dropped and several collections ran: it still works (the receiver is kept alive) or reports released, but never touches freed memory. Run under AddressSanitizer.
  • An alias saved from a JS method run by -dealloc: it reports released.

Found by reading the code paths; not reproduced yet.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions