I already knew that SharedArrayBuffer was unavailable by default in browsers as a mitigation for Spectre. But my understanding of Spectre stopped at a vague idea of a memory-related vulnerability.

SharedArrayBuffer was a low-level API for shared memory, so I assumed it enabled some kind of memory exploit. I left it at that, without being able to explain how Spectre worked or how an attack used this API.

Finally reading about it revealed a different problem. SharedArrayBuffer did not directly let you read somebody else’s memory. Shared memory could build a precise clock.

But why would measuring time accurately be a problem? Knowing how long an operation took sounded useful. Could that alone let you read memory that should be inaccessible? That was the missing connection for me.

The connection turned out to be measuring traces left by work the CPU performed ahead of time. Even if a secret never appears in a return value, a clock can help infer it when the value changes which locations are fast to read. I started with a memory API and ended up reading about clocks.

The explanation continued much further. Making clocks coarse was insufficient, which led to keeping secrets out of the attacker’s process and eventually to HTTP headers called COOP and COEP. Following why window relationships needed to be severed brought in browsing context groups and WindowProxy.

This article follows that sequence. It records what I understood from papers, browser implementers’ explanations, and Web specifications; I did not reproduce an attack on my machine. Unless stated otherwise, the code and diagrams are explanatory examples, not executed attacks or measurement results.

This became a long article. These are useful starting points for individual topics:

What surprised me first

Before reading, my vague picture was this:

Spectre is apparently a memory-related vulnerability
  → SharedArrayBuffer is a low-level shared-memory API
  → It probably helps with some kind of memory exploit
  → That is why Spectre mitigation makes it unavailable by default

The connection I came to understand was different:

A secret value is used during speculative execution
  → It leaves a corresponding CPU cache trace
  → Timing memory accesses can reveal the secret
  → A shared-memory clock can provide that measurement
  → Both timing restrictions and isolation of secrets matter

SharedArrayBuffer sits partway through this chain. It need not store the secret or directly provide access to it. Memory shared solely between the attacker’s own code can serve as a measuring instrument.

Being unavailable by default is different from being prohibited altogether. After a period of being disabled, the API became available to ordinary Web pages meeting conditions including cross-origin isolation. Chrome’s account of disabling and reintroducing it describes the clock problem.

CPUs proceed before a condition is resolved

Start with speculative execution.

Some code appears unable to proceed until a condition is known. Consider checking whether an array index is in bounds before reading the element. Reading the source from top to bottom puts the check first and the read second.

If the CPU waited for everything in that order, however, execution resources would sit unused while values arrived from memory. Instead, it predicts a branch and executes ahead before knowing whether the prediction is correct. A correct prediction hides some waiting time.

A wrong prediction does not commit the work on the wrong path as the program’s result. Project Zero’s initial report distinguishes execution from retirement, when results become committed.

I initially stumbled over the idea of executing something and then making it effectively unexecuted. For the program’s visible result, that is how the mechanism works. A value calculated down a mispredicted path does not simply become the correct return value.

But “the program gives the correct answer” and “nothing about its intermediate work is observable” are different guarantees.

Committed state and state maintained for speed

This distinction brings in the terms architectural state and microarchitectural state.

Architectural state includes registers and memory as exposed by the instruction set’s contract. Microarchitectural state belongs to the CPU’s implementation of that contract for speed, including caches and branch prediction machinery.

Discarding a calculation and keeping the wrong answer out of registers does not necessarily undo the caching of data accessed during that calculation.

The original paper uses that gap as an information channel. A secret need not appear in a return value if the internal state it influenced can be observed later.

I had interpreted “undoing execution” as “restoring absolutely everything, including timing.” That was the mistake.

Touching a location selected by a secret

Caches keep data close to the CPU to accelerate memory access. Reading the same data can take different amounts of time depending on whether it is cached.

Suppose a secret byte is 42. The attacker cannot receive 42 through an ordinary read. But what if speculative execution can use that value to access a corresponding location in an observation area?

