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"
HeaderValueWhat 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

CRITICAL −20SharedArrayBuffer used without COOP/COEP headers — cross-origin isolation absent, enabling Spectre timing attacks against the MCP client process
HIGH −16Non-atomic write-pointer update in shared ring buffer — concurrent Workers produce data collisions and lost tool output chunks
HIGH −14State machine transition without compareExchange — duplicate tool executions possible when two Workers both see IDLE state
MEDIUM −10No crossOriginIsolated guard before SharedArrayBuffer allocation — runtime TypeError in non-isolated contexts with no graceful fallback
MEDIUM −8COEP set to require-corp without auditing subresource CORP headers — third-party scripts silently fail to load after header deployment

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 →