Skip to content

Commit 31d4885

Browse files
lahmaclaude
andauthored
createRealm installs a full $262 on the new realm and returns it (#3044)
test262's INTERPRETING.md says $262.createRealm() "creates a new ECMAScript Realm, defines this API on the new realm's global object, and returns the $262 property of the new realm's global object". Jint returned the bare global and put only `global` and `evalScript` on it, so `otherGlobal.$262` was undefined and `otherGlobal.$262.detachArrayBuffer` unreachable; `other.global` resolved only because the global was made to point at itself. Install(Engine) now delegates to a new Install(Engine, Realm) that builds the whole API out of the target realm's intrinsics and returns it, with the realm's global carrying it under `$262`. Every function on it is created through the realm-pinned ClrFunction constructor, and the $262 object and the %AbstractModuleSource% prototype are created with OrdinaryObjectCreate against the realm's Object.prototype rather than Intrinsics.Object.Construct - a plain construction lands on `new JsObject(engine)`, which takes whichever realm is active, so the object would have come back wearing the caller's prototype. IsHTMLDDA takes a realm for the same reason. createRealm is part of the API it installs, so realms nest, and evalScript now always routes through EvaluateInRealm, which also gains RetainFunctionSourceText. `$262.global` exists at the top level too, which it did not before, and the created realm's global no longer carries a `global` self-reference: across all 284 test262 files that call createRealm, `.global` and `.evalScript` are the only properties ever read off the result, and nothing reads `.global` off the global itself. Frees staging/sm/TypedArray/set-wrapped.js. Its neighbour constructor-buffer-sequence.js gets further but still fails, for a reason that has nothing to do with realms - Jint evaluates ToIndex(length) before the "byteOffset modulo elementSize" RangeError of InitializeTypedArrayFromArrayBuffer step 3 - so it moves to a block of its own with that reason. The cross-realm banner is reworded: error creation is not realm-blind in general, most built-ins already throw through their own _realm; what is left is a handful of paths that take the running realm instead of the called function's, for the throw and for the allocated result alike. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e71e9a7 commit 31d4885

5 files changed

Lines changed: 160 additions & 46 deletions

File tree

Jint.Tests.Test262/Test262Harness.settings.json

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,22 @@
5757
"staging/sm/expressions/nullish-coalescing.js",
5858
"staging/sm/regress/regress-1507322-deep-weakmap.js",
5959

60-
// Cross-realm error identity. $262.createRealm() works, but an error Jint raises while running
61-
// a function belonging to another realm is constructed from the *current* realm's intrinsics,
62-
// so `e instanceof otherGlobal.TypeError` is false. The realm's global also does not carry its
63-
// own $262, so otherGlobal.$262.detachArrayBuffer is undefined. Removed by making error
64-
// creation realm-aware (and by installing $262 into every created realm).
60+
// Cross-realm identity of a built-in's *products*. $262.createRealm() now hands back a full $262
61+
// installed on the new realm's global, so otherGlobal.$262.detachArrayBuffer resolves and the
62+
// harness half of these is done; what is left is engine-side, and it is not that error creation
63+
// ignores realms in general - most built-ins already raise through their own _realm. It is that
64+
// a handful of paths reach for whichever realm happens to be *running* instead of the realm the
65+
// called function belongs to, which shows up two ways:
66+
// - the wrong realm's error. `otherGlobal.Iterator.prototype.every.call(...)` and friends throw
67+
// a TypeError (RangeError for toSorted) built from the caller's intrinsics, so the test sees
68+
// "a different error constructor with the same name" and `e instanceof otherGlobal.TypeError`
69+
// is false.
70+
// - the wrong realm's result. A built-in invoked cross-realm allocates its result from the
71+
// active realm, so `otherGlobal.Array.prototype.with.call([1,2,3], 1, 3)` comes back as an
72+
// instance of *this* Array, `g.Array.from(...)` is not an instance of `g.Array`, and
73+
// `otherGlobal.Iterator.from(iter)` hands the iterator straight back instead of wrapping it.
74+
// Removed by taking the function's own realm on those paths, for both the throw and the
75+
// allocation.
6576
"staging/sm/Array/change-array-by-copy-cross-compartment-create.js",
6677
"staging/sm/Array/change-array-by-copy-errors-from-correct-realm.js",
6778
"staging/sm/Array/from_realms.js",
@@ -73,8 +84,14 @@
7384
"staging/sm/Iterator/prototype/some/error-from-correct-realm.js",
7485
"staging/sm/Iterator/prototype/toArray/create-in-current-realm.js",
7586
"staging/sm/JSON/parse-with-source.js",
87+
88+
// Argument-validation order in the TypedArray-from-ArrayBuffer constructor - despite the file's
89+
// cross-realm cast, nothing about it is realm-specific and it fails the same way on a buffer from
90+
// this realm. InitializeTypedArrayFromArrayBuffer throws the RangeError for a byteOffset that is
91+
// not a multiple of the element size (step 3) before it evaluates ToIndex(length) (step 5); Jint
92+
// runs them the other way round, so `new Int32Array(buffer, 1, poisonedValue)` reports the
93+
// poisoned value's own error instead of the RangeError. Removed by fixing that ordering.
7694
"staging/sm/TypedArray/constructor-buffer-sequence.js",
77-
"staging/sm/TypedArray/set-wrapped.js",
7895

7996
// Legacy Function.prototype.caller / arguments.callee.caller. These are Annex B accessors whose
8097
// sloppy-mode observable behaviour (the calling function, null at the top of the stack, poisoned

Jint.Tests/Runtime/GuardFusionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ public void LooseNullEqualityHonorsHtmlDdaAnnexB()
227227
{
228228
var engine = new Engine();
229229
// an object with the [[IsHTMLDDA]] internal slot (the document.all shape)
230-
var htmlDDA = new Jint.Native.IsHTMLDDA(engine);
230+
var htmlDDA = new Jint.Native.IsHTMLDDA(engine, engine.Realm);
231231
engine.SetValue("htmlDDA", (Jint.Native.JsValue) htmlDDA);
232232

233233
// Annex B: loose equality with null/undefined is true (both orientations, both operators)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#nullable enable
2+
3+
using Jint.Native.Function;
4+
using Jint.Native.Object;
5+
6+
namespace Jint.Tests.Runtime;
7+
8+
/// <summary>
9+
/// test262's INTERPRETING.md specifies that <c>$262.createRealm()</c> "creates a new ECMAScript Realm,
10+
/// defines this API on the new realm's global object, and returns the <c>$262</c> property of the new
11+
/// realm's global object". So the value handed back is a full <c>$262</c> — not the bare global — and the
12+
/// new realm's global carries it, which is how <c>otherGlobal.$262.detachArrayBuffer</c> resolves.
13+
/// </summary>
14+
public class Test262HarnessObjectTests
15+
{
16+
[Fact]
17+
public void CreateRealmReturnsTheNewRealmsOwn262Object()
18+
{
19+
var engine = new Engine();
20+
Test262Object.Install(engine);
21+
22+
var created = engine.Evaluate("$262.createRealm()");
23+
created.Should().BeAssignableTo<ObjectInstance>();
24+
engine.SetValue("other", created);
25+
26+
foreach (var member in new[] { "createRealm", "detachArrayBuffer", "evalScript", "gc" })
27+
{
28+
engine.Evaluate("typeof other." + member).AsString().Should().Be("function", "$262." + member + " must exist on the created realm's $262");
29+
}
30+
31+
engine.Evaluate("typeof other.global").AsString().Should().Be("object");
32+
33+
// The returned object is the new realm's global's own $262, per INTERPRETING.md.
34+
engine.Evaluate("other.global.$262 === other").AsBoolean().Should().BeTrue();
35+
36+
// ... and the top-level $262 knows its own global too.
37+
engine.Evaluate("$262.global === globalThis").AsBoolean().Should().BeTrue();
38+
}
39+
40+
[Fact]
41+
public void CreatedRealmHasItsOwnGlobalAndIntrinsics()
42+
{
43+
var engine = new Engine();
44+
Test262Object.Install(engine);
45+
46+
engine.SetValue("other", engine.Evaluate("$262.createRealm()"));
47+
48+
engine.Evaluate("other.global !== globalThis").AsBoolean().Should().BeTrue();
49+
engine.Evaluate("other.global.Array !== Array").AsBoolean().Should().BeTrue();
50+
engine.Evaluate("other.global.TypeError !== TypeError").AsBoolean().Should().BeTrue();
51+
52+
// Nested realms: the created $262 can create one of its own.
53+
engine.Evaluate("other.createRealm().global !== other.global").AsBoolean().Should().BeTrue();
54+
}
55+
56+
[Fact]
57+
public void CreatedRealmsFunctionsBelongToThatRealm()
58+
{
59+
var engine = new Engine();
60+
Test262Object.Install(engine);
61+
62+
var created = (ObjectInstance) engine.Evaluate("$262.createRealm()");
63+
var newGlobal = (ObjectInstance) created.Get("global");
64+
65+
// The $262 object and every function on it must be built from the new realm's intrinsics,
66+
// never from whichever realm happened to be active when createRealm ran.
67+
created.Prototype.Should().BeSameAs(newGlobal.Get("Object").AsObject().Get("prototype"));
68+
69+
foreach (var member in new[] { "createRealm", "detachArrayBuffer", "evalScript", "gc" })
70+
{
71+
var function = created.Get(member).Should().BeAssignableTo<Function>().Subject;
72+
function._realm.Should().NotBeSameAs(engine.Realm, member + " must not be pinned to the principal realm");
73+
function._realm.GlobalObject.Should().BeSameAs(newGlobal);
74+
}
75+
}
76+
77+
[Fact]
78+
public void CreatedRealmDetachesABufferMadeInThatRealm()
79+
{
80+
var engine = new Engine();
81+
Test262Object.Install(engine);
82+
83+
engine.SetValue("other", engine.Evaluate("$262.createRealm()"));
84+
85+
engine.Evaluate("var buffer = other.evalScript('new ArrayBuffer(8)');");
86+
engine.Evaluate("buffer instanceof other.global.ArrayBuffer").AsBoolean().Should().BeTrue();
87+
engine.Evaluate("buffer.byteLength").AsNumber().Should().Be(8);
88+
89+
engine.Evaluate("other.detachArrayBuffer(buffer);");
90+
engine.Evaluate("buffer.byteLength").AsNumber().Should().Be(0);
91+
}
92+
}

Jint/Native/IsHTMLDDA.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ namespace Jint.Native;
1212
/// </summary>
1313
internal sealed class IsHTMLDDA : ObjectInstance, ICallable
1414
{
15-
internal IsHTMLDDA(Engine engine) : base(engine, ObjectClass.Object, InternalTypes.Object | InternalTypes.IsHTMLDDA | InternalTypes.Callable)
15+
internal IsHTMLDDA(Engine engine, Realm realm) : base(engine, ObjectClass.Object, InternalTypes.Object | InternalTypes.IsHTMLDDA | InternalTypes.Callable)
1616
{
17+
// The base constructor takes the prototype from whichever realm is active; this object may be
18+
// built for another one ($262.createRealm() installs a $262 into the realm it just made).
19+
_prototype = realm.Intrinsics.Object.PrototypeObject;
1720
}
1821

1922
JsValue ICallable.Call(JsValue thisObject, JsCallArguments arguments) => Null;

Jint/Test262Object.cs

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,62 +18,66 @@ internal static class Test262Object
1818
/// <summary>
1919
/// Installs the $262 object into the engine's global scope and returns it.
2020
/// </summary>
21-
public static ObjectInstance Install(Engine engine)
21+
public static ObjectInstance Install(Engine engine) => Install(engine, engine.Realm);
22+
23+
/// <summary>
24+
/// Installs a $262 object onto the global object of <paramref name="realm"/> and returns it.
25+
/// <para>
26+
/// Everything it carries is built from <paramref name="realm"/>'s own intrinsics and pinned to that
27+
/// realm, because <c>$262.createRealm()</c> installs an API into a realm other than the one running.
28+
/// A function left pinned to the caller's realm would report its errors — and hand out its
29+
/// prototypes — from the wrong one.
30+
/// </para>
31+
/// </summary>
32+
public static ObjectInstance Install(Engine engine, Realm realm)
2233
{
23-
var o = engine.Realm.Intrinsics.Object.Construct(Arguments.Empty);
34+
// Not Intrinsics.Object.Construct: for a plain construction that lands on `new JsObject(engine)`,
35+
// which takes the *active* realm's Object.prototype. Name the prototype explicitly instead.
36+
var o = ObjectInstance.OrdinaryObjectCreate(engine, realm.Intrinsics.Object.PrototypeObject);
37+
38+
// "A reference to the global object on which the $262 object lives."
39+
o.FastSetProperty("global", new PropertyDescriptor(realm.GlobalObject, true, true, true));
2440

2541
// %AbstractModuleSource% intrinsic - exposed via $262 for source-phase-imports tests
26-
o.FastSetProperty("AbstractModuleSource", new PropertyDescriptor(CreateAbstractModuleSource(engine), true, true, true));
42+
o.FastSetProperty("AbstractModuleSource", new PropertyDescriptor(CreateAbstractModuleSource(engine, realm), true, true, true));
2743

28-
o.FastSetProperty("evalScript", new PropertyDescriptor(new ClrFunction(engine, "evalScript",
44+
o.FastSetProperty("evalScript", new PropertyDescriptor(new ClrFunction(engine, realm, "evalScript",
2945
(_, args) =>
3046
{
3147
if (args.Length > 1)
3248
{
3349
throw new ArgumentException("only script parsing supported", nameof(args));
3450
}
3551

36-
var script = Engine.PrepareScript(args.At(0).AsString(), options: new ScriptPreparationOptions
37-
{
38-
ParsingOptions = ScriptParsingOptions.Default with { Tolerant = false, RetainFunctionSourceText = true },
39-
});
52+
return EvaluateInRealm(engine, realm, args.At(0).AsString());
53+
}, 0), true, true, true));
4054

41-
return engine.Evaluate(in script);
42-
}), true, true, true));
55+
// Per test262 INTERPRETING.md, createRealm "creates a new ECMAScript Realm, defines this API on
56+
// the new realm's global object, and returns the $262 property of the new realm's global object".
57+
// So the whole API - createRealm included, so realms can nest - goes onto the new global, and the
58+
// new realm's own $262 is what comes back.
59+
o.FastSetProperty("createRealm", new PropertyDescriptor(new ClrFunction(engine, realm, "createRealm",
60+
(_, _) => Install(engine, engine._host.CreateRealm()), 0), true, true, true));
4361

44-
o.FastSetProperty("createRealm", new PropertyDescriptor(new ClrFunction(engine, "createRealm",
45-
(_, _) =>
46-
{
47-
var realm = engine._host.CreateRealm();
48-
var newGlobal = realm.GlobalObject;
49-
newGlobal.Set("global", newGlobal);
50-
// Per test262 INTERPRETING.md, the object returned by createRealm exposes an evalScript
51-
// that evaluates in the new realm. Provide it so cross-realm tests (e.g. FallbackSymbol
52-
// per-realm) can run code against the freshly created realm's intrinsics.
53-
newGlobal.Set("evalScript", new ClrFunction(engine, "evalScript",
54-
(_, args) => EvaluateInRealm(engine, realm, args.At(0).AsString())));
55-
return newGlobal;
56-
}), true, true, true));
57-
58-
o.FastSetProperty("detachArrayBuffer", new PropertyDescriptor(new ClrFunction(engine, "detachArrayBuffer",
62+
o.FastSetProperty("detachArrayBuffer", new PropertyDescriptor(new ClrFunction(engine, realm, "detachArrayBuffer",
5963
(_, args) =>
6064
{
6165
var buffer = (JsArrayBuffer) args.At(0);
6266
buffer.DetachArrayBuffer();
6367
return JsValue.Undefined;
64-
}), true, true, true));
68+
}, 0), true, true, true));
6569

66-
o.FastSetProperty("gc", new PropertyDescriptor(new ClrFunction(engine, "gc",
70+
o.FastSetProperty("gc", new PropertyDescriptor(new ClrFunction(engine, realm, "gc",
6771
(_, _) =>
6872
{
6973
GC.Collect();
7074
GC.WaitForPendingFinalizers();
7175
return JsValue.Undefined;
72-
}), true, true, true));
76+
}, 0), true, true, true));
7377

74-
o.FastSetProperty("IsHTMLDDA", new PropertyDescriptor(new IsHTMLDDA(engine), true, true, true));
78+
o.FastSetProperty("IsHTMLDDA", new PropertyDescriptor(new IsHTMLDDA(engine, realm), true, true, true));
7579

76-
engine.SetValue("$262", o);
80+
realm.GlobalObject.Set("$262", o);
7781
return o;
7882
}
7983

