MCP Server Security · SharedArrayBuffer
MCP server SharedArrayBuffer security — COOP/COEP Spectre mitigations, shared memory race conditions, and atomics correctness
SharedArrayBuffer was disabled in all browsers after Spectre because it enables high-resolution timers for cross-process memory side-channel attacks. Re-enabling it requires cross-origin isolation via COOP and COEP headers. Browser-based MCP client UIs that use SharedArrayBuffer for streaming tool output or worker communication must implement this correctly — and must also prevent race conditions in concurrent tool call handlers that share memory.
Why SharedArrayBuffer was disabled and what cross-origin isolation means
Spectre (CVE-2018-3639) is a CPU speculative-execution side-channel that lets JavaScript in one process read memory from another process by measuring cache timing. The attack requires a high-resolution timer. SharedArrayBuffer with Atomics.wait() and Atomics.notify() provides a microsecond-resolution timer usable from a Worker thread — exactly the primitive Spectre needs. Browsers responded by disabling SharedArrayBuffer entirely in January 2018.
To re-enable it, the browser requires the page to be in a cross-origin isolated context. Cross-origin isolation means the page's process is guaranteed not to share memory with cross-origin documents, eliminating the attack surface for cross-process Spectre reads.
COOP and COEP: the two required headers
Cross-origin isolation requires both headers on every document that needs SharedArrayBuffer:
# On the MCP client UI server (Caddy example) header Cross-Origin-Opener-Policy "same-origin" header Cross-Origin-Embedder-Policy "require-corp"
| Header | Value | What it does |
|---|---|---|
Cross-Origin-Opener-Policy |
same-origin |
Prevents cross-origin pages from sharing a browsing context group with this document. Breaks window.opener references from cross-origin popups. |
Cross-Origin-Embedder-Policy |
require-corp |
Every subresource (images, scripts, fetch targets) must either be same-origin or carry a Cross-Origin-Resource-Policy header explicitly allowing embedding. |
Verify isolation is active before trying to use SharedArrayBuffer:
// Check cross-origin isolation before allocating
if (!crossOriginIsolated) {
console.error('SharedArrayBuffer unavailable: COOP/COEP headers not set');
// Fall back to MessageChannel or non-shared buffers
} else {
const sab = new SharedArrayBuffer(1024);
const view = new Int32Array(sab);
// Safe to share with Workers
}
COEP breaks cross-origin resource loading. Setting require-corp means every third-party script, image, or API endpoint your MCP client UI fetches must include Cross-Origin-Resource-Policy: cross-origin. CDN-hosted scripts (e.g., analytics) will fail to load unless their servers add this header. Audit all subresources before enabling COEP in production.
Race conditions in shared memory MCP tool contexts
A common pattern in browser-based MCP client UIs is using a SharedArrayBuffer as a lock-free ring buffer to stream tool call results from a Worker back to the main thread without copying. When multiple tool calls execute concurrently in separate Workers, all writing to the same buffer, race conditions appear in the write-pointer update.
// WRONG: non-atomic pointer update creates races
const buf = new SharedArrayBuffer(4096);
const view = new Int32Array(buf);
const WRITE_PTR = 0; // index 0 holds write pointer
function writeChunk(data) {
const ptr = view[WRITE_PTR]; // read
// --- another Worker can write here and increment ptr ---
view.set(data, ptr); // write at stale ptr (data collision)
view[WRITE_PTR] = ptr + data.length; // update (clobbers other Writer's update)
}
// CORRECT: use Atomics.add() for atomic read-and-increment
function writeChunkSafe(data) {
// Atomically claim a slot: returns the old value, sets new = old + length
const ptr = Atomics.add(view, WRITE_PTR, data.length);
const end = ptr + data.length;
if (end > view.length) {
// Buffer full — handle overflow (wrap, drop, or backpressure signal)
Atomics.sub(view, WRITE_PTR, data.length); // give back the slot
throw new Error('SAB_RING_BUFFER_FULL');
}
// Write data at the claimed slot
for (let i = 0; i < data.length; i++) {
Atomics.store(view, ptr + i, data[i]);
}
// Signal the reader that new data is available at [ptr..end)
Atomics.notify(view, WRITE_PTR, 1);
}
Atomics.compareExchange() for state machine transitions
When an MCP tool call lifecycle needs to track state (IDLE → RUNNING → DONE) in shared memory visible to multiple threads, use Atomics.compareExchange() to prevent two Workers from both transitioning from IDLE to RUNNING simultaneously:
const STATE_IDLE = 0;
const STATE_RUNNING = 1;
const STATE_DONE = 2;
const STATE_INDEX = 0;
const stateView = new Int32Array(new SharedArrayBuffer(4));
// Only one Worker can successfully transition IDLE → RUNNING
function tryClaimExecution() {
// compareExchange(typedArray, index, expectedValue, replacementValue)
// Returns the old value. If old === expected, the swap happened atomically.
const old = Atomics.compareExchange(stateView, STATE_INDEX, STATE_IDLE, STATE_RUNNING);
return old === STATE_IDLE; // true = this Worker won the race
}
function markDone() {
Atomics.store(stateView, STATE_INDEX, STATE_DONE);
Atomics.notify(stateView, STATE_INDEX, Infinity); // wake all waiters
}
// Worker entry point
if (tryClaimExecution()) {
try {
await runToolCall();
} finally {
markDone();
}
} else {
// Another Worker is already running this tool call — bail out
}
SkillAudit findings
See also: SSRF advanced patterns · Web Crypto API security · postMessage security
Audit your MCP server for SharedArrayBuffer misuse
SkillAudit scans for missing COOP/COEP headers, non-atomic shared memory access, and 40+ other security patterns.
Run free audit →