When working with iframes in my custom browser Omoikane , I kept coming back to the concept of Realms. At first I thought of it as simply “creating a separate JavaScript execution environment for child frames.” However, merely creating a separate environment wasn’t enough. When a parent accesses the child’s Array, when an iframe navigates to a different page, or when a timer that was scheduled in the child later fires, it becomes necessary to determine which Realm’s values are being used for which Document in each case.

In a previous article I discussed a Realm bug discovered during Pointer Lock input dispatch (source). This time, including the surrounding context, I’ll clarify exactly what Realms are separating.

The word “Realm”

The English term realm originally means a kingdom or the territorial domain ruled by a monarch. The American Heritage Dictionary entry for realm explains that it traces back from Middle English realme through Old French to the Latin regimen (“rule”). It also notes the influence of Old French reial (“king’s”). Today the word is used not only for kingdoms but also to denote a “domain” of knowledge or activity.

Understanding this meaning makes it easy to think of a JavaScript Realm as a “domain that has its own global environment and built‑in objects.” However, interpreting the “kingdom” metaphor as a literal security boundary is a mistake. Even iframes with the same origin can reference each other’s values across different Realms, and whether access is allowed is determined by rules other than the name “Realm.”

The ECMAScript specification does not explicitly state why the term was chosen. Rather than asserting the specification authors’ intent solely from the word’s etymology, the following discussion uses the everyday meaning of “domain” as a clue to examine where the boundaries lie in the specification.

What Is a Realm

When you work with JavaScript you often encounter window and globalThis, but you rarely have direct access to a value named Realm. This is not a standard API for creating something from a page like new Realm(); rather, it is a concept introduced in the ECMAScript specification to explain “which world’s built‑in objects and global environment this code uses.”

According to the specification, any JavaScript code that is executed is pre‑associated with a particular Realm. A Realm includes its own built‑in objects, its own global object and global environment, and the state associated with code loaded into that environment. In the specification this is expressed as Realm Record. Saying “JavaScript execution environment” in a general sense is too broad, but at least if you think of it as the unit that determines from which collection Array and Object are taken, and in which environment global names are resolved, the behavior becomes easier to follow.

flowchart LR
    subgraph parent["Parent page"]
        PA["Realm A<br/>Global environment A<br/>Array A / Date A"]
    end
    subgraph child["Child iframe"]
        CB["Realm B<br/>Global environment B<br/>Array B / Date B"]
    end
    PA -.->|Values can be accessed if same origin| CB

Both are JavaScript Array, but Array A and Array B are not the same function object. Their prototypes are also different objects. Adding a Realm is not merely adding another dictionary to store variables; it also creates an entire set of built‑in objects.

At this point one might wonder whether JavaScript values can be taken out of a Realm, but that is not the case. If the iframe shares the same origin, the parent can reference the child’s objects directly. Receiving a value that resides in another Realm is different from a value being created in the parent Realm. The object’s origin remains unchanged; only the reference crosses the boundary.

The Global Object and a Realm Are Not the Same

A Realm includes a global object, but it is not the global object itself. globalThis is also not a variable that refers to a Realm. For example, in a browser, when the HTML specification creates a new Realm, it designates a new Window as the global object, and it uses the WindowProxy of that browsing context to bind the global this that corresponds to globalThis. If you look at the HTML specification’s section on creating and initializing a Document object Creation of Window and WindowProxy in the HTML spec , the two are defined as distinct entities.

flowchart LR
    C["Visible from code<br/>window / globalThis"] --> P["WindowProxy<br/>entry point to the browsing context"]
    subgraph R["Realm Record"]
        E["Global environment<br/>name resolution"]
        W["Global object<br/>Window"]
        I["Built‑in objects<br/>Array / Object / ..."]
    end
    P -->|Delegation to the current Window| W

Because we normally write window === globalThis, we don’t usually become aware that a different object is involved behind the scenes. However, when an iframe navigates and loads a different Document, it is necessary to treat these two as separate. The WindowProxy that was being referenced remains as the entry point for the same browsing context, while the underlying Window and its Realm are swapped. If you equate a Realm with a single value called window, this explanation falls apart.