Secret is  7 → Access observation location corresponding to  7
Secret is 42 → Access observation location corresponding to 42
Secret is 99 → Access observation location corresponding to 99

The attacker later reads candidate locations. If the one corresponding to 42 is faster, they can infer that the secret may have been 42.

These locations should not be imagined as simply adjacent bytes. Caches handle data in groups, so candidates in the same group would be difficult to distinguish. For this explanation, imagine sufficiently separated locations for each candidate.

The secret is being converted into a difference in which location reads quickly. The observation area turns a numerical value into a timing signal.

That explains the need for a clock. The attacker does not need to know the time of day. They need to judge whether one read was slightly faster than another.

How does execution reach the secret despite a check?

The explanation still assumes that a secret can be read speculatively.

Spectre has multiple variants. Bounds check bypass, or variant 1, was the easiest starting point for me. Repeated valid inputs give the branch predictor a history suggesting that a check will pass. An out-of-bounds input is then supplied.

If the CPU predicts another successful check before the condition is resolved, it may proceed to a read that ordinary execution would not perform. The value selects an observation-area access. Discovering the misprediction and discarding its results need not erase the trace.

Here is explanatory pseudocode for the machine-code-side operations:

If index is smaller than the public region's length:
    value = read a byte at the location derived from base address and index
    touch the observation location corresponding to value

The source-level condition has not been removed. It exists, and committed execution obeys it. But that condition alone does not necessarily prevent observable traces produced during speculation.

A real attack needs suitable instructions, influence over inputs, enough speculative execution time, cache preparation, and noise handling. Writing an arbitrary array access does not extract secrets on every system. Here I am following the information path rather than reproducing each precondition.

Mixing Spectre and Meltdown makes this harder

Their disclosure around the same time had also blurred Spectre and Meltdown together in my head. Project Zero’s 2018 report identifies variants 1 and 2 as Spectre and variant 3 as Meltdown.

This article follows the Spectre side: inducing speculative instruction sequences and leaking information through a side channel. Reducing everything to “reading kernel memory from user space” obscures the connection to JavaScript and shared memory.

Explaining all Spectre attacks solely through variant 1’s array check would also be too narrow. Variant 2, for example, targets indirect branch predictions. Still, separating “read a secret,” “encode a trace,” and “measure the trace” in one variant made the subject easier for me.

JavaScript still runs on a CPU without exposing pointers

Ordinary JavaScript has no pointers for selecting arbitrary addresses. TypedArrays have bounds. That made exploiting a CPU issue from browser JavaScript feel somewhat distant.

But JavaScript engines ultimately run on CPUs. Both JIT-generated code and an interpreter’s implementation consist of machine instructions. Language-enforced boundaries and the information affected by CPU speculation exist at different layers.

V8’s retrospective on Spectre explains its conclusion that language checks alone cannot protect confidentiality within a process.

This does not mean an ordinary out-of-bounds JavaScript read starts returning somebody else’s memory. A value unavailable under the language’s semantics can still be inferred through execution timing.

Nor does disabling JIT automatically eliminate every problem of this kind. JIT output is not the only code the CPU executes. Which instructions an attack can use depends on the implementation, but calling this merely a bug in the JIT feature misidentifies its scope.

Memory safety has not lost its value, either. Preventing ordinary out-of-bounds accesses and preventing information leakage during speculation involve different boundaries.

What SharedArrayBuffer actually shares

Finally, back to SharedArrayBuffer.

SharedArrayBuffer lets multiple execution agents work with the same bytes. A common use is sharing memory between a page and a Dedicated Worker it created.

For example, the page can create a buffer and send it to a Worker:

const shared = new SharedArrayBuffer(4);
const counter = new Int32Array(shared);
const worker = new Worker("counter-worker.js");
worker.postMessage(shared);

This is an unexecuted fragment assuming an environment that meets the API’s requirements.

Unlike copying ordinary objects through postMessage() or transferring ownership of an ArrayBuffer, SharedArrayBuffer lets the receiver access the same shared data. Distinguishing JavaScript wrapper objects from their backing data helps here. The SharedArrayBuffer documentation makes that distinction.

