Attribute a cross-realm built-in's errors and result arrays to its own realm - #3049
Merged
Conversation
…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
force-pushed
the
staging/realm-leaks
branch
from
August 18, 2026 17:12
8160460 to
1382e85
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:ValidateArrayLengthused the running realm — the one line inArrayPrototypenot using_realmtoSorted,toReversed,with,toSplicedObjectInstance.GetCallableused the running realm (46 call sites)Iterator.prototypepredicatesArray.prototypewith/toReversed/toSorted/toSpliced,Array.from,Iterator.prototype.toArrayIterator.fromcompared against the running realm's%Iterator%Iterator.fromTypeErrorJSON.rawJSON(Symbol())CreateDynamicFunctionparsed in the caller's realmnew Functionwith a syntax errorGetCallableObjectInstance.GetCallableis deleted and re-declared asprivate protectedonPrototype,FunctionandArrayPrototype, using_realm.An
internal virtual OwnRealmonObjectInstancewas considered and rejected: it conflates "the realm of the object this was invoked on" with "the callee realm", and those coincide only whenthisis the built-in. Four of the 46 sites run withthis= the receiver, soArray.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.Realmexplicitly 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 twoprivate protectedconstructors gained a trailing optionalArrayConstructor?; the publicJsArray(Engine, …)constructors are unchanged in signature and behaviour (they pass nothing). Two newinternal JsArray(ArrayConstructor, …)constructors supply one, andArrayCreate,ArrayCreateLazyand bothConstructFastoverloads route through it, so_constructorand_prototypecan no longer disagree about realm.Performance
Every changed site trades
_engine.Realm— two field loads plus an execution-context stack peek — for a_realmfield load, so all of it is neutral-to-cheaper. The one addition is a null test per array construction, replacing a two-hopengine.Realm.Intrinsics.Arraywalk 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 changeEngine.Intrinsics/Engine.Global— public surface — for a hostClrFunctioninvoked cross-realm.Tests
Jint.Tests/Runtime/CrossRealmAttributionTests.cs— 36 tests viaShadowRealm(public API;$262is internal), 26 failing against unfixed code, asserting both shapes: an error isinstanceofthe callee realm's constructor, and a result array isinstanceofthe callee realm'sArray.test262: 102,350 passed, 0 failed (+24; twelve files × two modes).
Two corrections to the issue's grouping:
syntax/yield-as-identifier.jsis a realm bug, not a parser one, and is moved out of the parser block.TypedArray/set-wrapped.jsandconstructor-buffer-sequence.jsstay — both fail on$262missing from created realms, which #3044 addresses, so expect a conflict with that PR in this block.Refs #3021.