Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions Jint.Tests.PublicInterface/HostClrFunctionRealmTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Jint.Native;
using Jint.Runtime.Interop;

namespace Jint.Tests.PublicInterface;

/// <summary>
/// A <see cref="ClrFunction"/> a host builds against an <see cref="Engine"/> must belong to that engine's
/// principal realm, whatever else the engine has been asked to create in the meantime.
/// </summary>
/// <remarks>
/// The engine keeps the first intrinsics it built so that constructing a host function does not need a realm
/// passed in. Constructing a <c>ShadowRealm</c> — or <c>$262.createRealm()</c> under test262 — builds a second
/// set, and that assignment used to be unconditional, so every host function created afterwards took its
/// prototype from a realm the surrounding script cannot reach. It is reachable through an ordinary pattern:
/// a lazily registered global materializes on first read, which can easily be after a script has constructed
/// a shadow realm.
/// </remarks>
public class HostClrFunctionRealmTests
{
[Fact]
public void AHostFunctionBuiltBeforeAnyOtherRealmBelongsToTheMainRealm()
{
var engine = new Engine();

engine.SetValue("hostFn", new ClrFunction(engine, "before", static (_, _) => JsValue.Undefined));

engine.Evaluate("Object.getPrototypeOf(hostFn) === Function.prototype").AsBoolean().Should().BeTrue();
}

[Fact]
public void AHostFunctionBuiltAfterAShadowRealmStillBelongsToTheMainRealm()
{
var engine = new Engine();
engine.Evaluate("new ShadowRealm()");

engine.SetValue("hostFn", new ClrFunction(engine, "after", static (_, _) => JsValue.Undefined));

engine.Evaluate("Object.getPrototypeOf(hostFn) === Function.prototype").AsBoolean().Should()
.BeTrue("a host function belongs to the engine's principal realm, not to whichever realm was created last");
}

[Fact]
public void ALazilyRegisteredGlobalMaterializingAfterAShadowRealmIsStillAnOrdinaryFunction()
{
// The reachable shape of the same defect: the global is declared before anything runs and built on
// first read, which here happens after the script has constructed a shadow realm.
var engine = new Engine(options => options.AddLazyGlobal(
"log",
static e => new ClrFunction(e, "log", static (_, _) => JsValue.Undefined)));

engine.Evaluate("new ShadowRealm();");

engine.Evaluate("log instanceof Function").AsBoolean().Should()
.BeTrue("a host global must be a Function of the realm the script is running in");
}
}
12 changes: 10 additions & 2 deletions Jint/Runtime/Intrinsics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,16 @@ internal Intrinsics(Engine engine, Realm realm)
_realm = realm;

// we need to transfer state currently to some initialization, would otherwise require quite the
// ClrFunctionInstance constructor refactoring
_engine._originalIntrinsics = this;
// ClrFunctionInstance constructor refactoring.
//
// Only the first intrinsics built for an engine are its own: that set belongs to the realm
// InitializeHostDefinedRealm creates, which is the engine's principal one and the realm a host means
// when it builds a ClrFunction against the engine. Every later Intrinsics is a *second* realm's —
// ShadowRealm's, or $262.createRealm's — and assigning unconditionally handed the public
// ClrFunction(Engine, ...) constructor that realm's Function.prototype for every function built
// afterwards, so a global materialized after `new ShadowRealm()` came back wearing a prototype from
// a realm the script cannot even reach.
_engine._originalIntrinsics ??= this;

Object = new ObjectConstructor(engine, realm);
Function = new FunctionConstructor(engine, realm, Object.PrototypeObject);
Expand Down