Sharing does not expose all browser memory. It does not let a page enumerate and read buffers belonging to another origin.

The shared data mechanism in ECMAScript’s SharedArrayBuffer definition is separate from access to an entire OS process address space. My vague assumption that something low-level enabled memory exploits did not make distinctions like this.

Why shared memory becomes a clock

Put a counter in a shared buffer and keep incrementing it in a Worker. The observer reads it before and after the operation of interest.

Worker:
    Repeatedly increment a shared counter

Observer:
    before = read shared counter
    perform the memory access being examined
    after = read shared counter
    use after - before as a measure of elapsed time

The attacker can create the buffer themselves. They do not need to ask the holder of the secret to share memory for the clock.

Atomics can make the counter operations explicit: Atomics.add(counter, 0, 1) for updates and Atomics.load(counter, 0) for reads. Atomics coordinates shared-memory operations; it is not itself a clock API.

One count does not always correspond to a fixed number of seconds. CPU speed, Worker scheduling, and competition with other work affect it. The counter can wrap around, too. This explanatory arrangement does not establish a particular resolution on every machine.

Even so, observing something change concurrently provides a time reference. A clock-like instrument can be built without calling the browser’s clock API.

I had vaguely imagined shared memory being used for some memory exploit, and now it was being used as a clock. Another thread reads a counter updated by a Worker and uses the difference to measure cache access time. That use had never occurred to me.

Making performance.now() coarse was insufficient

Browsers reduced clock precision to make tiny cache timing differences harder to observe. Mozilla’s 2018 announcement names both explicit clocks such as performance.now() and implicit clocks such as SharedArrayBuffer.

Making only the explicit clock coarse leaves room for a clock built from shared memory. That led to disabling SharedArrayBuffer.

Hiding precise clocks was still not the end. V8’s retrospective describes amplification techniques that can make differences observable with coarse clocks.

Even if one difference is smaller than a clock’s tick, repeated differences of the same kind may become large enough to measure. Actual attacks are more complicated than straightforward addition, but coarse ticks do not prove that every timing difference is unobservable.

SharedArrayBuffer is therefore neither Spectre’s only entry point nor a necessary component. It can provide a powerful observation tool.

By this point, I could see how much of the mechanism I had skipped over in my understanding that it was unavailable by default as a Spectre mitigation.

Browsers distinguish loading data from exposing it

Next, consider the browser’s situation.

Loading resources from other origins is routine on the Web: displaying images, loading scripts and CSS, and embedding iframes.

But displaying or using something is different from letting a page’s JavaScript freely read its contents. A cross-origin image can be displayed while reading its pixels from a canvas remains restricted without CORS permission. The canvas image-use documentation also describes this distinction.

The browser has data needed for display
                ↓
Exposing that data to page JavaScript is a separate decision

This distinction supports both convenient external content and confidentiality between origins.

With Spectre, information can be inferred outside an API’s return values. Loading data internally and then deciding not to expose it to JavaScript is no longer sufficient. The web.dev explanation of cross-origin isolation illustrates the problem using pixels in a cross-origin image.

The problem is broader than keeping malicious images out of one’s own page. An attacker’s page may load data containing somebody else’s secrets and expose it to powerful observation tools.

Imagine an image served specifically to an authenticated user. If an attacker’s page can load it as an image, its data needs protection even if normal JavaScript reading restrictions remain. Whether cookies actually accompany the request also depends on SameSite attributes and browser treatment of third-party cookies. Cross-site embedding is not invariably authenticated.

The name COEP made me think of blocking dangerous external content. But an attacker can set that header, too. Asking what the attacker gives up by setting it made more sense to me.

Origin, site, and process are different things

Without separating the terminology, every mention of isolation starts to sound identical.

For ordinary HTTP URLs, origin comparisons involve scheme, host, and effective port. https://app.example.com and https://api.example.com are different origins. HTTP and HTTPS differ even with the same host.

A site is a broader grouping based on concepts including the registrable domain. Normally, HTTPS app.example.com and api.example.com are same-site but not same-origin. HTML’s definitions of origins and sites distinguish them.