@@ -86,7 +90,7 @@ private static JsValue EvaluateInRealm(Engine engine, Realm realm, string source
8690
{
8791
var script = Engine.PrepareScript(source, options: new ScriptPreparationOptions
8892
{
89-
ParsingOptions = ScriptParsingOptions.Default with { Tolerant = false },
93+
ParsingOptions = ScriptParsingOptions.Default with { Tolerant = false, RetainFunctionSourceText = true },
9094
});
9195

9296
var context = new ExecutionContext(
@@ -114,15 +118,13 @@ private static JsValue EvaluateInRealm(Engine engine, Realm realm, string source
114118
/// Creates the %AbstractModuleSource% intrinsic constructor and prototype.
115119
/// https://tc39.es/proposal-source-phase-imports/#sec-%abstractmodulesource%
116120
/// </summary>
117-
private static ClrFunction CreateAbstractModuleSource(Engine engine)
121+
private static ClrFunction CreateAbstractModuleSource(Engine engine, Realm realm)
118122
{
119-
var realm = engine.Realm;
120-
121-
// Create the prototype object
122-
var proto = engine.Realm.Intrinsics.Object.Construct(Arguments.Empty);
123+
// Create the prototype object (see the note in Install about naming the prototype explicitly)
124+
var proto = ObjectInstance.OrdinaryObjectCreate(engine, realm.Intrinsics.Object.PrototypeObject);
123125

124126
// @@toStringTag getter on prototype
125-
var toStringTagGetter = new ClrFunction(engine, "get [Symbol.toStringTag]", (thisObj, _) =>
127+
var toStringTagGetter = new ClrFunction(engine, realm, "get [Symbol.toStringTag]", (thisObj, _) =>
126128
{
127129
if (thisObj is not ObjectInstance)
128130
{
@@ -139,7 +141,7 @@ private static ClrFunction CreateAbstractModuleSource(Engine engine)
139141
PropertyFlag.Configurable));
140142

141143
// The constructor function that always throws TypeError
142-
var ctor = new ClrFunction(engine, "AbstractModuleSource", (_, _) =>
144+
var ctor = new ClrFunction(engine, realm, "AbstractModuleSource", (_, _) =>
143145
{
144146
Throw.TypeError(realm, "Abstract class constructor %AbstractModuleSource% cannot be invoked");
145147
return JsValue.Undefined;

0 commit comments

Comments
 (0)