# Cross-Origin Storage (COS)

Note

This feature is experimental. The underlying Cross-Origin Storage browser API is a WICG proposal that has not yet shipped in any browser. Emscripten’s support is provided as a progressive enhancement — the runtime falls back to the standard `fetch()` path automatically when the browser does not expose the API.

## Overview

The Cross-Origin Storage (COS) API is a proposed browser standard that allows web applications on different origins to share large cached files, identified by their cryptographic hashes. A file stored in COS by one site can be retrieved by any other site using the same hash, eliminating redundant downloads.

Emscripten’s CROSS_ORIGIN_STORAGE flag integrates this into the standard Wasm loading path. At build time, Emscripten computes the SHA-256 hash of the final `.wasm` binary. At runtime, the generated JavaScript tries to retrieve the compiled Wasm module from COS before falling back to a normal network fetch. If the module is not yet in COS it is stored there after download, making it available to other origins immediately.

### When to use this flag

COS only delivers a benefit when the `.wasm` binary is byte-identical across many different origins — that is, a popular library whose compiled binary is loaded by many independent sites. If every visitor to every site downloads the exact same bytes, COS means they only download it once, ever.

Good candidates are libraries or toolkits that are:

- popular enough that many independent sites load the same binary,

- distributed as a stable, version-pinned `.wasm` file, and

- a single primary `.wasm` file (COS only covers the binary that Emscripten compiles; any additional Wasm files loaded at runtime are not covered).

Do not enable this flag for application-specific Wasm code built for your own site. That binary is unique to you; no other origin will ever have the same hash, so it will never get a COS cache hit. The normal HTTP cache already handles per-origin caching efficiently.

The exception is a Wasm binary that you deploy across multiple origins you own — for example, the same library shared between `https://app.example.com` and `https://api.example.com`. In that case COS can eliminate the redundant download between your own origins. Use CROSS_ORIGIN_STORAGE_ORIGINS to restrict access to only those origins rather than opening the cache entry to the world.

## Usage

Pass CROSS_ORIGIN_STORAGE at link time:

    emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE

### Controlling which origins can read the cached file

The CROSS_ORIGIN_STORAGE_ORIGINS setting controls the `origins` field passed to `requestFileHandles()` on the write (cache-miss) path. It has no effect on the read (cache-hit) path. Three modes are available:

Globally available (default, no explicit setting needed) — any origin can retrieve the file. This is applied automatically when CROSS_ORIGIN_STORAGE is used without specifying CROSS_ORIGIN_STORAGE_ORIGINS:

    emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE

Use this for popular binaries loaded by many independent origins. This is the recommended mode for resources where global COS cache hits are expected.

Restricted to a specific set of origins — only the listed origins can retrieve the file:

    emcc hello.cpp -o hello.js \
        -sCROSS_ORIGIN_STORAGE \
        -sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com

Use this for proprietary resources shared across a controlled set of related sites. Each entry must be a valid serialized HTTPS origin (scheme + host + optional port, no path). Mixing `'*'` with explicit origins is a link-time error.

Same-site only — pass an explicit empty list to omit the `origins` field, making the file available only to same-site origins:

    emcc hello.cpp -o hello.js \
        -sCROSS_ORIGIN_STORAGE \
        -sCROSS_ORIGIN_STORAGE_ORIGINS=[]

Use this for resources that should be shared across subdomains of a single site but not beyond.

Note

The COS spec defines a visibility upgrade rule: a resource’s availability can be widened but never narrowed. If a resource is already stored as globally available (`'*'`), any subsequent attempt to store it with a more restrictive `origins` list is ignored by the browser.

This rule also has a security implication: because storing always requires writing the actual bytes of the resource, no third party can probe the cache to determine whether a restricted-origin entry was previously stored by another origin. A cache hit is only possible after an explicit write that provided the content, so COS cannot be used as a timing oracle to detect the presence of a resource that the probing origin cannot access.

### Requirements and restrictions

- The flag emits a warning when the target environment does not include the web (`-sENVIRONMENT=node`, `-sENVIRONMENT=shell`): `navigator.crossOriginStorage` is a browser API and is never available in those environments.

- It produces a hard link-time error in SINGLE_FILE mode (`-sSINGLE_FILE`): the Wasm binary is embedded directly into the JS output and has no standalone `.wasm` file or fetchable URL to key the hash on.

- It produces a hard link-time error with `-sWASM_ASYNC_COMPILATION=0`: the synchronous instantiation path bypasses `instantiateAsync()` entirely, so the COS code can never be reached.