An OS process is an implementation unit: where the browser actually executes those documents.

TermWhat it groups or separates
originThe basic authority boundary for access such as DOM access
siteA grouping broader than an origin
OS processAn implementation unit isolating address spaces and execution resources
browsing context groupA specification-level grouping for window and frame relationships
agent clusterA specification-level grouping defining, among other things, where memory can be shared
RealmA JavaScript execution environment with intrinsic objects and a global environment

Being in the same tab does not imply sharing a process. Being same-origin does not imply that any two environments in the browser can share the same memory.

In particular, a browsing context group is not another name for a process. Specification-level relationships and OS processes must be considered together, but they are different things.

Shared memory also has a defined sharing boundary

HTML’s agent cluster explanation lists a page and its Dedicated Worker as a pair that can share memory. A page and its Service Worker, in contrast, are in different agent clusters.

Independent same-origin tabs without an opener or ancestor relationship need not belong to the same agent cluster. Matching origins alone does not let an application distribute SharedArrayBuffer to any two execution environments.

I had also assumed that shared memory implied a shared process. Web specifications instead define sharing boundaries independent of each browser’s process model. They do not expose the OS structure directly as JavaScript APIs.

Site Isolation changes where secrets reside

If coarse clocks cannot completely stop observation, keeping secrets out of the address space executing attacker-controlled code matters.

Chromium’s Site Isolation documentation describes placing different sites in separate renderer processes and explains the motivation. Running cross-site iframes in separate processes is part of that model.

One tab on screen

  Site A's document     → Renderer process A
  Embedded site B       → Renderer process B

Appearing together on screen need not mean putting data in the same address space.

Separating documents alone does not explain every resource load, however. Images and scripts supply data used by the loading page. Protecting which responses reach a renderer process matters too.

Chromium’s CORB documentation explains protections against unnecessarily delivering cross-origin HTML, JSON, and similar content to the loading page’s process. I am reading this as an account of historical mitigations; CORB alone does not describe every response decision in every current browser.

“Reading across process boundaries” was too vague

By this point, the phrase “Spectre reads across process boundaries” was getting in the way of my understanding.

The Spectre family includes attacks that influence victim-side execution, and researchers have studied attacks across different security boundaries. But understanding this browser defense starts with data residing in the attacker’s process that JavaScript lacks permission to read.

V8’s documentation for embedders likewise recommends separating untrusted code from sensitive data into different processes.

Assuming that any memory can be read regardless of process makes the move toward process separation inexplicable. The attack variant, where code runs, and where secrets reside cannot all be omitted.

COOP constrains window relationships

Now consider COOP, one of the two headers often encountered when enabling SharedArrayBuffer.

Cross-Origin-Opener-Policy: same-origin

COOP stands for Cross-Origin-Opener-Policy. It concerns relationships between top-level documents and their openers or opened windows, rather than permission to load images or CSS.

Suppose page A opens page B using window.open(). Ordinarily, the opener receives a reference and the opened page has window.opener. Different origins restrict DOM access without necessarily eliminating window references.

const popup = window.open("https://other.example/", "_blank");

Popup blocking can return null; here, assume opening succeeds.

Cross-origin does not mean every connection is forbidden

Reading a cross-origin Window’s document encounters restrictions. But while the relationship remains intact, limited interfaces such as postMessage() are available. “The DOM is unreadable” and “a reference exists” must be considered separately.

COOP’s same-origin separates relationships with documents whose origins and policies do not meet its conditions. The COOP documentation distinguishes which document opens which, and their respective policies, when determining browsing context group membership.

When the groups separate, the opener relationship is severed. A saved reference from the opener can appear closed, for example with closed returning true. That does not mean the visible tab has actually closed. The page may remain open while the old reference relationship does not continue.

Conversely, COOP does not automatically sever every relationship, including same-origin pages with compatible policies.

