Skip to content

-mm=own: a class seen through an interface keeps its field families; interface calls open parameter families - #710

Merged
ASDAlexander77 merged 1 commit into
mainfrom
own-field-families-iface
Oct 28, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
own-field-families-iface

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Under -mm=own, a class whose objects are seen through an interface now keeps its callback field families. This PR also closes a hole where a callback passed through an interface call was never checked. Spec section 58 and the user doc's limits are updated.

Changes

A cast to an interface no longer opens field families. An interface reaches a class's fields only through a vtable built from the class (<Class>.<Interface>..vtbl). That vtable is built where the class is known: in this module, or in a module that names the class, where callableFromOutside of its constructor already opens the family. I checked each path in the IR:

  • A plain field's slot is a ts.PropertyRef of the class, which already opens the family.
  • A function field's slot is a thunk (..vtbl.<field>.thunk) that only loads the field and calls it. Storing through it is refused in every model.
  • castInterfaceToInterface only remaps the source vtable's slots.

A cast to any, a union, unknown or object still opens the family, because another module could cast the object back to the class.

Interface calls open callback parameter families (a fix). A method in an interface's vtable was treated like a class-vtable entry, so callsOf counted its vtable use as a call the family had seen. But ts.InterfaceSymbolRef calls pass arguments the family never sees, from any module the interface reaches. So in this program each's f family was closed and empty, and f(v) was taken to release nothing:

interface Runner { each(a: Item[], f: (v: Item) => void): number; }
// Walker.each: for (...) { const v = a[i]; f(v); s += v.n; }
r.each(pool, drain);   // drain: pool = []

drain frees pool and the element v it holds, and v.n is then read from freed memory. Main compiles this today. Now a new inInterfaceVTable check finds a ..vtbl global whose class's ..instanceOf names a different ..vtbl, and such an entry no longer counts as a seen call.

The cost: a callback parameter of a method an interface reaches stays open, even where the method is called on its class.

Tests

  • own_callback_interface: a class seen as two interfaces. The callback field is stored by the constructor and by a method called on the class. It runs under own, rc, gc and none, and own emits no counting.
  • own_err_callback_interface_param: the use-after-free above, refused at :34:11.
  • own_err_callback_interface_field: a callback passed through an interface's method and stored into the field, refused at :46:11.
  • Full ctest: 6058 of 6058 pass.
  • DefaultLib under own: 163 passed and 11 skipped out of 174, in release and debug, compiled and under the JIT. That's unchanged.

Not in this PR

Two bugs found along the way. Both happen in every model:

  • Assigning to an interface's function-typed field (h.f = show) fails with "saving to constant object", because the slot is a constant thunk.
  • Casting any to an interface the class doesn't implement, then calling through it, crashes with an access violation.

🤖 Generated with Claude Code

…interface calls open parameter families

An interface reaches a class's fields only through vtables built from the class,
so a cast to an interface no longer opens the class's callback field families
(any, unions, unknown and object still do).

A method in an interface's vtable is called through ts.InterfaceSymbolRef with
arguments its callback parameter families never saw; its vtable entry no longer
counts as a seen call. Before, r.each(pool, drain) through an interface compiled
with each's callback family closed and empty, so a call that freed pool's
element was taken to release nothing (spec 58).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit effb2d8 into main Oct 28, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the own-field-families-iface branch October 28, 2026 09:54
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.

1 participant