- It covers only the primary ``.wasm`` file. Secondary files produced by `-sSPLIT_MODULE` (`.deferred.wasm`) and side modules loaded at runtime via `dlopen` in `-sMAIN_MODULE` builds are fetched through the normal network path and are not stored in or retrieved from COS. A warning is emitted for both of these combinations.

- The COS API is a progressive enhancement. Browsers without the API continue to load the Wasm module via the normal `fetch()` and `WebAssembly.instantiateStreaming()` path without any error.

## How it works

### Build time

After all optimizations — including any `wasm-opt` passes run by Binaryen — Emscripten reads the final `.wasm` binary and hashes it. The hash object is embedded in the generated JavaScript glue as a build-time constant (currently SHA-256):

    Module['wasmHash'] = { algorithm: 'SHA-256', value: 'a3f2...c9d1' };

No extra files are produced; the hash is part of the regular `.js` output.

Warning

The hash is computed over the `.wasm` binary as emcc produces it, after emcc’s own internal Binaryen/`wasm-opt` pass. If your build pipeline runs additional wasm post-processing tools after emcc exits — for example, an external `wasm-strip` or `wasm-opt` invocation in a Makefile or CI script — those tools change the binary and invalidate the embedded hash.

In that case you must recompute the hash of the final `.wasm` and patch the value string in the generated `.js` yourself before shipping. A minimal shell snippet for doing so (SHA-256):

    # After all post-processing is complete:
    final_hash=$(sha256sum hello.wasm | awk '{print $1}')
    sed -i "s/'[0-9a-f]\{64\}'/'${final_hash}'/g" hello.js

On macOS, use `shasum -a 256` in place of `sha256sum`, and install GNU sed (`brew install gnu-sed`) or adapt the `sed` command for BSD sed syntax.

### Runtime (web only)

When the page loads, the generated JavaScript follows this logic:

1. Feature detection — check `'crossOriginStorage' in navigator`. If the API is absent, skip to the normal fetch path immediately.

2. Cache hit — call `navigator.crossOriginStorage.requestFileHandles([cosHash])`. If the handle is returned (the module is already in COS), read it with `handle.getFile()` → `.arrayBuffer()` and pass the bytes to `WebAssembly.instantiate()`. Then invoke `Module['onCOSCacheHit'](hash)` if defined.

3. Cache miss — if a `NotFoundError` is thrown, fetch the `.wasm` over the network as usual, invoke `Module['onCOSCacheMiss'](hash, url)` if defined, call `WebAssembly.instantiate()` immediately so the page loads without delay, and then write the bytes into COS in the background (fire-and-forget) using the `origins` value controlled by CROSS_ORIGIN_STORAGE_ORIGINS (`'*'` by default). Once the write completes, invoke `Module['onCOSStore'](hash)` if defined.

4. Fallback — any unexpected error (`NotAllowedError` from the browser, network failure during the miss path, etc.) is logged with `err()` and the runtime falls through to the standard streaming-instantiation path below. The page always loads.

### Instrumentation callbacks

Three optional `Module` properties let you observe COS events at runtime. They are opt-in: to include the callback code in the output, list them in `INCOMING_MODULE_JS_API` at link time:

    emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE \
        -sINCOMING_MODULE_JS_API=onCOSCacheHit,onCOSCacheMiss,onCOSStore

    var Module = {
      // Called when the Wasm binary was served from the cross-origin cache.
      onCOSCacheHit: (hash) => {
        console.log('Cache hit, SHA-256:', hash);
      },

      // Called when the Wasm binary was not in COS and was fetched over the
      // network.  |hash| is the hash that missed; |url| is the fallback URL.
      onCOSCacheMiss: (hash, url) => {
        console.log('Cache miss, SHA-256:', hash, 'fetched from:', url);
      },

      // Called after the Wasm binary has been successfully written to COS.
      onCOSStore: (hash) => {
        console.log('Stored in COS, SHA-256:', hash);
      },
    };

## Testing with the extension polyfill

Because no browser ships the COS API natively yet, you can experiment using the Cross-Origin Storage extension, which injects a `navigator.crossOriginStorage` polyfill on every page.

### Manual testing

1. Install the extension in Chrome.

2. Build your project with `-sCROSS_ORIGIN_STORAGE -sENVIRONMENT=web`.

3. Serve the output over HTTP (e.g. with `emrun` or `python3 -m http.server`).

4. Open the page — on the first load the Wasm binary is fetched and stored in COS. Open the same page in a second tab or from a different origin: the module is loaded from COS without a network request.