This should not be read as banning postMessage() because the operation itself is a Spectre attack. Browsers can already implement postMessage() across processes. COOP constrains the references a document maintains and the documents admitted to the same group, giving the browser room to establish stronger isolation. Being in the same group still does not necessarily mean being in the same process.

Separating groups is more than changing a label

The browsing context group specification concerns how related windows and frames are grouped.

Separating them would accomplish little if old references continued delivering operations unchanged to the new document. Equally, legitimate relationships remaining within a group should not be indiscriminately broken.

Parsing the string COOP from an HTTP header is therefore only part of a browser implementation. It also affects which relationships survive navigation and what existing references expose.

This is where I finally understood why explanations of an HTTP header lead into WindowProxy.

COEP constrains how other origins’ data enters

The other header is COEP, Cross-Origin-Embedder-Policy. A representative value is require-corp.

Cross-Origin-Embedder-Policy: require-corp

This mode requires CORS or CORP permission when loading cross-origin resources. It does not mean that every load is decided solely by a CORP header. Request modes determine different paths.

When reading the COEP documentation, separating the page’s policy from the resource provider’s permission helps.

HeaderWho primarily declares what
Cross-Origin-Embedder-PolicyThe embedding page declares which resource-loading arrangements it accepts
Cross-Origin-Resource-PolicyThe resource provider declares permitted no-CORS loading relationships
Access-Control-Allow-OriginThe resource provider declares which origins it permits through CORS

Permission through CORP

Consider a no-CORS load of a cross-origin image using an ordinary img:

<img src="https://assets.example/photo.png" alt="Example image">

The image provider can permit cross-origin loading under CORP with:

Cross-Origin-Resource-Policy: cross-origin

CORP also has same-origin and same-site values. Which value permits loading depends on the relationship to the requester. The Fetch Standard’s CORP definition defines the checks.

CORP permits that loading path. A cross-origin CORP value does not grant the same canvas pixel-reading access as CORS.

Permission through CORS

An image request can instead use CORS mode:

<img src="https://assets.example/photo.png" crossorigin="anonymous" alt="Example image">

The provider must return an appropriate Access-Control-Allow-Origin, and CORS checks must succeed. For a page with origin https://app.example, one example response is:

Access-Control-Allow-Origin: https://app.example

The resulting permitted uses differ from simply displaying an image. Setting COEP does not itself turn a no-CORS request into a CORS request. The COOP and COEP setup guide also includes adding crossorigin to images that use CORS.

The important distinction for me was not merely three similar header names. The page’s promise, the resource’s embedding permission, and CORS permission serve different purposes.

The credentialless alternative

COEP also has credentialless. Remembering only require-corp would create another gap in the explanation.

credentialless omits credentials from cross-origin no-CORS requests, providing a path for loading resources without explicit CORP permission. Chrome’s explanation describes the motivation for this mode.

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless

The scope here is cross-origin no-CORS requests. It does not anonymize all page traffic or eliminate all CORS checks. Omitting credentials also means that an image customized for an authenticated user cannot be retrieved in the same authenticated form.

One approach obtains permission; another omits credentials. That distinction makes COEP a more concrete commitment than an external-resource ban switch.

Why combine COOP and COEP?

The two headers address different parts of the problem.

COOP constrains reference relationships with related top-level documents. COEP constrains the loading of other origins’ resources. Declaring either does not declare the other’s restrictions.

That is why this is a representative configuration for enabling SharedArrayBuffer:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

The explanation of why isolation is required describes limiting unapproved other-origin content in exchange for access to powerful APIs.

An attacker setting these headers to use SharedArrayBuffer must also give up the freedom to import other-origin data without permission. Browser enforcement of that restriction matters.

crossOriginIsolated does not inspect a process list

A page can check, for example:

console.log(self.crossOriginIsolated);
console.log(typeof SharedArrayBuffer);

crossOriginIsolated indicates whether the environment has the required isolation capability. The property’s documentation describes secure context and Permissions Policy requirements alongside COOP and COEP.

A trustworthy environment such as HTTPS is a prerequisite. An iframe also depends on its parent’s conditions and permission delegation. Report-Only headers provide reports; they do not establish isolation in place of enforcing headers.

