Skip to content

Attribute a cross-realm built-in's errors and result arrays to its own realm - #3049

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:staging/realm-leaks
Aug 19, 2026
Merged

Attribute a cross-realm built-in's errors and result arrays to its own realm#3049
lahma merged 1 commit into
sebastienros:mainfrom
lahma:staging/realm-leaks

Conversation

@lahma

@lahma lahma commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

When a built-in belonging to one realm is called from another, the error it raises and the array it returns must come from its own realm, not from whichever realm happens to be running.

The existing exclusion comment said Jint builds errors from the current realm in general. That is not so — 913 of 1332 Throw.*Error(...) call sites already pass the built-in's own _realm, and script functions push the callee realm properly. What existed was a handful of specific leaks:

Leak Site
ValidateArrayLength used the running realm — the one line in ArrayPrototype not using _realm toSorted, toReversed, with, toSpliced
ObjectInstance.GetCallable used the running realm (46 call sites) the five Iterator.prototype predicates
A result array adopted the running realm's Array.prototype with/toReversed/toSorted/toSpliced, Array.from, Iterator.prototype.toArray
Iterator.from compared against the running realm's %Iterator% Iterator.from
A realm-less coercion TypeError JSON.rawJSON(Symbol())
CreateDynamicFunction parsed in the caller's realm new Function with a syntax error

GetCallable

ObjectInstance.GetCallable is deleted and re-declared as private protected on Prototype, Function and ArrayPrototype, using _realm.

An internal virtual OwnRealm on ObjectInstance was considered and rejected: it conflates "the realm of the object this was invoked on" with "the callee realm", and those coincide only when this is the built-in. Four of the 46 sites run with this = the receiver, so Array.prototype.map.call(someFunction, notCallable) would have started throwing in the receiver function's realm — a new, differently-wrong answer. Moving the method instead makes the compiler find those four: they no longer compile, and now spell _engine.Realm explicitly with a comment saying no callee realm is reachable there. That gap is real, pre-existing and unchanged, but it is now visible rather than silently wrong.

Result arrays, without touching public API

ArrayInstance's two private protected constructors gained a trailing optional ArrayConstructor?; the public JsArray(Engine, …) constructors are unchanged in signature and behaviour (they pass nothing). Two new internal JsArray(ArrayConstructor, …) constructors supply one, and ArrayCreate, ArrayCreateLazy and both ConstructFast overloads route through it, so _constructor and _prototype can no longer disagree about realm.

Performance

Every changed site trades _engine.Realm — two field loads plus an execution-context stack peek — for a _realm field load, so all of it is neutral-to-cheaper. The one addition is a null test per array construction, replacing a two-hop engine.Realm.Intrinsics.Array walk whenever a constructor is supplied.

Deliberately not done: making built-in calls push an ExecutionContext. That is the spec-shaped fix, but it costs a push/pop per built-in call on the leaf lane engineered to have none, and it would change Engine.Intrinsics/Engine.Global — public surface — for a host ClrFunction invoked cross-realm.

Tests

Jint.Tests/Runtime/CrossRealmAttributionTests.cs — 36 tests via ShadowRealm (public API; $262 is internal), 26 failing against unfixed code, asserting both shapes: an error is instanceof the callee realm's constructor, and a result array is instanceof the callee realm's Array.

test262: 102,350 passed, 0 failed (+24; twelve files × two modes).

Two corrections to the issue's grouping: syntax/yield-as-identifier.js is a realm bug, not a parser one, and is moved out of the parser block. TypedArray/set-wrapped.js and constructor-buffer-sequence.js stay — both fail on $262 missing from created realms, which #3044 addresses, so expect a conflict with that PR in this block.

Refs #3021.

…n realm

A built-in must attribute what it raises and what it creates to its own
[[Realm]], not to whichever realm happens to be running. Most of Jint
already does: 913 of 1332 Throw.*Error call sites pass the built-in's own
_realm. These are the sites that did not.

- Array.prototype's ValidateArrayLength raised its RangeError from
  _engine.Realm - the one line in that file not using _realm. It is what
  toSorted, toReversed, with and toSpliced all reach.

- ObjectInstance.GetCallable resolved every built-in's callback argument
  against _engine.Realm, over 46 call sites. It is removed and re-declared
  on Prototype, Function and ArrayPrototype, the three types that know
  their realm; the receiver-typed callers left behind (ObjectInstance's
  generic find, ArrayInstance's Map/Filter/FindWithCallback) now say
  _engine.Realm at the call site, so the one place no callee realm is
  reachable is visible rather than hidden behind a helper.

- Every array a built-in creates took the active realm's Array.prototype,
  because ArrayInstance's constructors read engine.Realm.Intrinsics.Array.
  They gain an optional ArrayConstructor, and JsArray gains two internal
  constructors that supply one. The public JsArray(Engine, ...) pair is
  untouched in signature and in behaviour: a null constructor still means
  the running realm.

- Iterator.from compared against the running realm's %Iterator% and built
  its wrapper from the running realm's %WrapForValidIteratorPrototype%;
  Iterator.prototype.toArray built its result from the running realm's
  %Array%.

- JSON.rawJSON(Symbol()) surfaced the realm-less TypeError that
  TypeConverter.ToString throws, which the statement list then attributed
  to the running realm. It is caught and re-raised through _realm, the
  pattern ArrayConstructor already uses.

- CreateDynamicFunction parsed through ParseScriptGuarded(callerRealm),
  so a syntax error in new Function(...) came from the caller. Step 21
  throws in the current realm, which for a built-in is its own [[Realm]] -
  and since Jint pushes no execution context for a built-in call,
  _engine.ExecutionContext still describes the caller there.

All of these read a _realm field where they used to read _engine.Realm,
which is one field load instead of two plus a stack peek, so nothing here
costs anything; the array constructors gain a single null test.

Frees twelve staging/sm files, verified individually against the pinned
test262 checkout in both modes. staging/sm/syntax/yield-as-identifier.js
was misfiled under parser early errors - Acornima raises the SyntaxError
correctly, it just came from the wrong realm - so it moves out of that
block. The cross-realm block keeps the two TypedArray files, whose actual
blocker is that a created realm's global carries no $262 of its own; the
banner now says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lahma
lahma force-pushed the staging/realm-leaks branch from 8160460 to 1382e85 Compare August 18, 2026 17:12
@lahma
lahma merged commit 96db032 into sebastienros:main Aug 19, 2026
4 of 5 checks passed
@lahma
lahma deleted the staging/realm-leaks branch August 19, 2026 10:28
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