The global environment and the global object are not completely synonymous. The global environment is the environment used to resolve names in code. The fact that top‑level let and var are treated differently shows that global names are not simply properties of an object. A Realm provides both the foundation for name resolution and the built‑in objects.

Creating a Scope Is Different from Creating a Realm

Calling a function creates a scope for local variables. Writing a block creates a scope for let and const. However, each function call does not generate a new Array constructor. Even if you define many functions in a script on the same page, they normally use the same Realm’s built‑in objects.

function first() {
  const local = 1;
  return Array;
}

function second() {
  const local = 2;
  return Array;
}

console.log(first() === second()); // true

The two local instances reside in separate scopes, but the returned Array is the same. The “visibility range of variables” and the “ownership of built‑in objects and the global environment” are distinct dimensions.

A Realm is not synonymous with a thread or an OS process. When a parent and a child iframe can synchronously call each other’s functions, execution moves between Realms. However, creating a Realm does not guarantee that the code will run in parallel on a separate thread. In Omoikane, a single Boa Context can contain multiple child Realms. Rebuilding the Rust‑side Context and adding a new Boa Realm are separate operations.

The “Current Realm” Changes During Execution

A Realm is the entity to which code belongs, while an execution context is the mechanism that tracks “which code is currently running and where to return after it finishes,” among other things. In the ECMAScript specification’s section on execution contexts ECMAScript specification execution contexts , an execution context carries Realm information, and the one at the top of the execution‑context stack is considered the currently executing context.

Consider a scenario where a function defined in a child iframe is called from the parent script, and that function in turn calls a function defined in the parent.

Executing a parent function → the current Realm is the parent
Calling a child function → while executing the child function, the context is the child
Returning to a parent function → while executing the returned parent function, the context is the parent
Return → back to the child function
Return → back to the original parent function

It does not mean that the Realm of the page that initiated the call is retained for the entire duration of processing. Conversely, just because the DOM node currently being processed belongs to a child Document does not imply that every function invoked there runs in the child’s Realm. The current Realm switches depending on which function’s code is being executed. This point led to the native‑continuation bug discussed later.

An ECMAScript function object is bound to the Realm in which it was created. Even if a callback created by the parent is passed to the child, the function is not automatically converted into a child‑Realm function the moment it is handed over. The caller, the function’s owning Realm, and the Realm of the object that receives it can each be different, so trying to determine everything with a single notion of “the current Realm” breaks down.

Even with the Same Origin, Not the Same Realm

The ECMAScript specification defines a Realm as a collection of built‑in objects, a global environment, and the code that belongs to that environment. The built‑in objects referred to here also include constructors such as Array and Date.

Realm in the ECMAScript specification

For example, create an iframe on a page that shares the same origin. Because it is the same origin, the parent’s JavaScript can access the child’s contentWindow. However, the parent’s Array and the child’s Array do not share a single object.

const frame = document.createElement("iframe");
document.body.appendChild(frame);

const child = frame.contentWindow;
const value = new child.Array(1, 2);

console.log(child.Array === Array);        // false
console.log(value instanceof child.Array); // true
console.log(value instanceof Array);       // false

“Whether access is permitted” and “whether they share the same execution environment” are separate questions. Origin concerns access control, whereas a Realm concerns the identity of the global environment and the built‑in objects used by code. If these are conflated, an implementation may assume that, because the origins are the same, it is acceptable to pass the parent’s Array to the child.

Omoikane also tests this aspect. In the test Test that checks the iframe's built‑in objects , we verify not only that the child’s Date and Array differ from the parent’s, but also that a function created in the parent using child.Function(...) can see the child’s global environment. Even if a value is written into the child’s global environment, the parent’s global environment must not acquire a value with the same name.

Even when instanceof is false, an array is still an array

In the example above, value instanceof Array evaluates to false. This is not because the array received from the child is corrupted. A normal instanceof determines the type by examining the constructor on the right‑hand side and its prototype. Since the parent’s Array.prototype and the child’s Array.prototype are distinct, placing the parent’s Array on the right‑hand side does not match.