A true value does not mean CPU speculation has stopped. It does not mean exactly one OS process belongs to the tab, nor that all cross-origin communication is forbidden.

The browser enforces specified relationship and loading constraints and determines that the environment can receive powerful capabilities. JavaScript observes that result.

SharedArrayBuffer and Atomics are not one undifferentiated ban

My initial picture lumped SharedArrayBuffer, Atomics, and high-resolution timers together as dangerous disabled features.

But individual APIs’ conditions and permitted uses need to be distinguished. For example, Atomics.wait() cannot block the browser’s main thread. Cross-origin isolation alone does not explain that restriction. The Atomics documentation distinguishes the operations’ properties.

The subject here is which environments receive powerful shared-memory observation capabilities, rather than disabling every memory-related API for the same reason.

WindowProxy keeps a window reference across navigation

I referred to saved references in the COOP discussion. Treating their targets as simply the documents themselves creates a problem.

Suppose code saves an iframe’s contentWindow in a variable. What happens when that iframe navigates to another document?

Invalidating every reference on every navigation would make window APIs awkward. But carrying the old document’s access permissions into the new one would be dangerous.

WindowProxy sits between those concerns. The WindowProxy explanation describes an object wrapping a Window, with the wrapped target changing when the browsing context navigates.

Saved reference
      │
      ▼
  WindowProxy ──→ Window before navigation
      │
      └────────→ Window after navigation

This diagram illustrates navigation within the same browsing context. It does not mean that a COOP browsing context group switch keeps forwarding the same relationship to the new target.

Despite its name, this is not a page-created JavaScript new Proxy(). It is a special object defined by the Web specification.

The same reference does not preserve access permission

Consider an iframe that starts same-origin and then navigates cross-origin. Holding a reference does not authorize the next access.

const frameWindow = frame.contentWindow;
// Suppose the iframe subsequently navigates to a cross-origin document.
// Holding frameWindow does not grant access to its DOM.

Permitted operations must be checked against the target and the accessing environment at the time of access. Reference identity and authority over a document are different things.

A COOP group switch adds the requirement not to continue the previous window relationship, beyond merely rejecting DOM access to the new document.

This is where a simple same-origin check stops being the whole explanation. Rejecting some properties across origins and discontinuing an old relationship after separating groups are different operations.

MessageEvent.source identity is observable behavior

Windows can communicate using postMessage(). The recipient can inspect event.origin for the sender’s origin and obtain a reference to the sending window through event.source.

For example, checking for messages from an opened popup might look like this:

window.addEventListener("message", (event) => {
  if (event.origin !== "https://partner.example") return;
  if (event.source !== popup) return;

  // Validate the expected data format before processing it.
});

This fragment assumes that popup holds the opened window reference and that COOP or another mechanism has not severed the relationship. It does not guarantee the safety of every possible message handler.

If the browser created an arbitrary new wrapper representing the same sender for each delivered message, event.source !== popup would stop working correctly.

HTML’s message-posting steps set source to the sending WindowProxy. Which object is referenced, beyond merely similar values or methods, is part of the contract.

Checking source identity does not replace checking the origin. Windows can navigate. event.origin records the origin at message-sending time rather than permanently guaranteeing the window’s current URL. The postMessage documentation discusses both targetOrigin for sending and sender validation on receipt.

Did it come from the same window?      → Check source
Did it come from the expected origin?  → Check origin
Does the data have the expected form?  → Validate in the application

Collapsing those into a single idea of a trusted message hides which part guarantees what.

Realm is another axis: which JavaScript environment?

I covered Realms in detail in JavaScript Realms as Seen Through Omoikane’s iframe Implementation. Here, the point is to distinguish them from origins and processes.

A Realm represents a JavaScript execution environment, including its global environment and intrinsic objects such as Array and Object. ECMAScript’s Realm definition specifies its components.

A same-origin iframe provides an accessible example. The parent’s Array and the iframe’s Array belong to their respective environments.

const frameArray = new frame.contentWindow.Array(1, 2, 3);

