Skip to main content

Enabling WebAssembly Multithreading: Configuring COOP and COEP Headers for Rust Wasm

 You have optimized your Rust logic, compiled to wasm32-unknown-unknown with atomics enabled, and implemented parallelization using Rayon. Yet, when you load the application in Chrome or Firefox, the WebAssembly module fails to instantiate, or the main thread panics with a specific, cryptic runtime error:

Uncaught ReferenceError: SharedArrayBuffer is not defined

This is not a Rust compilation error. It is a browser security enforcement. By default, modern browsers disable the SharedArrayBuffer constructor—the primitive required for WebAssembly threads to share memory—unless the context is "Cross-Origin Isolated."

To unlock multithreading in the browser, you must explicitly configure the Cross-Origin Opener Policy (COOP) and Cross-Origin Embedder Policy (COEP) headers on your server.

The Root Cause: Spectre and Side-Channels

The disabling of SharedArrayBuffer is a direct mitigation against Spectre and similar transient execution attacks.

In 2018, it was discovered that high-precision timers and shared memory segments could be leveraged to create side-channels. Malicious scripts could exploit race conditions in the CPU's speculative execution to read memory from other processes (e.g., data from a different tab or the browser kernel).

To prevent this, browsers removed the shared memory vectors. They reinstated them later, but only behind a strict security gate called Cross-Origin Isolation. This isolation ensures that your document cannot communicate with the window that opened it (preventing leaks to the opener) and prevents your document from loading cross-origin resources that haven't explicitly opted in (preventing leaks from embedded content).

The Fix: Server-Side Header Configuration

To enable SharedArrayBuffer, the document serving your WebAssembly application must return the following two HTTP response headers:

  1. Cross-Origin-Opener-Policy: same-origin
  2. Cross-Origin-Embedder-Policy: require-corp

Below are the configurations for three common serving environments: Nginx (Production), Webpack (Development), and a Rust backend (Axum).

1. Nginx Configuration

For static site deployments or reverse proxies, apply the headers within your server or location block.

server {
    listen 80;
    server_name wasm-app.example.com;
    root /var/www/html;
    index index.html;

    location / {
        # Isolate the browsing context
        add_header Cross-Origin-Opener-Policy "same-origin" always;
        
        # Require all embedded resources to opt-in to being loaded
        add_header Cross-Origin-Embedder-Policy "require-corp" always;

        try_files $uri $uri/ /index.html;
    }

    # Ensure WASM files are served with the correct MIME type
    location ~* \.wasm$ {
        add_header Cross-Origin-Opener-Policy "same-origin" always;
        add_header Cross-Origin-Embedder-Policy "require-corp" always;
        types { application/wasm wasm; }
    }
}

2. Webpack Dev Server

When developing locally with wasm-pack and Webpack, you must configure the dev server to inject these headers, or your local threaded builds will fail.

// webpack.config.js
const path = require('path');

module.exports = {
  entry: "./bootstrap.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bootstrap.js",
  },
  mode: "development",
  devServer: {
    headers: {
      "Cross-Origin-Opener-Policy": "same-origin",
      "Cross-Origin-Embedder-Policy": "require-corp",
    },
    client: {
      overlay: {
        errors: true,
        warnings: false,
      },
    },
  },
  experiments: {
    asyncWebAssembly: true,
  },
};

3. Rust Backend (Axum)

If you are serving your WebAssembly frontend directly from a Rust binary (a common pattern for self-contained binaries), use tower_http middleware to attach the headers.

use axum::{
    routing::get_service,
    Router,
    http::{HeaderValue, header::{PREFIX, CROSS_ORIGIN_OPENER_POLICY, CROSS_ORIGIN_EMBEDDER_POLICY}},
};
use tower_http::services::ServeDir;
use tower_http::set_header::SetResponseHeaderLayer;
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    // Define the static file service (where your .wasm, .js, .html live)
    let serve_dir = ServeDir::new("assets");

    // Apply the required security headers middleware
    let app = Router::new()
        .nest_service("/", get_service(serve_dir))
        .layer(
            SetResponseHeaderLayer::overriding(
                CROSS_ORIGIN_OPENER_POLICY,
                HeaderValue::from_static("same-origin")
            )
        )
        .layer(
            SetResponseHeaderLayer::overriding(
                CROSS_ORIGIN_EMBEDDER_POLICY,
                HeaderValue::from_static("require-corp")
            )
        );

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("Listening on {}", addr);
    
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Validating Cross-Origin Isolation

Once the headers are applied, reload your application. You can verify the state programmatically in the browser console.

if (window.crossOriginIsolated) {
    console.log("✅ Cross-Origin Isolated: SharedArrayBuffer is available.");
} else {
    console.error("❌ Not Isolated: Multithreading will fail.");
}

If this returns true, your Rust Wasm thread pool (e.g., rayon configured with wasm-bindgen-rayon) will initialize successfully.

The Consequence: External Resource Blocking

Enabling Cross-Origin-Embedder-Policy: require-corp has a significant side effect: it breaks external resources that do not explicitly opt-in.

If your application loads images from a CDN (e.g., AWS S3, Cloudinary) or scripts from a third party (e.g., Google Analytics), the browser will block them unless those external servers send the Cross-Origin-Resource-Policy (CORP) header.

The Fix for CDNs

If you control the CDN (e.g., S3), you must configure the bucket or CloudFront distribution to return:

Cross-Origin-Resource-Policy: cross-origin

If you do not control the external resource (e.g., a generic profile image from a social auth provider), you cannot display it directly in an <img> tag on a cross-origin isolated page without proxying it through your own backend.

Summary

  1. Spectre Mitigations disable SharedArrayBuffer by default.
  2. COOP (same-origin) isolates your process from other windows.
  3. COEP (require-corp) prevents loading non-compliant external resources.
  4. Together, they create a Cross-Origin Isolated environment, re-enabling SharedArrayBuffer and WebAssembly threads.
  5. Warning: Ensure your external assets (CDNs) serve Cross-Origin-Resource-Policy: cross-origin, or they will fail to load.