console.log(Object.getPrototypeOf(value) === child.Array.prototype); // true
console.log(Object.getPrototypeOf(value) === Array.prototype);       // false
console.log(Array.isArray(value));                          // true

Array.isArray() checks whether something is an array, so it returns true even for arrays that belong to a different Realm. In other words, code that uses instanceof Array to mean “this is a JavaScript array” can produce false positives when it receives a value from an iframe. This can also occur in application input validation, but it is even more serious for browser implementors. When a DOM API returns an array, creating it with the wrong Realm’s constructor causes the object’s identity, as seen by the caller, to differ from what the specification requires.

It’s not just arrays. Objects created in the child’s Date have a different constructor and prototype from those in the parent’s Date. The child’s Promise is also not the same function object as the parent’s Promise. Even if a value looks identical when examined only as typeof, checking the prototype or the identity of built‑in functions reveals which Realm it belongs to. The specification creates separate built‑in objects for each Realm precisely because such observable differences exist.

In this example, Array and Date are built‑in objects defined by ECMAScript, whereas document and DOM nodes are objects provided by the browser. To treat both as belonging to the child frame, both a step that separates the language engine’s Realm and a step that binds them to the child Document on the browser side are required. Simply having a child Realm created by Boa does not automatically ensure that the child’s document is correctly set up.

Origin governs access rights, Realm governs ownership

This point is easy to confuse. Having a different Realm does not automatically mean that the parent cannot access the child. An iframe with the same origin as its parent resides in a separate Realm, yet the parent can still access items such as contentWindow.document. Conversely, a cross‑origin iframe also has an environment in which code can run on the child side, but the parent cannot freely access the child’s global environment.

Parent and child with same origin
  Realm: separate
  Access to the DOM: possible if conditions are met

Cross‑origin parent and child
  Realm: separate
  Access to the DOM: restricted

An origin is generally determined by scheme, host, and port. Even if the domain name appears the same, differing schemes or ports mean the origins are not necessarily identical. Moreover, the sandbox attribute of an iframe can cause its effective origin to differ from what one would infer from its URL. One must not rely solely on the fact that a Realm has been created as the justification for granting access.

Omoikane’s child‑Realm creation process first compares the effective origin of the target Document with the origin of the document that embeds it. It also checks whether the sandbox permits same‑origin access. A child with an opaque origin such as data: should not be treated as same‑origin as the parent merely based on its visible URL. Using this determination, it decides what to pass to parent and top, and the parent’s contentWindow side also restricts access accordingly.

That said, this does not mean that Omoikane has fully implemented every cross‑origin behavior. What matters here is how the responsibilities are divided. A Realm indicates which global environment and built‑in objects an entity belongs to, while the origin and sandbox determine what is visible across boundaries. Both are required, and neither can substitute for the other.

Providing an execution environment per Document

Omoikane’s JavaScript engine is Boa, which creates a Realm for a child iframe using Context::create_realm() and switches to that Realm with enter_realm() when executing the target script. The iframe’s Document ID is bound to the Realm, and the DOM bindings are also registered in that environment. If a Realm already exists for the same Document, it is reused.

In the code Code that creates a Realm for a child iframe , the crucial point is determining at which stage the Realm should be created. Rather than initializing everything simply because an iframe exists, the Realm is created only when needed—such as when executing the child’s script or when accessing contentWindow from the same origin. Previously there was a path that created a child Realm even for iframes that contained no script, solely for the load event; that path has been corrected to avoid unnecessary initialization.

Switching Realms does not solve everything. How the child’s document, parent, top, and frameElement are presented is the responsibility of the host browser. In nested iframes, the child’s parent refers to the frame one level up, not the top‑level page. The URL that the child script reads and the CSP that applies to it must also be bound to the child’s Document.

Note that merely having a child Realm does not by itself guarantee cross‑origin safety. Which properties are exposed from the parent to the child, and vice versa, are controlled separately by origin and sandbox conditions. Issue #494 dealing with separating child Realms and WindowProxy was precisely a task that handled both of these aspects together.

This also clarifies why the child’s Document ID is bound to a Realm. Simply having a different Realm does not tell us whether that Realm corresponds to the Document currently displayed in the iframe. Even after navigation, if the parent retains references to old functions or values in variables, those references can persist. To distinguish between being able to use stale values and treating an old Document as still active, Omoikane tracks Document identity and validity separately.