Array.isArray(frameArray); // true
frameArray instanceof Array; // Usually false

Being an array and having the parent’s Array.prototype in the prototype chain are different properties. The explanation of instanceof across Realms describes this distinction.

Different Realms do not necessarily mean different origins. Same-origin documents can have separate Realms, and objects in different Realms can be accessible when the conditions permit. I also discussed this distinction in the article about fixing origin boundaries in internal host functions.

Why does the creation environment of exceptions and functions matter?

Suppose accessing another document throws an exception. Which environment supplies its constructor and prototype is observable from JavaScript.

The same applies to functions exposed for cross-origin operations. Making an allowed operation callable also raises questions about the function’s Realm and whether looking it up again returns the same object.

HTML’s CrossOriginGetOwnPropertyHelper defines a cache involving accessing and target environments and a property name, along with steps that create functions in the current Realm.

That does not justify a blanket rule that all cross-origin exceptions, Locations, and functions are created in the caller’s environment. The relevant steps must be followed for each operation and object. Exception Realms also connect to rules such as Web IDL’s exception algorithms.

Imagining an implementation makes it clear why inspecting only the currently displayed page’s origin is insufficient. The code currently executing, the environment where a function was created, and the environment associated with a target object are separate concerns.

This feels far from the original question about SharedArrayBuffer. But enforcing an isolated document’s relationships reaches all the way into these JavaScript-observable details.

Parsing isolation headers does not finish the implementation

When I previously wrote about Omoikane’s DOM and exception notifications, adding an API involved more than adding one method. Reading these specifications suggests the same for isolation.

The following describes things I would want to check when implementing a browser, based on the specifications above. It is not a claim that a particular PR implements all of them.

For example, a popup relationship can survive with matching origins and compatible policies, while navigation to an incompatible document severs it. Disabling all popups or preserving all references would both miss that distinction.

Likewise, preserving window reference identity must be distinguished from discontinuing old connections after a group switch. For postMessage(), the sender reference, sending-time origin, and delivery-time conditions must remain separate. Cross-origin property access also involves checks against the accessing environment and function object identity.

With COEP, neither allowing all images nor rejecting all cross-origin content is correct. The implementation must distinguish no-CORS requests, successful CORS checks, CORP permissions, and credential behavior.

Making crossOriginIsolated display the expected boolean before enforcing the underlying relationships and loads would leave the declaration inconsistent with behavior.

This does not mean JavaScript wrappers alone can complete CPU-level Spectre mitigation. Implementation protections, including process arrangements and response handling, must be considered alongside Web-visible specified behavior.

Things I had conflated

These were the points that repeatedly caught me out.

My initial rough understandingWhat I learned to distinguish
SharedArrayBuffer is restricted for Spectre because a low-level API probably helps with memory exploitsA clock built from the attacker’s own shared memory can measure cache traces
A failed check means nothing leakedInformation can survive in timing even when absent from committed results
Spectre requires SharedArrayBufferIt is one observation aid, not a universal prerequisite
JavaScript array accesses simply start returning out-of-bounds dataSpeculation and side channels are separate from language-level results
Spectre unconditionally reads other processes’ memoryWhere code executes matters, and process separation provides protection
COEP keeps attackers’ resources outIt also restricts importing other origins’ data without permission
COOP closes tabsA severed reference appearing closed differs from closing the UI
Groups, Realms, origins, and processes are similar isolation unitsThey concern distinct issues: references, execution environments, authority, and address spaces

The first row was the largest change for me. I knew the restriction was a Spectre mitigation, but had only vaguely connected the ideas of a memory vulnerability and a low-level API. Using that memory as a clock can help measure traces produced somewhere else.

From there, the explanation connected CPU traces that survive discarded work, data supposedly hidden from JavaScript, paths that bring data into a process, and references between windows.

Knowing that SharedArrayBuffer was restricted because of Spectre and explaining the mechanism and the need for HTTP headers were much further apart than I expected. I had filled that gap with “something low-level must be dangerous.” Following the clock connection step by step finally joined the pieces together.