Not a vulnerability — just something I stumbled onto while poking at WebAssembly. One line of JavaScript on a page that has no special headers gives you a working SharedArrayBuffer, and from there you can build a timer roughly 17× finer than performance.now().

Background

SharedArrayBuffer was locked behind cross-origin isolation (Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp) in 2020. The reason: an unprivileged page could use one to build a high-resolution timer, and a high-resolution timer is enough for Spectre-class side-channel attacks. On a non-isolated page, SharedArrayBuffer is supposed to be unavailable, and performance.now() precision is reduced — typically floored at 100 µs.

It turns out you can still get a working SharedArrayBuffer on such a page with one line:

const sab = new WebAssembly.Memory({ initial: 1, maximum: 1, shared: true }).buffer;

I originally found this via WebAssembly.instantiate() on a module that declared shared memory, then went looking for prior reports and found a much shorter form already existed. It was reported to Chrome at issues.chromium.org/issues/40057687 and dismissed as Won’t Fix. Because the trick is already public and not considered a security bug by the vendor, this is just a demonstration of what you can actually do with the resulting buffer — a working high-resolution timer in the browser, no Workers required, no special headers required.

How the timer works

  1. Construct a WebAssembly.Memory({ shared: true }); its .buffer is a real SharedArrayBuffer.
  2. Spawn a hidden, same-origin blob iframe. Because it shares the parent’s agent cluster, you can hand it the SAB by a direct cross-realm function call — no postMessage, no structured clone (the latter is still gated on cross-origin isolation and would refuse).
  3. Inside the iframe, drive a MessageChannel ping-pong that calls Atomics.add(counter, 0, 1) on every macrotask. MessageChannel is not subject to the 4 ms setTimeout clamp, so this typically reaches 50–200 kHz without freezing the main thread. (A true while (true) busy loop would freeze the page, since same-origin blob iframes share the parent’s event loop.)
  4. The parent reads the counter with Atomics.load, calibrates against a known sleep, and converts ticks to nanoseconds.

What you’ll see

Click Run measurement below. Typical numbers on a non-isolated Chromium-family browser: tick rate around 100,000 Hz, smallest distinguishable step around 6 µs, while performance.now() on the same page is floored at 100 µs. So the SAB-based counter ends up roughly 17× finer than the timer the browser intended you to have.

Caveats: calibration jitter is around ±10%, so the absolute µs values are noisy. The timer’s resolution (smallest distinguishable step) is real, but converting ticks to a wall-clock time unit is only ballpark-accurate.

Why Chrome considers this not-a-bug

Best guess from the issue thread: the team’s view is that the structured-clone gate (Worker.postMessage(sab) still throws on a non-isolated page) is sufficient to keep the buffer from crossing into another agent cluster. In-cluster sharing — what this page does — is considered acceptable since everything in the cluster already shares state in other ways. Whether that’s convincing depends on whether you think “high-resolution timer in the document’s agent cluster” is a useful primitive to deny on its own. Reasonable people disagree.

Manuel


Click "Run measurement" to calibrate the timer and gather diagnostics.