Furthermore, when executing a child script, the engine temporarily switches to the child’s Realm and returns to the original Realm once execution finishes. Forgetting to restore the original Realm can cause the parent’s subsequent code to see the child’s document and its built‑in objects. Conversely, restoring too early can let operations that should run in the child leak into the parent. enter_realm() is convenient, but a single call to it does not complete the boundary management.

contentWindow is not the page itself

If you store the iframe’s contentWindow in a variable and then navigate that iframe to a different URL, the reference identity remains unchanged. However, the Document and embedded constructors that become visible after the navigation are new instances.

const windowRef = frame.contentWindow;
const oldDate = windowRef.Date;

frame.srcdoc = "<p>new document</p>";
// After navigation completes:
console.log(windowRef === frame.contentWindow); // true
console.log(windowRef.Date === oldDate);         // false

If you fix contentWindow to be exactly the “global object of the current page,” this behavior cannot be achieved. In the HTML specification’s description of the WindowProxy exotic object, the WindowProxy that corresponds to a browsing context switches the underlying Window in response to navigation.

WindowProxy in the HTML specification
flowchart LR
    P["contentWindow<br/>WindowProxy X<br/>reference is the same"]
    A["Window A<br/>Realm A / Document A"]
    B["Window B<br/>Realm B / Document B"]
    P -->|Before navigation| A
    P -->|After cross‑document navigation| B

The reference to WindowProxy X stays the same, while the target Window changes. In contrast, even navigations that stay within the same Document—such as changing only the URL fragment—do not involve creating a new Realm each time. The “swap” being discussed here refers to navigation that loads a different Document.

Omoikane keeps contentWindow as a stable facade and updates its target based on the currently active Document and Realm. In the implementation of the contentWindow facade (see the link), if the origin is the same, operations are delegated to the child’s global object; for cross‑origin cases, references to any properties other than the permitted ones are rejected. When an iframe is removed from the DOM, the old facade is closed, and after reconnection it is treated as a new browsing context.

contentWindow facade implementation

In testing, we confirmed that values created from the pre‑navigation Date are retained, but reading the post‑navigation windowRef.Date returns a new constructor. Preserving the old value is not the same as making the old page appear as if it were still active. The current Omoikane facade implements only the necessary aspects and does not claim to fully reproduce the HTML specification’s WindowProxy.

This discrepancy also conflicts with the model that “navigating should erase all JavaScript objects.” If the parent retains objects derived from the old Date or from the old Document, those values can remain reachable by the garbage collector. However, whether those values can be used to freely manipulate the old browsing context is a separate issue. The persistence of old Realm values, the activation of a new Window, and the termination of old tasks must each be handled independently.

Callbacks are not sufficient when considering only the “caller”

When events are dispatched across iframes, the global environment referenced by a function depends on which Realm the function was created in. In Omoikane, there was a pathway that invoked listeners created in the parent Realm from input events that reached the child Document. While ordinary JavaScript function calls worked fine, inserting a native continuation on the Rust side caused the parent listener to lose sight of the global variables it referenced, resulting in ReferenceError.

The problem was not that the parent’s variables became inaccessible simply because the event reached the child. After Boa pushed the JavaScript callback frame onto the stack, it unconditionally restored the current Realm to the caller during the cleanup after the native call, overwriting even the Realm of the callback that was about to be executed.

Dispatch an event in the child Document
  -> Prepare a frame that invokes listeners in the parent Realm
  -> Switch Realms during cleanup after a native call
  -> The listener runs in a different Realm and cannot resolve the parent's name

In issue #768 about native continuation Realm bugs, we fixed the code to preserve the Realm in which a callback runs and to store a separate return target for after the callback finishes. We also tested nested calls, exceptions, suspension and resumption, and cases where garbage collection intervenes. This example clearly shows that a Realm is not merely a simple flag indicating “which iframe is currently being processed,” but is determined by the actual relationship between the function being executed and its execution context.

Native continuation Realm bug #768

Which Realm should the new value be created in?