### Automated browser testing

The Emscripten browser test suite includes COS tests that run against the polyfill extension. The extension must be available as an unpacked directory (containing `manifest.json`). A helper script downloads and unpacks it automatically:

    python3 test/setup_cos_extension.py

Then run the tests, passing the printed path as `EMTEST_COS_EXTENSION_PATH`:

    EMTEST_COS_EXTENSION_PATH=$(python3 test/setup_cos_extension.py --quiet) \
      python3 test/runner.py \
          browser.test_cross_origin_storage_fallback \
          browser.test_cross_origin_storage_miss_then_hit

`test_cross_origin_storage_fallback` does not require the extension and verifies that a `-sCROSS_ORIGIN_STORAGE` build loads correctly on browsers where the COS API is absent. `test_cross_origin_storage_miss_then_hit` requires the extension and exercises both the cache-miss store and cache-hit paths in sequence.

## Verifying the embedded hash

You can confirm that the hash embedded in the `.js` output matches the actual `.wasm` file using standard tools:

    # SHA-256 of the wasm file
    sha256sum hello.wasm

    # Extract the hash embedded in the JS
    grep -oP "value: '\K[0-9a-f]{64}" hello.js

Both values must be identical. The Emscripten test suite checks this automatically via `test_cross_origin_storage_js_output` in `test/test_other.py`.

## Custom `Module['instantiateWasm']` implementations

The COS fetch logic described above lives inside `instantiateAsync()`, which is the standard Emscripten wasm loading path. When a program provides its own `Module['instantiateWasm']` callback, Emscripten calls that callback directly and skips `instantiateAsync()` entirely, so the built-in COS code is never reached.

To support COS in a custom loader, Emscripten exposes the build-time SHA-256 hash as a named Module property:

    Module['wasmHash']  // { algorithm: 'SHA-256', value: '<64 hex chars>' }

This property is set by the generated JavaScript before `Module['instantiateWasm']` is called, so it is always available inside the callback. `Module` in this context is the config object passed to the module factory — whatever variable you use when calling `new Module(config)` or the equivalent factory function. A custom loader can read `Module['wasmHash']` via a reference to that config object:

    var Module = {
      instantiateWasm(imports, onSuccess) {
        // `this` inside the callback is Emscripten's internal Module object;
        // read the hash via the outer Module reference instead.
        const cosHash = Module['wasmHash'];
        if (cosHash?.value && globalThis.navigator?.crossOriginStorage) {
          navigator.crossOriginStorage.requestFileHandles([cosHash])
            .then(handles => handles[0].getFile())
            .then(f => f.arrayBuffer())
            .then(bytes => WebAssembly.instantiate(bytes, imports))
            .then(({instance, module}) => onSuccess(instance, module))
            .catch(err => {
              if (err.name !== 'NotFoundError') throw err;
              // cache miss — fetch normally and store in the background
              fetch('hello.wasm')
                .then(r => r.arrayBuffer())
                .then(bytes => {
                  WebAssembly.instantiate(bytes, imports)
                    .then(({instance, module}) => onSuccess(instance, module));
                  // fire-and-forget store
                  navigator.crossOriginStorage
                    .requestFileHandles([cosHash], { create: true, origins: '*' })
                    .then(wh => wh[0].createWritable())
                    .then(w => w.write(new Blob([bytes], {type:'application/wasm'}))
                                .then(() => w.close()));
                });
            });
          return;  // async; onSuccess called above
        }
        // fallback — normal streaming instantiation
        WebAssembly.instantiateStreaming(fetch('hello.wasm'), imports)
          .then(({instance, module}) => onSuccess(instance, module));
      },
    };

`Module['wasmHash']` is only present in builds compiled with CROSS_ORIGIN_STORAGE. Always guard on its truthiness before using it, as shown above, so the same loader code works in builds compiled without the flag.

## Relationship to other caching mechanisms

COS is a complement to, not a replacement for, existing browser caches:

- HTTP cache / Service Worker cache — still used for per-origin caching. COS adds cross-origin sharing on top.

- ``NODE_CODE_CACHING`` — a Node.js-specific V8 bytecode cache; unrelated to COS.

- IndexedDB / OPFS — per-origin storage; COS shares across origins.

## See also

- WICG Cross-Origin Storage explainer

- COS browser extension (Chrome Web Store)

- COS browser extension (source code)

- Emscripten Compiler Settings — `CROSS_ORIGIN_STORAGE` entry

- Building to WebAssembly — general guide to building Wasm with Emscripten