Even if a function can be executed in the correct Realm, the next question remains: where should the new objects returned by that function be created? When a parent calls a method on a child’s DOM object, the Realm of the currently executing function, the Realm of the parent that initiated the call, and the Realm of the child object being operated on do not necessarily match.

The HTML specification explains this distinction using the terms “current realm” and “relevant realm” for a particular object. The current realm refers to the Realm of the code that is currently executing. In contrast, the relevant realm denotes the Realm to which a platform object belongs. The HTML specification’s section on Realms, settings objects, and global objects also includes examples where the Realm of the calling code differs from that of the object being operated on.

Realm and settings object in the HTML specification

For example, if an API belonging to an object owned by the child creates a Promise that is tied to the child’s context, simply invoking that API from the parent does not mean the Promise should be created in the parent’s Promise. Which Realm the Promise belongs to is determined by the algorithm of that API. If you arbitrarily decide to use the Realm that is currently executing, the returned value may end up being an instance of a different Promise.

This is a tricky issue for API implementations that must take Realms into account. The location where a method is called, the Realm to which the method itself belongs, the Realm of the object passed as this, and the Realms of objects supplied as arguments can all differ. It isn’t a matter of comparing everything each time; you must verify which value the specification designates as the reference point, otherwise the implementation will only work correctly when the parent and child happen to share the same Realm.

Asynchronous operations require an owner and a generation

If you call setTimeout in a child iframe and then navigate immediately, that timer will not belong to the new Document. The same applies to requestAnimationFrame. If you only retain the callback function, by the time it is executed the Document in which that function was registered may no longer exist.

In Omoikane, scheduled tasks in a child Realm are associated with both the Realm and the Document ID. Before execution, we verify that the Document is still valid, and we do not run tasks that belong to an older generation. Because a Promise’s continuation may register a new timer, simply looking at the host’s state at the time of registration is insufficient; we must refer to the owning Document of the Realm in which the code is currently executing.

The code that binds timers to Realms (see the link) and the test that checks timers in an old child Realm (see the other link) confirm that after moving an iframe to a different Document, the old setTimeout and requestAnimationFrame do not modify the new Document. Realm isolation affects not only value identity but also the lifetime of asynchronous operations.

Timer and Realm association Test for a timer in the old child Realm

Testing Realms requires more than one perspective

Verifying a Realm implementation by merely confirming that a child script can read document is hardly sufficient. Omoikane’s tests also aim to check distinct aspects.

First, examine the ownership of values. The child’s Array and Date differ from the parent’s, so check whether values created from the child are linked to the child’s prototype. Next, look at the access boundaries. Verify that necessary operations can be performed from a parent to same‑origin children, and that cross‑origin or sandboxed paths that should be blocked are indeed blocked. Then consider the timeline. When navigating, ensure that WindowProxy retains its identity while switching to a new Realm. Finally, examine asynchronous processing. Ensure that a timer reserved for the old Document does not intrude into the new Document.

These issues can fail independently. For example, even if the parent can access the child’s document, if the child’s Date is the same as the parent’s Date, the Realm isolation is insufficient. Conversely, even if constructor identity is correct, if a timer from an old iframe modifies the new page, the lifecycle boundary is broken. In the case mentioned earlier involving native continuations, even when a normal function call succeeds, inserting native code introduced a separate problem.

Even when a test is named something like “Test for iframe,” it helps to clarify exactly what the test is protecting, making it easier to trace the cause. Because a Realm cannot be represented by a single flag, verification cannot be reduced to a single assertion.

Summary

When implementing an iframe with Omoikane, handling cross‑boundary interactions proved more difficult than the act of creating a Realm itself. Even with the same origin, Array is treated as a distinct entity. A reference to contentWindow may remain, but the underlying Document can be swapped out. Even if you invoke a parent’s function from a child’s event, that does not mean the function’s execution environment should be treated as belonging to the child. Timers must not run after the Document they were scheduled for has been removed.

Simply stating that “global variables should be separated per iframe” overlooks these points. To me, a Realm has become a boundary that determines the ownership of code and built‑in objects, ensuring that this ownership is not mistaken during navigation or asynchronous operations.