From 9024b2f5a2002fe83d71d45cab9786c41b5a062f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 08:50:59 +0000 Subject: [PATCH 01/74] feat: add CROSS_ORIGIN_STORAGE support for Wasm loading Adds a new -sCROSS_ORIGIN_STORAGE=1 compiler flag that enables progressive enhancement of Wasm loading via the Cross-Origin Storage browser API, targeting web environments only. Changes: - src/settings.js: adds CROSS_ORIGIN_STORAGE flag (default 0/off) - src/settings_internal.js: adds WASM_SHA256 internal variable - tools/link.py: computes SHA-256 of final .wasm after wasm-opt and injects it into settings as WASM_SHA256, available to JS templates - src/preamble.js: in instantiateAsync(), attempts COS cache lookup by hash before falling back to the normal streaming fetch path; on a cache miss, stores the fetched bytes in COS for reuse The COS path is guarded by #if ENVIRONMENT_MAY_BE_WEB and #if CROSS_ORIGIN_STORAGE so no dead code ships in other targets or when the feature is not enabled. --- src/preamble.js | 31 +++++++++++++++++++++++++++++++ src/settings.js | 14 ++++++++++++++ src/settings_internal.js | 5 +++++ tools/link.py | 15 +++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/src/preamble.js b/src/preamble.js index b039a508fac0a..d86e6b15c8bda 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -629,6 +629,37 @@ async function instantiateArrayBuffer(binaryFile, imports) { async function instantiateAsync(binary, binaryFile, imports) { #if !SINGLE_FILE +#if ENVIRONMENT_MAY_BE_WEB +#if CROSS_ORIGIN_STORAGE + // Cross-Origin Storage (COS) progressive enhancement. + // The SHA-256 hash of the .wasm binary is computed at link time and embedded + // here. At runtime we check whether the browser exposes the COS API; if so + // we attempt to retrieve the already-compiled module from the shared + // cross-origin cache keyed by its hash, avoiding a network round-trip. + // On a cache miss we fall through to the normal fetch path and then store the + // fetched bytes back into COS for other origins to reuse in the future. + var wasmHash = '{{{ WASM_SHA256 }}}'; + if (wasmHash && typeof crossOriginStorage !== 'undefined' && crossOriginStorage) { + try { + var cosResult = await crossOriginStorage.get(wasmHash); + if (cosResult) { + // Cache hit — instantiate directly from the stored ArrayBuffer. + return WebAssembly.instantiate(cosResult, imports); + } + // Cache miss — fetch over the network, store in COS, then instantiate. + var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); + var wasmBytes = await networkResponse.arrayBuffer(); + // Store asynchronously; don't block instantiation on the write. + crossOriginStorage.set(wasmHash, wasmBytes).catch((e) => err(`COS store failed: ${e}`)); + return WebAssembly.instantiate(wasmBytes, imports); + } catch (cosReason) { + // COS look-up failed for an unexpected reason; fall through to the + // standard streaming path below so the page still loads. + err(`Cross-Origin Storage lookup failed: ${cosReason}`); + } + } +#endif // CROSS_ORIGIN_STORAGE +#endif // ENVIRONMENT_MAY_BE_WEB if (!binary #if MIN_SAFARI_VERSION < 150000 // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming diff --git a/src/settings.js b/src/settings.js index c9e8ad2f6eed5..556bb7dd655c1 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2201,6 +2201,20 @@ var GROWABLE_ARRAYBUFFERS = false; // indirectly using `importScripts` var CROSS_ORIGIN = false; +// [experimental] Enables Cross-Origin Storage (COS) API support for Wasm +// loading on the Web target. When enabled, Emscripten will compute the +// SHA-256 hash of the final .wasm binary at link time, embed it in the +// generated JS glue, and use the browser COS API (if available) as a +// progressive enhancement: the runtime first tries to retrieve the Wasm +// module from the shared cross-origin cache keyed by its hash. On a cache +// miss the module is fetched over the network as usual and then stored in +// COS so that other origins can reuse it. Falls back transparently to the +// standard fetch path when the browser does not expose the COS API. +// Only meaningful for the Web environment; has no effect elsewhere. +// Usage: emcc -sCROSS_ORIGIN_STORAGE=1 ... +// [link] +var CROSS_ORIGIN_STORAGE = 0; + // This setting changes the behaviour of the ``-shared`` flag. When set to true // you get the old emscripten behaviour where the ``-shared`` flag actually // produces a normal object file (i.e. ``ld -r``). When set to true (the diff --git a/src/settings_internal.js b/src/settings_internal.js index ef72ff8a4789c..482943d54f7bf 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -127,6 +127,11 @@ var USER_EXPORTS = []; // name of the file containing wasm binary, if relevant var WASM_BINARY_FILE = ''; +// SHA-256 hex digest of the final .wasm binary, computed at link time. +// Populated automatically by tools/link.py when CROSS_ORIGIN_STORAGE=1. +// Available in JS glue templates as {{{ WASM_SHA256 }}}. +var WASM_SHA256 = ''; + // Base URL the source mapfile, if relevant var SOURCE_MAP_BASE = ''; diff --git a/tools/link.py b/tools/link.py index b4a575fe1ec30..55ea2699b1c11 100644 --- a/tools/link.py +++ b/tools/link.py @@ -4,6 +4,7 @@ # found in the LICENSE file. import base64 +import hashlib import json import logging import os @@ -1895,6 +1896,20 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat phase_binaryen(target, options, wasm_target) + # Compute the SHA-256 of the final .wasm binary and make it available to the + # JS glue preprocessor as {{{ WASM_SHA256 }}}. We do this after phase_binaryen + # so that wasm-opt transformations are already reflected in the hash. + if settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB: + if os.path.exists(wasm_target): + with open(wasm_target, 'rb') as f: + wasm_bytes = f.read() + settings.WASM_SHA256 = hashlib.sha256(wasm_bytes).hexdigest() + logger.debug(f'CROSS_ORIGIN_STORAGE: wasm SHA-256 = {settings.WASM_SHA256}') + else: + # Inline / SINGLE_FILE builds embed the wasm in JS; no file to hash. + logger.warning('CROSS_ORIGIN_STORAGE: wasm file not found for hashing; WASM_SHA256 will be empty') + settings.WASM_SHA256 = '' + # If we are not emitting any JS then we are all done now if options.oformat != OFormat.WASM: phase_final_emitting(options, target, js_target, wasm_target) From 403c59ff81eb7ba81d9320b5ea7199d248be467f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 08:57:06 +0000 Subject: [PATCH 02/74] fix: correct COS implementation to match WICG explainer API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation used a fabricated API surface. This commit aligns with the actual spec at https://github.com/WICG/cross-origin-storage Key corrections: - Feature detection: 'crossOriginStorage' in navigator (not typeof crossOriginStorage !== 'undefined') - Hash format: { algorithm: 'SHA-256', value: '' } (not a bare string; spec requires this exact object shape and throws TypeError if value is not a valid lowercase hex string of length 64) - Cache-hit read: requestFileHandles([hash]) → handle.getFile() → blob.arrayBuffer() → WebAssembly.instantiate() (not a .get() method that returns an ArrayBuffer directly) - Cache-miss write: requestFileHandles([hash], { create: true, origins: '*' }) → handle.createWritable() → writable.write(new Blob([bytes])) → writable.close() - origins:'*' makes the Wasm module globally available, which is appropriate for public Wasm assets used across many sites - the UA verifies the hash against the written Blob, so we must write a Blob (not a raw ArrayBuffer) - store is fire-and-forget (async IIFE) so instantiation is never blocked on the write completing - Error handling: distinguish NotFoundError (cache miss, recoverable) from NotAllowedError / other errors (fall through to standard path) --- src/preamble.js | 76 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/src/preamble.js b/src/preamble.js index d86e6b15c8bda..25f7fe6e0d8b8 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -632,30 +632,62 @@ async function instantiateAsync(binary, binaryFile, imports) { #if ENVIRONMENT_MAY_BE_WEB #if CROSS_ORIGIN_STORAGE // Cross-Origin Storage (COS) progressive enhancement. - // The SHA-256 hash of the .wasm binary is computed at link time and embedded - // here. At runtime we check whether the browser exposes the COS API; if so - // we attempt to retrieve the already-compiled module from the shared - // cross-origin cache keyed by its hash, avoiding a network round-trip. - // On a cache miss we fall through to the normal fetch path and then store the - // fetched bytes back into COS for other origins to reuse in the future. - var wasmHash = '{{{ WASM_SHA256 }}}'; - if (wasmHash && typeof crossOriginStorage !== 'undefined' && crossOriginStorage) { + // https://github.com/WICG/cross-origin-storage + // + // The SHA-256 hash of the final .wasm binary is computed at link time and + // embedded here as a build-time constant. At runtime we feature-detect the + // browser COS API via `'crossOriginStorage' in navigator`, then call + // navigator.crossOriginStorage.requestFileHandles() with the hash object + // required by the spec ({ algorithm: 'SHA-256', value: '' }). + // + // Cache-hit path: + // requestFileHandles() succeeds → getFile() → arrayBuffer() → instantiate + // + // Cache-miss path (NotFoundError): + // fetch() the wasm over the network → instantiate → store in COS with + // origins:'*' (appropriate for a public Wasm module that any site may + // benefit from). The store is fire-and-forget so it never delays startup. + // + // Any other error (NotAllowedError, network failure, …) falls through to the + // standard Emscripten streaming path so the page always loads. + var wasmHashValue = '{{{ WASM_SHA256 }}}'; + if (wasmHashValue && 'crossOriginStorage' in navigator) { + var cosHash = { algorithm: 'SHA-256', value: wasmHashValue }; try { - var cosResult = await crossOriginStorage.get(wasmHash); - if (cosResult) { - // Cache hit — instantiate directly from the stored ArrayBuffer. - return WebAssembly.instantiate(cosResult, imports); + var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); + // Cache hit — read the Blob and instantiate from its ArrayBuffer. + var cosFile = await cosHandles[0].getFile(); + var cosBytes = await cosFile.arrayBuffer(); + return WebAssembly.instantiate(cosBytes, imports); + } catch (cosErr) { + if (cosErr.name === 'NotFoundError') { + // Cache miss — fetch normally, then store in COS for future consumers. + try { + var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); + var wasmBytes = await networkResponse.arrayBuffer(); + // Fire-and-forget store; never block instantiation on the write. + (async () => { + try { + var writeHandles = await navigator.crossOriginStorage.requestFileHandles( + [cosHash], + { create: true, origins: '*' }, + ); + var writable = await writeHandles[0].createWritable(); + await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); + await writable.close(); + } catch (storeErr) { + err(`COS store failed: ${storeErr}`); + } + })(); + return WebAssembly.instantiate(wasmBytes, imports); + } catch (fetchErr) { + // Network fetch failed; fall through to the standard path. + err(`COS fallback fetch failed: ${fetchErr}`); + } + } else { + // NotAllowedError or unexpected error; fall through gracefully. + err(`Cross-Origin Storage lookup failed: ${cosErr}`); } - // Cache miss — fetch over the network, store in COS, then instantiate. - var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); - var wasmBytes = await networkResponse.arrayBuffer(); - // Store asynchronously; don't block instantiation on the write. - crossOriginStorage.set(wasmHash, wasmBytes).catch((e) => err(`COS store failed: ${e}`)); - return WebAssembly.instantiate(wasmBytes, imports); - } catch (cosReason) { - // COS look-up failed for an unexpected reason; fall through to the - // standard streaming path below so the page still loads. - err(`Cross-Origin Storage lookup failed: ${cosReason}`); } } #endif // CROSS_ORIGIN_STORAGE From 46570369e34436ea21b04742a6f2c07153a7b71f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:07:44 +0000 Subject: [PATCH 03/74] test: add test suite for CROSS_ORIGIN_STORAGE feature Five tests in test/test_other.py covering all meaningful static build output properties of the feature (no browser runtime required): test_cross_origin_storage_js_output - Verifies all expected API surface is present in the emitted JS when -sCROSS_ORIGIN_STORAGE=1 -sENVIRONMENT=web is used: 'crossOriginStorage' in navigator (feature detection) navigator.crossOriginStorage.requestFileHandles (correct API) algorithm: 'SHA-256' / value: '' (spec hash object shape) origins: '*' (globally-available flag for public Wasm) getFile() / createWritable() (read and write paths) 'NotFoundError' / 'NotAllowedError' (error discrimination) - Reads the emitted .wasm file and verifies the embedded hash value exactly matches hashlib.sha256(wasm_bytes).hexdigest() test_cross_origin_storage_disabled_by_default - Verifies crossOriginStorage is absent from JS output when the flag is not passed (default CROSS_ORIGIN_STORAGE=0) test_cross_origin_storage_not_emitted_for_node_target - Verifies the #if ENVIRONMENT_MAY_BE_WEB preprocessor guard strips all COS code when -sENVIRONMENT=node, even with the flag enabled test_cross_origin_storage_not_emitted_for_single_file - Verifies COS code is absent in SINGLE_FILE builds where there is no standalone .wasm file to hash test_cross_origin_storage_hash_changes_with_content - Compiles two different source files and asserts their embedded hashes differ, confirming the hash reflects actual binary content --- test/test_other.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/test/test_other.py b/test/test_other.py index c90480d7bc4c2..7cc7138bde9cb 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -5,6 +5,7 @@ import glob +import hashlib import importlib import itertools import json @@ -15327,3 +15328,95 @@ def test_logReadFiles(self): create_file('pre.js', 'Module.logReadFiles = 1;') output = self.do_runf('checksummer.c', args=['test.txt'], cflags=['--pre-js=pre.js']) self.assertContained('read file: /test.txt', output) + + # --------------------------------------------------------------------------- + # Tests for CROSS_ORIGIN_STORAGE (-sCROSS_ORIGIN_STORAGE=1) + # https://github.com/WICG/cross-origin-storage + # --------------------------------------------------------------------------- + + def test_cross_origin_storage_js_output(self): + """COS code is present in JS when the feature is enabled for the web target, + and the embedded hash is the correct SHA-256 of the compiled .wasm file.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js = read_file('hello.js') + + # Feature-detect pattern from the WICG explainer. + self.assertContained("'crossOriginStorage' in navigator", js) + + # Correct API call. + self.assertContained('navigator.crossOriginStorage.requestFileHandles', js) + + # Hash object shape required by the spec. + self.assertContained("algorithm: 'SHA-256'", js) + + # Globally-available flag appropriate for a public Wasm module. + self.assertContained("origins: '*'", js) + + # Cache-hit and cache-miss path markers. + self.assertContained('getFile()', js) + self.assertContained('createWritable()', js) + + # Error name discrimination. + self.assertContained("'NotFoundError'", js) + self.assertContained("'NotAllowedError'", js) + + # The hash embedded in the JS must be a 64-char lowercase hex string … + m = re.search(r"value:\s*'([0-9a-f]{64})'", js) + self.assertTrue(m, 'could not find a 64-char hex WASM_SHA256 value in JS output') + embedded_hash = m.group(1) + + # … and must exactly match the SHA-256 of the emitted .wasm file. + expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() + self.assertEqual(embedded_hash, expected_hash, + 'embedded WASM_SHA256 does not match actual .wasm SHA-256') + + def test_cross_origin_storage_disabled_by_default(self): + """COS code must NOT appear when the flag is omitted (default off).""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertNotContained('crossOriginStorage', js) + + def test_cross_origin_storage_not_emitted_for_node_target(self): + """COS code must NOT appear when targeting Node.js only, even with the flag + set; the #if ENVIRONMENT_MAY_BE_WEB guard should strip it.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=node', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertNotContained('crossOriginStorage', js) + + def test_cross_origin_storage_not_emitted_for_single_file(self): + """COS code must NOT appear in SINGLE_FILE builds (wasm is inlined as + base64; there is no standalone .wasm file to key the hash on).""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sSINGLE_FILE', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertNotContained('crossOriginStorage', js) + + def test_cross_origin_storage_hash_changes_with_content(self): + """Two different programs must produce different embedded hashes.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js_a = read_file('hello.js') + hash_a = re.search(r"value:\s*'([0-9a-f]{64})'", js_a).group(1) + + self.run_process([EMCC, test_file('hello_world_small.c'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-o', 'small.js']) + js_b = read_file('small.js') + hash_b = re.search(r"value:\s*'([0-9a-f]{64})'", js_b).group(1) + + self.assertNotEqual(hash_a, hash_b, + 'different programs should produce different WASM_SHA256 hashes') From 44f94e530b6dbea891f3bbf13ada792dac3adb74 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:12:05 +0000 Subject: [PATCH 04/74] docs: add Cross-Origin Storage guide and polish setting comment - site/source/docs/compiling/CrossOriginStorage.rst (new) Full guide covering: overview, usage, build-time / runtime behaviour, testing with the extension polyfill, hash verification, and relationship to other caching mechanisms. Links to the WICG explainer and extension. - site/source/docs/compiling/index.rst Register CrossOriginStorage in the section toctree and bullet list. - src/settings.js Expand the CROSS_ORIGIN_STORAGE comment into RST-friendly prose that the auto-generated settings_reference.rst page will render cleanly, including a cross-reference to the new guide page. --- .../docs/compiling/CrossOriginStorage.rst | 150 ++++++++++++++++++ site/source/docs/compiling/index.rst | 2 + src/settings.js | 33 ++-- 3 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 site/source/docs/compiling/CrossOriginStorage.rst diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst new file mode 100644 index 0000000000000..04cf2a89d31a0 --- /dev/null +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -0,0 +1,150 @@ +.. _CrossOriginStorage: + +========================== +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 ``-sCROSS_ORIGIN_STORAGE=1`` 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. + +This is particularly beneficial for popular Wasm modules that many sites +ship independently — game engines, scientific computing runtimes, and +frameworks such as Flutter or Pyodide — where users would otherwise download +the same bytes many times. + +Usage +===== + +Pass ``-sCROSS_ORIGIN_STORAGE=1`` at link time:: + + emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 + +The flag is a **link-time** setting and has no effect during compilation of +individual object files. + +Requirements and restrictions +------------------------------ + +- The flag only has an effect when the output targets the **web** environment. + It is silently ignored for Node.js-only or shell targets (``-sENVIRONMENT=node``). +- It has no effect in **SINGLE_FILE** mode (``-sSINGLE_FILE``), because the + Wasm binary is embedded inline as base64 and there is no standalone + ``.wasm`` file to hash. +- 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 computes its SHA-256 +digest. That digest is embedded in the generated JavaScript glue as a +build-time constant:: + + var wasmHashValue = 'a3f2...c9d1'; // 64 hex characters + +No extra files are produced; the hash is part of the regular ``.js`` output. + +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([{algorithm: 'SHA-256', value: wasmHashValue}])``. + If the handle is returned (the module is already in COS), read it with + ``handle.getFile()`` → ``.arrayBuffer()`` and pass the bytes to + ``WebAssembly.instantiate()``. + +3. **Cache miss** — if a ``NotFoundError`` is thrown, fetch the ``.wasm`` + over the network as usual, call ``WebAssembly.instantiate()`` immediately + so the page loads without delay, and then write the bytes into COS in the + background (fire-and-forget) with ``origins: '*'`` so any other origin + can benefit:: + + navigator.crossOriginStorage.requestFileHandles([hash], { create: true, origins: '*' }) + +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. + +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. + +1. Install the extension in Chrome. +2. Build your project with ``-sCROSS_ORIGIN_STORAGE=1 -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. + +Verifying the embedded hash +============================ + +You can confirm that the hash embedded in the ``.js`` output matches the +actual ``.wasm`` file using standard tools: + +.. code-block:: bash + + # 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``. + +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 (polyfill) `_ +- :ref:`settings-reference` — ``CROSS_ORIGIN_STORAGE`` entry +- :ref:`WebAssembly` — general guide to building Wasm with Emscripten diff --git a/site/source/docs/compiling/index.rst b/site/source/docs/compiling/index.rst index e8b03becc832d..6aa22ae9c5a70 100644 --- a/site/source/docs/compiling/index.rst +++ b/site/source/docs/compiling/index.rst @@ -13,6 +13,7 @@ This section contains topics about building projects and running the output. - :ref:`Deploying-Pages` covers topics related to hosting Emscripten compiled web pages on a CDN. - :ref:`GitLab` explains how to build and test projects on GitLab. - :ref:`Contrib-Ports` contains information about contrib ports. +- :ref:`CrossOriginStorage` explains how to enable the experimental Cross-Origin Storage integration for sharing Wasm modules across origins. .. toctree:: @@ -26,3 +27,4 @@ This section contains topics about building projects and running the output. Deploying-Pages GitLab Contrib-Ports + CrossOriginStorage diff --git a/src/settings.js b/src/settings.js index 556bb7dd655c1..b0f5947ad214a 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2202,16 +2202,29 @@ var GROWABLE_ARRAYBUFFERS = false; var CROSS_ORIGIN = false; // [experimental] Enables Cross-Origin Storage (COS) API support for Wasm -// loading on the Web target. When enabled, Emscripten will compute the -// SHA-256 hash of the final .wasm binary at link time, embed it in the -// generated JS glue, and use the browser COS API (if available) as a -// progressive enhancement: the runtime first tries to retrieve the Wasm -// module from the shared cross-origin cache keyed by its hash. On a cache -// miss the module is fetched over the network as usual and then stored in -// COS so that other origins can reuse it. Falls back transparently to the -// standard fetch path when the browser does not expose the COS API. -// Only meaningful for the Web environment; has no effect elsewhere. -// Usage: emcc -sCROSS_ORIGIN_STORAGE=1 ... +// loading on the Web target. +// +// When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` +// binary at link time, embeds it as a build-time constant in the generated +// JavaScript glue, and uses the browser COS API as a progressive enhancement: +// +// - **Cache hit**: the runtime calls +// ``navigator.crossOriginStorage.requestFileHandles()`` with the hash and, +// if the module is found, reads it directly from the cross-origin cache. +// - **Cache miss**: the module is fetched over the network as usual, then +// stored in COS in the background (non-blocking) with ``origins: '*'`` so +// any other origin can reuse it. +// - **Fallback**: when the browser does not expose the COS API, or when an +// unexpected error occurs, the runtime falls through to the standard +// ``fetch`` / ``WebAssembly.instantiateStreaming`` path transparently. +// +// Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no +// effect on Node.js or shell targets. Also has no effect in SINGLE_FILE +// builds where the Wasm binary is inlined as base64. +// +// See :ref:`CrossOriginStorage` for the full guide, including how to test +// with the COS browser extension polyfill. +// // [link] var CROSS_ORIGIN_STORAGE = 0; From 7ea1d45e13bf0074cdf1db6bb1666ad49787acd4 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:14:16 +0000 Subject: [PATCH 05/74] test: add cross_origin_storage browser example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A self-contained example in test/cross_origin_storage/ demonstrating the -sCROSS_ORIGIN_STORAGE=1 flag: main.cpp — minimal C++ source exporting a greet() function index.html — browser shell that reports COS API availability, hooks Module.print/printErr onto the page, and calls greet() after the runtime initialises README.md — build instructions, prerequisites (COS extension), run instructions, and hash-verification one-liner The example follows the same pattern as test/vite/ and test/webpack/: a directory you build and serve locally to exercise a browser-only feature. --- test/cross_origin_storage/README.md | 66 +++++++++++++++++++++ test/cross_origin_storage/index.html | 88 ++++++++++++++++++++++++++++ test/cross_origin_storage/main.cpp | 30 ++++++++++ 3 files changed, 184 insertions(+) create mode 100644 test/cross_origin_storage/README.md create mode 100644 test/cross_origin_storage/index.html create mode 100644 test/cross_origin_storage/main.cpp diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md new file mode 100644 index 0000000000000..8bef98c5a1cf5 --- /dev/null +++ b/test/cross_origin_storage/README.md @@ -0,0 +1,66 @@ +# Cross-Origin Storage (COS) example + +This example demonstrates Emscripten's experimental +`-sCROSS_ORIGIN_STORAGE=1` flag, which integrates the +[Cross-Origin Storage browser API](https://github.com/WICG/cross-origin-storage) +into the standard Wasm loading path. + +## What it does + +On the **first load** the `.wasm` module is fetched over the network and +stored in the cross-origin cache, keyed by its SHA-256 hash. + +On **subsequent loads** — from the same origin or any other — the module is +retrieved from the cache without a network request for the binary. + +## Prerequisites + +The COS API is not yet natively supported by any browser. Install the +polyfill extension to try it today: + +- **Chrome Web Store**: + [Cross-Origin Storage](https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih) + +## Build + +```bash +emcc main.cpp -o index.js \ + -sCROSS_ORIGIN_STORAGE=1 \ + -sENVIRONMENT=web \ + -sEXPORTED_RUNTIME_METHODS=ccall \ + -sEXPORTED_FUNCTIONS=_greet \ + -sALLOW_MEMORY_GROWTH +``` + +This produces `index.js` and `index.wasm`. The SHA-256 hash of `index.wasm` +is embedded in `index.js` at build time — you can verify them match: + +```bash +sha256sum index.wasm +grep -oP "value: '\K[0-9a-f]{64}" index.js +``` + +## Run + +Serve the directory over HTTP (the `file://` protocol does not support +`fetch()`): + +```bash +emrun . +# or +python3 -m http.server +``` + +Open `http://localhost:8080` (or whichever port your server uses) in a +browser with the COS extension installed. + +Open DevTools → Console. On the first load you should see a cache-miss log +message and a network request for `index.wasm` in the Network tab. Reload +the page — the network request disappears and the console shows that the +module was served from the cross-origin cache. + +## See also + +- [COS Emscripten docs](../../site/source/docs/compiling/CrossOriginStorage.rst) +- [WICG explainer](https://github.com/WICG/cross-origin-storage) +- [COS extension source](https://github.com/web-ai-community/cross-origin-storage-extension) diff --git a/test/cross_origin_storage/index.html b/test/cross_origin_storage/index.html new file mode 100644 index 0000000000000..fb0e82f17ab7d --- /dev/null +++ b/test/cross_origin_storage/index.html @@ -0,0 +1,88 @@ + + + + + + Emscripten — Cross-Origin Storage example + + + +

Emscripten — Cross-Origin Storage example

+

+ Open the browser DevTools console to see detailed COS timing. + Reload the page after the first load to observe the cache-hit path. +

+
Loading…
+ + + + + + + diff --git a/test/cross_origin_storage/main.cpp b/test/cross_origin_storage/main.cpp new file mode 100644 index 0000000000000..dd33fce450ac6 --- /dev/null +++ b/test/cross_origin_storage/main.cpp @@ -0,0 +1,30 @@ +// Copyright 2025 The Emscripten Authors. All rights reserved. +// Emscripten is available under two separate licenses, the MIT license and the +// University of Illinois/NCSA Open Source License. Both these licenses can be +// found in the LICENSE file. + +// Example: Cross-Origin Storage (COS) integration +// +// Build with: +// emcc main.cpp -o index.js \ +// -sCROSS_ORIGIN_STORAGE=1 \ +// -sENVIRONMENT=web \ +// -sEXPORTED_RUNTIME_METHODS=ccall \ +// -sEXPORTED_FUNCTIONS=_greet \ +// -sALLOW_MEMORY_GROWTH +// +// Serve the directory over HTTP (e.g. `emrun .` or `python3 -m http.server`) +// and open index.html in a browser that has the COS extension installed. + +#include +#include + +// Called from JavaScript after the module loads. +extern "C" { + +EMSCRIPTEN_KEEPALIVE +const char* greet() { + return "Hello from WebAssembly!"; +} + +} // extern "C" From 5c65fe50b5b5e904666901d5181bc747750c5e98 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:19:22 +0000 Subject: [PATCH 06/74] feat: add COS instrumentation callbacks and update example/docs/tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/preamble.js Add three optional Module callbacks invoked at each COS runtime event: - Module['onCOSCacheHit'](hash) — Wasm served from cross-origin cache - Module['onCOSCacheMiss'](url) — Wasm not in COS, fetched from network - Module['onCOSStore'](hash) — Wasm successfully written to COS test/cross_origin_storage/index.html Rewrite to use the new callbacks exclusively. No mention of any particular implementation. Reports: - whether the COS API is active or not - on miss: the network URL the Wasm was fetched from, then the hash once it has been stored - on hit: the SHA-256 hash of the Wasm resource served from COS test/cross_origin_storage/README.md Remove extension references; describe only the observable behaviour (which path was taken, hash, URL). site/source/docs/compiling/CrossOriginStorage.rst Document the three callbacks with a usage example, and fold them into the runtime flow description. test/test_other.py Assert all three Module callback strings are present in the emitted JS when CROSS_ORIGIN_STORAGE=1. --- .../docs/compiling/CrossOriginStorage.rst | 35 ++++++++++++-- src/preamble.js | 9 ++++ test/cross_origin_storage/README.md | 24 ++++------ test/cross_origin_storage/index.html | 47 ++++++++++--------- test/test_other.py | 5 ++ 5 files changed, 78 insertions(+), 42 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 04cf2a89d31a0..69206471a71d4 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -83,12 +83,14 @@ When the page loads, the generated JavaScript follows this logic: 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, call ``WebAssembly.instantiate()`` immediately - so the page loads without delay, and then write the bytes into COS in the - background (fire-and-forget) with ``origins: '*'`` so any other origin - can benefit:: + over the network as usual, invoke ``Module['onCOSCacheMiss'](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) with ``origins: '*'`` so any other origin can benefit. + Once the write completes, invoke ``Module['onCOSStore'](hash)`` if defined:: navigator.crossOriginStorage.requestFileHandles([hash], { create: true, origins: '*' }) @@ -97,6 +99,31 @@ When the page loads, the generated JavaScript follows this logic: 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: + +.. code-block:: javascript + + 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. |url| is the resolved URL of the .wasm file. + onCOSCacheMiss: (url) => { + console.log('Cache miss, 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 ==================================== diff --git a/src/preamble.js b/src/preamble.js index 25f7fe6e0d8b8..62682ea839d13 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -658,6 +658,9 @@ async function instantiateAsync(binary, binaryFile, imports) { // Cache hit — read the Blob and instantiate from its ArrayBuffer. var cosFile = await cosHandles[0].getFile(); var cosBytes = await cosFile.arrayBuffer(); + // Optional instrumentation callback: Module['onCOSCacheHit'](hash) + // Called when the Wasm binary is served from the cross-origin cache. + if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](wasmHashValue); return WebAssembly.instantiate(cosBytes, imports); } catch (cosErr) { if (cosErr.name === 'NotFoundError') { @@ -665,6 +668,9 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); var wasmBytes = await networkResponse.arrayBuffer(); + // Optional instrumentation callback: Module['onCOSCacheMiss'](url) + // Called when the Wasm binary is not in COS and is fetched over the network. + if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](binaryFile); // Fire-and-forget store; never block instantiation on the write. (async () => { try { @@ -675,6 +681,9 @@ async function instantiateAsync(binary, binaryFile, imports) { var writable = await writeHandles[0].createWritable(); await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); await writable.close(); + // Optional instrumentation callback: Module['onCOSStore'](hash) + // Called after the Wasm binary has been successfully written to COS. + if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](wasmHashValue); } catch (storeErr) { err(`COS store failed: ${storeErr}`); } diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md index 8bef98c5a1cf5..7e347edd8e434 100644 --- a/test/cross_origin_storage/README.md +++ b/test/cross_origin_storage/README.md @@ -13,13 +13,8 @@ stored in the cross-origin cache, keyed by its SHA-256 hash. On **subsequent loads** — from the same origin or any other — the module is retrieved from the cache without a network request for the binary. -## Prerequisites - -The COS API is not yet natively supported by any browser. Install the -polyfill extension to try it today: - -- **Chrome Web Store**: - [Cross-Origin Storage](https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih) +The page reports which path was taken and, where applicable, the SHA-256 hash +of the Wasm resource and the URL it was fetched from. ## Build @@ -33,7 +28,7 @@ emcc main.cpp -o index.js \ ``` This produces `index.js` and `index.wasm`. The SHA-256 hash of `index.wasm` -is embedded in `index.js` at build time — you can verify them match: +is embedded in `index.js` at build time — you can verify they match: ```bash sha256sum index.wasm @@ -51,16 +46,15 @@ emrun . python3 -m http.server ``` -Open `http://localhost:8080` (or whichever port your server uses) in a -browser with the COS extension installed. +Open the page in a browser with the Cross-Origin Storage API available. + +The page will report: -Open DevTools → Console. On the first load you should see a cache-miss log -message and a network request for `index.wasm` in the Network tab. Reload -the page — the network request disappears and the console shows that the -module was served from the cross-origin cache. +- whether the COS API is active +- on a cache miss: the URL the Wasm was fetched from, and confirmation once it has been stored in COS with its hash +- on a cache hit: the SHA-256 hash of the Wasm resource served from COS ## See also - [COS Emscripten docs](../../site/source/docs/compiling/CrossOriginStorage.rst) - [WICG explainer](https://github.com/WICG/cross-origin-storage) -- [COS extension source](https://github.com/web-ai-community/cross-origin-storage-extension) diff --git a/test/cross_origin_storage/index.html b/test/cross_origin_storage/index.html index fb0e82f17ab7d..9b284c699a107 100644 --- a/test/cross_origin_storage/index.html +++ b/test/cross_origin_storage/index.html @@ -2,16 +2,8 @@ @@ -40,10 +32,7 @@

Emscripten — Cross-Origin Storage example

-

- Open the browser DevTools console to see detailed COS timing. - Reload the page after the first load to observe the cache-hit path. -

+

Reload the page after the first load to observe the cache-hit path.

Loading…
diff --git a/test/test_other.py b/test/test_other.py index 7cc7138bde9cb..44155575f680f 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15363,6 +15363,11 @@ def test_cross_origin_storage_js_output(self): self.assertContained("'NotFoundError'", js) self.assertContained("'NotAllowedError'", js) + # Instrumentation callbacks invoked at each COS event. + self.assertContained("Module['onCOSCacheHit']", js) + self.assertContained("Module['onCOSCacheMiss']", js) + self.assertContained("Module['onCOSStore']", js) + # The hash embedded in the JS must be a 64-char lowercase hex string … m = re.search(r"value:\s*'([0-9a-f]{64})'", js) self.assertTrue(m, 'could not find a 64-char hex WASM_SHA256 value in JS output') From e12341304319788f073e13d7b3c82f217281751d Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:22:34 +0000 Subject: [PATCH 07/74] fix: add link-time validation and changelog entry for CROSS_ORIGIN_STORAGE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/link.py - CROSS_ORIGIN_STORAGE + SINGLE_FILE → hard error at link time (wasm is inlined as base64; there is no file to hash) - CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 → warning (sync instantiation bypasses the async COS fetch path entirely) test/test_other.py Replace test_cross_origin_storage_not_emitted_for_single_file with two sharper tests: - test_cross_origin_storage_error_with_single_file: asserts the expected error message via assert_fail - test_cross_origin_storage_warning_without_async_compilation: asserts the warning appears in stderr ChangeLog.md Add entry under 6.0.1 (in development) describing the new flag, its build-time/runtime behaviour, the three Module callbacks, and the two incompatible flag combinations. --- ChangeLog.md | 14 ++++++++++++++ test/test_other.py | 23 ++++++++++++++++------- tools/link.py | 6 ++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index ed8db891bb5d6..db4cf7f6333e8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,20 @@ See docs/process.md for more on how version tagging works. 6.0.1 (in development) ---------------------- +- New experimental ``-sCROSS_ORIGIN_STORAGE=1`` linker flag that integrates + the proposed `Cross-Origin Storage browser API + `_ into the Wasm loading path + as a progressive enhancement (web target only). At build time Emscripten + computes the SHA-256 hash of the final ``.wasm`` binary and embeds it in the + generated JS glue. At runtime, if the browser exposes + ``navigator.crossOriginStorage``, the runtime first attempts to retrieve the + module from the shared cross-origin cache (cache hit); on a miss it fetches + normally and stores the binary in COS for future use by any origin. Falls + back transparently to the standard fetch path when the API is unavailable. + Three optional ``Module`` callbacks are available for instrumentation: + ``Module['onCOSCacheHit'](hash)``, ``Module['onCOSCacheMiss'](url)``, and + ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` + (hard error) and ``-sWASM_ASYNC_COMPILATION=0`` (warning). 6.0.0 - 06/04/26 ---------------- diff --git a/test/test_other.py b/test/test_other.py index 44155575f680f..f098cb664557a 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15396,16 +15396,25 @@ def test_cross_origin_storage_not_emitted_for_node_target(self): js = read_file('hello.js') self.assertNotContained('crossOriginStorage', js) - def test_cross_origin_storage_not_emitted_for_single_file(self): - """COS code must NOT appear in SINGLE_FILE builds (wasm is inlined as - base64; there is no standalone .wasm file to key the hash on).""" - self.run_process([EMCC, test_file('hello_world.cpp'), + def test_cross_origin_storage_error_with_single_file(self): + """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" + self.assert_fail([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', '-sSINGLE_FILE', - '-o', 'hello.js']) - js = read_file('hello.js') - self.assertNotContained('crossOriginStorage', js) + '-o', 'hello.js'], + 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') + + def test_cross_origin_storage_warning_without_async_compilation(self): + """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must warn.""" + proc = self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sWASM_ASYNC_COMPILATION=0', + '-o', 'hello.js'], + stderr=PIPE) + self.assertContained('CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0', + proc.stderr) def test_cross_origin_storage_hash_changes_with_content(self): """Two different programs must produce different embedded hashes.""" diff --git a/tools/link.py b/tools/link.py index 55ea2699b1c11..8f71ce69dec2a 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1207,6 +1207,12 @@ def limit_incoming_module_api(): if settings.WASM == 2 and settings.SINGLE_FILE: exit_with_error('cannot have both WASM=2 and SINGLE_FILE enabled at the same time') + if settings.CROSS_ORIGIN_STORAGE: + if settings.SINGLE_FILE: + exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined and cannot be identified by hash)') + if not settings.WASM_ASYNC_COMPILATION: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') + if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML: exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output') From e094e46c8c4e982ce6f18b3bfaa61656aa69a866 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:24:23 +0000 Subject: [PATCH 08/74] fix: remove incorrect 'base64' claim in SINGLE_FILE + COS descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SINGLE_FILE uses a custom UTF-8 binary embedding by default (SINGLE_FILE_BINARY_ENCODE=1), not base64. The point that matters is that the wasm is inlined directly into the JS output with no standalone .wasm file or fetchable URL — the encoding method is irrelevant to COS. Updated: settings.js comment, link.py error message, CrossOriginStorage.rst, and ChangeLog.md. --- ChangeLog.md | 3 ++- site/source/docs/compiling/CrossOriginStorage.rst | 7 ++++--- src/settings.js | 2 +- tools/link.py | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index db4cf7f6333e8..9ac6d40118bdb 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -33,7 +33,8 @@ See docs/process.md for more on how version tagging works. Three optional ``Module`` callbacks are available for instrumentation: ``Module['onCOSCacheHit'](hash)``, ``Module['onCOSCacheMiss'](url)``, and ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` - (hard error) and ``-sWASM_ASYNC_COMPILATION=0`` (warning). + (hard error — the wasm is inlined directly into the JS output with no + fetchable URL) and ``-sWASM_ASYNC_COMPILATION=0`` (warning). 6.0.0 - 06/04/26 ---------------- diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 69206471a71d4..b6231b398322e 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -48,9 +48,10 @@ Requirements and restrictions - The flag only has an effect when the output targets the **web** environment. It is silently ignored for Node.js-only or shell targets (``-sENVIRONMENT=node``). -- It has no effect in **SINGLE_FILE** mode (``-sSINGLE_FILE``), because the - Wasm binary is embedded inline as base64 and there is no standalone - ``.wasm`` file to hash. +- 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. - 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. diff --git a/src/settings.js b/src/settings.js index b0f5947ad214a..fde3c3d41bf08 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2220,7 +2220,7 @@ var CROSS_ORIGIN = false; // // Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no // effect on Node.js or shell targets. Also has no effect in SINGLE_FILE -// builds where the Wasm binary is inlined as base64. +// builds where the Wasm binary is inlined directly into the JS output. // // See :ref:`CrossOriginStorage` for the full guide, including how to test // with the COS browser extension polyfill. diff --git a/tools/link.py b/tools/link.py index 8f71ce69dec2a..f0c8a7b3d80da 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1209,7 +1209,7 @@ def limit_incoming_module_api(): if settings.CROSS_ORIGIN_STORAGE: if settings.SINGLE_FILE: - exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined and cannot be identified by hash)') + exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on)') if not settings.WASM_ASYNC_COMPILATION: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') From df886f9287e1320f4b9da3ab4e7e4afe1317f260 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:27:32 +0000 Subject: [PATCH 09/74] feat: warn when CROSS_ORIGIN_STORAGE is used with SPLIT_MODULE or MAIN_MODULE The COS integration only hashes and caches the primary .wasm output. Emscripten can produce additional Wasm files that are outside its scope: - SPLIT_MODULE: produces .deferred.wasm / ..wasm secondary files that are fetched lazily when a deferred function is first called. - MAIN_MODULE: side modules loaded at runtime via dlopen are separate .wasm files fetched through the normal network path. Both combinations now emit a link-time warning explaining that only the primary .wasm is covered, so developers are not surprised when secondary files are always fetched from the network. Tests added for both warning cases in test/test_other.py. CrossOriginStorage.rst updated to document all four limitations (SINGLE_FILE, WASM_ASYNC_COMPILATION=0, SPLIT_MODULE, MAIN_MODULE). --- .../docs/compiling/CrossOriginStorage.rst | 8 +++++++ test/test_other.py | 22 +++++++++++++++++++ tools/link.py | 4 ++++ 3 files changed, 34 insertions(+) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index b6231b398322e..ce3e298159ed1 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -52,6 +52,14 @@ Requirements and restrictions (``-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 emits a **warning** with ``-sWASM_ASYNC_COMPILATION=0``: the + synchronous instantiation path bypasses ``instantiateAsync()`` entirely, + so the COS code is never 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. diff --git a/test/test_other.py b/test/test_other.py index f098cb664557a..d1e23978f960a 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15416,6 +15416,28 @@ def test_cross_origin_storage_warning_without_async_compilation(self): self.assertContained('CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0', proc.stderr) + def test_cross_origin_storage_warning_with_split_module(self): + """CROSS_ORIGIN_STORAGE + SPLIT_MODULE must warn that secondary files are not covered.""" + proc = self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sSPLIT_MODULE', + '-o', 'hello.js'], + stderr=PIPE) + self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', + proc.stderr) + + def test_cross_origin_storage_warning_with_main_module(self): + """CROSS_ORIGIN_STORAGE + MAIN_MODULE must warn that side modules are not covered.""" + proc = self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sMAIN_MODULE', + '-o', 'hello.js'], + stderr=PIPE) + self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', + proc.stderr) + def test_cross_origin_storage_hash_changes_with_content(self): """Two different programs must produce different embedded hashes.""" self.run_process([EMCC, test_file('hello_world.cpp'), diff --git a/tools/link.py b/tools/link.py index f0c8a7b3d80da..6712513424181 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1212,6 +1212,10 @@ def limit_incoming_module_api(): exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on)') if not settings.WASM_ASYNC_COMPILATION: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') + if settings.SPLIT_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') + if settings.MAIN_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML: exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output') From 894cedc2d4dfa23e0f07b4f96bec1d76c8ba24c6 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:29:51 +0000 Subject: [PATCH 10/74] fix: warn when CROSS_ORIGIN_STORAGE is used with SIDE_MODULE SIDE_MODULE builds output only a .wasm file with no JS glue, so there is nothing to embed the hash into or to perform the COS lookup at runtime. Add a warning to make this explicit. Also corrects the earlier reasoning about SPLIT_MODULE: the secondary .deferred.wasm files are produced by a user-run offline wasm-split step with profiling data, not during the Emscripten link, so they genuinely cannot be hashed at link time. --- tools/link.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/link.py b/tools/link.py index 6712513424181..5000cc76b341c 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1216,6 +1216,8 @@ def limit_incoming_module_api(): diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') if settings.MAIN_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') + if settings.SIDE_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML: exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output') From 4c872ed4043f24ecafa1c190fa5af1a005f81888 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:54:45 +0000 Subject: [PATCH 11/74] docs: clarify that COS only makes sense for widely-shared public Wasm binaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The key insight is that COS only delivers a benefit when the .wasm binary is byte-identical across many origins — i.e. a publicly distributed library (SQLite Wasm, Pyodide, CanvasKit, ffmpeg.wasm) served from a CDN. Application-specific Wasm gains nothing that the HTTP cache does not already provide, because no other origin will ever have the same hash. - src/settings.js: lead the CROSS_ORIGIN_STORAGE comment with a 'When to use this flag' paragraph listing good candidates and explicitly warning against using it for app-specific Wasm. - src/preamble.js: add the same guidance to the runtime comment block. - CrossOriginStorage.rst: add a 'When to use this flag' subsection before Usage with a concrete list of good candidates and a clear 'Do not' statement for application-specific code. --- .../docs/compiling/CrossOriginStorage.rst | 27 ++++++++++++++++--- src/preamble.js | 9 +++++-- src/settings.js | 12 +++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index ce3e298159ed1..6e2807349253d 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -28,10 +28,29 @@ 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. -This is particularly beneficial for popular Wasm modules that many sites -ship independently — game engines, scientific computing runtimes, and -frameworks such as Flutter or Pyodide — where users would otherwise download -the same bytes many times. +When to use this flag +--------------------- + +COS only delivers a benefit when the ``.wasm`` binary is **byte-identical +across many different origins** — that is, a publicly distributed library +that many sites load from the same CDN URL. If every visitor to every site +downloads the exact same bytes, COS means they only download it once, ever. + +Good candidates: + +- **SQLite Wasm** — the same ``sqlite3.wasm`` build is loaded by many + independent sites. +- **Pyodide** — ``pyodide.asm.wasm`` is a large, stable binary served from + a public CDN and used across many origins. +- **CanvasKit (Flutter)** — ``canvaskit.wasm`` is requested hundreds of + thousands of times daily from thousands of distinct hosts. +- **ffmpeg.wasm**, **libsodium.wasm**, **WebR** — similarly widely shared, + version-stable, CDN-distributed binaries. + +**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. Usage ===== diff --git a/src/preamble.js b/src/preamble.js index 62682ea839d13..01c708447d5f2 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -634,6 +634,11 @@ async function instantiateAsync(binary, binaryFile, imports) { // Cross-Origin Storage (COS) progressive enhancement. // https://github.com/WICG/cross-origin-storage // + // COS is only beneficial when this .wasm binary is byte-identical across + // many origins — i.e. a publicly distributed library (SQLite Wasm, Pyodide, + // CanvasKit, ffmpeg.wasm, …) fetched from a CDN. Application-specific Wasm + // gains nothing from COS that the normal HTTP cache does not already provide. + // // The SHA-256 hash of the final .wasm binary is computed at link time and // embedded here as a build-time constant. At runtime we feature-detect the // browser COS API via `'crossOriginStorage' in navigator`, then call @@ -645,8 +650,8 @@ async function instantiateAsync(binary, binaryFile, imports) { // // Cache-miss path (NotFoundError): // fetch() the wasm over the network → instantiate → store in COS with - // origins:'*' (appropriate for a public Wasm module that any site may - // benefit from). The store is fire-and-forget so it never delays startup. + // origins:'*' so any origin can reuse the same public binary. + // The store is fire-and-forget so it never delays startup. // // Any other error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. diff --git a/src/settings.js b/src/settings.js index fde3c3d41bf08..497ac126582f5 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2204,6 +2204,18 @@ var CROSS_ORIGIN = false; // [experimental] Enables Cross-Origin Storage (COS) API support for Wasm // loading on the Web target. // +// **When to use this flag** +// +// COS is only beneficial for Wasm binaries that are byte-identical across +// many different origins — i.e. publicly distributed libraries fetched from +// a CDN, where the same compiled binary is loaded by thousands of sites. +// Good examples: SQLite Wasm, Pyodide, CanvasKit (Flutter), ffmpeg.wasm, +// libsodium.wasm. +// +// If your ``.wasm`` file is bespoke application code built specifically for +// your site, COS gives you nothing that the normal HTTP cache does not +// already provide. Do not enable this flag for application-specific Wasm. +// // When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` // binary at link time, embeds it as a build-time constant in the generated // JavaScript glue, and uses the browser COS API as a progressive enhancement: From 343d0b9233093554876142561c733b3bb555ae7a Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 17:47:02 +0000 Subject: [PATCH 12/74] feat: add CROSS_ORIGIN_STORAGE_ORIGINS setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exposes the 'origins' field of the COS API's CrossOriginStorageRequestFileHandleOptions dictionary as a new -sCROSS_ORIGIN_STORAGE_ORIGINS linker setting. Three modes, matching the spec: ['*'] (default) — globally available; any origin can read the file. Appropriate for widely-shared public CDN assets (SQLite Wasm, Pyodide, CanvasKit, …). ['https://app.example.com', 'https://api.example.com'] — restricted to a specific set of trusted HTTPS origins. For proprietary resources shared across related sites without making them globally enumerable. [] — same-site only; the 'origins' field is omitted entirely. The file is available only to same-site origins. Link-time validation: - '*' must not be mixed with explicit origins (error) - each explicit origin must match https://host[:port] with no path (error) preamble.js emits the correct JS literal via {{{ JSON.stringify(...) }}}: - ['*'] → origins: '*' - ['https://...'] → origins: ["https://..."] - [] → { create: true } (no origins key) Tests (test/test_other.py): - default emits origins: '*' - explicit list emits a JS array - empty list emits { create: true } with no origins key - error on '*' mixed with explicit origins - error on non-HTTPS origin - error on origin with a path component Docs (CrossOriginStorage.rst): new subsection with all three modes, example command lines, and a note on the spec's visibility upgrade rule. ChangeLog.md updated. --- ChangeLog.md | 3 + .../docs/compiling/CrossOriginStorage.rst | 51 ++++++++++++++ src/preamble.js | 7 +- src/settings.js | 38 +++++++++++ test/test_other.py | 66 +++++++++++++++++++ tools/link.py | 13 ++++ 6 files changed, 177 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 9ac6d40118bdb..40966dd1c56bc 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -35,6 +35,9 @@ See docs/process.md for more on how version tagging works. ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` (hard error — the wasm is inlined directly into the JS output with no fetchable URL) and ``-sWASM_ASYNC_COMPILATION=0`` (warning). + The companion ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` setting controls which + origins may read the cached file: ``['*']`` (default, globally available), + an explicit HTTPS origin list (restricted), or ``[]`` (same-site only). 6.0.0 - 06/04/26 ---------------- diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 6e2807349253d..43f3b3395f778 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -62,6 +62,57 @@ Pass ``-sCROSS_ORIGIN_STORAGE=1`` at link time:: The flag is a **link-time** setting and has no effect during compilation of individual object files. +Controlling which origins can read the cached file +-------------------------------------------------- + +The ``-sCROSS_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) — any origin can retrieve the file: + +.. code-block:: bash + + emcc hello.cpp -o hello.js \ + -sCROSS_ORIGIN_STORAGE=1 \ + -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] + +Use this for widely-shared public binaries distributed from a CDN (SQLite +Wasm, Pyodide, CanvasKit, …). 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: + +.. code-block:: bash + + emcc hello.cpp -o hello.js \ + -sCROSS_ORIGIN_STORAGE=1 \ + '-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 serialised HTTPS origin (scheme + host + +optional port, no path). Mixing ``'*'`` with explicit origins is a +**link-time error**. + +**Same-site only** — the ``origins`` field is omitted, so the file is +available only to same-site origins: + +.. code-block:: bash + + emcc hello.cpp -o hello.js \ + -sCROSS_ORIGIN_STORAGE=1 \ + -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. + Requirements and restrictions ------------------------------ diff --git a/src/preamble.js b/src/preamble.js index 01c708447d5f2..a4647e66a518d 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -651,6 +651,7 @@ async function instantiateAsync(binary, binaryFile, imports) { // Cache-miss path (NotFoundError): // fetch() the wasm over the network → instantiate → store in COS with // origins:'*' so any origin can reuse the same public binary. + // (Controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS; see settings.js.) // The store is fire-and-forget so it never delays startup. // // Any other error (NotAllowedError, network failure, …) falls through to the @@ -681,7 +682,11 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var writeHandles = await navigator.crossOriginStorage.requestFileHandles( [cosHash], - { create: true, origins: '*' }, +#if CROSS_ORIGIN_STORAGE_ORIGINS.length + { create: true, origins: {{{ JSON.stringify(CROSS_ORIGIN_STORAGE_ORIGINS.length === 1 && CROSS_ORIGIN_STORAGE_ORIGINS[0] === '*' ? '*' : CROSS_ORIGIN_STORAGE_ORIGINS) }}} }, +#else + { create: true }, +#endif ); var writable = await writeHandles[0].createWritable(); await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); diff --git a/src/settings.js b/src/settings.js index 497ac126582f5..da063c42f9be6 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2240,6 +2240,44 @@ var CROSS_ORIGIN = false; // [link] var CROSS_ORIGIN_STORAGE = 0; +// Controls which origins may read the Wasm binary after it has been stored in +// the Cross-Origin Storage (COS) cache. Only meaningful when +// ``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) +// path; it is only applied during the write (cache-miss) path. +// +// Three modes are supported, matching the ``origins`` field of the COS API's +// ``CrossOriginStorageRequestFileHandleOptions`` dictionary: +// +// **Globally available** (default) — any origin can retrieve the file:: +// +// -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] +// +// This is appropriate for widely-used public binaries (SQLite Wasm, Pyodide, +// CanvasKit, …) distributed from a public CDN where many unrelated sites load +// the exact same bytes. The explainer recommends ``origins: '*'`` for such +// resources. +// +// **Restricted to a specific set of origins** — only listed origins can +// retrieve the file:: +// +// -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] +// +// Useful when a proprietary Wasm binary is shared across a controlled set of +// related sites (for example, two subdomains of the same company) but should +// not be globally enumerable. Each value must be a valid serialized HTTPS +// origin (scheme + host + optional port, no path). +// +// **Same-site only** — omit the ``origins`` field entirely so the file is +// available only to same-site origins:: +// +// -sCROSS_ORIGIN_STORAGE_ORIGINS=[] +// +// Useful for resources shared across subdomains of a single site. +// +// Mixing ``'*'`` with explicit origins is a link-time error. +// [link] +var CROSS_ORIGIN_STORAGE_ORIGINS = ['*']; + // This setting changes the behaviour of the ``-shared`` flag. When set to true // you get the old emscripten behaviour where the ``-shared`` flag actually // produces a normal object file (i.e. ``ld -r``). When set to true (the diff --git a/test/test_other.py b/test/test_other.py index d1e23978f960a..1f6a63ebdc256 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15456,3 +15456,69 @@ def test_cross_origin_storage_hash_changes_with_content(self): self.assertNotEqual(hash_a, hash_b, 'different programs should produce different WASM_SHA256 hashes') + + # --------------------------------------------------------------------------- + # Tests for CROSS_ORIGIN_STORAGE_ORIGINS + # --------------------------------------------------------------------------- + + def test_cross_origin_storage_origins_default_is_global(self): + """Default CROSS_ORIGIN_STORAGE_ORIGINS=['*'] must emit origins:'*'.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-o', 'hello.js']) + self.assertContained("origins: '*'", read_file('hello.js')) + + def test_cross_origin_storage_origins_explicit_list(self): + """An explicit origins list must be emitted as a JS array.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://app.example.com","https://api.example.com"]', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertContained('"https://app.example.com"', js) + self.assertContained('"https://api.example.com"', js) + self.assertNotContained("origins: '*'", js) + + def test_cross_origin_storage_origins_same_site(self): + """Empty origins list must omit the origins key entirely (same-site only).""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=[]', + '-o', 'hello.js']) + js = read_file('hello.js') + # { create: true } with no origins field + self.assertContained('{ create: true }', js) + self.assertNotContained('origins', js) + + def test_cross_origin_storage_origins_error_mixed_wildcard(self): + """Mixing '*' with explicit origins must be a link-time error.""" + self.assert_fail( + [EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]', + '-o', 'hello.js'], + "'*' must not be mixed with explicit origins") + + def test_cross_origin_storage_origins_error_invalid_origin(self): + """A non-HTTPS or malformed origin must be a link-time error.""" + self.assert_fail( + [EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]', + '-o', 'hello.js'], + 'is not a valid HTTPS origin') + + def test_cross_origin_storage_origins_error_origin_with_path(self): + """An origin with a path component must be a link-time error.""" + self.assert_fail( + [EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]', + '-o', 'hello.js'], + 'is not a valid HTTPS origin') diff --git a/tools/link.py b/tools/link.py index 5000cc76b341c..15c645e81b9d3 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1218,6 +1218,19 @@ def limit_incoming_module_api(): diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') if settings.SIDE_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') + # Validate CROSS_ORIGIN_STORAGE_ORIGINS. + origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS + if not isinstance(origins, list): + exit_with_error('CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. [\'*\'] or [\'https://example.com\']') + if '*' in origins and len(origins) > 1: + exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") + for o in origins: + if o == '*': + continue + # Each explicit origin must be a valid serialised HTTPS origin: + # scheme "https://", host, optional ":port", no path/query/fragment. + if not re.fullmatch(r'https://[^/]+(:\d+)?', o): + exit_with_error(f"CROSS_ORIGIN_STORAGE_ORIGINS: {o!r} is not a valid HTTPS origin (expected 'https://host' or 'https://host:port')") if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML: exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output') From d87413a9c6164d633c93ca51d563b4fc4c47bf92 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 17:52:18 +0000 Subject: [PATCH 13/74] fix: make '*' the implicit default for CROSS_ORIGIN_STORAGE_ORIGINS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the default value was ['*'] in settings.js, which made it impossible to distinguish 'user did not pass the setting' from 'user explicitly passed ["*"]'. Now: - Default in settings.js is [] (empty sentinel) - link.py checks user_settings: if CROSS_ORIGIN_STORAGE_ORIGINS was not explicitly passed, it is resolved to ['*'] at link time (globally available — the right default for a public Wasm binary) - Explicitly passing =[] means same-site only (origins field omitted) - Explicitly passing =['https://...'] means restricted list This means the common case requires no extra flags: emcc -sCROSS_ORIGIN_STORAGE=1 → origins: '*' Docs and tests updated to reflect the new sentinel semantics. An extra test asserts that explicitly passing ['*'] gives the same result as the implicit default. --- .../docs/compiling/CrossOriginStorage.rst | 13 ++++---- src/settings.js | 32 ++++++++----------- test/test_other.py | 13 +++++++- tools/link.py | 10 ++++-- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 43f3b3395f778..6873b42debdd7 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -69,13 +69,14 @@ The ``-sCROSS_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) — any origin can retrieve the file: +**Globally available** (default, no explicit setting needed) — any origin +can retrieve the file. This is applied automatically when +``-sCROSS_ORIGIN_STORAGE=1`` is used without specifying +``-sCROSS_ORIGIN_STORAGE_ORIGINS``: .. code-block:: bash - emcc hello.cpp -o hello.js \ - -sCROSS_ORIGIN_STORAGE=1 \ - -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] + emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 Use this for widely-shared public binaries distributed from a CDN (SQLite Wasm, Pyodide, CanvasKit, …). This is the recommended mode for resources @@ -95,8 +96,8 @@ sites. Each entry must be a valid serialised HTTPS origin (scheme + host + optional port, no path). Mixing ``'*'`` with explicit origins is a **link-time error**. -**Same-site only** — the ``origins`` field is omitted, so the file is -available only to same-site origins: +**Same-site only** — pass an explicit empty list to omit the ``origins`` +field, making the file available only to same-site origins: .. code-block:: bash diff --git a/src/settings.js b/src/settings.js index da063c42f9be6..59f4ef349a203 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2248,35 +2248,29 @@ var CROSS_ORIGIN_STORAGE = 0; // Three modes are supported, matching the ``origins`` field of the COS API's // ``CrossOriginStorageRequestFileHandleOptions`` dictionary: // -// **Globally available** (default) — any origin can retrieve the file:: +// **Globally available** (default when the setting is not explicitly passed) — +// any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is +// used without specifying this setting, ``origins: '*'`` is used automatically. +// Appropriate for widely-used public binaries (SQLite Wasm, Pyodide, +// CanvasKit, …) distributed from a public CDN. // -// -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] -// -// This is appropriate for widely-used public binaries (SQLite Wasm, Pyodide, -// CanvasKit, …) distributed from a public CDN where many unrelated sites load -// the exact same bytes. The explainer recommends ``origins: '*'`` for such -// resources. -// -// **Restricted to a specific set of origins** — only listed origins can +// **Restricted to a specific set of origins** — only listed HTTPS origins can // retrieve the file:: // // -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] // -// Useful when a proprietary Wasm binary is shared across a controlled set of -// related sites (for example, two subdomains of the same company) but should -// not be globally enumerable. Each value must be a valid serialized HTTPS -// origin (scheme + host + optional port, no path). +// For proprietary resources shared across a controlled set of related sites. +// Each value must be a valid serialised HTTPS origin (scheme + host + optional +// port, no path). Mixing ``'*'`` with explicit origins is a link-time error. // -// **Same-site only** — omit the ``origins`` field entirely so the file is -// available only to same-site origins:: +// **Same-site only** — pass the setting with an empty list to omit the +// ``origins`` field, making the file available only to same-site origins:: // // -sCROSS_ORIGIN_STORAGE_ORIGINS=[] // -// Useful for resources shared across subdomains of a single site. -// -// Mixing ``'*'`` with explicit origins is a link-time error. +// For resources shared across subdomains of a single site but not beyond. // [link] -var CROSS_ORIGIN_STORAGE_ORIGINS = ['*']; +var CROSS_ORIGIN_STORAGE_ORIGINS = []; // This setting changes the behaviour of the ``-shared`` flag. When set to true // you get the old emscripten behaviour where the ``-shared`` flag actually diff --git a/test/test_other.py b/test/test_other.py index 1f6a63ebdc256..7c330709b98ad 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15462,13 +15462,24 @@ def test_cross_origin_storage_hash_changes_with_content(self): # --------------------------------------------------------------------------- def test_cross_origin_storage_origins_default_is_global(self): - """Default CROSS_ORIGIN_STORAGE_ORIGINS=['*'] must emit origins:'*'.""" + """Without -sCROSS_ORIGIN_STORAGE_ORIGINS, the default must be origins:'*' + (globally available). The user only needs -sCROSS_ORIGIN_STORAGE=1.""" self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', '-o', 'hello.js']) self.assertContained("origins: '*'", read_file('hello.js')) + def test_cross_origin_storage_origins_explicit_wildcard(self): + """Explicitly passing -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] must also emit + origins:'*', matching the implicit default.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + "-sCROSS_ORIGIN_STORAGE_ORIGINS=['*']", + '-o', 'hello.js']) + self.assertContained("origins: '*'", read_file('hello.js')) + def test_cross_origin_storage_origins_explicit_list(self): """An explicit origins list must be emitted as a JS array.""" self.run_process([EMCC, test_file('hello_world.cpp'), diff --git a/tools/link.py b/tools/link.py index 15c645e81b9d3..956ba0c3ebbd2 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1218,10 +1218,16 @@ def limit_incoming_module_api(): diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') if settings.SIDE_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') - # Validate CROSS_ORIGIN_STORAGE_ORIGINS. + # Resolve and validate CROSS_ORIGIN_STORAGE_ORIGINS. + # The default in settings.js is [] (empty sentinel). When the user has + # not explicitly passed -sCROSS_ORIGIN_STORAGE_ORIGINS we default to ['*'] + # (globally available), which is the appropriate mode for widely-shared + # public Wasm binaries. An explicit =[] means same-site only. + if 'CROSS_ORIGIN_STORAGE_ORIGINS' not in user_settings: + settings.CROSS_ORIGIN_STORAGE_ORIGINS = ['*'] origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS if not isinstance(origins, list): - exit_with_error('CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. [\'*\'] or [\'https://example.com\']') + exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. ['*'] or ['https://example.com']") if '*' in origins and len(origins) > 1: exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") for o in origins: From 186e8cb9f107282aa7bb3bdbfafc3993f1a09cd7 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 18:00:05 +0000 Subject: [PATCH 14/74] =?UTF-8?q?fix:=20final=20polish=20=E2=80=94=20warni?= =?UTF-8?q?ngs,=20stale=20comments,=20doc=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/link.py - Add warning when CROSS_ORIGIN_STORAGE=1 is used with a non-web environment (ENVIRONMENT=node, shell, etc.): navigator.crossOriginStorage is a browser API and is never available outside the browser. This makes the non-web case consistent with all other no-op combinations which already warn (WASM_ASYNC_COMPILATION=0, SPLIT_MODULE, MAIN_MODULE, SIDE_MODULE). - Fix stale 'Inline / SINGLE_FILE builds' comment in the hash computation else-branch: SINGLE_FILE is now a hard error so that branch is only reached in unexpected build configurations. src/preamble.js - Fix stale comment that hardcoded origins:'*'; replace with a reference to -sCROSS_ORIGIN_STORAGE_ORIGINS so the comment stays accurate regardless of the setting value. test/test_other.py - Update test_cross_origin_storage_not_emitted_for_node_target to also assert the new warning is emitted (matching the pattern of all other warning tests). CrossOriginStorage.rst - 'silently ignored' → 'emits a warning' for non-web targets. - Fix stale origins:'*' hardcode in the 'How it works / Cache miss' step; now references -sCROSS_ORIGIN_STORAGE_ORIGINS instead. --- .../docs/compiling/CrossOriginStorage.rst | 13 +++++++------ src/preamble.js | 8 ++++---- test/test_other.py | 17 ++++++++++------- tools/link.py | 7 ++++++- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 6873b42debdd7..43bbe376af58d 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -117,8 +117,10 @@ site but not beyond. Requirements and restrictions ------------------------------ -- The flag only has an effect when the output targets the **web** environment. - It is silently ignored for Node.js-only or shell targets (``-sENVIRONMENT=node``). +- 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 @@ -169,10 +171,9 @@ When the page loads, the generated JavaScript follows this logic: over the network as usual, invoke ``Module['onCOSCacheMiss'](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) with ``origins: '*'`` so any other origin can benefit. - Once the write completes, invoke ``Module['onCOSStore'](hash)`` if defined:: - - navigator.crossOriginStorage.requestFileHandles([hash], { create: true, origins: '*' }) + (fire-and-forget) using the ``origins`` value controlled by + ``-sCROSS_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 diff --git a/src/preamble.js b/src/preamble.js index a4647e66a518d..5a514d8e66817 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -649,10 +649,10 @@ async function instantiateAsync(binary, binaryFile, imports) { // requestFileHandles() succeeds → getFile() → arrayBuffer() → instantiate // // Cache-miss path (NotFoundError): - // fetch() the wasm over the network → instantiate → store in COS with - // origins:'*' so any origin can reuse the same public binary. - // (Controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS; see settings.js.) - // The store is fire-and-forget so it never delays startup. + // fetch() the wasm over the network → instantiate → store in COS. + // The origins field is controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS + // (default: '*', globally available). The store is fire-and-forget so + // it never delays startup. // // Any other error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. diff --git a/test/test_other.py b/test/test_other.py index 7c330709b98ad..3456b9aadfcce 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15388,13 +15388,16 @@ def test_cross_origin_storage_disabled_by_default(self): def test_cross_origin_storage_not_emitted_for_node_target(self): """COS code must NOT appear when targeting Node.js only, even with the flag - set; the #if ENVIRONMENT_MAY_BE_WEB guard should strip it.""" - self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', - '-sENVIRONMENT=node', - '-o', 'hello.js']) - js = read_file('hello.js') - self.assertNotContained('crossOriginStorage', js) + set; the #if ENVIRONMENT_MAY_BE_WEB guard strips it. A warning must also + be emitted since the flag does nothing in this configuration.""" + proc = self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=node', + '-o', 'hello.js'], + stderr=PIPE) + self.assertNotContained('crossOriginStorage', read_file('hello.js')) + self.assertContained('CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web', + proc.stderr) def test_cross_origin_storage_error_with_single_file(self): """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" diff --git a/tools/link.py b/tools/link.py index 956ba0c3ebbd2..17ab5ad64bfd0 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1210,6 +1210,8 @@ def limit_incoming_module_api(): if settings.CROSS_ORIGIN_STORAGE: if settings.SINGLE_FILE: exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on)') + if not settings.ENVIRONMENT_MAY_BE_WEB: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web (navigator.crossOriginStorage is not available outside the browser)') if not settings.WASM_ASYNC_COMPILATION: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') if settings.SPLIT_MODULE: @@ -1937,7 +1939,10 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat settings.WASM_SHA256 = hashlib.sha256(wasm_bytes).hexdigest() logger.debug(f'CROSS_ORIGIN_STORAGE: wasm SHA-256 = {settings.WASM_SHA256}') else: - # Inline / SINGLE_FILE builds embed the wasm in JS; no file to hash. + # wasm_target does not exist — this should not normally be reached since + # SINGLE_FILE (which inlines the wasm) is already rejected above with a + # hard error. Log a warning defensively in case of an unexpected build + # configuration. logger.warning('CROSS_ORIGIN_STORAGE: wasm file not found for hashing; WASM_SHA256 will be empty') settings.WASM_SHA256 = '' From 461b275a162908d1f97963a9e0a9e4d160ac092a Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 18:03:42 +0000 Subject: [PATCH 15/74] test: add missing SIDE_MODULE warning test for CROSS_ORIGIN_STORAGE The warning was implemented in tools/link.py but never tested. Added test_cross_origin_storage_warning_with_side_module to assert the expected message appears in stderr. --- test/test_other.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/test_other.py b/test/test_other.py index 3456b9aadfcce..c1a9966bb055b 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15441,6 +15441,16 @@ def test_cross_origin_storage_warning_with_main_module(self): self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', proc.stderr) + def test_cross_origin_storage_warning_with_side_module(self): + """CROSS_ORIGIN_STORAGE + SIDE_MODULE must warn: no JS glue is emitted.""" + proc = self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sSIDE_MODULE', + '-o', 'hello.wasm'], + stderr=PIPE) + self.assertContained('CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds', + proc.stderr) + def test_cross_origin_storage_hash_changes_with_content(self): """Two different programs must produce different embedded hashes.""" self.run_process([EMCC, test_file('hello_world.cpp'), From 1410b97eab61e178dac76403018602e18e751c6c Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 10:16:38 +0200 Subject: [PATCH 16/74] fix: correct COS test failures and add demo Makefile --- src/preamble.js | 18 ++++++++------ test/cross_origin_storage/Makefile | 21 ++++++++++++++++ test/test_other.py | 2 +- tools/link.py | 40 +++++++++++++++++++----------- 4 files changed, 58 insertions(+), 23 deletions(-) create mode 100644 test/cross_origin_storage/Makefile diff --git a/src/preamble.js b/src/preamble.js index 5a514d8e66817..629d5df799ef6 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -656,9 +656,8 @@ async function instantiateAsync(binary, binaryFile, imports) { // // Any other error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. - var wasmHashValue = '{{{ WASM_SHA256 }}}'; - if (wasmHashValue && 'crossOriginStorage' in navigator) { - var cosHash = { algorithm: 'SHA-256', value: wasmHashValue }; + var cosHash = { algorithm: 'SHA-256', value: '{{{ WASM_SHA256 }}}' }; + if (cosHash.value && 'crossOriginStorage' in navigator) { try { var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); // Cache hit — read the Blob and instantiate from its ArrayBuffer. @@ -666,7 +665,7 @@ async function instantiateAsync(binary, binaryFile, imports) { var cosBytes = await cosFile.arrayBuffer(); // Optional instrumentation callback: Module['onCOSCacheHit'](hash) // Called when the Wasm binary is served from the cross-origin cache. - if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](wasmHashValue); + if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](cosHash.value); return WebAssembly.instantiate(cosBytes, imports); } catch (cosErr) { if (cosErr.name === 'NotFoundError') { @@ -682,8 +681,10 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var writeHandles = await navigator.crossOriginStorage.requestFileHandles( [cosHash], -#if CROSS_ORIGIN_STORAGE_ORIGINS.length - { create: true, origins: {{{ JSON.stringify(CROSS_ORIGIN_STORAGE_ORIGINS.length === 1 && CROSS_ORIGIN_STORAGE_ORIGINS[0] === '*' ? '*' : CROSS_ORIGIN_STORAGE_ORIGINS) }}} }, +#if CROSS_ORIGIN_STORAGE_ORIGINS.length === 1 && CROSS_ORIGIN_STORAGE_ORIGINS[0] === '*' + { create: true, origins: '*' }, +#elif CROSS_ORIGIN_STORAGE_ORIGINS.length + { create: true, origins: {{{ JSON.stringify(CROSS_ORIGIN_STORAGE_ORIGINS) }}} }, #else { create: true }, #endif @@ -693,7 +694,7 @@ async function instantiateAsync(binary, binaryFile, imports) { await writable.close(); // Optional instrumentation callback: Module['onCOSStore'](hash) // Called after the Wasm binary has been successfully written to COS. - if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](wasmHashValue); + if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](cosHash.value); } catch (storeErr) { err(`COS store failed: ${storeErr}`); } @@ -703,8 +704,9 @@ async function instantiateAsync(binary, binaryFile, imports) { // Network fetch failed; fall through to the standard path. err(`COS fallback fetch failed: ${fetchErr}`); } + } else if (cosErr.name === 'NotAllowedError') { + err(`COS: permission denied.`); } else { - // NotAllowedError or unexpected error; fall through gracefully. err(`Cross-Origin Storage lookup failed: ${cosErr}`); } } diff --git a/test/cross_origin_storage/Makefile b/test/cross_origin_storage/Makefile new file mode 100644 index 0000000000000..0c5e4216bb8bf --- /dev/null +++ b/test/cross_origin_storage/Makefile @@ -0,0 +1,21 @@ +# Copyright 2026 The Emscripten Authors. All rights reserved. +# Emscripten is available under two separate licenses, the MIT license and the +# University of Illinois/NCSA Open Source License. Both these licenses can be +# found in the LICENSE file. + +EMCC ?= emcc + +.PHONY: all clean + +all: index.js + +index.js: main.cpp + $(EMCC) main.cpp -o index.js \ + -sCROSS_ORIGIN_STORAGE=1 \ + -sENVIRONMENT=web \ + -sEXPORTED_RUNTIME_METHODS=ccall \ + -sEXPORTED_FUNCTIONS=_greet \ + -sALLOW_MEMORY_GROWTH + +clean: + rm -f index.js index.wasm diff --git a/test/test_other.py b/test/test_other.py index c1a9966bb055b..47e3857ecdcb7 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15515,7 +15515,7 @@ def test_cross_origin_storage_origins_same_site(self): js = read_file('hello.js') # { create: true } with no origins field self.assertContained('{ create: true }', js) - self.assertNotContained('origins', js) + self.assertNotContained('origins:', js) def test_cross_origin_storage_origins_error_mixed_wildcard(self): """Mixing '*' with explicit origins must be a link-time error.""" diff --git a/tools/link.py b/tools/link.py index 17ab5ad64bfd0..071f8b06d31d4 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1916,6 +1916,20 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat settings.TARGET_JS_NAME = os.path.basename(js_target) + # Compute the SHA-256 of the wasm binary before phase_emscript so the hash + # is available to the JS-glue template as {{{ WASM_SHA256 }}}. We read + # in_wasm here (the pre-binaryen binary); after phase_binaryen we recompute + # the hash and patch the JS if binaryen changed the binary. + if settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB: + if os.path.exists(in_wasm): + with open(in_wasm, 'rb') as f: + wasm_bytes_pre = f.read() + settings.WASM_SHA256 = hashlib.sha256(wasm_bytes_pre).hexdigest() + logger.debug(f'CROSS_ORIGIN_STORAGE: pre-binaryen wasm SHA-256 = {settings.WASM_SHA256}') + else: + logger.warning('CROSS_ORIGIN_STORAGE: wasm file not found for hashing; WASM_SHA256 will be empty') + settings.WASM_SHA256 = '' + metadata = phase_emscript(in_wasm, wasm_target, js_syms, base_metadata) if settings.EMBIND_AOT: @@ -1929,22 +1943,20 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat phase_binaryen(target, options, wasm_target) - # Compute the SHA-256 of the final .wasm binary and make it available to the - # JS glue preprocessor as {{{ WASM_SHA256 }}}. We do this after phase_binaryen - # so that wasm-opt transformations are already reflected in the hash. - if settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB: + # After binaryen, recompute the hash from the final wasm and patch the JS + # if binaryen changed the binary (so the embedded hash stays accurate). + if final_js and settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB and settings.WASM_SHA256: if os.path.exists(wasm_target): with open(wasm_target, 'rb') as f: - wasm_bytes = f.read() - settings.WASM_SHA256 = hashlib.sha256(wasm_bytes).hexdigest() - logger.debug(f'CROSS_ORIGIN_STORAGE: wasm SHA-256 = {settings.WASM_SHA256}') - else: - # wasm_target does not exist — this should not normally be reached since - # SINGLE_FILE (which inlines the wasm) is already rejected above with a - # hard error. Log a warning defensively in case of an unexpected build - # configuration. - logger.warning('CROSS_ORIGIN_STORAGE: wasm file not found for hashing; WASM_SHA256 will be empty') - settings.WASM_SHA256 = '' + wasm_bytes_post = f.read() + post_hash = hashlib.sha256(wasm_bytes_post).hexdigest() + if post_hash != settings.WASM_SHA256: + logger.debug(f'CROSS_ORIGIN_STORAGE: binaryen changed wasm; updating SHA-256 to {post_hash}') + js_content = read_file(final_js) + js_content = js_content.replace(f"value: '{settings.WASM_SHA256}'", + f"value: '{post_hash}'") + write_file(final_js, js_content) + settings.WASM_SHA256 = post_hash # If we are not emitting any JS then we are all done now if options.oformat != OFormat.WASM: From 9533aafc6f7abf08f6ec64ae8a736c38c67d9696 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 10:31:16 +0200 Subject: [PATCH 17/74] docs: replace concrete project examples with generic descriptions --- .../docs/compiling/CrossOriginStorage.rst | 19 ++++++++----------- src/preamble.js | 6 +++--- src/settings.js | 8 ++++---- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 43bbe376af58d..6498f2138e3e3 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -36,16 +36,13 @@ across many different origins** — that is, a publicly distributed library that many sites load from the same CDN URL. If every visitor to every site downloads the exact same bytes, COS means they only download it once, ever. -Good candidates: +Good candidates are libraries or toolkits that are: -- **SQLite Wasm** — the same ``sqlite3.wasm`` build is loaded by many - independent sites. -- **Pyodide** — ``pyodide.asm.wasm`` is a large, stable binary served from - a public CDN and used across many origins. -- **CanvasKit (Flutter)** — ``canvaskit.wasm`` is requested hundreds of - thousands of times daily from thousands of distinct hosts. -- **ffmpeg.wasm**, **libsodium.wasm**, **WebR** — similarly widely shared, - version-stable, CDN-distributed binaries. +- distributed from a public CDN as a stable, version-pinned ``.wasm`` binary, +- loaded by many independent sites (i.e. many distinct origins), 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 @@ -78,8 +75,8 @@ can retrieve the file. This is applied automatically when emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 -Use this for widely-shared public binaries distributed from a CDN (SQLite -Wasm, Pyodide, CanvasKit, …). This is the recommended mode for resources +Use this for widely-shared public binaries distributed from a CDN and 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 diff --git a/src/preamble.js b/src/preamble.js index 629d5df799ef6..d7dbb441597a8 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -635,9 +635,9 @@ async function instantiateAsync(binary, binaryFile, imports) { // https://github.com/WICG/cross-origin-storage // // COS is only beneficial when this .wasm binary is byte-identical across - // many origins — i.e. a publicly distributed library (SQLite Wasm, Pyodide, - // CanvasKit, ffmpeg.wasm, …) fetched from a CDN. Application-specific Wasm - // gains nothing from COS that the normal HTTP cache does not already provide. + // many origins — i.e. a library distributed from a CDN and shared by many + // independent sites. Application-specific Wasm gains nothing from COS that + // the normal HTTP cache does not already provide. // // The SHA-256 hash of the final .wasm binary is computed at link time and // embedded here as a build-time constant. At runtime we feature-detect the diff --git a/src/settings.js b/src/settings.js index 59f4ef349a203..6a0cbe41c0813 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2209,8 +2209,8 @@ var CROSS_ORIGIN = false; // COS is only beneficial for Wasm binaries that are byte-identical across // many different origins — i.e. publicly distributed libraries fetched from // a CDN, where the same compiled binary is loaded by thousands of sites. -// Good examples: SQLite Wasm, Pyodide, CanvasKit (Flutter), ffmpeg.wasm, -// libsodium.wasm. +// Good candidates are libraries or toolkits distributed from a public CDN +// as a stable, version-pinned binary loaded by many independent sites. // // If your ``.wasm`` file is bespoke application code built specifically for // your site, COS gives you nothing that the normal HTTP cache does not @@ -2251,8 +2251,8 @@ var CROSS_ORIGIN_STORAGE = 0; // **Globally available** (default when the setting is not explicitly passed) — // any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is // used without specifying this setting, ``origins: '*'`` is used automatically. -// Appropriate for widely-used public binaries (SQLite Wasm, Pyodide, -// CanvasKit, …) distributed from a public CDN. +// Appropriate for widely-used public binaries distributed from a CDN +// that are shared across many independent origins. // // **Restricted to a specific set of origins** — only listed HTTPS origins can // retrieve the file:: From a32dad55f53390fb3858bf50fab7d3c87bc1f7e2 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 11:38:58 +0200 Subject: [PATCH 18/74] docs: add Chrome Web Store link for COS extension in See also --- site/source/docs/compiling/CrossOriginStorage.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 6498f2138e3e3..bbf899133f84c 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -250,6 +250,7 @@ See also ======== - `WICG Cross-Origin Storage explainer `_ -- `COS browser extension (polyfill) `_ +- `COS browser extension (Chrome Web Store) `_ +- `COS browser extension (source code) `_ - :ref:`settings-reference` — ``CROSS_ORIGIN_STORAGE`` entry - :ref:`WebAssembly` — general guide to building Wasm with Emscripten From 80d15c280e8781540ae1ed2f714514de795de19d Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 11:40:16 +0200 Subject: [PATCH 19/74] docs: replace CDN framing with 'popular' in COS guide --- site/source/docs/compiling/CrossOriginStorage.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index bbf899133f84c..4e23c7c3a4f37 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -32,14 +32,14 @@ When to use this flag --------------------- COS only delivers a benefit when the ``.wasm`` binary is **byte-identical -across many different origins** — that is, a publicly distributed library -that many sites load from the same CDN URL. If every visitor to every site +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: -- distributed from a public CDN as a stable, version-pinned ``.wasm`` binary, -- loaded by many independent sites (i.e. many distinct origins), and +- 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). @@ -75,9 +75,8 @@ can retrieve the file. This is applied automatically when emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 -Use this for widely-shared public binaries distributed from a CDN and loaded -by many independent origins. This is the recommended mode for resources -where global COS cache hits are expected. +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: From 6635a1457704ca76ab2cfd28b4d307c0ac49618b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 11:42:27 +0200 Subject: [PATCH 20/74] docs: note multi-origin exception and origins flag in COS when-to-use --- site/source/docs/compiling/CrossOriginStorage.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 4e23c7c3a4f37..8701c8b7bd6d0 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -49,6 +49,13 @@ 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 ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` to +restrict access to only those origins rather than opening the cache entry to +the world. + Usage ===== From 7522109a401e02e3030aa976d323dff6aa8c444b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 11:50:10 +0200 Subject: [PATCH 21/74] feat: pass hash as first arg to onCOSCacheMiss(hash, url) --- site/source/docs/compiling/CrossOriginStorage.rst | 8 ++++---- src/preamble.js | 4 ++-- test/cross_origin_storage/index.html | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 8701c8b7bd6d0..58c83ff708c2c 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -171,7 +171,7 @@ When the page loads, the generated JavaScript follows this logic: 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'](url)`` if + 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 @@ -197,9 +197,9 @@ Three optional ``Module`` properties let you observe COS events at runtime: }, // Called when the Wasm binary was not in COS and was fetched over the - // network. |url| is the resolved URL of the .wasm file. - onCOSCacheMiss: (url) => { - console.log('Cache miss, fetched from:', url); + // network. |hash| is the SHA-256 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. diff --git a/src/preamble.js b/src/preamble.js index d7dbb441597a8..f6917603ce9dc 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -673,9 +673,9 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); var wasmBytes = await networkResponse.arrayBuffer(); - // Optional instrumentation callback: Module['onCOSCacheMiss'](url) + // Optional instrumentation callback: Module['onCOSCacheMiss'](hash, url) // Called when the Wasm binary is not in COS and is fetched over the network. - if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](binaryFile); + if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](cosHash.value, binaryFile); // Fire-and-forget store; never block instantiation on the write. (async () => { try { diff --git a/test/cross_origin_storage/index.html b/test/cross_origin_storage/index.html index 9b284c699a107..ca7f592187b8f 100644 --- a/test/cross_origin_storage/index.html +++ b/test/cross_origin_storage/index.html @@ -62,8 +62,8 @@

Emscripten — Cross-Origin Storage example

}, // Called when the Wasm binary was not in COS and was fetched over the network. - onCOSCacheMiss: (url) => { - log('↓ Wasm not in Cross-Origin Storage — fetched from network: ' + url, 'warn'); + onCOSCacheMiss: (hash, url) => { + log('↓ Wasm not in Cross-Origin Storage — fetched from network: ' + url + ' (SHA-256: ' + hash + ')', 'warn'); }, // Called after the Wasm binary has been successfully written to COS. From 485d6abfd7849949f59e06e7b00dd91fb2a55ee1 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 11:57:51 +0200 Subject: [PATCH 22/74] docs: remove CDN mention and add visibility/security note in preamble.js Replace "distributed from a CDN" with "popular library loaded by many independent sites", and add a short note explaining that COS cannot be used as a timing oracle for restricted entries: a cache hit requires an explicit prior write that provided the actual bytes. --- src/preamble.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/preamble.js b/src/preamble.js index f6917603ce9dc..ff843f0877dff 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -635,9 +635,9 @@ async function instantiateAsync(binary, binaryFile, imports) { // https://github.com/WICG/cross-origin-storage // // COS is only beneficial when this .wasm binary is byte-identical across - // many origins — i.e. a library distributed from a CDN and shared by many - // independent sites. Application-specific Wasm gains nothing from COS that - // the normal HTTP cache does not already provide. + // many origins — i.e. a popular library loaded by many independent sites. + // Application-specific Wasm gains nothing from COS that the normal HTTP + // cache does not already provide. // // The SHA-256 hash of the final .wasm binary is computed at link time and // embedded here as a build-time constant. At runtime we feature-detect the @@ -654,6 +654,13 @@ async function instantiateAsync(binary, binaryFile, imports) { // (default: '*', globally available). The store is fire-and-forget so // it never delays startup. // + // Visibility and security: the spec allows widening visibility (e.g. a + // restricted entry promoted to globally available) but never narrowing it. + // Because storing always requires writing the actual bytes, no third party + // can probe the cache to discover whether a restricted entry was previously + // stored by another origin — a cache hit is only possible after an explicit + // write that provided the content. + // // Any other error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. var cosHash = { algorithm: 'SHA-256', value: '{{{ WASM_SHA256 }}}' }; From 6e0f82bfc605a72789d0099e374e7284c4929694 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:01:20 +0200 Subject: [PATCH 23/74] docs: add timing-oracle security note to visibility upgrade callout in RST --- site/source/docs/compiling/CrossOriginStorage.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 58c83ff708c2c..79556b57884e8 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -117,6 +117,13 @@ site but not beyond. 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 ------------------------------ From b38d5bc6f7a733fcc76d8255db2ddb23fd7872a6 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:09:08 +0200 Subject: [PATCH 24/74] docs: remove CDN mentions from CROSS_ORIGIN_STORAGE settings comments --- src/settings.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/settings.js b/src/settings.js index 6a0cbe41c0813..71d73c0a38385 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2207,10 +2207,10 @@ var CROSS_ORIGIN = false; // **When to use this flag** // // COS is only beneficial for Wasm binaries that are byte-identical across -// many different origins — i.e. publicly distributed libraries fetched from -// a CDN, where the same compiled binary is loaded by thousands of sites. -// Good candidates are libraries or toolkits distributed from a public CDN -// as a stable, version-pinned binary loaded by many independent sites. +// many different origins — i.e. popular libraries where the same compiled +// binary is loaded by many independent sites. +// Good candidates are libraries or toolkits distributed as a stable, +// version-pinned binary loaded by many independent sites. // // If your ``.wasm`` file is bespoke application code built specifically for // your site, COS gives you nothing that the normal HTTP cache does not @@ -2251,8 +2251,8 @@ var CROSS_ORIGIN_STORAGE = 0; // **Globally available** (default when the setting is not explicitly passed) — // any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is // used without specifying this setting, ``origins: '*'`` is used automatically. -// Appropriate for widely-used public binaries distributed from a CDN -// that are shared across many independent origins. +// Appropriate for widely-used public binaries shared across many independent +// origins. // // **Restricted to a specific set of origins** — only listed HTTPS origins can // retrieve the file:: From 4141f1392ec10e8541d21e94cdd221a283a71826 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:11:48 +0200 Subject: [PATCH 25/74] style: normalize double spaces after periods in COS settings comments --- src/settings.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/settings.js b/src/settings.js index 71d73c0a38385..356cf1b8a2c0f 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2214,7 +2214,7 @@ var CROSS_ORIGIN = false; // // If your ``.wasm`` file is bespoke application code built specifically for // your site, COS gives you nothing that the normal HTTP cache does not -// already provide. Do not enable this flag for application-specific Wasm. +// already provide. Do not enable this flag for application-specific Wasm. // // When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` // binary at link time, embeds it as a build-time constant in the generated @@ -2231,7 +2231,7 @@ var CROSS_ORIGIN = false; // ``fetch`` / ``WebAssembly.instantiateStreaming`` path transparently. // // Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no -// effect on Node.js or shell targets. Also has no effect in SINGLE_FILE +// effect on Node.js or shell targets. Also has no effect in SINGLE_FILE // builds where the Wasm binary is inlined directly into the JS output. // // See :ref:`CrossOriginStorage` for the full guide, including how to test @@ -2241,15 +2241,15 @@ var CROSS_ORIGIN = false; var CROSS_ORIGIN_STORAGE = 0; // Controls which origins may read the Wasm binary after it has been stored in -// the Cross-Origin Storage (COS) cache. Only meaningful when -// ``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) +// the Cross-Origin Storage (COS) cache. Only meaningful when +// ``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) // path; it is only applied during the write (cache-miss) path. // // Three modes are supported, matching the ``origins`` field of the COS API's // ``CrossOriginStorageRequestFileHandleOptions`` dictionary: // // **Globally available** (default when the setting is not explicitly passed) — -// any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is +// any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is // used without specifying this setting, ``origins: '*'`` is used automatically. // Appropriate for widely-used public binaries shared across many independent // origins. @@ -2261,7 +2261,7 @@ var CROSS_ORIGIN_STORAGE = 0; // // For proprietary resources shared across a controlled set of related sites. // Each value must be a valid serialised HTTPS origin (scheme + host + optional -// port, no path). Mixing ``'*'`` with explicit origins is a link-time error. +// port, no path). Mixing ``'*'`` with explicit origins is a link-time error. // // **Same-site only** — pass the setting with an empty list to omit the // ``origins`` field, making the file available only to same-site origins:: From 906d53df8c62a914e8dd49a226c94eedc828aa48 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:14:57 +0200 Subject: [PATCH 26/74] style: use American spelling 'serialized' consistently --- site/source/docs/compiling/CrossOriginStorage.rst | 2 +- src/settings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 79556b57884e8..947385e6f734b 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -95,7 +95,7 @@ retrieve the file: '-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 serialised HTTPS origin (scheme + host + +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**. diff --git a/src/settings.js b/src/settings.js index 356cf1b8a2c0f..292033b1d6ff8 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2260,7 +2260,7 @@ var CROSS_ORIGIN_STORAGE = 0; // -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] // // For proprietary resources shared across a controlled set of related sites. -// Each value must be a valid serialised HTTPS origin (scheme + host + optional +// Each value 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 the setting with an empty list to omit the From 1c944e79f904e0ee4c5802984a4092e491af8307 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:17:51 +0200 Subject: [PATCH 27/74] docs: describe progressive enhancement and fallback path in COS example README --- test/cross_origin_storage/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md index 7e347edd8e434..c28f7f91c24af 100644 --- a/test/cross_origin_storage/README.md +++ b/test/cross_origin_storage/README.md @@ -7,11 +7,17 @@ into the standard Wasm loading path. ## What it does -On the **first load** the `.wasm` module is fetched over the network and -stored in the cross-origin cache, keyed by its SHA-256 hash. +COS is a **progressive enhancement**: when the browser exposes the +`navigator.crossOriginStorage` API, loading takes one of two paths: -On **subsequent loads** — from the same origin or any other — the module is -retrieved from the cache without a network request for the binary. +- **Cache miss** (first load): the `.wasm` module is fetched over the network + and stored in the cross-origin cache, keyed by its SHA-256 hash. +- **Cache hit** (subsequent loads, same or any other origin): the module is + retrieved from the cache without a network request for the binary. + +When the browser does not expose the COS API, or when an unexpected error +occurs, the runtime falls back to the standard `fetch()` / +`WebAssembly.instantiateStreaming()` path — the page always loads. The page reports which path was taken and, where applicable, the SHA-256 hash of the Wasm resource and the URL it was fetched from. From 66560946e268791416769cd7d873583d80db74bd Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:57:16 +0200 Subject: [PATCH 28/74] fix: ruff docstring formatting and regenerate settings_reference.rst --- .../tools_reference/settings_reference.rst | 80 +++++++++++++++++++ test/test_other.py | 26 +++--- 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index c61cd19e1dc15..118a7eae70e15 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3351,6 +3351,86 @@ indirectly using `importScripts` Default value: false +.. _cross_origin_storage: + +CROSS_ORIGIN_STORAGE +==================== + +[experimental] Enables Cross-Origin Storage (COS) API support for Wasm +loading on the Web target. + +**When to use this flag** + +COS is only beneficial for Wasm binaries that are byte-identical across +many different origins — i.e. popular libraries where the same compiled +binary is loaded by many independent sites. +Good candidates are libraries or toolkits distributed as a stable, +version-pinned binary loaded by many independent sites. + +If your ``.wasm`` file is bespoke application code built specifically for +your site, COS gives you nothing that the normal HTTP cache does not +already provide. Do not enable this flag for application-specific Wasm. + +When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` +binary at link time, embeds it as a build-time constant in the generated +JavaScript glue, and uses the browser COS API as a progressive enhancement: + +- **Cache hit**: the runtime calls + ``navigator.crossOriginStorage.requestFileHandles()`` with the hash and, + if the module is found, reads it directly from the cross-origin cache. +- **Cache miss**: the module is fetched over the network as usual, then + stored in COS in the background (non-blocking) with ``origins: '*'`` so + any other origin can reuse it. +- **Fallback**: when the browser does not expose the COS API, or when an + unexpected error occurs, the runtime falls through to the standard + ``fetch`` / ``WebAssembly.instantiateStreaming`` path transparently. + +Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no +effect on Node.js or shell targets. Also has no effect in SINGLE_FILE +builds where the Wasm binary is inlined directly into the JS output. + +See :ref:`CrossOriginStorage` for the full guide, including how to test +with the COS browser extension polyfill. + +Default value: 0 + +.. _cross_origin_storage_origins: + +CROSS_ORIGIN_STORAGE_ORIGINS +============================ + +Controls which origins may read the Wasm binary after it has been stored in +the Cross-Origin Storage (COS) cache. Only meaningful when +``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) +path; it is only applied during the write (cache-miss) path. + +Three modes are supported, matching the ``origins`` field of the COS API's +``CrossOriginStorageRequestFileHandleOptions`` dictionary: + +**Globally available** (default when the setting is not explicitly passed) — +any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is +used without specifying this setting, ``origins: '*'`` is used automatically. +Appropriate for widely-used public binaries shared across many independent +origins. + +**Restricted to a specific set of origins** — only listed HTTPS origins can +retrieve the file:: + + -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] + +For proprietary resources shared across a controlled set of related sites. +Each value 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 the setting with an empty list to omit the +``origins`` field, making the file available only to same-site origins:: + + -sCROSS_ORIGIN_STORAGE_ORIGINS=[] + +For resources shared across subdomains of a single site but not beyond. + +Default value: [] + .. _fake_dylibs: FAKE_DYLIBS diff --git a/test/test_other.py b/test/test_other.py index 47e3857ecdcb7..33d729ff16469 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15335,8 +15335,10 @@ def test_logReadFiles(self): # --------------------------------------------------------------------------- def test_cross_origin_storage_js_output(self): - """COS code is present in JS when the feature is enabled for the web target, - and the embedded hash is the correct SHA-256 of the compiled .wasm file.""" + """COS code is present in JS when the feature is enabled for the web target. + + The embedded hash must be the correct SHA-256 of the compiled .wasm file. + """ self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', @@ -15387,9 +15389,11 @@ def test_cross_origin_storage_disabled_by_default(self): self.assertNotContained('crossOriginStorage', js) def test_cross_origin_storage_not_emitted_for_node_target(self): - """COS code must NOT appear when targeting Node.js only, even with the flag - set; the #if ENVIRONMENT_MAY_BE_WEB guard strips it. A warning must also - be emitted since the flag does nothing in this configuration.""" + """COS code must NOT appear when targeting Node.js only, even with the flag set. + + The #if ENVIRONMENT_MAY_BE_WEB guard strips it. A warning must also be + emitted since the flag does nothing in this configuration. + """ proc = self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=node', @@ -15475,8 +15479,10 @@ def test_cross_origin_storage_hash_changes_with_content(self): # --------------------------------------------------------------------------- def test_cross_origin_storage_origins_default_is_global(self): - """Without -sCROSS_ORIGIN_STORAGE_ORIGINS, the default must be origins:'*' - (globally available). The user only needs -sCROSS_ORIGIN_STORAGE=1.""" + """Without -sCROSS_ORIGIN_STORAGE_ORIGINS, the default must be origins:'*'. + + Globally available; the user only needs -sCROSS_ORIGIN_STORAGE=1. + """ self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', @@ -15484,8 +15490,10 @@ def test_cross_origin_storage_origins_default_is_global(self): self.assertContained("origins: '*'", read_file('hello.js')) def test_cross_origin_storage_origins_explicit_wildcard(self): - """Explicitly passing -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] must also emit - origins:'*', matching the implicit default.""" + """Explicitly passing -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] must emit origins:'*'. + + This matches the implicit default. + """ self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', From 0ebc15182fc5f89c4ba38ffe9b3f9d63dd4ac458 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 13:26:19 +0200 Subject: [PATCH 29/74] docs: warn about post-emcc wasm post-processing invalidating the embedded hash --- .../docs/compiling/CrossOriginStorage.rst | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 947385e6f734b..e8ba9337c4062 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -162,6 +162,28 @@ build-time constant:: 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 SHA-256 of the final ``.wasm`` and + patch the hash string in the generated ``.js`` yourself before shipping. + A minimal shell snippet for doing so: + + .. code-block:: bash + + # After all post-processing is complete: + final_hash=$(sha256sum hello.wasm | awk '{print $1}') + sed -i "s/value: '[0-9a-f]\{64\}'/value: '${final_hash}'/" 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) ------------------ From d4b44f2b5ee17c76a3407c7e0dba7635a5b7c5c8 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 13:49:00 +0200 Subject: [PATCH 30/74] feat(cross-origin-storage): expose Module['wasmSHA256'] for custom instantiateWasm loaders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a program supplies its own Module['instantiateWasm'] callback, Emscripten calls it directly and skips instantiateAsync(), so the built-in COS fetch logic is never reached. To give custom loaders the information they need to implement their own COS-aware path, expose the build-time SHA-256 as Module['wasmSHA256'] (set before instantiateWasm is called) whenever -sCROSS_ORIGIN_STORAGE=1 is set. - src/preamble.js: assign Module['wasmSHA256'] from the WASM_SHA256 template literal, guarded by #if CROSS_ORIGIN_STORAGE, before the Module['instantiateWasm'] dispatch. - test/test_other.py: two new tests — one that checks the property is present and matches the .wasm SHA-256, one that checks it is absent without the flag. - site/source/docs/compiling/CrossOriginStorage.rst: new section "Custom Module['instantiateWasm'] implementations" documenting the bypass limitation and the Module['wasmSHA256'] escape hatch with a full worked example. --- .../docs/compiling/CrossOriginStorage.rst | 61 +++++++++++++++++++ src/preamble.js | 10 +++ test/test_other.py | 32 ++++++++++ 3 files changed, 103 insertions(+) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index e8ba9337c4062..1bef546854a1b 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -270,6 +270,67 @@ 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: + +.. code-block:: javascript + + Module['wasmSHA256'] // 64-character lowercase hex string, e.g. 'a3f2…c9d1' + +This property is set by the generated JavaScript before +``Module['instantiateWasm']`` is called, so it is always available inside the +callback. A custom loader can use it to implement the same cache-hit / +cache-miss / store / fallback logic as the built-in path: + +.. code-block:: javascript + + var Module = { + instantiateWasm(imports, onSuccess) { + const cosHash = { algorithm: 'SHA-256', value: Module['wasmSHA256'] }; + if (cosHash.value && 'crossOriginStorage' in navigator) { + 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['wasmSHA256']`` is only present in builds compiled with +``-sCROSS_ORIGIN_STORAGE=1``. 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 ========================================== diff --git a/src/preamble.js b/src/preamble.js index ff843f0877dff..1b361432912cd 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -925,6 +925,16 @@ function getWasmImports() { var info = getWasmImports(); +#if CROSS_ORIGIN_STORAGE + // Expose the build-time SHA-256 hash of the .wasm binary as a named Module + // property so that custom Module['instantiateWasm'] implementations can read + // it without having to parse the JS source. The COS fetch logic lives inside + // instantiateAsync(), which is bypassed when a custom instantiateWasm is + // provided. Such loaders can use Module['wasmSHA256'] to implement their own + // COS-aware loading path. + Module['wasmSHA256'] = '{{{ WASM_SHA256 }}}'; +#endif + #if expectToReceiveOnModule('instantiateWasm') // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback // to manually instantiate the Wasm module themselves. This allows pages to diff --git a/test/test_other.py b/test/test_other.py index 33d729ff16469..fbf5e182d0ff8 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15554,3 +15554,35 @@ def test_cross_origin_storage_origins_error_origin_with_path(self): '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]', '-o', 'hello.js'], 'is not a valid HTTPS origin') + + def test_cross_origin_storage_wasm_sha256_module_property(self): + """Module['wasmSHA256'] must be set in the JS output and match the .wasm hash. + + Custom Module['instantiateWasm'] implementations bypass instantiateAsync() + and therefore cannot reach the COS fetch logic that lives there. They can + read Module['wasmSHA256'] instead to get the build-time hash without parsing + the JS source. + """ + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js = read_file('hello.js') + + # The property must be present with a 64-char hex value. + m = re.search(r"Module\['wasmSHA256'\]\s*=\s*'([0-9a-f]{64})'", js) + self.assertTrue(m, "Module['wasmSHA256'] not found in JS output") + embedded_hash = m.group(1) + + # It must equal the SHA-256 of the actual .wasm file. + expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() + self.assertEqual(embedded_hash, expected_hash, + "Module['wasmSHA256'] does not match the actual .wasm SHA-256") + + def test_cross_origin_storage_wasm_sha256_absent_without_flag(self): + """Module['wasmSHA256'] must NOT appear when CROSS_ORIGIN_STORAGE is off.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertNotContained("Module['wasmSHA256']", js) From 7a7efe713030ed637addbec6e2ec9504328f0b5f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 14:06:10 +0200 Subject: [PATCH 31/74] fix(cross-origin-storage): also patch Module['wasmSHA256'] in post-binaryen fixup; clarify Module reference in docs - tools/link.py: the post-binaryen hash patch was already updating value: '...' in the cosHash object but missed the new Module['wasmSHA256'] = '...' assignment added to preamble.js. Patch both so the exposed property stays accurate after binaryen modifies the wasm binary. - site/source/docs/compiling/CrossOriginStorage.rst: clarify that Module['wasmSHA256'] is set on the config object (Module = moduleArg in the generated JS), so custom loaders that use an intermediate module wrapper should read it via the outer config reference rather than via `this` inside the callback. --- site/source/docs/compiling/CrossOriginStorage.rst | 8 ++++++-- tools/link.py | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 1bef546854a1b..2db296a2fb32c 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -288,13 +288,17 @@ hash as a named Module property: This property is set by the generated JavaScript before ``Module['instantiateWasm']`` is called, so it is always available inside the -callback. A custom loader can use it to implement the same cache-hit / -cache-miss / store / fallback logic as the built-in path: +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['wasmSHA256']`` +via a reference to that config object: .. code-block:: javascript 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 = { algorithm: 'SHA-256', value: Module['wasmSHA256'] }; if (cosHash.value && 'crossOriginStorage' in navigator) { navigator.crossOriginStorage.requestFileHandles([cosHash]) diff --git a/tools/link.py b/tools/link.py index 071f8b06d31d4..23c4a5d85fecf 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1955,6 +1955,8 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat js_content = read_file(final_js) js_content = js_content.replace(f"value: '{settings.WASM_SHA256}'", f"value: '{post_hash}'") + js_content = js_content.replace(f"Module['wasmSHA256'] = '{settings.WASM_SHA256}'", + f"Module['wasmSHA256'] = '{post_hash}'") write_file(final_js, js_content) settings.WASM_SHA256 = post_hash From 05dafe664cca7759b685a856ead30d1b63d1fb01 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 11:47:07 +0200 Subject: [PATCH 32/74] docs: drop =1 from -sCROSS_ORIGIN_STORAGE flag references in COS guide --- .../docs/compiling/CrossOriginStorage.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 2db296a2fb32c..f0430566d162f 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -21,7 +21,7 @@ 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 ``-sCROSS_ORIGIN_STORAGE=1`` flag integrates this into the +Emscripten's ``-sCROSS_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 @@ -59,9 +59,9 @@ the world. Usage ===== -Pass ``-sCROSS_ORIGIN_STORAGE=1`` at link time:: +Pass ``-sCROSS_ORIGIN_STORAGE`` at link time:: - emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 + emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE The flag is a **link-time** setting and has no effect during compilation of individual object files. @@ -75,12 +75,12 @@ 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 -``-sCROSS_ORIGIN_STORAGE=1`` is used without specifying +``-sCROSS_ORIGIN_STORAGE`` is used without specifying ``-sCROSS_ORIGIN_STORAGE_ORIGINS``: .. code-block:: bash - emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 + 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. @@ -91,7 +91,7 @@ retrieve the file: .. code-block:: bash emcc hello.cpp -o hello.js \ - -sCROSS_ORIGIN_STORAGE=1 \ + -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 @@ -105,7 +105,7 @@ field, making the file available only to same-site origins: .. code-block:: bash emcc hello.cpp -o hello.js \ - -sCROSS_ORIGIN_STORAGE=1 \ + -sCROSS_ORIGIN_STORAGE \ -sCROSS_ORIGIN_STORAGE_ORIGINS=[] Use this for resources that should be shared across subdomains of a single @@ -246,7 +246,7 @@ the `Cross-Origin Storage extension which injects a ``navigator.crossOriginStorage`` polyfill on every page. 1. Install the extension in Chrome. -2. Build your project with ``-sCROSS_ORIGIN_STORAGE=1 -sENVIRONMENT=web``. +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 @@ -331,7 +331,7 @@ via a reference to that config object: }; ``Module['wasmSHA256']`` is only present in builds compiled with -``-sCROSS_ORIGIN_STORAGE=1``. Always guard on its truthiness before using it, +``-sCROSS_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. From ed5611e197dae0f279ae1903cd1040c0644f5321 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 11:52:37 +0200 Subject: [PATCH 33/74] docs: use :ref: cross-references for settings in COS guide prose --- .../source/docs/compiling/CrossOriginStorage.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index f0430566d162f..6b3897cbe6030 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -21,7 +21,7 @@ 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 ``-sCROSS_ORIGIN_STORAGE`` flag integrates this into the +Emscripten's :ref:`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 @@ -52,14 +52,14 @@ 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 ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` to +download between your own origins. Use :ref:`CROSS_ORIGIN_STORAGE_ORIGINS` to restrict access to only those origins rather than opening the cache entry to the world. Usage ===== -Pass ``-sCROSS_ORIGIN_STORAGE`` at link time:: +Pass :ref:`CROSS_ORIGIN_STORAGE` at link time:: emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE @@ -69,14 +69,14 @@ individual object files. Controlling which origins can read the cached file -------------------------------------------------- -The ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` setting controls the ``origins`` field +The :ref:`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 -``-sCROSS_ORIGIN_STORAGE`` is used without specifying -``-sCROSS_ORIGIN_STORAGE_ORIGINS``: +:ref:`CROSS_ORIGIN_STORAGE` is used without specifying +:ref:`CROSS_ORIGIN_STORAGE_ORIGINS`: .. code-block:: bash @@ -204,7 +204,7 @@ When the page loads, the generated JavaScript follows this logic: 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 - ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` (``'*'`` by default). + :ref:`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, @@ -331,7 +331,7 @@ via a reference to that config object: }; ``Module['wasmSHA256']`` is only present in builds compiled with -``-sCROSS_ORIGIN_STORAGE``. Always guard on its truthiness before using it, +:ref:`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. From db36d8bc10cb1af6e960308b51921baa0f28870c Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 11:54:37 +0200 Subject: [PATCH 34/74] docs: remove redundant link-time note from COS usage section --- site/source/docs/compiling/CrossOriginStorage.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 6b3897cbe6030..dd7057ca98384 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -63,9 +63,6 @@ Pass :ref:`CROSS_ORIGIN_STORAGE` at link time:: emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE -The flag is a **link-time** setting and has no effect during compilation of -individual object files. - Controlling which origins can read the cached file -------------------------------------------------- From 5dfb0c33c548c79de20704c4c77b985204d3bfbd Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 11:56:06 +0200 Subject: [PATCH 35/74] docs: use unquoted comma-separated syntax for CROSS_ORIGIN_STORAGE_ORIGINS example --- site/source/docs/compiling/CrossOriginStorage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index dd7057ca98384..ce3666eb12fa3 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -89,7 +89,7 @@ retrieve the file: emcc hello.cpp -o hello.js \ -sCROSS_ORIGIN_STORAGE \ - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://app.example.com","https://api.example.com"]' + -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 + From f1ce6671a90e0e8a0be0beff58837fcffbe5db0b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:00:50 +0200 Subject: [PATCH 36/74] docs: use comma-separated syntax for CROSS_ORIGIN_STORAGE_ORIGINS examples --- src/settings.js | 2 +- test/test_other.py | 2 +- tools/link.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/settings.js b/src/settings.js index c826b76251d14..3631078356e72 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2258,7 +2258,7 @@ var CROSS_ORIGIN_STORAGE = 0; // **Restricted to a specific set of origins** — only listed HTTPS origins can // retrieve the file:: // -// -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] +// -sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com // // For proprietary resources shared across a controlled set of related sites. // Each value must be a valid serialized HTTPS origin (scheme + host + optional diff --git a/test/test_other.py b/test/test_other.py index 9fb641df6d37c..3f9a40cacac55 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15535,7 +15535,7 @@ def test_cross_origin_storage_origins_explicit_list(self): self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://app.example.com","https://api.example.com"]', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com', '-o', 'hello.js']) js = read_file('hello.js') self.assertContained('"https://app.example.com"', js) diff --git a/tools/link.py b/tools/link.py index 551674f4a36cb..a8748f0b57cc1 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1232,7 +1232,7 @@ def limit_incoming_module_api(): settings.CROSS_ORIGIN_STORAGE_ORIGINS = ['*'] origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS if not isinstance(origins, list): - exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. ['*'] or ['https://example.com']") + exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. https://example.com or https://a.com,https://b.com") if '*' in origins and len(origins) > 1: exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") for o in origins: From 9c1056dc04486048506b2b0542a67e4a9bbc2d4f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:03:02 +0200 Subject: [PATCH 37/74] fix: make CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 a hard error --- .../docs/compiling/CrossOriginStorage.rst | 6 +++--- test/test_other.py | 18 ++++++++---------- tools/link.py | 2 +- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index ce3666eb12fa3..607cc2589d6c9 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -132,9 +132,9 @@ Requirements and restrictions (``-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 emits a **warning** with ``-sWASM_ASYNC_COMPILATION=0``: the - synchronous instantiation path bypasses ``instantiateAsync()`` entirely, - so the COS code is never reached. +- 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 diff --git a/test/test_other.py b/test/test_other.py index 3f9a40cacac55..92403e026f875 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15441,16 +15441,14 @@ def test_cross_origin_storage_error_with_single_file(self): '-o', 'hello.js'], 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') - def test_cross_origin_storage_warning_without_async_compilation(self): - """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must warn.""" - proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', - '-sENVIRONMENT=web', - '-sWASM_ASYNC_COMPILATION=0', - '-o', 'hello.js'], - stderr=PIPE) - self.assertContained('CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0', - proc.stderr) + def test_cross_origin_storage_error_without_async_compilation(self): + """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must be a hard link-time error.""" + self.assert_fail([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sWASM_ASYNC_COMPILATION=0', + '-o', 'hello.js'], + 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') def test_cross_origin_storage_warning_with_split_module(self): """CROSS_ORIGIN_STORAGE + SPLIT_MODULE must warn that secondary files are not covered.""" diff --git a/tools/link.py b/tools/link.py index a8748f0b57cc1..4c8bae9637fe1 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1216,7 +1216,7 @@ def limit_incoming_module_api(): if not settings.ENVIRONMENT_MAY_BE_WEB: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web (navigator.crossOriginStorage is not available outside the browser)') if not settings.WASM_ASYNC_COMPILATION: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') + exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') if settings.SPLIT_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') if settings.MAIN_MODULE: From 419d0bfaaa05bfb1942287ba286a97700bb600f7 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:04:52 +0200 Subject: [PATCH 38/74] docs: update build-time constant example to match current cosHash object form --- site/source/docs/compiling/CrossOriginStorage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 607cc2589d6c9..3538da955de5e 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -155,7 +155,7 @@ After all optimizations — including any ``wasm-opt`` passes run by Binaryen digest. That digest is embedded in the generated JavaScript glue as a build-time constant:: - var wasmHashValue = 'a3f2...c9d1'; // 64 hex characters + var cosHash = { algorithm: 'SHA-256', value: 'a3f2...c9d1' }; // value: 64 hex characters No extra files are produced; the hash is part of the regular ``.js`` output. @@ -190,7 +190,7 @@ When the page loads, the generated JavaScript follows this logic: If the API is absent, skip to the normal fetch path immediately. 2. **Cache hit** — call - ``navigator.crossOriginStorage.requestFileHandles([{algorithm: 'SHA-256', value: wasmHashValue}])``. + ``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()``. From 37988bf03bb6cf100a0d995f2de41e5fb54a27cd Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:09:10 +0200 Subject: [PATCH 39/74] docs: use Module['wasmSHA256'] consistently and fix sed to patch both hash occurrences --- site/source/docs/compiling/CrossOriginStorage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 3538da955de5e..3691fa20e8f79 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -155,7 +155,7 @@ After all optimizations — including any ``wasm-opt`` passes run by Binaryen digest. That digest is embedded in the generated JavaScript glue as a build-time constant:: - var cosHash = { algorithm: 'SHA-256', value: 'a3f2...c9d1' }; // value: 64 hex characters + Module['wasmSHA256'] = 'a3f2...c9d1'; // 64 hex characters No extra files are produced; the hash is part of the regular ``.js`` output. @@ -175,7 +175,7 @@ No extra files are produced; the hash is part of the regular ``.js`` output. # After all post-processing is complete: final_hash=$(sha256sum hello.wasm | awk '{print $1}') - sed -i "s/value: '[0-9a-f]\{64\}'/value: '${final_hash}'/" hello.js + 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 From d459be38e2cb13744e3cdc3d017633821d46edf9 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:11:23 +0200 Subject: [PATCH 40/74] refactor: rename wasmSHA256 to wasmHash and expose {algorithm,value} object --- site/source/docs/compiling/CrossOriginStorage.rst | 12 ++++++------ src/preamble.js | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 3691fa20e8f79..d9a09f8d657d1 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -155,7 +155,7 @@ After all optimizations — including any ``wasm-opt`` passes run by Binaryen digest. That digest is embedded in the generated JavaScript glue as a build-time constant:: - Module['wasmSHA256'] = 'a3f2...c9d1'; // 64 hex characters + Module['wasmHash'] = { algorithm: 'SHA-256', value: 'a3f2...c9d1' }; No extra files are produced; the hash is part of the regular ``.js`` output. @@ -281,13 +281,13 @@ hash as a named Module property: .. code-block:: javascript - Module['wasmSHA256'] // 64-character lowercase hex string, e.g. 'a3f2…c9d1' + 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['wasmSHA256']`` +equivalent factory function. A custom loader can read ``Module['wasmHash']`` via a reference to that config object: .. code-block:: javascript @@ -296,8 +296,8 @@ via a reference to that config object: instantiateWasm(imports, onSuccess) { // `this` inside the callback is Emscripten's internal Module object; // read the hash via the outer Module reference instead. - const cosHash = { algorithm: 'SHA-256', value: Module['wasmSHA256'] }; - if (cosHash.value && 'crossOriginStorage' in navigator) { + const cosHash = Module['wasmHash']; + if (cosHash?.value && 'crossOriginStorage' in navigator) { navigator.crossOriginStorage.requestFileHandles([cosHash]) .then(handles => handles[0].getFile()) .then(f => f.arrayBuffer()) @@ -327,7 +327,7 @@ via a reference to that config object: }, }; -``Module['wasmSHA256']`` is only present in builds compiled with +``Module['wasmHash']`` is only present in builds compiled with :ref:`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. diff --git a/src/preamble.js b/src/preamble.js index 2b0f453001183..81772aae784dc 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -923,13 +923,13 @@ function getWasmImports() { var info = getWasmImports(); #if CROSS_ORIGIN_STORAGE - // Expose the build-time SHA-256 hash of the .wasm binary as a named Module - // property so that custom Module['instantiateWasm'] implementations can read - // it without having to parse the JS source. The COS fetch logic lives inside + // Expose the build-time hash of the .wasm binary as a named Module property + // so that custom Module['instantiateWasm'] implementations can read it + // without having to parse the JS source. The COS fetch logic lives inside // instantiateAsync(), which is bypassed when a custom instantiateWasm is - // provided. Such loaders can use Module['wasmSHA256'] to implement their own + // provided. Such loaders can use Module['wasmHash'] to implement their own // COS-aware loading path. - Module['wasmSHA256'] = '{{{ WASM_SHA256 }}}'; + Module['wasmHash'] = { algorithm: 'SHA-256', value: '{{{ WASM_SHA256 }}}' }; #endif #if expectToReceiveOnModule('instantiateWasm') From 0efc11975a621e33fa80a784035b22e1b65aa4af Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:55:47 +0200 Subject: [PATCH 41/74] refactor: address multiple PR review comments on CROSS_ORIGIN_STORAGE - Use INCOMPATIBLE_SETTINGS in settings.py for CROSS_ORIGIN_STORAGE + SINGLE_FILE and CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 pairs, removing manual error checks from link.py - Change default CROSS_ORIGIN_STORAGE_ORIGINS from [] (sentinel) to ['*'] in settings.js, removing the manual default override in link.py - Type-check of ORIGINS is now handled automatically by settings.py - Replace {{{ WASM_SHA256 }}} template substitution with algorithm-agnostic <<< WASM_HASH_ALGORITHM >>> / <<< WASM_HASH_VALUE >>> late replacements; hash is computed once post-binaryen, removing the pre-binaryen pass - Use utils.read_binary() instead of manual file open in link.py - cosHash in preamble.js now references Module['wasmHash'] directly instead of redeclaring the hash object, so the value appears only once in the JS - Trim long inline comment blocks in preamble.js and settings.js; point to the CrossOriginStorage guide for full details - Use optional chaining (?.) for Module['onCOSCacheHit/Miss/Store'] calls - Drop =1 suffix from all -sCROSS_ORIGIN_STORAGE references in ChangeLog, test_other.py comments, and docs - Rename test_cross_origin_storage_wasm_sha256_* tests to wasmHash variants --- ChangeLog.md | 9 +-- .../docs/compiling/CrossOriginStorage.rst | 12 +-- .../tools_reference/settings_reference.rst | 76 +++++-------------- src/preamble.js | 59 +++----------- src/settings.js | 74 +++++------------- test/test_other.py | 61 ++++++++------- tools/link.py | 56 ++++---------- tools/settings.py | 2 + 8 files changed, 101 insertions(+), 248 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index ff2ffa06ac5ab..c87b26bf76f2d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,7 +20,7 @@ See docs/process.md for more on how version tagging works. 6.0.1 (in development) ---------------------- -- New experimental ``-sCROSS_ORIGIN_STORAGE=1`` linker flag that integrates +- New experimental ``-sCROSS_ORIGIN_STORAGE`` linker flag that integrates the proposed `Cross-Origin Storage browser API `_ into the Wasm loading path as a progressive enhancement (web target only). At build time Emscripten @@ -31,10 +31,9 @@ See docs/process.md for more on how version tagging works. normally and stores the binary in COS for future use by any origin. Falls back transparently to the standard fetch path when the API is unavailable. Three optional ``Module`` callbacks are available for instrumentation: - ``Module['onCOSCacheHit'](hash)``, ``Module['onCOSCacheMiss'](url)``, and - ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` - (hard error — the wasm is inlined directly into the JS output with no - fetchable URL) and ``-sWASM_ASYNC_COMPILATION=0`` (warning). + ``Module['onCOSCacheHit'](hash)``, ``Module['onCOSCacheMiss'](hash, url)``, + and ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` + and ``-sWASM_ASYNC_COMPILATION=0`` (both produce hard link-time errors). The companion ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` setting controls which origins may read the cached file: ``['*']`` (default, globally available), an explicit HTTPS origin list (restricted), or ``[]`` (same-site only). diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index d9a09f8d657d1..28cf1d1018baf 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -151,9 +151,9 @@ Build time ---------- After all optimizations — including any ``wasm-opt`` passes run by Binaryen -— Emscripten reads the final ``.wasm`` binary and computes its SHA-256 -digest. That digest is embedded in the generated JavaScript glue as a -build-time constant:: +— 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' }; @@ -167,9 +167,9 @@ No extra files are produced; the hash is part of the regular ``.js`` output. Makefile or CI script — those tools change the binary and **invalidate the embedded hash**. - In that case you must recompute the SHA-256 of the final ``.wasm`` and - patch the hash string in the generated ``.js`` yourself before shipping. - A minimal shell snippet for doing so: + 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): .. code-block:: bash diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 3b06997634018..9f1a46a13c871 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3359,40 +3359,17 @@ CROSS_ORIGIN_STORAGE ==================== [experimental] Enables Cross-Origin Storage (COS) API support for Wasm -loading on the Web target. +loading on the Web target. At link time Emscripten computes the SHA-256 +hash of the final ``.wasm`` binary and embeds it in the generated JS. +At runtime the COS API is used as a progressive enhancement: the binary is +fetched from the shared cross-origin cache on a hit, or stored there after +a network fetch on a miss; when the API is absent or errors the runtime +falls through to the standard fetch path. -**When to use this flag** +Only meaningful for the Web environment. Incompatible with SINGLE_FILE and +WASM_ASYNC_COMPILATION=0 (both produce hard link-time errors). -COS is only beneficial for Wasm binaries that are byte-identical across -many different origins — i.e. popular libraries where the same compiled -binary is loaded by many independent sites. -Good candidates are libraries or toolkits distributed as a stable, -version-pinned binary loaded by many independent sites. - -If your ``.wasm`` file is bespoke application code built specifically for -your site, COS gives you nothing that the normal HTTP cache does not -already provide. Do not enable this flag for application-specific Wasm. - -When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` -binary at link time, embeds it as a build-time constant in the generated -JavaScript glue, and uses the browser COS API as a progressive enhancement: - -- **Cache hit**: the runtime calls - ``navigator.crossOriginStorage.requestFileHandles()`` with the hash and, - if the module is found, reads it directly from the cross-origin cache. -- **Cache miss**: the module is fetched over the network as usual, then - stored in COS in the background (non-blocking) with ``origins: '*'`` so - any other origin can reuse it. -- **Fallback**: when the browser does not expose the COS API, or when an - unexpected error occurs, the runtime falls through to the standard - ``fetch`` / ``WebAssembly.instantiateStreaming`` path transparently. - -Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no -effect on Node.js or shell targets. Also has no effect in SINGLE_FILE -builds where the Wasm binary is inlined directly into the JS output. - -See :ref:`CrossOriginStorage` for the full guide, including how to test -with the COS browser extension polyfill. +See :ref:`CrossOriginStorage` for the full guide. Default value: 0 @@ -3401,37 +3378,20 @@ Default value: 0 CROSS_ORIGIN_STORAGE_ORIGINS ============================ -Controls which origins may read the Wasm binary after it has been stored in -the Cross-Origin Storage (COS) cache. Only meaningful when -``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) -path; it is only applied during the write (cache-miss) path. +Controls which origins may read the Wasm binary from the COS cache. Only +meaningful when ``-sCROSS_ORIGIN_STORAGE`` is set. Applied only during the +write (cache-miss) path, not the read (cache-hit) path. -Three modes are supported, matching the ``origins`` field of the COS API's -``CrossOriginStorageRequestFileHandleOptions`` dictionary: +``['*']`` (default) — any origin can retrieve the file. +Explicit HTTPS origin list — restricted to those origins only:: -**Globally available** (default when the setting is not explicitly passed) — -any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is -used without specifying this setting, ``origins: '*'`` is used automatically. -Appropriate for widely-used public binaries shared across many independent -origins. + -sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com -**Restricted to a specific set of origins** — only listed HTTPS origins can -retrieve the file:: +``[]`` — same-site only (omits the ``origins`` field entirely). - -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] +Mixing ``'*'`` with explicit origins is a link-time error. -For proprietary resources shared across a controlled set of related sites. -Each value 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 the setting with an empty list to omit the -``origins`` field, making the file available only to same-site origins:: - - -sCROSS_ORIGIN_STORAGE_ORIGINS=[] - -For resources shared across subdomains of a single site but not beyond. - -Default value: [] +Default value: ['*'] .. _fake_dylibs: diff --git a/src/preamble.js b/src/preamble.js index 81772aae784dc..66d5af8cdb8f8 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -630,46 +630,16 @@ async function instantiateAsync(binary, binaryFile, imports) { #if CROSS_ORIGIN_STORAGE // Cross-Origin Storage (COS) progressive enhancement. // https://github.com/WICG/cross-origin-storage - // - // COS is only beneficial when this .wasm binary is byte-identical across - // many origins — i.e. a popular library loaded by many independent sites. - // Application-specific Wasm gains nothing from COS that the normal HTTP - // cache does not already provide. - // - // The SHA-256 hash of the final .wasm binary is computed at link time and - // embedded here as a build-time constant. At runtime we feature-detect the - // browser COS API via `'crossOriginStorage' in navigator`, then call - // navigator.crossOriginStorage.requestFileHandles() with the hash object - // required by the spec ({ algorithm: 'SHA-256', value: '' }). - // - // Cache-hit path: - // requestFileHandles() succeeds → getFile() → arrayBuffer() → instantiate - // - // Cache-miss path (NotFoundError): - // fetch() the wasm over the network → instantiate → store in COS. - // The origins field is controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS - // (default: '*', globally available). The store is fire-and-forget so - // it never delays startup. - // - // Visibility and security: the spec allows widening visibility (e.g. a - // restricted entry promoted to globally available) but never narrowing it. - // Because storing always requires writing the actual bytes, no third party - // can probe the cache to discover whether a restricted entry was previously - // stored by another origin — a cache hit is only possible after an explicit - // write that provided the content. - // - // Any other error (NotAllowedError, network failure, …) falls through to the + // Any error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. - var cosHash = { algorithm: 'SHA-256', value: '{{{ WASM_SHA256 }}}' }; - if (cosHash.value && 'crossOriginStorage' in navigator) { + var cosHash = Module['wasmHash']; + if (cosHash?.value && 'crossOriginStorage' in navigator) { try { var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); // Cache hit — read the Blob and instantiate from its ArrayBuffer. var cosFile = await cosHandles[0].getFile(); var cosBytes = await cosFile.arrayBuffer(); - // Optional instrumentation callback: Module['onCOSCacheHit'](hash) - // Called when the Wasm binary is served from the cross-origin cache. - if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](cosHash.value); + Module['onCOSCacheHit']?.(cosHash.value); return WebAssembly.instantiate(cosBytes, imports); } catch (cosErr) { if (cosErr.name === 'NotFoundError') { @@ -677,9 +647,7 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); var wasmBytes = await networkResponse.arrayBuffer(); - // Optional instrumentation callback: Module['onCOSCacheMiss'](hash, url) - // Called when the Wasm binary is not in COS and is fetched over the network. - if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](cosHash.value, binaryFile); + Module['onCOSCacheMiss']?.(cosHash.value, binaryFile); // Fire-and-forget store; never block instantiation on the write. (async () => { try { @@ -696,16 +664,14 @@ async function instantiateAsync(binary, binaryFile, imports) { var writable = await writeHandles[0].createWritable(); await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); await writable.close(); - // Optional instrumentation callback: Module['onCOSStore'](hash) - // Called after the Wasm binary has been successfully written to COS. - if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](cosHash.value); + Module['onCOSStore']?.(cosHash.value); } catch (storeErr) { err(`COS store failed: ${storeErr}`); } })(); return WebAssembly.instantiate(wasmBytes, imports); } catch (fetchErr) { - // Network fetch failed; fall through to the standard path. + // Network fetch failed; fall through to the standard path below. err(`COS fallback fetch failed: ${fetchErr}`); } } else if (cosErr.name === 'NotAllowedError') { @@ -713,6 +679,7 @@ async function instantiateAsync(binary, binaryFile, imports) { } else { err(`Cross-Origin Storage lookup failed: ${cosErr}`); } + // Fall through to the standard streaming path below. } } #endif // CROSS_ORIGIN_STORAGE @@ -923,13 +890,9 @@ function getWasmImports() { var info = getWasmImports(); #if CROSS_ORIGIN_STORAGE - // Expose the build-time hash of the .wasm binary as a named Module property - // so that custom Module['instantiateWasm'] implementations can read it - // without having to parse the JS source. The COS fetch logic lives inside - // instantiateAsync(), which is bypassed when a custom instantiateWasm is - // provided. Such loaders can use Module['wasmHash'] to implement their own - // COS-aware loading path. - Module['wasmHash'] = { algorithm: 'SHA-256', value: '{{{ WASM_SHA256 }}}' }; + // Expose the build-time hash so that custom Module['instantiateWasm'] + // callbacks can implement their own COS-aware loading path. + Module['wasmHash'] = { algorithm: '<<< WASM_HASH_ALGORITHM >>>', value: '<<< WASM_HASH_VALUE >>>' }; #endif #if expectToReceiveOnModule('instantiateWasm') diff --git a/src/settings.js b/src/settings.js index 3631078356e72..90d773dc160f6 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2203,75 +2203,35 @@ var GROWABLE_ARRAYBUFFERS = false; var CROSS_ORIGIN = false; // [experimental] Enables Cross-Origin Storage (COS) API support for Wasm -// loading on the Web target. +// loading on the Web target. At link time Emscripten computes the SHA-256 +// hash of the final ``.wasm`` binary and embeds it in the generated JS. +// At runtime the COS API is used as a progressive enhancement: the binary is +// fetched from the shared cross-origin cache on a hit, or stored there after +// a network fetch on a miss; when the API is absent or errors the runtime +// falls through to the standard fetch path. // -// **When to use this flag** +// Only meaningful for the Web environment. Incompatible with SINGLE_FILE and +// WASM_ASYNC_COMPILATION=0 (both produce hard link-time errors). // -// COS is only beneficial for Wasm binaries that are byte-identical across -// many different origins — i.e. popular libraries where the same compiled -// binary is loaded by many independent sites. -// Good candidates are libraries or toolkits distributed as a stable, -// version-pinned binary loaded by many independent sites. -// -// If your ``.wasm`` file is bespoke application code built specifically for -// your site, COS gives you nothing that the normal HTTP cache does not -// already provide. Do not enable this flag for application-specific Wasm. -// -// When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` -// binary at link time, embeds it as a build-time constant in the generated -// JavaScript glue, and uses the browser COS API as a progressive enhancement: -// -// - **Cache hit**: the runtime calls -// ``navigator.crossOriginStorage.requestFileHandles()`` with the hash and, -// if the module is found, reads it directly from the cross-origin cache. -// - **Cache miss**: the module is fetched over the network as usual, then -// stored in COS in the background (non-blocking) with ``origins: '*'`` so -// any other origin can reuse it. -// - **Fallback**: when the browser does not expose the COS API, or when an -// unexpected error occurs, the runtime falls through to the standard -// ``fetch`` / ``WebAssembly.instantiateStreaming`` path transparently. -// -// Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no -// effect on Node.js or shell targets. Also has no effect in SINGLE_FILE -// builds where the Wasm binary is inlined directly into the JS output. -// -// See :ref:`CrossOriginStorage` for the full guide, including how to test -// with the COS browser extension polyfill. +// See :ref:`CrossOriginStorage` for the full guide. // // [link] var CROSS_ORIGIN_STORAGE = 0; -// Controls which origins may read the Wasm binary after it has been stored in -// the Cross-Origin Storage (COS) cache. Only meaningful when -// ``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) -// path; it is only applied during the write (cache-miss) path. +// Controls which origins may read the Wasm binary from the COS cache. Only +// meaningful when ``-sCROSS_ORIGIN_STORAGE`` is set. Applied only during the +// write (cache-miss) path, not the read (cache-hit) path. // -// Three modes are supported, matching the ``origins`` field of the COS API's -// ``CrossOriginStorageRequestFileHandleOptions`` dictionary: -// -// **Globally available** (default when the setting is not explicitly passed) — -// any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is -// used without specifying this setting, ``origins: '*'`` is used automatically. -// Appropriate for widely-used public binaries shared across many independent -// origins. -// -// **Restricted to a specific set of origins** — only listed HTTPS origins can -// retrieve the file:: +// ``['*']`` (default) — any origin can retrieve the file. +// Explicit HTTPS origin list — restricted to those origins only:: // // -sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com // -// For proprietary resources shared across a controlled set of related sites. -// Each value 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 the setting with an empty list to omit the -// ``origins`` field, making the file available only to same-site origins:: -// -// -sCROSS_ORIGIN_STORAGE_ORIGINS=[] +// ``[]`` — same-site only (omits the ``origins`` field entirely). // -// For resources shared across subdomains of a single site but not beyond. +// Mixing ``'*'`` with explicit origins is a link-time error. // [link] -var CROSS_ORIGIN_STORAGE_ORIGINS = []; +var CROSS_ORIGIN_STORAGE_ORIGINS = ['*']; // This setting changes the behaviour of the ``-shared`` flag. When set to true // you get the old emscripten behaviour where the ``-shared`` flag actually diff --git a/test/test_other.py b/test/test_other.py index 92403e026f875..9c6f9734209c6 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15359,7 +15359,7 @@ def test_logReadFiles(self): self.assertContained('read file: /test.txt', output) # --------------------------------------------------------------------------- - # Tests for CROSS_ORIGIN_STORAGE (-sCROSS_ORIGIN_STORAGE=1) + # Tests for CROSS_ORIGIN_STORAGE (-sCROSS_ORIGIN_STORAGE) # https://github.com/WICG/cross-origin-storage # --------------------------------------------------------------------------- @@ -15369,7 +15369,7 @@ def test_cross_origin_storage_js_output(self): The embedded hash must be the correct SHA-256 of the compiled .wasm file. """ self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') @@ -15401,13 +15401,13 @@ def test_cross_origin_storage_js_output(self): # The hash embedded in the JS must be a 64-char lowercase hex string … m = re.search(r"value:\s*'([0-9a-f]{64})'", js) - self.assertTrue(m, 'could not find a 64-char hex WASM_SHA256 value in JS output') + self.assertTrue(m, 'could not find a 64-char hex hash value in JS output') embedded_hash = m.group(1) # … and must exactly match the SHA-256 of the emitted .wasm file. expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() self.assertEqual(embedded_hash, expected_hash, - 'embedded WASM_SHA256 does not match actual .wasm SHA-256') + 'embedded wasm hash does not match actual .wasm SHA-256') def test_cross_origin_storage_disabled_by_default(self): """COS code must NOT appear when the flag is omitted (default off).""" @@ -15424,7 +15424,7 @@ def test_cross_origin_storage_not_emitted_for_node_target(self): emitted since the flag does nothing in this configuration. """ proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=node', '-o', 'hello.js'], stderr=PIPE) @@ -15435,7 +15435,7 @@ def test_cross_origin_storage_not_emitted_for_node_target(self): def test_cross_origin_storage_error_with_single_file(self): """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sSINGLE_FILE', '-o', 'hello.js'], @@ -15444,7 +15444,7 @@ def test_cross_origin_storage_error_with_single_file(self): def test_cross_origin_storage_error_without_async_compilation(self): """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sWASM_ASYNC_COMPILATION=0', '-o', 'hello.js'], @@ -15453,7 +15453,7 @@ def test_cross_origin_storage_error_without_async_compilation(self): def test_cross_origin_storage_warning_with_split_module(self): """CROSS_ORIGIN_STORAGE + SPLIT_MODULE must warn that secondary files are not covered.""" proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sSPLIT_MODULE', '-o', 'hello.js'], @@ -15464,7 +15464,7 @@ def test_cross_origin_storage_warning_with_split_module(self): def test_cross_origin_storage_warning_with_main_module(self): """CROSS_ORIGIN_STORAGE + MAIN_MODULE must warn that side modules are not covered.""" proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sMAIN_MODULE', '-o', 'hello.js'], @@ -15475,7 +15475,7 @@ def test_cross_origin_storage_warning_with_main_module(self): def test_cross_origin_storage_warning_with_side_module(self): """CROSS_ORIGIN_STORAGE + SIDE_MODULE must warn: no JS glue is emitted.""" proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sSIDE_MODULE', '-o', 'hello.wasm'], stderr=PIPE) @@ -15485,14 +15485,14 @@ def test_cross_origin_storage_warning_with_side_module(self): def test_cross_origin_storage_hash_changes_with_content(self): """Two different programs must produce different embedded hashes.""" self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) js_a = read_file('hello.js') hash_a = re.search(r"value:\s*'([0-9a-f]{64})'", js_a).group(1) self.run_process([EMCC, test_file('hello_world_small.c'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'small.js']) js_b = read_file('small.js') @@ -15511,7 +15511,7 @@ def test_cross_origin_storage_origins_default_is_global(self): Globally available; the user only needs -sCROSS_ORIGIN_STORAGE=1. """ self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) self.assertContained("origins: '*'", read_file('hello.js')) @@ -15522,7 +15522,7 @@ def test_cross_origin_storage_origins_explicit_wildcard(self): This matches the implicit default. """ self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', "-sCROSS_ORIGIN_STORAGE_ORIGINS=['*']", '-o', 'hello.js']) @@ -15531,7 +15531,7 @@ def test_cross_origin_storage_origins_explicit_wildcard(self): def test_cross_origin_storage_origins_explicit_list(self): """An explicit origins list must be emitted as a JS array.""" self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com', '-o', 'hello.js']) @@ -15543,7 +15543,7 @@ def test_cross_origin_storage_origins_explicit_list(self): def test_cross_origin_storage_origins_same_site(self): """Empty origins list must omit the origins key entirely (same-site only).""" self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=[]', '-o', 'hello.js']) @@ -15556,7 +15556,7 @@ def test_cross_origin_storage_origins_error_mixed_wildcard(self): """Mixing '*' with explicit origins must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]', '-o', 'hello.js'], @@ -15566,7 +15566,7 @@ def test_cross_origin_storage_origins_error_invalid_origin(self): """A non-HTTPS or malformed origin must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]', '-o', 'hello.js'], @@ -15576,43 +15576,42 @@ def test_cross_origin_storage_origins_error_origin_with_path(self): """An origin with a path component must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]', '-o', 'hello.js'], 'is not a valid HTTPS origin') - def test_cross_origin_storage_wasm_sha256_module_property(self): - """Module['wasmSHA256'] must be set in the JS output and match the .wasm hash. + def test_cross_origin_storage_wasm_hash_module_property(self): + """Module['wasmHash'] must be set in the JS output and match the .wasm hash. Custom Module['instantiateWasm'] implementations bypass instantiateAsync() - and therefore cannot reach the COS fetch logic that lives there. They can - read Module['wasmSHA256'] instead to get the build-time hash without parsing - the JS source. + and can read Module['wasmHash'] to get the build-time hash object. """ self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') # The property must be present with a 64-char hex value. - m = re.search(r"Module\['wasmSHA256'\]\s*=\s*'([0-9a-f]{64})'", js) - self.assertTrue(m, "Module['wasmSHA256'] not found in JS output") + m = re.search(r"Module\['wasmHash'\]\s*=\s*\{[^}]*value:\s*'([0-9a-f]{64})'", js) + self.assertTrue(m, "Module['wasmHash'] not found in JS output") embedded_hash = m.group(1) # It must equal the SHA-256 of the actual .wasm file. expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() self.assertEqual(embedded_hash, expected_hash, - "Module['wasmSHA256'] does not match the actual .wasm SHA-256") + "Module['wasmHash'] does not match the actual .wasm SHA-256") - def test_cross_origin_storage_wasm_sha256_absent_without_flag(self): - """Module['wasmSHA256'] must NOT appear when CROSS_ORIGIN_STORAGE is off.""" + def test_cross_origin_storage_wasm_hash_absent_without_flag(self): + """Module['wasmHash'] must NOT appear when CROSS_ORIGIN_STORAGE is off.""" self.run_process([EMCC, test_file('hello_world.cpp'), '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') - self.assertNotContained("Module['wasmSHA256']", js) + self.assertNotContained("Module['wasmHash']", js) + def test_deprecated_settings(self): err = self.run_process([EMCC, '-sMEMORY64', test_file('hello_world.c')], stderr=PIPE).stderr self.assertContained('emcc: warning: MEMORY64 is deprecated (prefer the standard -m64 or --target=wasm64 flags). Please open a bug if you have a continuing need for this setting [-Wdeprecated]', err) diff --git a/tools/link.py b/tools/link.py index 4c8bae9637fe1..4bac04dca3489 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1211,34 +1211,21 @@ def limit_incoming_module_api(): exit_with_error('cannot have both WASM=2 and SINGLE_FILE enabled at the same time') if settings.CROSS_ORIGIN_STORAGE: - if settings.SINGLE_FILE: - exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on)') if not settings.ENVIRONMENT_MAY_BE_WEB: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web (navigator.crossOriginStorage is not available outside the browser)') - if not settings.WASM_ASYNC_COMPILATION: - exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') if settings.SPLIT_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') if settings.MAIN_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') if settings.SIDE_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') - # Resolve and validate CROSS_ORIGIN_STORAGE_ORIGINS. - # The default in settings.js is [] (empty sentinel). When the user has - # not explicitly passed -sCROSS_ORIGIN_STORAGE_ORIGINS we default to ['*'] - # (globally available), which is the appropriate mode for widely-shared - # public Wasm binaries. An explicit =[] means same-site only. - if 'CROSS_ORIGIN_STORAGE_ORIGINS' not in user_settings: - settings.CROSS_ORIGIN_STORAGE_ORIGINS = ['*'] origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS - if not isinstance(origins, list): - exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. https://example.com or https://a.com,https://b.com") if '*' in origins and len(origins) > 1: exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") for o in origins: if o == '*': continue - # Each explicit origin must be a valid serialised HTTPS origin: + # Each explicit origin must be a valid serialized HTTPS origin: # scheme "https://", host, optional ":port", no path/query/fragment. if not re.fullmatch(r'https://[^/]+(:\d+)?', o): exit_with_error(f"CROSS_ORIGIN_STORAGE_ORIGINS: {o!r} is not a valid HTTPS origin (expected 'https://host' or 'https://host:port')") @@ -1919,20 +1906,6 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat settings.TARGET_JS_NAME = os.path.basename(js_target) - # Compute the SHA-256 of the wasm binary before phase_emscript so the hash - # is available to the JS-glue template as {{{ WASM_SHA256 }}}. We read - # in_wasm here (the pre-binaryen binary); after phase_binaryen we recompute - # the hash and patch the JS if binaryen changed the binary. - if settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB: - if os.path.exists(in_wasm): - with open(in_wasm, 'rb') as f: - wasm_bytes_pre = f.read() - settings.WASM_SHA256 = hashlib.sha256(wasm_bytes_pre).hexdigest() - logger.debug(f'CROSS_ORIGIN_STORAGE: pre-binaryen wasm SHA-256 = {settings.WASM_SHA256}') - else: - logger.warning('CROSS_ORIGIN_STORAGE: wasm file not found for hashing; WASM_SHA256 will be empty') - settings.WASM_SHA256 = '' - metadata = phase_emscript(in_wasm, wasm_target, js_syms, base_metadata) if settings.EMBIND_AOT: @@ -1946,22 +1919,19 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat phase_binaryen(target, options, wasm_target) - # After binaryen, recompute the hash from the final wasm and patch the JS - # if binaryen changed the binary (so the embedded hash stays accurate). - if final_js and settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB and settings.WASM_SHA256: + # Compute the hash of the final wasm (after binaryen) and substitute the + # <<< WASM_HASH_ALGORITHM >>> / <<< WASM_HASH_VALUE >>> placeholders that + # preamble.js left in the generated JS. + if final_js and settings.CROSS_ORIGIN_STORAGE: if os.path.exists(wasm_target): - with open(wasm_target, 'rb') as f: - wasm_bytes_post = f.read() - post_hash = hashlib.sha256(wasm_bytes_post).hexdigest() - if post_hash != settings.WASM_SHA256: - logger.debug(f'CROSS_ORIGIN_STORAGE: binaryen changed wasm; updating SHA-256 to {post_hash}') - js_content = read_file(final_js) - js_content = js_content.replace(f"value: '{settings.WASM_SHA256}'", - f"value: '{post_hash}'") - js_content = js_content.replace(f"Module['wasmSHA256'] = '{settings.WASM_SHA256}'", - f"Module['wasmSHA256'] = '{post_hash}'") - write_file(final_js, js_content) - settings.WASM_SHA256 = post_hash + wasm_bytes = utils.read_binary(wasm_target) + wasm_hash_algorithm = 'SHA-256' + wasm_hash_value = hashlib.sha256(wasm_bytes).hexdigest() + logger.debug(f'CROSS_ORIGIN_STORAGE: wasm {wasm_hash_algorithm} = {wasm_hash_value}') + js_content = read_file(final_js) + js_content = do_replace(js_content, '<<< WASM_HASH_ALGORITHM >>>', wasm_hash_algorithm) + js_content = do_replace(js_content, '<<< WASM_HASH_VALUE >>>', wasm_hash_value) + write_file(final_js, js_content) # If we are not emitting any JS then we are all done now if options.oformat != OFormat.WASM: diff --git a/tools/settings.py b/tools/settings.py index 87e1e65581d65..ddff42e4b2de8 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -148,6 +148,8 @@ ('LEGACY_VM_SUPPORT', 'MEMORY64', None), ('CROSS_ORIGIN', 'NO_DYNAMIC_EXECUTION', None), ('CROSS_ORIGIN', 'NO_PTHREADS', None), + ('CROSS_ORIGIN_STORAGE', 'SINGLE_FILE', 'the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on'), + ('CROSS_ORIGIN_STORAGE', 'NO_WASM_ASYNC_COMPILATION', 'synchronous instantiation does not use the COS fetch path'), ] EXPERIMENTAL_SETTINGS = { From ce79cdba799b836eadcc6c652918ae09f0a22416 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 13:08:25 +0200 Subject: [PATCH 42/74] refactor: make COS instrumentation callbacks opt-in via INCOMING_MODULE_JS_API Add onCOSCacheHit, onCOSCacheMiss, onCOSStore to EXTRA_INCOMING_JS_API in cmdline.py so they are valid but not included by default. Wrap each call in preamble.js with expectToReceiveOnModule() guards so the callback code is only emitted when the user lists them in INCOMING_MODULE_JS_API. Update tests and docs accordingly. --- .../docs/compiling/CrossOriginStorage.rst | 9 +++++++-- src/preamble.js | 6 ++++++ test/test_other.py | 20 +++++++++++++++---- tools/cmdline.py | 3 +++ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 28cf1d1018baf..e08f8cc4f9242 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -212,7 +212,12 @@ When the page loads, the generated JavaScript follows this logic: Instrumentation callbacks ------------------------- -Three optional ``Module`` properties let you observe COS events at runtime: +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 .. code-block:: javascript @@ -223,7 +228,7 @@ Three optional ``Module`` properties let you observe COS events at runtime: }, // Called when the Wasm binary was not in COS and was fetched over the - // network. |hash| is the SHA-256 that missed; |url| is the fallback URL. + // 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); }, diff --git a/src/preamble.js b/src/preamble.js index 66d5af8cdb8f8..952e691c0eaf4 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -639,7 +639,9 @@ async function instantiateAsync(binary, binaryFile, imports) { // Cache hit — read the Blob and instantiate from its ArrayBuffer. var cosFile = await cosHandles[0].getFile(); var cosBytes = await cosFile.arrayBuffer(); +#if expectToReceiveOnModule('onCOSCacheHit') Module['onCOSCacheHit']?.(cosHash.value); +#endif return WebAssembly.instantiate(cosBytes, imports); } catch (cosErr) { if (cosErr.name === 'NotFoundError') { @@ -647,7 +649,9 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); var wasmBytes = await networkResponse.arrayBuffer(); +#if expectToReceiveOnModule('onCOSCacheMiss') Module['onCOSCacheMiss']?.(cosHash.value, binaryFile); +#endif // Fire-and-forget store; never block instantiation on the write. (async () => { try { @@ -664,7 +668,9 @@ async function instantiateAsync(binary, binaryFile, imports) { var writable = await writeHandles[0].createWritable(); await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); await writable.close(); +#if expectToReceiveOnModule('onCOSStore') Module['onCOSStore']?.(cosHash.value); +#endif } catch (storeErr) { err(`COS store failed: ${storeErr}`); } diff --git a/test/test_other.py b/test/test_other.py index 9c6f9734209c6..db6f80c1b493b 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15394,10 +15394,10 @@ def test_cross_origin_storage_js_output(self): self.assertContained("'NotFoundError'", js) self.assertContained("'NotAllowedError'", js) - # Instrumentation callbacks invoked at each COS event. - self.assertContained("Module['onCOSCacheHit']", js) - self.assertContained("Module['onCOSCacheMiss']", js) - self.assertContained("Module['onCOSStore']", js) + # Callbacks are opt-in; must be absent from a default build. + self.assertNotContained("Module['onCOSCacheHit']", js) + self.assertNotContained("Module['onCOSCacheMiss']", js) + self.assertNotContained("Module['onCOSStore']", js) # The hash embedded in the JS must be a 64-char lowercase hex string … m = re.search(r"value:\s*'([0-9a-f]{64})'", js) @@ -15409,6 +15409,18 @@ def test_cross_origin_storage_js_output(self): self.assertEqual(embedded_hash, expected_hash, 'embedded wasm hash does not match actual .wasm SHA-256') + def test_cross_origin_storage_callbacks_opt_in(self): + """COS instrumentation callbacks are emitted only when opted in via INCOMING_MODULE_JS_API.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE', + '-sENVIRONMENT=web', + '-sINCOMING_MODULE_JS_API=onCOSCacheHit,onCOSCacheMiss,onCOSStore', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertContained("Module['onCOSCacheHit']", js) + self.assertContained("Module['onCOSCacheMiss']", js) + self.assertContained("Module['onCOSStore']", js) + def test_cross_origin_storage_disabled_by_default(self): """COS code must NOT appear when the flag is omitted (default off).""" self.run_process([EMCC, test_file('hello_world.cpp'), diff --git a/tools/cmdline.py b/tools/cmdline.py index b66249da8f46a..4b38e18ea0081 100644 --- a/tools/cmdline.py +++ b/tools/cmdline.py @@ -47,6 +47,9 @@ 'onRealloc', 'onFree', 'onSbrkGrow', + 'onCOSCacheHit', + 'onCOSCacheMiss', + 'onCOSStore', ] logger = logging.getLogger('args') From 9596b822d6853509869fff7fd595b9b9a78d9653 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 16:10:22 +0200 Subject: [PATCH 43/74] Add browser tests --- .../docs/compiling/CrossOriginStorage.rst | 26 + test/browser_common.py | 10 + test/cross_origin_storage/Makefile | 2 +- test/cross_origin_storage/README.md | 2 +- test/cross_origin_storage/index.html | 2 +- test/cross_origin_storage/index.js | 1759 +++++++++++++++++ test/cross_origin_storage/index.wasm | Bin 0 -> 994 bytes test/cross_origin_storage/main.cpp | 2 +- test/setup_cos_extension.py | 172 ++ test/test_browser.py | 63 + test/test_other.py | 4 +- tools/settings.py | 1 + 12 files changed, 2037 insertions(+), 6 deletions(-) create mode 100644 test/cross_origin_storage/index.js create mode 100755 test/cross_origin_storage/index.wasm create mode 100755 test/setup_cos_extension.py diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index e08f8cc4f9242..9a6dc4dfc7881 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -247,6 +247,9 @@ 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``). @@ -254,6 +257,29 @@ which injects a ``navigator.crossOriginStorage`` polyfill on every page. 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 ============================ diff --git a/test/browser_common.py b/test/browser_common.py index 97d0a55d5f2e1..522bc3cf42a23 100644 --- a/test/browser_common.py +++ b/test/browser_common.py @@ -66,6 +66,14 @@ EMTEST_HEADLESS = None EMTEST_CAPTURE_STDIO = int(os.getenv('EMTEST_CAPTURE_STDIO', '0')) +# Path to an unpacked Chrome extension implementing the Cross-Origin Storage +# polyfill. When set, the extension is loaded via --load-extension when +# launching a Chromium-based browser, enabling the COS browser test paths. +# Point this at a local clone of: +# https://github.com/web-ai-community/cross-origin-storage-extension +# (the directory that contains manifest.json). +EMTEST_COS_EXTENSION_PATH = os.getenv('EMTEST_COS_EXTENSION_PATH', '') + # Triggers the browser to restart after every given number of tests. # 0: Disabled (reuse the browser instance to run all tests. Default) # 1: Restart a fresh browser instance for every browser test. @@ -365,6 +373,8 @@ def configure_test_browser(): EMTEST_BROWSER += ' ' + ' '.join(config.default_flags) if EMTEST_HEADLESS == 1: EMTEST_BROWSER += f" {config.headless_flags}" + if EMTEST_COS_EXTENSION_PATH and is_chrome(): + EMTEST_BROWSER += f' --load-extension="{EMTEST_COS_EXTENSION_PATH}"' # Create a server and a web page. When a test runs, we tell the server about it, diff --git a/test/cross_origin_storage/Makefile b/test/cross_origin_storage/Makefile index 0c5e4216bb8bf..00d75aaee3a15 100644 --- a/test/cross_origin_storage/Makefile +++ b/test/cross_origin_storage/Makefile @@ -11,7 +11,7 @@ all: index.js index.js: main.cpp $(EMCC) main.cpp -o index.js \ - -sCROSS_ORIGIN_STORAGE=1 \ + -sCROSS_ORIGIN_STORAGE \ -sENVIRONMENT=web \ -sEXPORTED_RUNTIME_METHODS=ccall \ -sEXPORTED_FUNCTIONS=_greet \ diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md index c28f7f91c24af..5e8ecf1a24135 100644 --- a/test/cross_origin_storage/README.md +++ b/test/cross_origin_storage/README.md @@ -26,7 +26,7 @@ of the Wasm resource and the URL it was fetched from. ```bash emcc main.cpp -o index.js \ - -sCROSS_ORIGIN_STORAGE=1 \ + -sCROSS_ORIGIN_STORAGE \ -sENVIRONMENT=web \ -sEXPORTED_RUNTIME_METHODS=ccall \ -sEXPORTED_FUNCTIONS=_greet \ diff --git a/test/cross_origin_storage/index.html b/test/cross_origin_storage/index.html index ca7f592187b8f..0ca1b7989453e 100644 --- a/test/cross_origin_storage/index.html +++ b/test/cross_origin_storage/index.html @@ -81,7 +81,7 @@

Emscripten — Cross-Origin Storage example

diff --git a/test/cross_origin_storage/index.js b/test/cross_origin_storage/index.js new file mode 100644 index 0000000000000..890b272da8989 --- /dev/null +++ b/test/cross_origin_storage/index.js @@ -0,0 +1,1759 @@ +// include: shell.js +// include: minimum_runtime_check.js +(function() { + // "30.0.0" -> 300000 + function humanReadableVersionToPacked(str) { + str = str.split('-')[0]; // Remove any trailing part from e.g. "12.53.3-alpha" + var vers = str.split('.').slice(0, 3); + while(vers.length < 3) vers.push('00'); + vers = vers.map((n, i, arr) => n.padStart(2, '0')); + return vers.join(''); + } + // 300000 -> "30.0.0" + var packedVersionToHumanReadable = n => [n / 10000 | 0, (n / 100 | 0) % 100, n % 100].join('.'); + + var TARGET_NOT_SUPPORTED = 2147483647; + + // Note: We use a typeof check here instead of optional chaining using + // globalThis because older browsers might not have globalThis defined. + var currentNodeVersion = typeof process !== 'undefined' && process.versions?.node ? humanReadableVersionToPacked(process.versions.node) : TARGET_NOT_SUPPORTED; + if (currentNodeVersion < TARGET_NOT_SUPPORTED) { + throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); + } + if (currentNodeVersion < 2147483647) { + throw new Error(`This emscripten-generated code requires node v${ packedVersionToHumanReadable(2147483647) } (detected v${packedVersionToHumanReadable(currentNodeVersion)})`); + } + + var userAgent = typeof navigator !== 'undefined' && navigator.userAgent; + if (!userAgent) { + return; + } + + var currentSafariVersion = userAgent.includes("Safari/") && !userAgent.includes("Chrome/") && userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/) ? humanReadableVersionToPacked(userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentSafariVersion < 150000) { + throw new Error(`This emscripten-generated code requires Safari v${ packedVersionToHumanReadable(150000) } (detected v${currentSafariVersion})`); + } + + var currentFirefoxVersion = userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentFirefoxVersion < 79) { + throw new Error(`This emscripten-generated code requires Firefox v79 (detected v${currentFirefoxVersion})`); + } + + var currentChromeVersion = userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentChromeVersion < 85) { + throw new Error(`This emscripten-generated code requires Chrome v85 (detected v${currentChromeVersion})`); + } +})(); + +// end include: minimum_runtime_check.js +// The Module object: Our interface to the outside world. We import +// and export values on it. There are various ways Module can be used: +// 1. Not defined. We create it here +// 2. A function parameter, function(moduleArg) => Promise +// 3. pre-run appended it, var Module = {}; ..generated code.. +// 4. External script tag defines var Module. +// We need to check if Module already exists (e.g. case 3 above). +// Substitution will be replaced with actual code on later stage of the build, +// this way Closure Compiler will not mangle it (e.g. case 4. above). +// Note that if you want to run closure, and also to use Module +// after the generated code, you will need to define var Module = {}; +// before the code. Then that object will be used in the code, and you +// can continue to use Module afterwards as well. +var Module = typeof Module != 'undefined' ? Module : {}; + +// Determine the runtime environment we are in. You can customize this by +// setting the ENVIRONMENT setting at compile time (see settings.js). + +// Attempt to auto-detect the environment +var ENVIRONMENT_IS_WEB = !!globalThis.window; +var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; +// N.b. Electron.js environment is simultaneously a NODE-environment, but +// also a web environment. +var ENVIRONMENT_IS_NODE = globalThis.process?.versions?.node && globalThis.process?.type != 'renderer'; +var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; + +// --pre-jses are emitted after the Module integration code, so that they can +// refer to Module (if they choose; they can also define Module) + + +var programArgs = []; +var thisProgram = './this.program'; +var quit_ = (status, toThrow) => { + throw toThrow; +}; + +// In MODULARIZE mode _scriptName needs to be captured already at the very top of the page immediately when the page is parsed, so it is generated there +// before the page load. In non-MODULARIZE modes generate it here. +var _scriptName = globalThis.document?.currentScript?.src; + +// `/` should be present at the end if `scriptDirectory` is not empty +var scriptDirectory = ''; +function locateFile(path) { + if (Module['locateFile']) { + return Module['locateFile'](path, scriptDirectory); + } + return scriptDirectory + path; +} + +// Hooks that are implemented differently in different runtime environments. +var readAsync, readBinary; + +if (ENVIRONMENT_IS_SHELL) { + +} else + +// Note that this includes Node.js workers when relevant (pthreads is enabled). +// Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and +// ENVIRONMENT_IS_NODE. +if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { + try { + scriptDirectory = new URL('.', _scriptName).href; // includes trailing slash + } catch { + // Must be a `blob:` or `data:` URL (e.g. `blob:http://site.com/etc/etc`), we cannot + // infer anything from them. + } + + if (!(globalThis.window || globalThis.WorkerGlobalScope)) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); + + { +// include: web_or_worker_shell_read.js +readAsync = async (url) => { + assert(!isFileURI(url), "readAsync does not work with file:// URLs"); + var response = await fetch(url, { credentials: 'same-origin' }); + if (response.ok) { + return response.arrayBuffer(); + } + throw new Error(response.status + ' : ' + response.url); + }; +// end include: web_or_worker_shell_read.js + } +} else +{ + throw new Error('environment detection error'); +} + +var out = console.log.bind(console); +var err = console.error.bind(console); + +var IDBFS = 'IDBFS is no longer included by default; build with -lidbfs.js'; +var PROXYFS = 'PROXYFS is no longer included by default; build with -lproxyfs.js'; +var WORKERFS = 'WORKERFS is no longer included by default; build with -lworkerfs.js'; +var FETCHFS = 'FETCHFS is no longer included by default; build with -lfetchfs.js'; +var ICASEFS = 'ICASEFS is no longer included by default; build with -licasefs.js'; +var JSFILEFS = 'JSFILEFS is no longer included by default; build with -ljsfilefs.js'; +var OPFS = 'OPFS is no longer included by default; build with -lopfs.js'; + +var NODEFS = 'NODEFS is no longer included by default; build with -lnodefs.js'; + +// perform assertions in shell.js after we set up out() and err(), as otherwise +// if an assertion fails it cannot print the message + +assert(!ENVIRONMENT_IS_WORKER, 'worker environment detected but not enabled at build time (add `worker` to `-sENVIRONMENT` to enable)'); + +assert(!ENVIRONMENT_IS_NODE, 'node environment detected but not enabled at build time (add `node` to `-sENVIRONMENT` to enable)'); + +assert(!ENVIRONMENT_IS_SHELL, 'shell environment detected but not enabled at build time (add `shell` to `-sENVIRONMENT` to enable)'); + +// end include: shell.js + +// include: preamble.js +// === Preamble library stuff === + +// Documentation for the public APIs defined in this file must be updated in: +// site/source/docs/api_reference/preamble.js.rst +// A prebuilt local version of the documentation is available at: +// site/build/text/docs/api_reference/preamble.js.txt +// You can also build docs locally as HTML or other formats in site/ +// An online HTML version (which may be of a different version of Emscripten) +// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html + +var wasmBinary; + +if (!globalThis.WebAssembly) { + err('no native wasm support detected'); +} + +// Wasm globals + +//======================================== +// Runtime essentials +//======================================== + +// whether we are quitting the application. no code should run after this. +// set in exit() and abort() +var ABORT = false; + +// set by exit() and abort(). Passed to 'onExit' handler. +// NOTE: This is also used as the process return code in shell environments +// but only when noExitRuntime is false. +var EXITSTATUS; + +// In STRICT mode, we only define assert() when ASSERTIONS is set. i.e. we +// don't define it at all in release modes. This matches the behaviour of +// MINIMAL_RUNTIME. +// TODO(sbc): Make this the default even without STRICT enabled. +/** @type {function(*, string=)} */ +function assert(condition, text) { + if (!condition) { + abort('Assertion failed' + (text ? ': ' + text : '')); + } +} + +// We used to include malloc/free by default in the past. Show a helpful error in +// builds with assertions. +function _malloc() { + abort('malloc() called but not included in the build - add `_malloc` to EXPORTED_FUNCTIONS'); +} +function _free() { + // Show a helpful error since we used to include free by default in the past. + abort('free() called but not included in the build - add `_free` to EXPORTED_FUNCTIONS'); +} + +/** + * Indicates whether filename is delivered via file protocol (as opposed to http/https) + * @noinline + */ +var isFileURI = (filename) => filename.startsWith('file://'); + +// include: runtime_common.js +// include: runtime_stack_check.js +// Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode. +function writeStackCookie() { + var max = _emscripten_stack_get_end(); + assert((max & 3) == 0); + // If the stack ends at address zero we write our cookies 4 bytes into the + // stack. This prevents interference with SAFE_HEAP and ASAN which also + // monitor writes to address zero. + if (max == 0) { + max += 4; + } + // The stack grow downwards towards _emscripten_stack_get_end. + // We write cookies to the final two words in the stack and detect if they are + // ever overwritten. + HEAPU32[((max)>>2)] = 0x02135467; + HEAPU32[(((max)+(4))>>2)] = 0x89BACDFE; + // Also test the global address 0 for integrity. + HEAPU32[((0)>>2)] = 1668509029; +} + +function checkStackCookie() { + if (ABORT) return; + var max = _emscripten_stack_get_end(); + // See writeStackCookie(). + if (max == 0) { + max += 4; + } + var cookie1 = HEAPU32[((max)>>2)]; + var cookie2 = HEAPU32[(((max)+(4))>>2)]; + if (cookie1 != 0x02135467 || cookie2 != 0x89BACDFE) { + abort(`Stack overflow! Stack cookie has been overwritten at ${ptrToString(max)}, expected hex dwords 0x89BACDFE and 0x2135467, but received ${ptrToString(cookie2)} ${ptrToString(cookie1)}`); + } + // Also test the global address 0 for integrity. + if (HEAPU32[((0)>>2)] != 0x63736d65 /* 'emsc' */) { + abort('Runtime error: The application has corrupted its heap memory area (address zero)!'); + } +} +// end include: runtime_stack_check.js +// include: runtime_exceptions.js +// Base Emscripten EH error class +class EmscriptenEH {} + +class EmscriptenSjLj extends EmscriptenEH {} + +// end include: runtime_exceptions.js +// include: runtime_debug.js +var runtimeDebug = true; // Switch to false at runtime to disable logging at the right times + +// Used by XXXXX_DEBUG settings to output debug messages. +function dbg(...args) { + if (!runtimeDebug && typeof runtimeDebug != 'undefined') return; + // TODO(sbc): Make this configurable somehow. Its not always convenient for + // logging to show up as warnings. + console.warn(...args); +} + +// Endianness check +(() => { + var h16 = new Int16Array(1); + var h8 = new Int8Array(h16.buffer); + h16[0] = 0x6373; + if (h8[0] !== 0x73 || h8[1] !== 0x63) abort('Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)'); +})(); + +function consumedModuleProp(prop) { + if (!Object.getOwnPropertyDescriptor(Module, prop)) { + Object.defineProperty(Module, prop, { + configurable: true, + set() { + abort(`Attempt to set \`Module.${prop}\` after it has already been processed. This can happen, for example, when code is injected via '--post-js' rather than '--pre-js'`); + + } + }); + } +} + +function makeInvalidEarlyAccess(name) { + return () => assert(false, `call to '${name}' via reference taken before Wasm module initialization`); + +} + +function ignoredModuleProp(prop) { + if (Object.getOwnPropertyDescriptor(Module, prop)) { + abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`); + } +} + +// forcing the filesystem exports a few things by default +function isExportedByForceFilesystem(name) { + return name === 'FS_createPath' || + name === 'FS_createDataFile' || + name === 'FS_createPreloadedFile' || + name === 'FS_preloadFile' || + name === 'FS_unlink' || + name === 'addRunDependency' || + // The old FS has some functionality that WasmFS lacks. + name === 'FS_createLazyFile' || + name === 'FS_createDevice' || + name === 'removeRunDependency'; +} + +/** + * Intercept access to a symbols in the global symbol. This enables us to give + * informative warnings/errors when folks attempt to use symbols they did not + * include in their build, or no symbols that no longer exist. + * + * We don't define this in MODULARIZE mode since in that mode emscripten symbols + * are never placed in the global scope. + */ +function hookGlobalSymbolAccess(sym, func) { + if (!Object.getOwnPropertyDescriptor(globalThis, sym)) { + Object.defineProperty(globalThis, sym, { + configurable: true, + get() { + func(); + return undefined; + } + }); + } +} + +function missingGlobal(sym, msg) { + hookGlobalSymbolAccess(sym, () => { + warnOnce(`\`${sym}\` is no longer defined by emscripten. ${msg}`); + }); +} + +missingGlobal('buffer', 'Please use HEAP8.buffer or wasmMemory.buffer'); +missingGlobal('asm', 'Please use wasmExports instead'); + +function missingLibrarySymbol(sym) { + hookGlobalSymbolAccess(sym, () => { + // Can't `abort()` here because it would break code that does runtime + // checks. e.g. `if (typeof SDL === 'undefined')`. + var msg = `\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`; + // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE requires the name as it appears in + // library.js, which means $name for a JS name with no prefix, or name + // for a JS name like _name. + var librarySymbol = sym; + if (!librarySymbol.startsWith('_')) { + librarySymbol = '$' + sym; + } + msg += ` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`; + if (isExportedByForceFilesystem(sym)) { + msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; + } + warnOnce(msg); + }); + + // Any symbol that is not included from the JS library is also (by definition) + // not exported on the Module object. + unexportedRuntimeSymbol(sym); +} + +function unexportedRuntimeSymbol(sym) { + if (!Object.getOwnPropertyDescriptor(Module, sym)) { + Object.defineProperty(Module, sym, { + configurable: true, + get() { + var msg = `'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`; + if (isExportedByForceFilesystem(sym)) { + msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; + } + abort(msg); + }, + }); + } +} + +// end include: runtime_debug.js +// Memory management + +var runtimeInitialized = false; + + + +function updateMemoryViews() { + var b = wasmMemory.buffer; + HEAP8 = new Int8Array(b); + HEAP16 = new Int16Array(b); + HEAPU8 = new Uint8Array(b); + HEAPU16 = new Uint16Array(b); + HEAP32 = new Int32Array(b); + HEAPU32 = new Uint32Array(b); + HEAPF32 = new Float32Array(b); + HEAPF64 = new Float64Array(b); + HEAP64 = new BigInt64Array(b); + HEAPU64 = new BigUint64Array(b); +} + +// include: memoryprofiler.js +// end include: memoryprofiler.js +// end include: runtime_common.js +assert(globalThis.Int32Array && globalThis.Float64Array && Int32Array.prototype.subarray && Int32Array.prototype.set, + 'JS engine does not provide full typed array support'); + +function preRun() { + if (Module['preRun']) { + if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; + while (Module['preRun'].length) { + addOnPreRun(Module['preRun'].shift()); + } + } + consumedModuleProp('preRun'); + // Begin ATPRERUNS hooks + callRuntimeCallbacks(onPreRuns); + // End ATPRERUNS hooks +} + +function initRuntime() { + assert(!runtimeInitialized); + runtimeInitialized = true; + + checkStackCookie(); + + // No ATINITS hooks + + wasmExports['__wasm_call_ctors'](); + + // No ATPOSTCTORS hooks +} + +function postRun() { + checkStackCookie(); + // PThreads reuse the runtime from the main thread. + + if (Module['postRun']) { + if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; + while (Module['postRun'].length) { + addOnPostRun(Module['postRun'].shift()); + } + } + consumedModuleProp('postRun'); + + // Begin ATPOSTRUNS hooks + callRuntimeCallbacks(onPostRuns); + // End ATPOSTRUNS hooks +} + +/** + * @param {string|number=} what + */ +function abort(what) { + Module['onAbort']?.(what); + + what = `Aborted(${what})`; + // TODO(sbc): Should we remove printing and leave it up to whoever + // catches the exception? + err(what); + + ABORT = true; + + // Use a wasm runtime error, because a JS error might be seen as a foreign + // exception, which means we'd run destructors on it. We need the error to + // simply make the program stop. + // FIXME This approach does not work in Wasm EH because it currently does not assume + // all RuntimeErrors are from traps; it decides whether a RuntimeError is from + // a trap or not based on a hidden field within the object. So at the moment + // we don't have a way of throwing a wasm trap from JS. TODO Make a JS API that + // allows this in the wasm spec. + + // Suppress closure compiler warning here. Closure compiler's builtin extern + // definition for WebAssembly.RuntimeError claims it takes no arguments even + // though it can. + // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure gets fixed. + /** @suppress {checkTypes} */ + var e = new WebAssembly.RuntimeError(what); + + // Throw the error whether or not MODULARIZE is set because abort is used + // in code paths apart from instantiation where an exception is expected + // to be thrown when abort is called. + throw e; +} + +// show errors on likely calls to FS when it was not included +function fsMissing() { + abort('Filesystem support (FS) was not included. The problem is that you are using files from JS, but files were not used from C/C++, so filesystem support was not auto-included. You can force-include filesystem support with -sFORCE_FILESYSTEM'); +} +var FS = { + init: fsMissing, + createDataFile: fsMissing, + createPreloadedFile: fsMissing, + createLazyFile: fsMissing, + open: fsMissing, + mkdev: fsMissing, + registerDevice: fsMissing, + analyzePath: fsMissing, + ErrnoError: fsMissing, +}; + + +function createExportWrapper(name, nargs) { + return (...args) => { + assert(runtimeInitialized, `native function \`${name}\` called before runtime initialization`); + var f = wasmExports[name]; + assert(f, `exported native function \`${name}\` not found`); + // Only assert for too many arguments. Too few can be valid since the missing arguments will be zero filled. + assert(args.length <= nargs, `native function \`${name}\` called with ${args.length} args but expects ${nargs}`); + return f(...args); + }; +} + +var wasmBinaryFile; + +function findWasmBinary() { + return locateFile('index.wasm'); +} + +function getBinarySync(file) { + if (file == wasmBinaryFile && wasmBinary) { + return new Uint8Array(wasmBinary); + } + if (readBinary) { + return readBinary(file); + } + // Throwing a plain string here, even though it not normally advisable since + // this gets turning into an `abort` in instantiateArrayBuffer. + throw 'both async and sync fetching of the wasm failed'; +} + +async function getWasmBinary(binaryFile) { + // If we don't have the binary yet, load it asynchronously using readAsync. + if (!wasmBinary) { + // Fetch the binary using readAsync + try { + var response = await readAsync(binaryFile); + return new Uint8Array(response); + } catch { + // Fall back to getBinarySync below; + } + } + + // Otherwise, getBinarySync should be able to get it synchronously + return getBinarySync(binaryFile); +} + +async function instantiateArrayBuffer(binaryFile, imports) { + try { + var binary = await getWasmBinary(binaryFile); + var instance = await WebAssembly.instantiate(binary, imports); + return instance; + } catch (reason) { + err(`failed to asynchronously prepare wasm: ${reason}`); + + // Warn on some common problems. + if (isFileURI(binaryFile)) { + err(`warning: Loading from a file URI (${binaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`); + } + abort(reason); + } +} + +async function instantiateAsync(binary, binaryFile, imports) { + // Cross-Origin Storage (COS) progressive enhancement. + // https://github.com/WICG/cross-origin-storage + // + // COS is only beneficial when this .wasm binary is byte-identical across + // many origins — i.e. a popular library loaded by many independent sites. + // Application-specific Wasm gains nothing from COS that the normal HTTP + // cache does not already provide. + // + // The SHA-256 hash of the final .wasm binary is computed at link time and + // embedded here as a build-time constant. At runtime we feature-detect the + // browser COS API via `'crossOriginStorage' in navigator`, then call + // navigator.crossOriginStorage.requestFileHandles() with the hash object + // required by the spec ({ algorithm: 'SHA-256', value: '' }). + // + // Cache-hit path: + // requestFileHandles() succeeds → getFile() → arrayBuffer() → instantiate + // + // Cache-miss path (NotFoundError): + // fetch() the wasm over the network → instantiate → store in COS. + // The origins field is controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS + // (default: '*', globally available). The store is fire-and-forget so + // it never delays startup. + // + // Visibility and security: the spec allows widening visibility (e.g. a + // restricted entry promoted to globally available) but never narrowing it. + // Because storing always requires writing the actual bytes, no third party + // can probe the cache to discover whether a restricted entry was previously + // stored by another origin — a cache hit is only possible after an explicit + // write that provided the content. + // + // Any other error (NotAllowedError, network failure, …) falls through to the + // standard Emscripten streaming path so the page always loads. + var cosHash = { algorithm: 'SHA-256', value: 'a617cb51176b7e0c729fb776ff688310ecbe035a0da79fcac66b1bf98028e793' }; + if (cosHash.value && 'crossOriginStorage' in navigator) { + try { + var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); + // Cache hit — read the Blob and instantiate from its ArrayBuffer. + var cosFile = await cosHandles[0].getFile(); + var cosBytes = await cosFile.arrayBuffer(); + // Optional instrumentation callback: Module['onCOSCacheHit'](hash) + // Called when the Wasm binary is served from the cross-origin cache. + if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](cosHash.value); + return WebAssembly.instantiate(cosBytes, imports); + } catch (cosErr) { + if (cosErr.name === 'NotFoundError') { + // Cache miss — fetch normally, then store in COS for future consumers. + try { + var networkResponse = await fetch(binaryFile, { credentials: 'same-origin' }); + var wasmBytes = await networkResponse.arrayBuffer(); + // Optional instrumentation callback: Module['onCOSCacheMiss'](hash, url) + // Called when the Wasm binary is not in COS and is fetched over the network. + if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](cosHash.value, binaryFile); + // Fire-and-forget store; never block instantiation on the write. + (async () => { + try { + var writeHandles = await navigator.crossOriginStorage.requestFileHandles( + [cosHash], + { create: true, origins: '*' }, + ); + var writable = await writeHandles[0].createWritable(); + await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); + await writable.close(); + // Optional instrumentation callback: Module['onCOSStore'](hash) + // Called after the Wasm binary has been successfully written to COS. + if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](cosHash.value); + } catch (storeErr) { + err(`COS store failed: ${storeErr}`); + } + })(); + return WebAssembly.instantiate(wasmBytes, imports); + } catch (fetchErr) { + // Network fetch failed; fall through to the standard path. + err(`COS fallback fetch failed: ${fetchErr}`); + } + } else if (cosErr.name === 'NotAllowedError') { + err(`COS: permission denied.`); + } else { + err(`Cross-Origin Storage lookup failed: ${cosErr}`); + } + } + } + if (!binary + ) { + try { + var response = fetch(binaryFile, { credentials: 'same-origin' }); + var instantiationResult = await WebAssembly.instantiateStreaming(response, imports); + return instantiationResult; + } catch (reason) { + // We expect the most common failure cause to be a bad MIME type for the binary, + // in which case falling back to ArrayBuffer instantiation should work. + err(`wasm streaming compile failed: ${reason}`); + err('falling back to ArrayBuffer instantiation'); + // fall back of instantiateArrayBuffer below + }; + } + return instantiateArrayBuffer(binaryFile, imports); +} + +function getWasmImports() { + // prepare imports + var imports = { + 'env': wasmImports, + 'wasi_snapshot_preview1': wasmImports, + }; + return imports; +} + +// Create the wasm instance. +// Receives the wasm imports, returns the exports. +async function createWasm() { + // Load the wasm module and create an instance of using native support in the JS engine. + // handle a generated wasm instance, receiving its exports and + // performing other necessary setup + /** @param {WebAssembly.Module=} module*/ + function receiveInstance(instance, module) { + wasmExports = instance.exports; + + assignWasmExports(wasmExports); + + updateMemoryViews(); + + removeRunDependency('wasm-instantiate'); + return wasmExports; + } + addRunDependency('wasm-instantiate'); + + // Prefer streaming instantiation if available. + // Async compilation can be confusing when an error on the page overwrites Module + // (for example, if the order of elements is wrong, and the one defining Module is + // later), so we save Module and check it later. + var trueModule = Module; + function receiveInstantiationResult(result) { + // 'result' is a ResultObject object which has both the module and instance. + // receiveInstance() will swap in the exports (to Module.asm) so they can be called + assert(Module === trueModule, 'the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?'); + trueModule = null; + // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. + // When the regression is fixed, can restore the above PTHREADS-enabled path. + return receiveInstance(result['instance']); + } + + var info = getWasmImports(); + + // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback + // to manually instantiate the Wasm module themselves. This allows pages to + // run the instantiation parallel to any other async startup actions they are + // performing. + // Also pthreads and wasm workers initialize the wasm instance through this + // path. + if (Module['instantiateWasm']) { + return new Promise((resolve, reject) => { + try { + Module['instantiateWasm'](info, (inst, mod) => { + resolve(receiveInstance(inst, mod)); + }); + } catch(e) { + err(`Module.instantiateWasm callback failed with error: ${e}`); + reject(e); + } + }); + } + + wasmBinaryFile ??= findWasmBinary(); + var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info); + var exports = receiveInstantiationResult(result); + return exports; +} + +// end include: preamble.js + +// Begin JS library code + + + class ExitStatus { + name = 'ExitStatus'; + constructor(status) { + this.message = `Program terminated with exit(${status})`; + this.status = status; + } + } + + /** @type {!Int16Array} */ + var HEAP16; + + /** @type {!Int32Array} */ + var HEAP32; + + /** not-@type {!BigInt64Array} */ + var HEAP64; + + /** @type {!Int8Array} */ + var HEAP8; + + /** @type {!Float32Array} */ + var HEAPF32; + + /** @type {!Float64Array} */ + var HEAPF64; + + /** @type {!Uint16Array} */ + var HEAPU16; + + /** @type {!Uint32Array} */ + var HEAPU32; + + /** not-@type {!BigUint64Array} */ + var HEAPU64; + + /** @type {!Uint8Array} */ + var HEAPU8; + + var callRuntimeCallbacks = (callbacks) => { + while (callbacks.length > 0) { + // Pass the module as the first argument. + callbacks.shift()(Module); + } + }; + var onPostRuns = []; + var addOnPostRun = (cb) => onPostRuns.push(cb); + + var onPreRuns = []; + var addOnPreRun = (cb) => onPreRuns.push(cb); + + var runDependencies = 0; + + + var dependenciesFulfilled = null; + + var runDependencyTracking = { + }; + + var runDependencyWatcher = null; + var removeRunDependency = (id) => { + runDependencies--; + + Module['monitorRunDependencies']?.(runDependencies); + + assert(id, 'removeRunDependency requires an ID'); + assert(runDependencyTracking[id]); + delete runDependencyTracking[id]; + if (runDependencies == 0) { + if (runDependencyWatcher !== null) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + } + if (dependenciesFulfilled) { + var callback = dependenciesFulfilled; + dependenciesFulfilled = null; + callback(); // can add another dependenciesFulfilled + } + } + }; + + + var addRunDependency = (id) => { + runDependencies++; + + Module['monitorRunDependencies']?.(runDependencies); + + assert(id, 'addRunDependency requires an ID') + assert(!runDependencyTracking[id]); + runDependencyTracking[id] = 1; + if (runDependencyWatcher === null && globalThis.setInterval) { + // Check for missing dependencies every few seconds + runDependencyWatcher = setInterval(() => { + if (ABORT) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + return; + } + var shown = false; + for (var dep in runDependencyTracking) { + if (!shown) { + shown = true; + err('still waiting on run dependencies:'); + } + err(`dependency: ${dep}`); + } + if (shown) { + err('(end of list)'); + } + }, 10000); + } + }; + + + + /** + * @param {number} ptr + * @param {string} type + */ + function getValue(ptr, type = 'i8') { + if (type.endsWith('*')) type = '*'; + switch (type) { + case 'i1': return HEAP8[ptr]; + case 'i8': return HEAP8[ptr]; + case 'i16': return HEAP16[((ptr)>>1)]; + case 'i32': return HEAP32[((ptr)>>2)]; + case 'i64': return HEAP64[((ptr)>>3)]; + case 'float': return HEAPF32[((ptr)>>2)]; + case 'double': return HEAPF64[((ptr)>>3)]; + case '*': return HEAPU32[((ptr)>>2)]; + default: abort(`invalid type for getValue: ${type}`); + } + } + + var noExitRuntime = true; + + function ptrToString(ptr) { + assert(typeof ptr === 'number', `ptrToString expects a number, got ${typeof ptr}`); + // Convert to 32-bit unsigned value + ptr >>>= 0; + return '0x' + ptr.toString(16).padStart(8, '0'); + } + + + + /** + * @param {number} ptr + * @param {number} value + * @param {string} type + */ + function setValue(ptr, value, type = 'i8') { + if (type.endsWith('*')) type = '*'; + switch (type) { + case 'i1': HEAP8[ptr] = value; break; + case 'i8': HEAP8[ptr] = value; break; + case 'i16': HEAP16[((ptr)>>1)] = value; break; + case 'i32': HEAP32[((ptr)>>2)] = value; break; + case 'i64': HEAP64[((ptr)>>3)] = BigInt(value); break; + case 'float': HEAPF32[((ptr)>>2)] = value; break; + case 'double': HEAPF64[((ptr)>>3)] = value; break; + case '*': HEAPU32[((ptr)>>2)] = value; break; + default: abort(`invalid type for setValue: ${type}`); + } + } + + var stackRestore = (val) => __emscripten_stack_restore(val); + + var stackSave = () => _emscripten_stack_get_current(); + + var warnOnce = (text) => { + warnOnce.shown ||= {}; + if (!warnOnce.shown[text]) { + warnOnce.shown[text] = 1; + err(text); + } + }; + + + + var getCFunc = (ident) => { + var func = Module['_' + ident]; // closure exported function + assert(func, `Cannot call unknown function ${ident}, make sure it is exported`); + return func; + }; + + var writeArrayToMemory = (array, buffer) => { + assert(array.length >= 0, 'writeArrayToMemory array must have a length (should be an array or typed array)') + HEAP8.set(array, buffer); + }; + + var lengthBytesUTF8 = (str) => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code + // unit, not a Unicode code point of the character! So decode + // UTF16->UTF32->UTF8. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + var c = str.charCodeAt(i); // possibly a lead surrogate + if (c <= 0x7F) { + len++; + } else if (c <= 0x7FF) { + len += 2; + } else if (c >= 0xD800 && c <= 0xDFFF) { + len += 4; ++i; + } else { + len += 3; + } + } + return len; + }; + + var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { + assert(typeof str === 'string', `stringToUTF8Array expects a string (got ${typeof str})`); + // Parameter maxBytesToWrite is not optional. Negative values, 0, null, + // undefined and false each don't write out any bytes. + if (!(maxBytesToWrite > 0)) + return 0; + + var startIdx = outIdx; + var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. + for (var i = 0; i < str.length; ++i) { + // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description + // and https://www.ietf.org/rfc/rfc2279.txt + // and https://tools.ietf.org/html/rfc3629 + var u = str.codePointAt(i); + if (u <= 0x7F) { + if (outIdx >= endIdx) break; + heap[outIdx++] = u; + } else if (u <= 0x7FF) { + if (outIdx + 1 >= endIdx) break; + heap[outIdx++] = 0xC0 | (u >> 6); + heap[outIdx++] = 0x80 | (u & 63); + } else if (u <= 0xFFFF) { + if (outIdx + 2 >= endIdx) break; + heap[outIdx++] = 0xE0 | (u >> 12); + heap[outIdx++] = 0x80 | ((u >> 6) & 63); + heap[outIdx++] = 0x80 | (u & 63); + } else { + if (outIdx + 3 >= endIdx) break; + if (u > 0x10FFFF) warnOnce(`Invalid Unicode code point ${ptrToString(u)} encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).`); + heap[outIdx++] = 0xF0 | (u >> 18); + heap[outIdx++] = 0x80 | ((u >> 12) & 63); + heap[outIdx++] = 0x80 | ((u >> 6) & 63); + heap[outIdx++] = 0x80 | (u & 63); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + i++; + } + } + // Null-terminate the pointer to the buffer. + heap[outIdx] = 0; + return outIdx - startIdx; + }; + var stringToUTF8 = (str, outPtr, maxBytesToWrite) => { + assert(typeof maxBytesToWrite == 'number', 'stringToUTF8 requires a third parameter that specifies the length of the output buffer'); + return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); + }; + + var stackAlloc = (sz) => __emscripten_stack_alloc(sz); + var stringToUTF8OnStack = (str) => { + var size = lengthBytesUTF8(str) + 1; + var ret = stackAlloc(size); + stringToUTF8(str, ret, size); + return ret; + }; + + + + + var UTF8Decoder = globalThis.TextDecoder && new TextDecoder(); + + var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { + var maxIdx = idx + maxBytesToRead; + if (ignoreNul) return maxIdx; + // TextDecoder needs to know the byte length in advance, it doesn't stop on + // null terminator by itself. + // As a tiny code save trick, compare idx against maxIdx using a negation, + // so that maxBytesToRead=undefined/NaN means Infinity. + while (heapOrArray[idx] && !(idx >= maxIdx)) ++idx; + return idx; + }; + + + /** + * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given + * array that contains uint8 values, returns a copy of that string as a + * Javascript String object. + * heapOrArray is either a regular array, or a JavaScript typed array view. + * @param {number=} idx + * @param {number=} maxBytesToRead + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ + var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead, ignoreNul) => { + + var endPtr = findStringEnd(heapOrArray, idx, maxBytesToRead, ignoreNul); + + // When using conditional TextDecoder, skip it for short strings as the overhead of the native call is not worth it. + if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) { + return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr)); + } + var str = ''; + while (idx < endPtr) { + // For UTF8 byte structure, see: + // http://en.wikipedia.org/wiki/UTF-8#Description + // https://www.ietf.org/rfc/rfc2279.txt + // https://tools.ietf.org/html/rfc3629 + var u0 = heapOrArray[idx++]; + if (!(u0 & 0x80)) { str += String.fromCharCode(u0); continue; } + var u1 = heapOrArray[idx++] & 63; + if ((u0 & 0xE0) == 0xC0) { str += String.fromCharCode(((u0 & 31) << 6) | u1); continue; } + var u2 = heapOrArray[idx++] & 63; + if ((u0 & 0xF0) == 0xE0) { + u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; + } else { + if ((u0 & 0xF8) != 0xF0) warnOnce(`Invalid UTF-8 leading byte ${ptrToString(u0)} encountered when deserializing a UTF-8 string in wasm memory to a JS string!`); + u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heapOrArray[idx++] & 63); + } + + if (u0 < 0x10000) { + str += String.fromCharCode(u0); + } else { + var ch = u0 - 0x10000; + str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); + } + } + return str; + }; + + /** + * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the + * emscripten HEAP, returns a copy of that string as a Javascript String object. + * + * @param {number} ptr + * @param {number=} maxBytesToRead - An optional length that specifies the + * maximum number of bytes to read. You can omit this parameter to scan the + * string until the first 0 byte. If maxBytesToRead is passed, and the string + * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the + * string will cut short at that byte index. + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ + var UTF8ToString = (ptr, maxBytesToRead, ignoreNul) => { + assert(typeof ptr == 'number', `UTF8ToString expects a number (got ${typeof ptr})`); + return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead, ignoreNul) : ''; + }; + + /** + * @param {string|null=} returnType + * @param {Array=} argTypes + * @param {Array=} args + * @param {Object=} opts + */ + var ccall = (ident, returnType, argTypes, args, opts) => { + // For fast lookup of conversion functions + var toC = { + 'string': (str) => { + var ret = 0; + if (str !== null && str !== undefined && str !== 0) { // null string + ret = stringToUTF8OnStack(str); + } + return ret; + }, + 'array': (arr) => { + var ret = stackAlloc(arr.length); + writeArrayToMemory(arr, ret); + return ret; + } + }; + + function convertReturnValue(ret) { + if (returnType === 'string') { + return UTF8ToString(ret); + } + if (returnType === 'boolean') return Boolean(ret); + return ret; + } + + var func = getCFunc(ident); + var cArgs = []; + var stack = 0; + assert(returnType !== 'array', 'return type should not be "array"'); + if (args) { + for (var i = 0; i < args.length; i++) { + var converter = toC[argTypes[i]]; + if (converter) { + if (stack === 0) stack = stackSave(); + cArgs[i] = converter(args[i]); + } else { + cArgs[i] = args[i]; + } + } + } + var ret = func(...cArgs); + function onDone(ret) { + if (stack !== 0) stackRestore(stack); + return convertReturnValue(ret); + } + + ret = onDone(ret); + return ret; + }; +// End JS library code + +// include: postlibrary.js +// This file is included after the automatically-generated JS library code +// but before the wasm module is created. + +{ + + // Begin ATMODULES hooks + if (Module['noExitRuntime']) noExitRuntime = Module['noExitRuntime']; +if (Module['print']) out = Module['print']; +if (Module['printErr']) err = Module['printErr']; +if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; + +Module['FS_createDataFile'] = FS.createDataFile; +Module['FS_createPreloadedFile'] = FS.createPreloadedFile; + + // End ATMODULES hooks + + checkIncomingModuleAPI(); + + if (Module['arguments']) programArgs = Module['arguments']; + if (Module['thisProgram']) thisProgram = Module['thisProgram']; + + // Assertions on removed incoming Module JS APIs. + assert(typeof Module['memoryInitializerPrefixURL'] == 'undefined', 'Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['pthreadMainPrefixURL'] == 'undefined', 'Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['cdInitializerPrefixURL'] == 'undefined', 'Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['filePackagePrefixURL'] == 'undefined', 'Module.filePackagePrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['read'] == 'undefined', 'Module.read option was removed'); + assert(typeof Module['readAsync'] == 'undefined', 'Module.readAsync option was removed (modify readAsync in JS)'); + assert(typeof Module['readBinary'] == 'undefined', 'Module.readBinary option was removed (modify readBinary in JS)'); + assert(typeof Module['setWindowTitle'] == 'undefined', 'Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)'); + assert(typeof Module['TOTAL_MEMORY'] == 'undefined', 'Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY'); + assert(typeof Module['ENVIRONMENT'] == 'undefined', 'Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)'); + assert(typeof Module['STACK_SIZE'] == 'undefined', 'STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time') + // If memory is defined in wasm, the user can't provide it, or set INITIAL_MEMORY + assert(typeof Module['wasmMemory'] == 'undefined', 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally'); + assert(typeof Module['INITIAL_MEMORY'] == 'undefined', 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically'); + + if (Module['preInit']) { + if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; + while (Module['preInit'].length > 0) { + Module['preInit'].shift()(); + } + } + consumedModuleProp('preInit'); +} + +// Begin runtime exports + Module['ccall'] = ccall; + var missingLibrarySymbols = [ + 'writeI53ToI64', + 'writeI53ToI64Clamped', + 'writeI53ToI64Signaling', + 'writeI53ToU64Clamped', + 'writeI53ToU64Signaling', + 'readI53FromI64', + 'readI53FromU64', + 'convertI32PairToI53', + 'convertI32PairToI53Checked', + 'convertU32PairToI53', + 'bigintToI53Checked', + 'getTempRet0', + 'setTempRet0', + 'createNamedFunction', + 'zeroMemory', + 'exitJS', + 'getHeapMax', + 'growMemory', + 'withStackSave', + 'strError', + 'inetPton4', + 'inetNtop4', + 'inetPton6', + 'inetNtop6', + 'readSockaddr', + 'writeSockaddr', + 'readEmAsmArgs', + 'jstoi_q', + 'getExecutableName', + 'autoResumeAudioContext', + 'getDynCaller', + 'dynCall', + 'handleException', + 'keepRuntimeAlive', + 'runtimeKeepalivePush', + 'runtimeKeepalivePop', + 'callUserCallback', + 'maybeExit', + 'asyncLoad', + 'asmjsMangle', + 'alignMemory', + 'mmapAlloc', + 'HandleAllocator', + 'getUniqueRunDependency', + 'addOnInit', + 'addOnPostCtor', + 'addOnPreMain', + 'addOnExit', + 'STACK_SIZE', + 'STACK_ALIGN', + 'POINTER_SIZE', + 'ASSERTIONS', + 'cwrap', + 'convertJsFunctionToWasm', + 'getEmptyTableSlot', + 'updateTableMap', + 'getFunctionAddress', + 'addFunction', + 'removeFunction', + 'intArrayFromString', + 'intArrayToString', + 'AsciiToString', + 'stringToAscii', + 'UTF16ToString', + 'stringToUTF16', + 'lengthBytesUTF16', + 'UTF32ToString', + 'stringToUTF32', + 'lengthBytesUTF32', + 'stringToNewUTF8', + 'registerKeyEventCallback', + 'maybeCStringToJsString', + 'findEventTarget', + 'getBoundingClientRect', + 'fillMouseEventData', + 'registerMouseEventCallback', + 'registerWheelEventCallback', + 'registerUiEventCallback', + 'registerFocusEventCallback', + 'fillDeviceOrientationEventData', + 'registerDeviceOrientationEventCallback', + 'fillDeviceMotionEventData', + 'registerDeviceMotionEventCallback', + 'screenOrientation', + 'fillOrientationChangeEventData', + 'registerOrientationChangeEventCallback', + 'fillFullscreenChangeEventData', + 'registerFullscreenChangeEventCallback', + 'JSEvents_requestFullscreen', + 'JSEvents_resizeCanvasForFullscreen', + 'registerRestoreOldStyle', + 'hideEverythingExceptGivenElement', + 'restoreHiddenElements', + 'setLetterbox', + 'softFullscreenResizeWebGLRenderTarget', + 'doRequestFullscreen', + 'fillPointerlockChangeEventData', + 'registerPointerlockChangeEventCallback', + 'registerPointerlockErrorEventCallback', + 'requestPointerLock', + 'fillVisibilityChangeEventData', + 'registerVisibilityChangeEventCallback', + 'registerTouchEventCallback', + 'fillGamepadEventData', + 'registerGamepadEventCallback', + 'registerBeforeUnloadEventCallback', + 'fillBatteryEventData', + 'registerBatteryEventCallback', + 'setCanvasElementSize', + 'getCanvasElementSize', + 'jsStackTrace', + 'getCallstack', + 'convertPCtoSourceLocation', + 'getEnvStrings', + 'checkWasiClock', + 'flush_NO_FILESYSTEM', + 'wasiRightsToMuslOFlags', + 'wasiOFlagsToMuslOFlags', + 'initRandomFill', + 'randomFill', + 'safeSetTimeout', + 'setImmediateWrapped', + 'safeRequestAnimationFrame', + 'clearImmediateWrapped', + 'registerPostMainLoop', + 'registerPreMainLoop', + 'getPromise', + 'makePromise', + 'addPromise', + 'idsToPromises', + 'makePromiseCallback', + 'ExceptionInfo', + 'findMatchingCatch', + 'incrementUncaughtExceptionCount', + 'decrementUncaughtExceptionCount', + 'Browser_asyncPrepareDataCounter', + 'isLeapYear', + 'ydayFromDate', + 'arraySum', + 'addDays', + 'getSocketFromFD', + 'getSocketAddress', + 'FS_createPreloadedFile', + 'FS_preloadFile', + 'FS_modeStringToFlags', + 'FS_getMode', + 'FS_fileDataToTypedArray', + 'FS_stdin_getChar', + 'FS_mkdirTree', + '_setNetworkCallback', + 'heapObjectForWebGLType', + 'toTypedArrayIndex', + 'webgl_enable_ANGLE_instanced_arrays', + 'webgl_enable_OES_vertex_array_object', + 'webgl_enable_WEBGL_draw_buffers', + 'webgl_enable_WEBGL_multi_draw', + 'webgl_enable_EXT_polygon_offset_clamp', + 'webgl_enable_EXT_clip_control', + 'webgl_enable_WEBGL_polygon_mode', + 'emscriptenWebGLGet', + 'computeUnpackAlignedImageSize', + 'colorChannelsInGlTextureFormat', + 'emscriptenWebGLGetTexPixelData', + 'emscriptenWebGLGetUniform', + 'webglGetProgramUniformLocation', + 'webglGetUniformLocation', + 'webglPrepareUniformLocationsBeforeFirstUse', + 'webglGetLeftBracePos', + 'emscriptenWebGLGetVertexAttrib', + '__glGetActiveAttribOrUniform', + 'writeGLArray', + 'registerWebGlEventCallback', + 'runAndAbortIfError', + 'ALLOC_NORMAL', + 'ALLOC_STACK', + 'allocate', + 'writeStringToMemory', + 'writeAsciiToMemory', + 'allocateUTF8', + 'allocateUTF8OnStack', + 'demangle', + 'stackTrace', + 'getNativeTypeSize', +]; +missingLibrarySymbols.forEach(missingLibrarySymbol) + + var unexportedSymbols = [ + 'run', + 'out', + 'err', + 'callMain', + 'abort', + 'wasmExports', + 'writeStackCookie', + 'checkStackCookie', + 'INT53_MAX', + 'INT53_MIN', + 'HEAP8', + 'HEAPU8', + 'HEAP16', + 'HEAPU16', + 'HEAP32', + 'HEAPU32', + 'HEAPF32', + 'HEAPF64', + 'HEAP64', + 'HEAPU64', + 'stackSave', + 'stackRestore', + 'stackAlloc', + 'ptrToString', + 'ENV', + 'ERRNO_CODES', + 'DNS', + 'Protocols', + 'Sockets', + 'timers', + 'warnOnce', + 'readEmAsmArgsArray', + 'wasmTable', + 'wasmMemory', + 'noExitRuntime', + 'addRunDependency', + 'removeRunDependency', + 'addOnPreRun', + 'addOnPostRun', + 'freeTableIndexes', + 'functionsInTableMap', + 'setValue', + 'getValue', + 'PATH', + 'PATH_FS', + 'UTF8Decoder', + 'UTF8ArrayToString', + 'UTF8ToString', + 'stringToUTF8Array', + 'stringToUTF8', + 'lengthBytesUTF8', + 'UTF16Decoder', + 'stringToUTF8OnStack', + 'writeArrayToMemory', + 'JSEvents', + 'specialHTMLTargets', + 'findCanvasEventTarget', + 'currentFullscreenStrategy', + 'restoreOldWindowedStyle', + 'UNWIND_CACHE', + 'ExitStatus', + 'emSetImmediate', + 'emClearImmediate_deps', + 'emClearImmediate', + 'promiseMap', + 'uncaughtExceptionCount', + 'exceptionCaught', + 'Browser', + 'requestFullscreen', + 'requestFullScreen', + 'setCanvasSize', + 'getUserMedia', + 'createContext', + 'getPreloadedImageData__data', + 'wget', + 'MONTH_DAYS_REGULAR', + 'MONTH_DAYS_LEAP', + 'MONTH_DAYS_REGULAR_CUMULATIVE', + 'MONTH_DAYS_LEAP_CUMULATIVE', + 'SYSCALLS', + 'preloadPlugins', + 'FS_stdin_getChar_buffer', + 'FS_unlink', + 'FS_createPath', + 'FS_createDevice', + 'FS_readFile', + 'FS', + 'FS_root', + 'FS_mounts', + 'FS_devices', + 'FS_streams', + 'FS_nextInode', + 'FS_nameTable', + 'FS_currentPath', + 'FS_initialized', + 'FS_ignorePermissions', + 'FS_filesystems', + 'FS_syncFSRequests', + 'FS_lookupPath', + 'FS_getPath', + 'FS_hashName', + 'FS_hashAddNode', + 'FS_hashRemoveNode', + 'FS_lookupNode', + 'FS_createNode', + 'FS_destroyNode', + 'FS_isRoot', + 'FS_isMountpoint', + 'FS_isFile', + 'FS_isDir', + 'FS_isLink', + 'FS_isChrdev', + 'FS_isBlkdev', + 'FS_isFIFO', + 'FS_isSocket', + 'FS_flagsToPermissionString', + 'FS_nodePermissions', + 'FS_mayLookup', + 'FS_mayCreate', + 'FS_mayDelete', + 'FS_mayOpen', + 'FS_checkOpExists', + 'FS_nextfd', + 'FS_getStreamChecked', + 'FS_getStream', + 'FS_createStream', + 'FS_closeStream', + 'FS_dupStream', + 'FS_doSetAttr', + 'FS_chrdev_stream_ops', + 'FS_major', + 'FS_minor', + 'FS_makedev', + 'FS_registerDevice', + 'FS_getDevice', + 'FS_getMounts', + 'FS_syncfs', + 'FS_mount', + 'FS_unmount', + 'FS_lookup', + 'FS_mknod', + 'FS_statfs', + 'FS_statfsStream', + 'FS_statfsNode', + 'FS_create', + 'FS_mkdir', + 'FS_mkdev', + 'FS_symlink', + 'FS_rename', + 'FS_rmdir', + 'FS_readdir', + 'FS_readlink', + 'FS_stat', + 'FS_fstat', + 'FS_lstat', + 'FS_doChmod', + 'FS_chmod', + 'FS_lchmod', + 'FS_fchmod', + 'FS_doChown', + 'FS_chown', + 'FS_lchown', + 'FS_fchown', + 'FS_doTruncate', + 'FS_truncate', + 'FS_ftruncate', + 'FS_utime', + 'FS_open', + 'FS_close', + 'FS_isClosed', + 'FS_llseek', + 'FS_read', + 'FS_write', + 'FS_mmap', + 'FS_msync', + 'FS_ioctl', + 'FS_writeFile', + 'FS_cwd', + 'FS_chdir', + 'FS_createDefaultDirectories', + 'FS_createDefaultDevices', + 'FS_createSpecialDirectories', + 'FS_createStandardStreams', + 'FS_staticInit', + 'FS_init', + 'FS_quit', + 'FS_findObject', + 'FS_analyzePath', + 'FS_createFile', + 'FS_createDataFile', + 'FS_forceLoadFile', + 'FS_createLazyFile', + 'MEMFS', + 'TTY', + 'PIPEFS', + 'SOCKFS', + 'tempFixedLengthArray', + 'miniTempWebGLFloatBuffers', + 'miniTempWebGLIntBuffers', + 'GL', + 'AL', + 'GLUT', + 'EGL', + 'GLEW', + 'IDBStore', + 'SDL', + 'SDL_gfx', + 'print', + 'printErr', + 'jstoi_s', +]; +unexportedSymbols.forEach(unexportedRuntimeSymbol); + + // End runtime exports + // Begin JS library exports + // End JS library exports + +// end include: postlibrary.js + +function checkIncomingModuleAPI() { + ignoredModuleProp('fetchSettings'); + ignoredModuleProp('logReadFiles'); + ignoredModuleProp('loadSplitModule'); + ignoredModuleProp('onMalloc'); + ignoredModuleProp('onRealloc'); + ignoredModuleProp('onFree'); + ignoredModuleProp('onSbrkGrow'); +} + +// Imports from the Wasm binary. +var _greet = Module['_greet'] = makeInvalidEarlyAccess('_greet'); +var _fflush = makeInvalidEarlyAccess('_fflush'); +var _emscripten_stack_init = makeInvalidEarlyAccess('_emscripten_stack_init'); +var _emscripten_stack_get_free = makeInvalidEarlyAccess('_emscripten_stack_get_free'); +var _emscripten_stack_get_base = makeInvalidEarlyAccess('_emscripten_stack_get_base'); +var _emscripten_stack_get_end = makeInvalidEarlyAccess('_emscripten_stack_get_end'); +var __emscripten_stack_restore = makeInvalidEarlyAccess('__emscripten_stack_restore'); +var __emscripten_stack_alloc = makeInvalidEarlyAccess('__emscripten_stack_alloc'); +var _emscripten_stack_get_current = makeInvalidEarlyAccess('_emscripten_stack_get_current'); +var memory = makeInvalidEarlyAccess('memory'); +var __indirect_function_table = makeInvalidEarlyAccess('__indirect_function_table'); +var wasmMemory = makeInvalidEarlyAccess('wasmMemory'); + +function assignWasmExports(wasmExports) { + assert(typeof wasmExports['greet'] != 'undefined', 'missing Wasm export: greet'); + assert(typeof wasmExports['fflush'] != 'undefined', 'missing Wasm export: fflush'); + assert(typeof wasmExports['emscripten_stack_init'] != 'undefined', 'missing Wasm export: emscripten_stack_init'); + assert(typeof wasmExports['emscripten_stack_get_free'] != 'undefined', 'missing Wasm export: emscripten_stack_get_free'); + assert(typeof wasmExports['emscripten_stack_get_base'] != 'undefined', 'missing Wasm export: emscripten_stack_get_base'); + assert(typeof wasmExports['emscripten_stack_get_end'] != 'undefined', 'missing Wasm export: emscripten_stack_get_end'); + assert(typeof wasmExports['_emscripten_stack_restore'] != 'undefined', 'missing Wasm export: _emscripten_stack_restore'); + assert(typeof wasmExports['_emscripten_stack_alloc'] != 'undefined', 'missing Wasm export: _emscripten_stack_alloc'); + assert(typeof wasmExports['emscripten_stack_get_current'] != 'undefined', 'missing Wasm export: emscripten_stack_get_current'); + assert(typeof wasmExports['memory'] != 'undefined', 'missing Wasm export: memory'); + assert(typeof wasmExports['__indirect_function_table'] != 'undefined', 'missing Wasm export: __indirect_function_table'); + _greet = Module['_greet'] = createExportWrapper('greet', 0); + _fflush = createExportWrapper('fflush', 1); + _emscripten_stack_init = wasmExports['emscripten_stack_init']; + _emscripten_stack_get_free = wasmExports['emscripten_stack_get_free']; + _emscripten_stack_get_base = wasmExports['emscripten_stack_get_base']; + _emscripten_stack_get_end = wasmExports['emscripten_stack_get_end']; + __emscripten_stack_restore = wasmExports['_emscripten_stack_restore']; + __emscripten_stack_alloc = wasmExports['_emscripten_stack_alloc']; + _emscripten_stack_get_current = wasmExports['emscripten_stack_get_current']; + memory = wasmMemory = wasmExports['memory']; + __indirect_function_table = wasmExports['__indirect_function_table']; +} + +var wasmImports = { + +}; + + +// include: postamble.js +// === Auto-generated postamble setup entry stuff === + +var calledRun; + +function stackCheckInit() { + // This is normally called automatically during __wasm_call_ctors but need to + // get these values before even running any of the ctors so we call it redundantly + // here. + _emscripten_stack_init(); + // TODO(sbc): Move writeStackCookie to native to to avoid this. + writeStackCookie(); +} + +function run() { + + if (runDependencies > 0) { + dependenciesFulfilled = run; + return; + } + + stackCheckInit(); + + preRun(); + + // a preRun added a dependency, run will be called later + if (runDependencies > 0) { + dependenciesFulfilled = run; + return; + } + + function doRun() { + // run may have just been called through dependencies being fulfilled just in this very frame, + // or while the async setStatus time below was happening + assert(!calledRun); + calledRun = true; + Module['calledRun'] = true; + + if (ABORT) return; + + initRuntime(); + + Module['onRuntimeInitialized']?.(); + consumedModuleProp('onRuntimeInitialized'); + + assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'); + + postRun(); + } + + if (Module['setStatus']) { + Module['setStatus']('Running...'); + setTimeout(() => { + setTimeout(() => Module['setStatus'](''), 1); + doRun(); + }, 1); + } else + { + doRun(); + } + checkStackCookie(); +} + +function checkUnflushedContent() { + // Compiler settings do not allow exiting the runtime, so flushing + // the streams is not possible. but in ASSERTIONS mode we check + // if there was something to flush, and if so tell the user they + // should request that the runtime be exitable. + // Normally we would not even include flush() at all, but in ASSERTIONS + // builds we do so just for this check, and here we see if there is any + // content to flush, that is, we check if there would have been + // something a non-ASSERTIONS build would have not seen. + // How we flush the streams depends on whether we are in SYSCALLS_REQUIRE_FILESYSTEM=0 + // mode (which has its own special function for this; otherwise, all + // the code is inside libc) + var oldOut = out; + var oldErr = err; + var has = false; + out = err = (x) => { + has = true; + } + try { // it doesn't matter if it fails + _fflush(0); + } catch(e) {} + out = oldOut; + err = oldErr; + if (has) { + warnOnce('stdio streams had content in them that was not flushed. you should set EXIT_RUNTIME to 1 (see the Emscripten FAQ), or make sure to emit a newline when you printf etc.'); + warnOnce('(this may also be due to not including full filesystem support - try building with -sFORCE_FILESYSTEM)'); + } +} + +var wasmExports; + +// With async instantation wasmExports is assigned asynchronously when the +// instance is received. +createWasm(); + +run(); + +// end include: postamble.js + diff --git a/test/cross_origin_storage/index.wasm b/test/cross_origin_storage/index.wasm new file mode 100755 index 0000000000000000000000000000000000000000..a7b17d387c1852563c9f194925da15d7892a7698 GIT binary patch literal 994 zcmaJ=&2H2%5T3EK+hyH0TPV<0m2kIvSSiXCI5tQi-hgA8O}$Il{MB&^y)>ah{2w`R z;J^d$1UwzK(@Kb~kP>_R%{SjnCS!=I3;_Ti^p9!NG}!3vqQQ$}fMd`nY(T%*PkmtE9+770ptC(4WuKx_Sx2yCSP%nUqT8VWpyYsbdla zWOoH$2rZ+Pf!^$!MU?<&=f*gZpFnRnT=7X!>6!(w`>Q_vMMVtj`zt}Fkhqpo|D@mQy4Z1l;*addT>eH-cOl%sCoGgC@7MpI_&UAsM#qf~qb9e%e=*Vuw(E~cvNhX%V_HV`9+-j0W_7ysyDQ+i ze;xdyRNg&v7yRqV_lq+j(;{DZ!>mqKau%hv@Z7An7r|eo#VkrI?@nIiN=9Xw Date: Tue, 9 Jun 2026 17:20:56 +0200 Subject: [PATCH 44/74] =?UTF-8?q?fix:=20CI=20failures=20=E2=80=94=20ruff,?= =?UTF-8?q?=20build-docs,=20and=20browser-test=20experimental=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test/setup_cos_extension.py: use r""" because the docstring contains backslash-continuation sequences (ruff D301) - test/test_browser.py: add trailing comma in skipTest string concat (ruff COM812); add -Wno-experimental to both COS browser tests so that the new EXPERIMENTAL_SETTINGS warning does not become an error under the browser test runner's implicit -Werror - src/settings.js: move [experimental] onto its own comment line for CROSS_ORIGIN_STORAGE so that update_settings_docs.py recognises it as a tag (the parser requires the entire line to be just [tag]) - site/source/docs/tools_reference/settings_reference.rst: regenerated via tools/maint/update_settings_docs.py to pick up the tag change --- site/source/docs/tools_reference/settings_reference.rst | 4 +++- src/settings.js | 3 ++- test/setup_cos_extension.py | 2 +- test/test_browser.py | 5 +++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 9f1a46a13c871..74a7e1314aae0 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3358,7 +3358,7 @@ Default value: false CROSS_ORIGIN_STORAGE ==================== -[experimental] Enables Cross-Origin Storage (COS) API support for Wasm +Enables Cross-Origin Storage (COS) API support for Wasm loading on the Web target. At link time Emscripten computes the SHA-256 hash of the final ``.wasm`` binary and embeds it in the generated JS. At runtime the COS API is used as a progressive enhancement: the binary is @@ -3371,6 +3371,8 @@ WASM_ASYNC_COMPILATION=0 (both produce hard link-time errors). See :ref:`CrossOriginStorage` for the full guide. +.. note:: This is an experimental setting + Default value: 0 .. _cross_origin_storage_origins: diff --git a/src/settings.js b/src/settings.js index 90d773dc160f6..ce8b7e7ed16c3 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2202,7 +2202,8 @@ var GROWABLE_ARRAYBUFFERS = false; // indirectly using `importScripts` var CROSS_ORIGIN = false; -// [experimental] Enables Cross-Origin Storage (COS) API support for Wasm +// [experimental] +// Enables Cross-Origin Storage (COS) API support for Wasm // loading on the Web target. At link time Emscripten computes the SHA-256 // hash of the final ``.wasm`` binary and embeds it in the generated JS. // At runtime the COS API is used as a progressive enhancement: the binary is diff --git a/test/setup_cos_extension.py b/test/setup_cos_extension.py index 8e7ed11ee0c36..970ffdf40b7c1 100755 --- a/test/setup_cos_extension.py +++ b/test/setup_cos_extension.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: Apache-2.0 -"""Download the Cross-Origin Storage Chrome extension for COS browser tests. +r"""Download the Cross-Origin Storage Chrome extension for COS browser tests. The COS extension polyfills navigator.crossOriginStorage in Chrome so that automated browser tests can exercise the cache-miss and cache-hit paths without diff --git a/test/test_browser.py b/test/test_browser.py index 188809ab9f43b..67034aa3317d6 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5707,7 +5707,7 @@ def test_cross_origin_storage_fallback(self): if not is_chrome(): self.skipTest('cross-origin storage tests require a Chromium-based browser') self.btest_exit('browser_test_hello_world.c', - cflags=['-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web']) + cflags=['-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-Wno-experimental']) def test_cross_origin_storage_miss_then_hit(self): """COS flag: first load triggers a cache-miss store; second load is a hit. @@ -5725,7 +5725,7 @@ def test_cross_origin_storage_miss_then_hit(self): 'set EMTEST_COS_EXTENSION_PATH to the COS extension directory; ' 'run test/setup_cos_extension.py to download it automatically. ' 'Note: --load-extension requires Chromium or Chrome for Testing, ' - 'not the official Google Chrome release.' + 'not the official Google Chrome release.', ) # Restart the browser with a fresh user-data-dir so the extension starts @@ -5750,6 +5750,7 @@ def test_cross_origin_storage_miss_then_hit(self): self.compile_btest('browser_test_hello_world.c', [ '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', + '-Wno-experimental', '-sINCOMING_MODULE_JS_API=onAbort,onExit,onCOSStore,onCOSCacheHit', '--pre-js', 'cos_pre.js', '-o', 'page.html', From 748585db91e08f2c04d3a6b748e5cf116abdedc0 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 17:36:31 +0200 Subject: [PATCH 45/74] remove: dead WASM_SHA256 internal setting The setting was never wired up: it was always '' and no JS glue template ever referenced it with {{{ WASM_SHA256 }}}. The actual hash injection uses the <<< WASM_HASH_ALGORITHM >>> / <<< WASM_HASH_VALUE >>> late- replacement placeholders substituted by do_replace() in tools/link.py. --- src/settings_internal.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/settings_internal.js b/src/settings_internal.js index 482943d54f7bf..ef72ff8a4789c 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -127,11 +127,6 @@ var USER_EXPORTS = []; // name of the file containing wasm binary, if relevant var WASM_BINARY_FILE = ''; -// SHA-256 hex digest of the final .wasm binary, computed at link time. -// Populated automatically by tools/link.py when CROSS_ORIGIN_STORAGE=1. -// Available in JS glue templates as {{{ WASM_SHA256 }}}. -var WASM_SHA256 = ''; - // Base URL the source mapfile, if relevant var SOURCE_MAP_BASE = ''; From 765ed7a74981ba08f02d3313a19f8bea361a3a44 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 17:45:19 +0200 Subject: [PATCH 46/74] style: add -O2 to COS example build and browser tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without an optimisation flag the JS output is ~60 KB; with -O2 it drops to ~10 KB. Other browser tests in the suite pass -O2 (or higher) explicitly — follow the same convention. --- test/cross_origin_storage/Makefile | 1 + test/cross_origin_storage/README.md | 1 + test/cross_origin_storage/main.cpp | 1 + test/test_browser.py | 3 ++- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/cross_origin_storage/Makefile b/test/cross_origin_storage/Makefile index 00d75aaee3a15..024872280c5e6 100644 --- a/test/cross_origin_storage/Makefile +++ b/test/cross_origin_storage/Makefile @@ -11,6 +11,7 @@ all: index.js index.js: main.cpp $(EMCC) main.cpp -o index.js \ + -O2 \ -sCROSS_ORIGIN_STORAGE \ -sENVIRONMENT=web \ -sEXPORTED_RUNTIME_METHODS=ccall \ diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md index 5e8ecf1a24135..b289998c20a46 100644 --- a/test/cross_origin_storage/README.md +++ b/test/cross_origin_storage/README.md @@ -26,6 +26,7 @@ of the Wasm resource and the URL it was fetched from. ```bash emcc main.cpp -o index.js \ + -O2 \ -sCROSS_ORIGIN_STORAGE \ -sENVIRONMENT=web \ -sEXPORTED_RUNTIME_METHODS=ccall \ diff --git a/test/cross_origin_storage/main.cpp b/test/cross_origin_storage/main.cpp index cade88d44ef37..946e88d7d9ed0 100644 --- a/test/cross_origin_storage/main.cpp +++ b/test/cross_origin_storage/main.cpp @@ -7,6 +7,7 @@ // // Build with: // emcc main.cpp -o index.js \ +// -O2 \ // -sCROSS_ORIGIN_STORAGE \ // -sENVIRONMENT=web \ // -sEXPORTED_RUNTIME_METHODS=ccall \ diff --git a/test/test_browser.py b/test/test_browser.py index 67034aa3317d6..c4436aa9854d0 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5707,7 +5707,7 @@ def test_cross_origin_storage_fallback(self): if not is_chrome(): self.skipTest('cross-origin storage tests require a Chromium-based browser') self.btest_exit('browser_test_hello_world.c', - cflags=['-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-Wno-experimental']) + cflags=['-O2', '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-Wno-experimental']) def test_cross_origin_storage_miss_then_hit(self): """COS flag: first load triggers a cache-miss store; second load is a hit. @@ -5748,6 +5748,7 @@ def test_cross_origin_storage_miss_then_hit(self): }; ''') self.compile_btest('browser_test_hello_world.c', [ + '-O2', '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-Wno-experimental', From e5b74d5e6b61f94bbad067260fef0316b04c8ee4 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 17:50:09 +0200 Subject: [PATCH 47/74] chore: untrack generated COS example build artifacts index.js and index.wasm are emcc outputs, not source files. Add them to .gitignore and remove them from the index. --- .gitignore | 4 + test/cross_origin_storage/index.js | 1759 -------------------------- test/cross_origin_storage/index.wasm | Bin 994 -> 0 bytes 3 files changed, 4 insertions(+), 1759 deletions(-) delete mode 100644 test/cross_origin_storage/index.js delete mode 100755 test/cross_origin_storage/index.wasm diff --git a/.gitignore b/.gitignore index 234517cc97797..33e2179a20b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,10 @@ coverage.xml # Test output /out/ +# COS example build artifacts +/test/cross_origin_storage/index.js +/test/cross_origin_storage/index.wasm + # When updating the website we check it out here. /site/emscripten-site/ diff --git a/test/cross_origin_storage/index.js b/test/cross_origin_storage/index.js deleted file mode 100644 index 890b272da8989..0000000000000 --- a/test/cross_origin_storage/index.js +++ /dev/null @@ -1,1759 +0,0 @@ -// include: shell.js -// include: minimum_runtime_check.js -(function() { - // "30.0.0" -> 300000 - function humanReadableVersionToPacked(str) { - str = str.split('-')[0]; // Remove any trailing part from e.g. "12.53.3-alpha" - var vers = str.split('.').slice(0, 3); - while(vers.length < 3) vers.push('00'); - vers = vers.map((n, i, arr) => n.padStart(2, '0')); - return vers.join(''); - } - // 300000 -> "30.0.0" - var packedVersionToHumanReadable = n => [n / 10000 | 0, (n / 100 | 0) % 100, n % 100].join('.'); - - var TARGET_NOT_SUPPORTED = 2147483647; - - // Note: We use a typeof check here instead of optional chaining using - // globalThis because older browsers might not have globalThis defined. - var currentNodeVersion = typeof process !== 'undefined' && process.versions?.node ? humanReadableVersionToPacked(process.versions.node) : TARGET_NOT_SUPPORTED; - if (currentNodeVersion < TARGET_NOT_SUPPORTED) { - throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - } - if (currentNodeVersion < 2147483647) { - throw new Error(`This emscripten-generated code requires node v${ packedVersionToHumanReadable(2147483647) } (detected v${packedVersionToHumanReadable(currentNodeVersion)})`); - } - - var userAgent = typeof navigator !== 'undefined' && navigator.userAgent; - if (!userAgent) { - return; - } - - var currentSafariVersion = userAgent.includes("Safari/") && !userAgent.includes("Chrome/") && userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/) ? humanReadableVersionToPacked(userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/)[1]) : TARGET_NOT_SUPPORTED; - if (currentSafariVersion < 150000) { - throw new Error(`This emscripten-generated code requires Safari v${ packedVersionToHumanReadable(150000) } (detected v${currentSafariVersion})`); - } - - var currentFirefoxVersion = userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; - if (currentFirefoxVersion < 79) { - throw new Error(`This emscripten-generated code requires Firefox v79 (detected v${currentFirefoxVersion})`); - } - - var currentChromeVersion = userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; - if (currentChromeVersion < 85) { - throw new Error(`This emscripten-generated code requires Chrome v85 (detected v${currentChromeVersion})`); - } -})(); - -// end include: minimum_runtime_check.js -// The Module object: Our interface to the outside world. We import -// and export values on it. There are various ways Module can be used: -// 1. Not defined. We create it here -// 2. A function parameter, function(moduleArg) => Promise -// 3. pre-run appended it, var Module = {}; ..generated code.. -// 4. External script tag defines var Module. -// We need to check if Module already exists (e.g. case 3 above). -// Substitution will be replaced with actual code on later stage of the build, -// this way Closure Compiler will not mangle it (e.g. case 4. above). -// Note that if you want to run closure, and also to use Module -// after the generated code, you will need to define var Module = {}; -// before the code. Then that object will be used in the code, and you -// can continue to use Module afterwards as well. -var Module = typeof Module != 'undefined' ? Module : {}; - -// Determine the runtime environment we are in. You can customize this by -// setting the ENVIRONMENT setting at compile time (see settings.js). - -// Attempt to auto-detect the environment -var ENVIRONMENT_IS_WEB = !!globalThis.window; -var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; -// N.b. Electron.js environment is simultaneously a NODE-environment, but -// also a web environment. -var ENVIRONMENT_IS_NODE = globalThis.process?.versions?.node && globalThis.process?.type != 'renderer'; -var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; - -// --pre-jses are emitted after the Module integration code, so that they can -// refer to Module (if they choose; they can also define Module) - - -var programArgs = []; -var thisProgram = './this.program'; -var quit_ = (status, toThrow) => { - throw toThrow; -}; - -// In MODULARIZE mode _scriptName needs to be captured already at the very top of the page immediately when the page is parsed, so it is generated there -// before the page load. In non-MODULARIZE modes generate it here. -var _scriptName = globalThis.document?.currentScript?.src; - -// `/` should be present at the end if `scriptDirectory` is not empty -var scriptDirectory = ''; -function locateFile(path) { - if (Module['locateFile']) { - return Module['locateFile'](path, scriptDirectory); - } - return scriptDirectory + path; -} - -// Hooks that are implemented differently in different runtime environments. -var readAsync, readBinary; - -if (ENVIRONMENT_IS_SHELL) { - -} else - -// Note that this includes Node.js workers when relevant (pthreads is enabled). -// Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and -// ENVIRONMENT_IS_NODE. -if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { - try { - scriptDirectory = new URL('.', _scriptName).href; // includes trailing slash - } catch { - // Must be a `blob:` or `data:` URL (e.g. `blob:http://site.com/etc/etc`), we cannot - // infer anything from them. - } - - if (!(globalThis.window || globalThis.WorkerGlobalScope)) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - - { -// include: web_or_worker_shell_read.js -readAsync = async (url) => { - assert(!isFileURI(url), "readAsync does not work with file:// URLs"); - var response = await fetch(url, { credentials: 'same-origin' }); - if (response.ok) { - return response.arrayBuffer(); - } - throw new Error(response.status + ' : ' + response.url); - }; -// end include: web_or_worker_shell_read.js - } -} else -{ - throw new Error('environment detection error'); -} - -var out = console.log.bind(console); -var err = console.error.bind(console); - -var IDBFS = 'IDBFS is no longer included by default; build with -lidbfs.js'; -var PROXYFS = 'PROXYFS is no longer included by default; build with -lproxyfs.js'; -var WORKERFS = 'WORKERFS is no longer included by default; build with -lworkerfs.js'; -var FETCHFS = 'FETCHFS is no longer included by default; build with -lfetchfs.js'; -var ICASEFS = 'ICASEFS is no longer included by default; build with -licasefs.js'; -var JSFILEFS = 'JSFILEFS is no longer included by default; build with -ljsfilefs.js'; -var OPFS = 'OPFS is no longer included by default; build with -lopfs.js'; - -var NODEFS = 'NODEFS is no longer included by default; build with -lnodefs.js'; - -// perform assertions in shell.js after we set up out() and err(), as otherwise -// if an assertion fails it cannot print the message - -assert(!ENVIRONMENT_IS_WORKER, 'worker environment detected but not enabled at build time (add `worker` to `-sENVIRONMENT` to enable)'); - -assert(!ENVIRONMENT_IS_NODE, 'node environment detected but not enabled at build time (add `node` to `-sENVIRONMENT` to enable)'); - -assert(!ENVIRONMENT_IS_SHELL, 'shell environment detected but not enabled at build time (add `shell` to `-sENVIRONMENT` to enable)'); - -// end include: shell.js - -// include: preamble.js -// === Preamble library stuff === - -// Documentation for the public APIs defined in this file must be updated in: -// site/source/docs/api_reference/preamble.js.rst -// A prebuilt local version of the documentation is available at: -// site/build/text/docs/api_reference/preamble.js.txt -// You can also build docs locally as HTML or other formats in site/ -// An online HTML version (which may be of a different version of Emscripten) -// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html - -var wasmBinary; - -if (!globalThis.WebAssembly) { - err('no native wasm support detected'); -} - -// Wasm globals - -//======================================== -// Runtime essentials -//======================================== - -// whether we are quitting the application. no code should run after this. -// set in exit() and abort() -var ABORT = false; - -// set by exit() and abort(). Passed to 'onExit' handler. -// NOTE: This is also used as the process return code in shell environments -// but only when noExitRuntime is false. -var EXITSTATUS; - -// In STRICT mode, we only define assert() when ASSERTIONS is set. i.e. we -// don't define it at all in release modes. This matches the behaviour of -// MINIMAL_RUNTIME. -// TODO(sbc): Make this the default even without STRICT enabled. -/** @type {function(*, string=)} */ -function assert(condition, text) { - if (!condition) { - abort('Assertion failed' + (text ? ': ' + text : '')); - } -} - -// We used to include malloc/free by default in the past. Show a helpful error in -// builds with assertions. -function _malloc() { - abort('malloc() called but not included in the build - add `_malloc` to EXPORTED_FUNCTIONS'); -} -function _free() { - // Show a helpful error since we used to include free by default in the past. - abort('free() called but not included in the build - add `_free` to EXPORTED_FUNCTIONS'); -} - -/** - * Indicates whether filename is delivered via file protocol (as opposed to http/https) - * @noinline - */ -var isFileURI = (filename) => filename.startsWith('file://'); - -// include: runtime_common.js -// include: runtime_stack_check.js -// Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode. -function writeStackCookie() { - var max = _emscripten_stack_get_end(); - assert((max & 3) == 0); - // If the stack ends at address zero we write our cookies 4 bytes into the - // stack. This prevents interference with SAFE_HEAP and ASAN which also - // monitor writes to address zero. - if (max == 0) { - max += 4; - } - // The stack grow downwards towards _emscripten_stack_get_end. - // We write cookies to the final two words in the stack and detect if they are - // ever overwritten. - HEAPU32[((max)>>2)] = 0x02135467; - HEAPU32[(((max)+(4))>>2)] = 0x89BACDFE; - // Also test the global address 0 for integrity. - HEAPU32[((0)>>2)] = 1668509029; -} - -function checkStackCookie() { - if (ABORT) return; - var max = _emscripten_stack_get_end(); - // See writeStackCookie(). - if (max == 0) { - max += 4; - } - var cookie1 = HEAPU32[((max)>>2)]; - var cookie2 = HEAPU32[(((max)+(4))>>2)]; - if (cookie1 != 0x02135467 || cookie2 != 0x89BACDFE) { - abort(`Stack overflow! Stack cookie has been overwritten at ${ptrToString(max)}, expected hex dwords 0x89BACDFE and 0x2135467, but received ${ptrToString(cookie2)} ${ptrToString(cookie1)}`); - } - // Also test the global address 0 for integrity. - if (HEAPU32[((0)>>2)] != 0x63736d65 /* 'emsc' */) { - abort('Runtime error: The application has corrupted its heap memory area (address zero)!'); - } -} -// end include: runtime_stack_check.js -// include: runtime_exceptions.js -// Base Emscripten EH error class -class EmscriptenEH {} - -class EmscriptenSjLj extends EmscriptenEH {} - -// end include: runtime_exceptions.js -// include: runtime_debug.js -var runtimeDebug = true; // Switch to false at runtime to disable logging at the right times - -// Used by XXXXX_DEBUG settings to output debug messages. -function dbg(...args) { - if (!runtimeDebug && typeof runtimeDebug != 'undefined') return; - // TODO(sbc): Make this configurable somehow. Its not always convenient for - // logging to show up as warnings. - console.warn(...args); -} - -// Endianness check -(() => { - var h16 = new Int16Array(1); - var h8 = new Int8Array(h16.buffer); - h16[0] = 0x6373; - if (h8[0] !== 0x73 || h8[1] !== 0x63) abort('Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)'); -})(); - -function consumedModuleProp(prop) { - if (!Object.getOwnPropertyDescriptor(Module, prop)) { - Object.defineProperty(Module, prop, { - configurable: true, - set() { - abort(`Attempt to set \`Module.${prop}\` after it has already been processed. This can happen, for example, when code is injected via '--post-js' rather than '--pre-js'`); - - } - }); - } -} - -function makeInvalidEarlyAccess(name) { - return () => assert(false, `call to '${name}' via reference taken before Wasm module initialization`); - -} - -function ignoredModuleProp(prop) { - if (Object.getOwnPropertyDescriptor(Module, prop)) { - abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`); - } -} - -// forcing the filesystem exports a few things by default -function isExportedByForceFilesystem(name) { - return name === 'FS_createPath' || - name === 'FS_createDataFile' || - name === 'FS_createPreloadedFile' || - name === 'FS_preloadFile' || - name === 'FS_unlink' || - name === 'addRunDependency' || - // The old FS has some functionality that WasmFS lacks. - name === 'FS_createLazyFile' || - name === 'FS_createDevice' || - name === 'removeRunDependency'; -} - -/** - * Intercept access to a symbols in the global symbol. This enables us to give - * informative warnings/errors when folks attempt to use symbols they did not - * include in their build, or no symbols that no longer exist. - * - * We don't define this in MODULARIZE mode since in that mode emscripten symbols - * are never placed in the global scope. - */ -function hookGlobalSymbolAccess(sym, func) { - if (!Object.getOwnPropertyDescriptor(globalThis, sym)) { - Object.defineProperty(globalThis, sym, { - configurable: true, - get() { - func(); - return undefined; - } - }); - } -} - -function missingGlobal(sym, msg) { - hookGlobalSymbolAccess(sym, () => { - warnOnce(`\`${sym}\` is no longer defined by emscripten. ${msg}`); - }); -} - -missingGlobal('buffer', 'Please use HEAP8.buffer or wasmMemory.buffer'); -missingGlobal('asm', 'Please use wasmExports instead'); - -function missingLibrarySymbol(sym) { - hookGlobalSymbolAccess(sym, () => { - // Can't `abort()` here because it would break code that does runtime - // checks. e.g. `if (typeof SDL === 'undefined')`. - var msg = `\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`; - // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE requires the name as it appears in - // library.js, which means $name for a JS name with no prefix, or name - // for a JS name like _name. - var librarySymbol = sym; - if (!librarySymbol.startsWith('_')) { - librarySymbol = '$' + sym; - } - msg += ` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`; - if (isExportedByForceFilesystem(sym)) { - msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; - } - warnOnce(msg); - }); - - // Any symbol that is not included from the JS library is also (by definition) - // not exported on the Module object. - unexportedRuntimeSymbol(sym); -} - -function unexportedRuntimeSymbol(sym) { - if (!Object.getOwnPropertyDescriptor(Module, sym)) { - Object.defineProperty(Module, sym, { - configurable: true, - get() { - var msg = `'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`; - if (isExportedByForceFilesystem(sym)) { - msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; - } - abort(msg); - }, - }); - } -} - -// end include: runtime_debug.js -// Memory management - -var runtimeInitialized = false; - - - -function updateMemoryViews() { - var b = wasmMemory.buffer; - HEAP8 = new Int8Array(b); - HEAP16 = new Int16Array(b); - HEAPU8 = new Uint8Array(b); - HEAPU16 = new Uint16Array(b); - HEAP32 = new Int32Array(b); - HEAPU32 = new Uint32Array(b); - HEAPF32 = new Float32Array(b); - HEAPF64 = new Float64Array(b); - HEAP64 = new BigInt64Array(b); - HEAPU64 = new BigUint64Array(b); -} - -// include: memoryprofiler.js -// end include: memoryprofiler.js -// end include: runtime_common.js -assert(globalThis.Int32Array && globalThis.Float64Array && Int32Array.prototype.subarray && Int32Array.prototype.set, - 'JS engine does not provide full typed array support'); - -function preRun() { - if (Module['preRun']) { - if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; - while (Module['preRun'].length) { - addOnPreRun(Module['preRun'].shift()); - } - } - consumedModuleProp('preRun'); - // Begin ATPRERUNS hooks - callRuntimeCallbacks(onPreRuns); - // End ATPRERUNS hooks -} - -function initRuntime() { - assert(!runtimeInitialized); - runtimeInitialized = true; - - checkStackCookie(); - - // No ATINITS hooks - - wasmExports['__wasm_call_ctors'](); - - // No ATPOSTCTORS hooks -} - -function postRun() { - checkStackCookie(); - // PThreads reuse the runtime from the main thread. - - if (Module['postRun']) { - if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; - while (Module['postRun'].length) { - addOnPostRun(Module['postRun'].shift()); - } - } - consumedModuleProp('postRun'); - - // Begin ATPOSTRUNS hooks - callRuntimeCallbacks(onPostRuns); - // End ATPOSTRUNS hooks -} - -/** - * @param {string|number=} what - */ -function abort(what) { - Module['onAbort']?.(what); - - what = `Aborted(${what})`; - // TODO(sbc): Should we remove printing and leave it up to whoever - // catches the exception? - err(what); - - ABORT = true; - - // Use a wasm runtime error, because a JS error might be seen as a foreign - // exception, which means we'd run destructors on it. We need the error to - // simply make the program stop. - // FIXME This approach does not work in Wasm EH because it currently does not assume - // all RuntimeErrors are from traps; it decides whether a RuntimeError is from - // a trap or not based on a hidden field within the object. So at the moment - // we don't have a way of throwing a wasm trap from JS. TODO Make a JS API that - // allows this in the wasm spec. - - // Suppress closure compiler warning here. Closure compiler's builtin extern - // definition for WebAssembly.RuntimeError claims it takes no arguments even - // though it can. - // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure gets fixed. - /** @suppress {checkTypes} */ - var e = new WebAssembly.RuntimeError(what); - - // Throw the error whether or not MODULARIZE is set because abort is used - // in code paths apart from instantiation where an exception is expected - // to be thrown when abort is called. - throw e; -} - -// show errors on likely calls to FS when it was not included -function fsMissing() { - abort('Filesystem support (FS) was not included. The problem is that you are using files from JS, but files were not used from C/C++, so filesystem support was not auto-included. You can force-include filesystem support with -sFORCE_FILESYSTEM'); -} -var FS = { - init: fsMissing, - createDataFile: fsMissing, - createPreloadedFile: fsMissing, - createLazyFile: fsMissing, - open: fsMissing, - mkdev: fsMissing, - registerDevice: fsMissing, - analyzePath: fsMissing, - ErrnoError: fsMissing, -}; - - -function createExportWrapper(name, nargs) { - return (...args) => { - assert(runtimeInitialized, `native function \`${name}\` called before runtime initialization`); - var f = wasmExports[name]; - assert(f, `exported native function \`${name}\` not found`); - // Only assert for too many arguments. Too few can be valid since the missing arguments will be zero filled. - assert(args.length <= nargs, `native function \`${name}\` called with ${args.length} args but expects ${nargs}`); - return f(...args); - }; -} - -var wasmBinaryFile; - -function findWasmBinary() { - return locateFile('index.wasm'); -} - -function getBinarySync(file) { - if (file == wasmBinaryFile && wasmBinary) { - return new Uint8Array(wasmBinary); - } - if (readBinary) { - return readBinary(file); - } - // Throwing a plain string here, even though it not normally advisable since - // this gets turning into an `abort` in instantiateArrayBuffer. - throw 'both async and sync fetching of the wasm failed'; -} - -async function getWasmBinary(binaryFile) { - // If we don't have the binary yet, load it asynchronously using readAsync. - if (!wasmBinary) { - // Fetch the binary using readAsync - try { - var response = await readAsync(binaryFile); - return new Uint8Array(response); - } catch { - // Fall back to getBinarySync below; - } - } - - // Otherwise, getBinarySync should be able to get it synchronously - return getBinarySync(binaryFile); -} - -async function instantiateArrayBuffer(binaryFile, imports) { - try { - var binary = await getWasmBinary(binaryFile); - var instance = await WebAssembly.instantiate(binary, imports); - return instance; - } catch (reason) { - err(`failed to asynchronously prepare wasm: ${reason}`); - - // Warn on some common problems. - if (isFileURI(binaryFile)) { - err(`warning: Loading from a file URI (${binaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`); - } - abort(reason); - } -} - -async function instantiateAsync(binary, binaryFile, imports) { - // Cross-Origin Storage (COS) progressive enhancement. - // https://github.com/WICG/cross-origin-storage - // - // COS is only beneficial when this .wasm binary is byte-identical across - // many origins — i.e. a popular library loaded by many independent sites. - // Application-specific Wasm gains nothing from COS that the normal HTTP - // cache does not already provide. - // - // The SHA-256 hash of the final .wasm binary is computed at link time and - // embedded here as a build-time constant. At runtime we feature-detect the - // browser COS API via `'crossOriginStorage' in navigator`, then call - // navigator.crossOriginStorage.requestFileHandles() with the hash object - // required by the spec ({ algorithm: 'SHA-256', value: '' }). - // - // Cache-hit path: - // requestFileHandles() succeeds → getFile() → arrayBuffer() → instantiate - // - // Cache-miss path (NotFoundError): - // fetch() the wasm over the network → instantiate → store in COS. - // The origins field is controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS - // (default: '*', globally available). The store is fire-and-forget so - // it never delays startup. - // - // Visibility and security: the spec allows widening visibility (e.g. a - // restricted entry promoted to globally available) but never narrowing it. - // Because storing always requires writing the actual bytes, no third party - // can probe the cache to discover whether a restricted entry was previously - // stored by another origin — a cache hit is only possible after an explicit - // write that provided the content. - // - // Any other error (NotAllowedError, network failure, …) falls through to the - // standard Emscripten streaming path so the page always loads. - var cosHash = { algorithm: 'SHA-256', value: 'a617cb51176b7e0c729fb776ff688310ecbe035a0da79fcac66b1bf98028e793' }; - if (cosHash.value && 'crossOriginStorage' in navigator) { - try { - var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); - // Cache hit — read the Blob and instantiate from its ArrayBuffer. - var cosFile = await cosHandles[0].getFile(); - var cosBytes = await cosFile.arrayBuffer(); - // Optional instrumentation callback: Module['onCOSCacheHit'](hash) - // Called when the Wasm binary is served from the cross-origin cache. - if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](cosHash.value); - return WebAssembly.instantiate(cosBytes, imports); - } catch (cosErr) { - if (cosErr.name === 'NotFoundError') { - // Cache miss — fetch normally, then store in COS for future consumers. - try { - var networkResponse = await fetch(binaryFile, { credentials: 'same-origin' }); - var wasmBytes = await networkResponse.arrayBuffer(); - // Optional instrumentation callback: Module['onCOSCacheMiss'](hash, url) - // Called when the Wasm binary is not in COS and is fetched over the network. - if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](cosHash.value, binaryFile); - // Fire-and-forget store; never block instantiation on the write. - (async () => { - try { - var writeHandles = await navigator.crossOriginStorage.requestFileHandles( - [cosHash], - { create: true, origins: '*' }, - ); - var writable = await writeHandles[0].createWritable(); - await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); - await writable.close(); - // Optional instrumentation callback: Module['onCOSStore'](hash) - // Called after the Wasm binary has been successfully written to COS. - if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](cosHash.value); - } catch (storeErr) { - err(`COS store failed: ${storeErr}`); - } - })(); - return WebAssembly.instantiate(wasmBytes, imports); - } catch (fetchErr) { - // Network fetch failed; fall through to the standard path. - err(`COS fallback fetch failed: ${fetchErr}`); - } - } else if (cosErr.name === 'NotAllowedError') { - err(`COS: permission denied.`); - } else { - err(`Cross-Origin Storage lookup failed: ${cosErr}`); - } - } - } - if (!binary - ) { - try { - var response = fetch(binaryFile, { credentials: 'same-origin' }); - var instantiationResult = await WebAssembly.instantiateStreaming(response, imports); - return instantiationResult; - } catch (reason) { - // We expect the most common failure cause to be a bad MIME type for the binary, - // in which case falling back to ArrayBuffer instantiation should work. - err(`wasm streaming compile failed: ${reason}`); - err('falling back to ArrayBuffer instantiation'); - // fall back of instantiateArrayBuffer below - }; - } - return instantiateArrayBuffer(binaryFile, imports); -} - -function getWasmImports() { - // prepare imports - var imports = { - 'env': wasmImports, - 'wasi_snapshot_preview1': wasmImports, - }; - return imports; -} - -// Create the wasm instance. -// Receives the wasm imports, returns the exports. -async function createWasm() { - // Load the wasm module and create an instance of using native support in the JS engine. - // handle a generated wasm instance, receiving its exports and - // performing other necessary setup - /** @param {WebAssembly.Module=} module*/ - function receiveInstance(instance, module) { - wasmExports = instance.exports; - - assignWasmExports(wasmExports); - - updateMemoryViews(); - - removeRunDependency('wasm-instantiate'); - return wasmExports; - } - addRunDependency('wasm-instantiate'); - - // Prefer streaming instantiation if available. - // Async compilation can be confusing when an error on the page overwrites Module - // (for example, if the order of elements is wrong, and the one defining Module is - // later), so we save Module and check it later. - var trueModule = Module; - function receiveInstantiationResult(result) { - // 'result' is a ResultObject object which has both the module and instance. - // receiveInstance() will swap in the exports (to Module.asm) so they can be called - assert(Module === trueModule, 'the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?'); - trueModule = null; - // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. - // When the regression is fixed, can restore the above PTHREADS-enabled path. - return receiveInstance(result['instance']); - } - - var info = getWasmImports(); - - // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback - // to manually instantiate the Wasm module themselves. This allows pages to - // run the instantiation parallel to any other async startup actions they are - // performing. - // Also pthreads and wasm workers initialize the wasm instance through this - // path. - if (Module['instantiateWasm']) { - return new Promise((resolve, reject) => { - try { - Module['instantiateWasm'](info, (inst, mod) => { - resolve(receiveInstance(inst, mod)); - }); - } catch(e) { - err(`Module.instantiateWasm callback failed with error: ${e}`); - reject(e); - } - }); - } - - wasmBinaryFile ??= findWasmBinary(); - var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info); - var exports = receiveInstantiationResult(result); - return exports; -} - -// end include: preamble.js - -// Begin JS library code - - - class ExitStatus { - name = 'ExitStatus'; - constructor(status) { - this.message = `Program terminated with exit(${status})`; - this.status = status; - } - } - - /** @type {!Int16Array} */ - var HEAP16; - - /** @type {!Int32Array} */ - var HEAP32; - - /** not-@type {!BigInt64Array} */ - var HEAP64; - - /** @type {!Int8Array} */ - var HEAP8; - - /** @type {!Float32Array} */ - var HEAPF32; - - /** @type {!Float64Array} */ - var HEAPF64; - - /** @type {!Uint16Array} */ - var HEAPU16; - - /** @type {!Uint32Array} */ - var HEAPU32; - - /** not-@type {!BigUint64Array} */ - var HEAPU64; - - /** @type {!Uint8Array} */ - var HEAPU8; - - var callRuntimeCallbacks = (callbacks) => { - while (callbacks.length > 0) { - // Pass the module as the first argument. - callbacks.shift()(Module); - } - }; - var onPostRuns = []; - var addOnPostRun = (cb) => onPostRuns.push(cb); - - var onPreRuns = []; - var addOnPreRun = (cb) => onPreRuns.push(cb); - - var runDependencies = 0; - - - var dependenciesFulfilled = null; - - var runDependencyTracking = { - }; - - var runDependencyWatcher = null; - var removeRunDependency = (id) => { - runDependencies--; - - Module['monitorRunDependencies']?.(runDependencies); - - assert(id, 'removeRunDependency requires an ID'); - assert(runDependencyTracking[id]); - delete runDependencyTracking[id]; - if (runDependencies == 0) { - if (runDependencyWatcher !== null) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - } - if (dependenciesFulfilled) { - var callback = dependenciesFulfilled; - dependenciesFulfilled = null; - callback(); // can add another dependenciesFulfilled - } - } - }; - - - var addRunDependency = (id) => { - runDependencies++; - - Module['monitorRunDependencies']?.(runDependencies); - - assert(id, 'addRunDependency requires an ID') - assert(!runDependencyTracking[id]); - runDependencyTracking[id] = 1; - if (runDependencyWatcher === null && globalThis.setInterval) { - // Check for missing dependencies every few seconds - runDependencyWatcher = setInterval(() => { - if (ABORT) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - return; - } - var shown = false; - for (var dep in runDependencyTracking) { - if (!shown) { - shown = true; - err('still waiting on run dependencies:'); - } - err(`dependency: ${dep}`); - } - if (shown) { - err('(end of list)'); - } - }, 10000); - } - }; - - - - /** - * @param {number} ptr - * @param {string} type - */ - function getValue(ptr, type = 'i8') { - if (type.endsWith('*')) type = '*'; - switch (type) { - case 'i1': return HEAP8[ptr]; - case 'i8': return HEAP8[ptr]; - case 'i16': return HEAP16[((ptr)>>1)]; - case 'i32': return HEAP32[((ptr)>>2)]; - case 'i64': return HEAP64[((ptr)>>3)]; - case 'float': return HEAPF32[((ptr)>>2)]; - case 'double': return HEAPF64[((ptr)>>3)]; - case '*': return HEAPU32[((ptr)>>2)]; - default: abort(`invalid type for getValue: ${type}`); - } - } - - var noExitRuntime = true; - - function ptrToString(ptr) { - assert(typeof ptr === 'number', `ptrToString expects a number, got ${typeof ptr}`); - // Convert to 32-bit unsigned value - ptr >>>= 0; - return '0x' + ptr.toString(16).padStart(8, '0'); - } - - - - /** - * @param {number} ptr - * @param {number} value - * @param {string} type - */ - function setValue(ptr, value, type = 'i8') { - if (type.endsWith('*')) type = '*'; - switch (type) { - case 'i1': HEAP8[ptr] = value; break; - case 'i8': HEAP8[ptr] = value; break; - case 'i16': HEAP16[((ptr)>>1)] = value; break; - case 'i32': HEAP32[((ptr)>>2)] = value; break; - case 'i64': HEAP64[((ptr)>>3)] = BigInt(value); break; - case 'float': HEAPF32[((ptr)>>2)] = value; break; - case 'double': HEAPF64[((ptr)>>3)] = value; break; - case '*': HEAPU32[((ptr)>>2)] = value; break; - default: abort(`invalid type for setValue: ${type}`); - } - } - - var stackRestore = (val) => __emscripten_stack_restore(val); - - var stackSave = () => _emscripten_stack_get_current(); - - var warnOnce = (text) => { - warnOnce.shown ||= {}; - if (!warnOnce.shown[text]) { - warnOnce.shown[text] = 1; - err(text); - } - }; - - - - var getCFunc = (ident) => { - var func = Module['_' + ident]; // closure exported function - assert(func, `Cannot call unknown function ${ident}, make sure it is exported`); - return func; - }; - - var writeArrayToMemory = (array, buffer) => { - assert(array.length >= 0, 'writeArrayToMemory array must have a length (should be an array or typed array)') - HEAP8.set(array, buffer); - }; - - var lengthBytesUTF8 = (str) => { - var len = 0; - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code - // unit, not a Unicode code point of the character! So decode - // UTF16->UTF32->UTF8. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - var c = str.charCodeAt(i); // possibly a lead surrogate - if (c <= 0x7F) { - len++; - } else if (c <= 0x7FF) { - len += 2; - } else if (c >= 0xD800 && c <= 0xDFFF) { - len += 4; ++i; - } else { - len += 3; - } - } - return len; - }; - - var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { - assert(typeof str === 'string', `stringToUTF8Array expects a string (got ${typeof str})`); - // Parameter maxBytesToWrite is not optional. Negative values, 0, null, - // undefined and false each don't write out any bytes. - if (!(maxBytesToWrite > 0)) - return 0; - - var startIdx = outIdx; - var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. - for (var i = 0; i < str.length; ++i) { - // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description - // and https://www.ietf.org/rfc/rfc2279.txt - // and https://tools.ietf.org/html/rfc3629 - var u = str.codePointAt(i); - if (u <= 0x7F) { - if (outIdx >= endIdx) break; - heap[outIdx++] = u; - } else if (u <= 0x7FF) { - if (outIdx + 1 >= endIdx) break; - heap[outIdx++] = 0xC0 | (u >> 6); - heap[outIdx++] = 0x80 | (u & 63); - } else if (u <= 0xFFFF) { - if (outIdx + 2 >= endIdx) break; - heap[outIdx++] = 0xE0 | (u >> 12); - heap[outIdx++] = 0x80 | ((u >> 6) & 63); - heap[outIdx++] = 0x80 | (u & 63); - } else { - if (outIdx + 3 >= endIdx) break; - if (u > 0x10FFFF) warnOnce(`Invalid Unicode code point ${ptrToString(u)} encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).`); - heap[outIdx++] = 0xF0 | (u >> 18); - heap[outIdx++] = 0x80 | ((u >> 12) & 63); - heap[outIdx++] = 0x80 | ((u >> 6) & 63); - heap[outIdx++] = 0x80 | (u & 63); - // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. - // We need to manually skip over the second code unit for correct iteration. - i++; - } - } - // Null-terminate the pointer to the buffer. - heap[outIdx] = 0; - return outIdx - startIdx; - }; - var stringToUTF8 = (str, outPtr, maxBytesToWrite) => { - assert(typeof maxBytesToWrite == 'number', 'stringToUTF8 requires a third parameter that specifies the length of the output buffer'); - return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); - }; - - var stackAlloc = (sz) => __emscripten_stack_alloc(sz); - var stringToUTF8OnStack = (str) => { - var size = lengthBytesUTF8(str) + 1; - var ret = stackAlloc(size); - stringToUTF8(str, ret, size); - return ret; - }; - - - - - var UTF8Decoder = globalThis.TextDecoder && new TextDecoder(); - - var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { - var maxIdx = idx + maxBytesToRead; - if (ignoreNul) return maxIdx; - // TextDecoder needs to know the byte length in advance, it doesn't stop on - // null terminator by itself. - // As a tiny code save trick, compare idx against maxIdx using a negation, - // so that maxBytesToRead=undefined/NaN means Infinity. - while (heapOrArray[idx] && !(idx >= maxIdx)) ++idx; - return idx; - }; - - - /** - * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given - * array that contains uint8 values, returns a copy of that string as a - * Javascript String object. - * heapOrArray is either a regular array, or a JavaScript typed array view. - * @param {number=} idx - * @param {number=} maxBytesToRead - * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. - * @return {string} - */ - var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead, ignoreNul) => { - - var endPtr = findStringEnd(heapOrArray, idx, maxBytesToRead, ignoreNul); - - // When using conditional TextDecoder, skip it for short strings as the overhead of the native call is not worth it. - if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) { - return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr)); - } - var str = ''; - while (idx < endPtr) { - // For UTF8 byte structure, see: - // http://en.wikipedia.org/wiki/UTF-8#Description - // https://www.ietf.org/rfc/rfc2279.txt - // https://tools.ietf.org/html/rfc3629 - var u0 = heapOrArray[idx++]; - if (!(u0 & 0x80)) { str += String.fromCharCode(u0); continue; } - var u1 = heapOrArray[idx++] & 63; - if ((u0 & 0xE0) == 0xC0) { str += String.fromCharCode(((u0 & 31) << 6) | u1); continue; } - var u2 = heapOrArray[idx++] & 63; - if ((u0 & 0xF0) == 0xE0) { - u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; - } else { - if ((u0 & 0xF8) != 0xF0) warnOnce(`Invalid UTF-8 leading byte ${ptrToString(u0)} encountered when deserializing a UTF-8 string in wasm memory to a JS string!`); - u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heapOrArray[idx++] & 63); - } - - if (u0 < 0x10000) { - str += String.fromCharCode(u0); - } else { - var ch = u0 - 0x10000; - str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); - } - } - return str; - }; - - /** - * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the - * emscripten HEAP, returns a copy of that string as a Javascript String object. - * - * @param {number} ptr - * @param {number=} maxBytesToRead - An optional length that specifies the - * maximum number of bytes to read. You can omit this parameter to scan the - * string until the first 0 byte. If maxBytesToRead is passed, and the string - * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the - * string will cut short at that byte index. - * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. - * @return {string} - */ - var UTF8ToString = (ptr, maxBytesToRead, ignoreNul) => { - assert(typeof ptr == 'number', `UTF8ToString expects a number (got ${typeof ptr})`); - return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead, ignoreNul) : ''; - }; - - /** - * @param {string|null=} returnType - * @param {Array=} argTypes - * @param {Array=} args - * @param {Object=} opts - */ - var ccall = (ident, returnType, argTypes, args, opts) => { - // For fast lookup of conversion functions - var toC = { - 'string': (str) => { - var ret = 0; - if (str !== null && str !== undefined && str !== 0) { // null string - ret = stringToUTF8OnStack(str); - } - return ret; - }, - 'array': (arr) => { - var ret = stackAlloc(arr.length); - writeArrayToMemory(arr, ret); - return ret; - } - }; - - function convertReturnValue(ret) { - if (returnType === 'string') { - return UTF8ToString(ret); - } - if (returnType === 'boolean') return Boolean(ret); - return ret; - } - - var func = getCFunc(ident); - var cArgs = []; - var stack = 0; - assert(returnType !== 'array', 'return type should not be "array"'); - if (args) { - for (var i = 0; i < args.length; i++) { - var converter = toC[argTypes[i]]; - if (converter) { - if (stack === 0) stack = stackSave(); - cArgs[i] = converter(args[i]); - } else { - cArgs[i] = args[i]; - } - } - } - var ret = func(...cArgs); - function onDone(ret) { - if (stack !== 0) stackRestore(stack); - return convertReturnValue(ret); - } - - ret = onDone(ret); - return ret; - }; -// End JS library code - -// include: postlibrary.js -// This file is included after the automatically-generated JS library code -// but before the wasm module is created. - -{ - - // Begin ATMODULES hooks - if (Module['noExitRuntime']) noExitRuntime = Module['noExitRuntime']; -if (Module['print']) out = Module['print']; -if (Module['printErr']) err = Module['printErr']; -if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; - -Module['FS_createDataFile'] = FS.createDataFile; -Module['FS_createPreloadedFile'] = FS.createPreloadedFile; - - // End ATMODULES hooks - - checkIncomingModuleAPI(); - - if (Module['arguments']) programArgs = Module['arguments']; - if (Module['thisProgram']) thisProgram = Module['thisProgram']; - - // Assertions on removed incoming Module JS APIs. - assert(typeof Module['memoryInitializerPrefixURL'] == 'undefined', 'Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead'); - assert(typeof Module['pthreadMainPrefixURL'] == 'undefined', 'Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead'); - assert(typeof Module['cdInitializerPrefixURL'] == 'undefined', 'Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead'); - assert(typeof Module['filePackagePrefixURL'] == 'undefined', 'Module.filePackagePrefixURL option was removed, use Module.locateFile instead'); - assert(typeof Module['read'] == 'undefined', 'Module.read option was removed'); - assert(typeof Module['readAsync'] == 'undefined', 'Module.readAsync option was removed (modify readAsync in JS)'); - assert(typeof Module['readBinary'] == 'undefined', 'Module.readBinary option was removed (modify readBinary in JS)'); - assert(typeof Module['setWindowTitle'] == 'undefined', 'Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)'); - assert(typeof Module['TOTAL_MEMORY'] == 'undefined', 'Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY'); - assert(typeof Module['ENVIRONMENT'] == 'undefined', 'Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)'); - assert(typeof Module['STACK_SIZE'] == 'undefined', 'STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time') - // If memory is defined in wasm, the user can't provide it, or set INITIAL_MEMORY - assert(typeof Module['wasmMemory'] == 'undefined', 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally'); - assert(typeof Module['INITIAL_MEMORY'] == 'undefined', 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically'); - - if (Module['preInit']) { - if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; - while (Module['preInit'].length > 0) { - Module['preInit'].shift()(); - } - } - consumedModuleProp('preInit'); -} - -// Begin runtime exports - Module['ccall'] = ccall; - var missingLibrarySymbols = [ - 'writeI53ToI64', - 'writeI53ToI64Clamped', - 'writeI53ToI64Signaling', - 'writeI53ToU64Clamped', - 'writeI53ToU64Signaling', - 'readI53FromI64', - 'readI53FromU64', - 'convertI32PairToI53', - 'convertI32PairToI53Checked', - 'convertU32PairToI53', - 'bigintToI53Checked', - 'getTempRet0', - 'setTempRet0', - 'createNamedFunction', - 'zeroMemory', - 'exitJS', - 'getHeapMax', - 'growMemory', - 'withStackSave', - 'strError', - 'inetPton4', - 'inetNtop4', - 'inetPton6', - 'inetNtop6', - 'readSockaddr', - 'writeSockaddr', - 'readEmAsmArgs', - 'jstoi_q', - 'getExecutableName', - 'autoResumeAudioContext', - 'getDynCaller', - 'dynCall', - 'handleException', - 'keepRuntimeAlive', - 'runtimeKeepalivePush', - 'runtimeKeepalivePop', - 'callUserCallback', - 'maybeExit', - 'asyncLoad', - 'asmjsMangle', - 'alignMemory', - 'mmapAlloc', - 'HandleAllocator', - 'getUniqueRunDependency', - 'addOnInit', - 'addOnPostCtor', - 'addOnPreMain', - 'addOnExit', - 'STACK_SIZE', - 'STACK_ALIGN', - 'POINTER_SIZE', - 'ASSERTIONS', - 'cwrap', - 'convertJsFunctionToWasm', - 'getEmptyTableSlot', - 'updateTableMap', - 'getFunctionAddress', - 'addFunction', - 'removeFunction', - 'intArrayFromString', - 'intArrayToString', - 'AsciiToString', - 'stringToAscii', - 'UTF16ToString', - 'stringToUTF16', - 'lengthBytesUTF16', - 'UTF32ToString', - 'stringToUTF32', - 'lengthBytesUTF32', - 'stringToNewUTF8', - 'registerKeyEventCallback', - 'maybeCStringToJsString', - 'findEventTarget', - 'getBoundingClientRect', - 'fillMouseEventData', - 'registerMouseEventCallback', - 'registerWheelEventCallback', - 'registerUiEventCallback', - 'registerFocusEventCallback', - 'fillDeviceOrientationEventData', - 'registerDeviceOrientationEventCallback', - 'fillDeviceMotionEventData', - 'registerDeviceMotionEventCallback', - 'screenOrientation', - 'fillOrientationChangeEventData', - 'registerOrientationChangeEventCallback', - 'fillFullscreenChangeEventData', - 'registerFullscreenChangeEventCallback', - 'JSEvents_requestFullscreen', - 'JSEvents_resizeCanvasForFullscreen', - 'registerRestoreOldStyle', - 'hideEverythingExceptGivenElement', - 'restoreHiddenElements', - 'setLetterbox', - 'softFullscreenResizeWebGLRenderTarget', - 'doRequestFullscreen', - 'fillPointerlockChangeEventData', - 'registerPointerlockChangeEventCallback', - 'registerPointerlockErrorEventCallback', - 'requestPointerLock', - 'fillVisibilityChangeEventData', - 'registerVisibilityChangeEventCallback', - 'registerTouchEventCallback', - 'fillGamepadEventData', - 'registerGamepadEventCallback', - 'registerBeforeUnloadEventCallback', - 'fillBatteryEventData', - 'registerBatteryEventCallback', - 'setCanvasElementSize', - 'getCanvasElementSize', - 'jsStackTrace', - 'getCallstack', - 'convertPCtoSourceLocation', - 'getEnvStrings', - 'checkWasiClock', - 'flush_NO_FILESYSTEM', - 'wasiRightsToMuslOFlags', - 'wasiOFlagsToMuslOFlags', - 'initRandomFill', - 'randomFill', - 'safeSetTimeout', - 'setImmediateWrapped', - 'safeRequestAnimationFrame', - 'clearImmediateWrapped', - 'registerPostMainLoop', - 'registerPreMainLoop', - 'getPromise', - 'makePromise', - 'addPromise', - 'idsToPromises', - 'makePromiseCallback', - 'ExceptionInfo', - 'findMatchingCatch', - 'incrementUncaughtExceptionCount', - 'decrementUncaughtExceptionCount', - 'Browser_asyncPrepareDataCounter', - 'isLeapYear', - 'ydayFromDate', - 'arraySum', - 'addDays', - 'getSocketFromFD', - 'getSocketAddress', - 'FS_createPreloadedFile', - 'FS_preloadFile', - 'FS_modeStringToFlags', - 'FS_getMode', - 'FS_fileDataToTypedArray', - 'FS_stdin_getChar', - 'FS_mkdirTree', - '_setNetworkCallback', - 'heapObjectForWebGLType', - 'toTypedArrayIndex', - 'webgl_enable_ANGLE_instanced_arrays', - 'webgl_enable_OES_vertex_array_object', - 'webgl_enable_WEBGL_draw_buffers', - 'webgl_enable_WEBGL_multi_draw', - 'webgl_enable_EXT_polygon_offset_clamp', - 'webgl_enable_EXT_clip_control', - 'webgl_enable_WEBGL_polygon_mode', - 'emscriptenWebGLGet', - 'computeUnpackAlignedImageSize', - 'colorChannelsInGlTextureFormat', - 'emscriptenWebGLGetTexPixelData', - 'emscriptenWebGLGetUniform', - 'webglGetProgramUniformLocation', - 'webglGetUniformLocation', - 'webglPrepareUniformLocationsBeforeFirstUse', - 'webglGetLeftBracePos', - 'emscriptenWebGLGetVertexAttrib', - '__glGetActiveAttribOrUniform', - 'writeGLArray', - 'registerWebGlEventCallback', - 'runAndAbortIfError', - 'ALLOC_NORMAL', - 'ALLOC_STACK', - 'allocate', - 'writeStringToMemory', - 'writeAsciiToMemory', - 'allocateUTF8', - 'allocateUTF8OnStack', - 'demangle', - 'stackTrace', - 'getNativeTypeSize', -]; -missingLibrarySymbols.forEach(missingLibrarySymbol) - - var unexportedSymbols = [ - 'run', - 'out', - 'err', - 'callMain', - 'abort', - 'wasmExports', - 'writeStackCookie', - 'checkStackCookie', - 'INT53_MAX', - 'INT53_MIN', - 'HEAP8', - 'HEAPU8', - 'HEAP16', - 'HEAPU16', - 'HEAP32', - 'HEAPU32', - 'HEAPF32', - 'HEAPF64', - 'HEAP64', - 'HEAPU64', - 'stackSave', - 'stackRestore', - 'stackAlloc', - 'ptrToString', - 'ENV', - 'ERRNO_CODES', - 'DNS', - 'Protocols', - 'Sockets', - 'timers', - 'warnOnce', - 'readEmAsmArgsArray', - 'wasmTable', - 'wasmMemory', - 'noExitRuntime', - 'addRunDependency', - 'removeRunDependency', - 'addOnPreRun', - 'addOnPostRun', - 'freeTableIndexes', - 'functionsInTableMap', - 'setValue', - 'getValue', - 'PATH', - 'PATH_FS', - 'UTF8Decoder', - 'UTF8ArrayToString', - 'UTF8ToString', - 'stringToUTF8Array', - 'stringToUTF8', - 'lengthBytesUTF8', - 'UTF16Decoder', - 'stringToUTF8OnStack', - 'writeArrayToMemory', - 'JSEvents', - 'specialHTMLTargets', - 'findCanvasEventTarget', - 'currentFullscreenStrategy', - 'restoreOldWindowedStyle', - 'UNWIND_CACHE', - 'ExitStatus', - 'emSetImmediate', - 'emClearImmediate_deps', - 'emClearImmediate', - 'promiseMap', - 'uncaughtExceptionCount', - 'exceptionCaught', - 'Browser', - 'requestFullscreen', - 'requestFullScreen', - 'setCanvasSize', - 'getUserMedia', - 'createContext', - 'getPreloadedImageData__data', - 'wget', - 'MONTH_DAYS_REGULAR', - 'MONTH_DAYS_LEAP', - 'MONTH_DAYS_REGULAR_CUMULATIVE', - 'MONTH_DAYS_LEAP_CUMULATIVE', - 'SYSCALLS', - 'preloadPlugins', - 'FS_stdin_getChar_buffer', - 'FS_unlink', - 'FS_createPath', - 'FS_createDevice', - 'FS_readFile', - 'FS', - 'FS_root', - 'FS_mounts', - 'FS_devices', - 'FS_streams', - 'FS_nextInode', - 'FS_nameTable', - 'FS_currentPath', - 'FS_initialized', - 'FS_ignorePermissions', - 'FS_filesystems', - 'FS_syncFSRequests', - 'FS_lookupPath', - 'FS_getPath', - 'FS_hashName', - 'FS_hashAddNode', - 'FS_hashRemoveNode', - 'FS_lookupNode', - 'FS_createNode', - 'FS_destroyNode', - 'FS_isRoot', - 'FS_isMountpoint', - 'FS_isFile', - 'FS_isDir', - 'FS_isLink', - 'FS_isChrdev', - 'FS_isBlkdev', - 'FS_isFIFO', - 'FS_isSocket', - 'FS_flagsToPermissionString', - 'FS_nodePermissions', - 'FS_mayLookup', - 'FS_mayCreate', - 'FS_mayDelete', - 'FS_mayOpen', - 'FS_checkOpExists', - 'FS_nextfd', - 'FS_getStreamChecked', - 'FS_getStream', - 'FS_createStream', - 'FS_closeStream', - 'FS_dupStream', - 'FS_doSetAttr', - 'FS_chrdev_stream_ops', - 'FS_major', - 'FS_minor', - 'FS_makedev', - 'FS_registerDevice', - 'FS_getDevice', - 'FS_getMounts', - 'FS_syncfs', - 'FS_mount', - 'FS_unmount', - 'FS_lookup', - 'FS_mknod', - 'FS_statfs', - 'FS_statfsStream', - 'FS_statfsNode', - 'FS_create', - 'FS_mkdir', - 'FS_mkdev', - 'FS_symlink', - 'FS_rename', - 'FS_rmdir', - 'FS_readdir', - 'FS_readlink', - 'FS_stat', - 'FS_fstat', - 'FS_lstat', - 'FS_doChmod', - 'FS_chmod', - 'FS_lchmod', - 'FS_fchmod', - 'FS_doChown', - 'FS_chown', - 'FS_lchown', - 'FS_fchown', - 'FS_doTruncate', - 'FS_truncate', - 'FS_ftruncate', - 'FS_utime', - 'FS_open', - 'FS_close', - 'FS_isClosed', - 'FS_llseek', - 'FS_read', - 'FS_write', - 'FS_mmap', - 'FS_msync', - 'FS_ioctl', - 'FS_writeFile', - 'FS_cwd', - 'FS_chdir', - 'FS_createDefaultDirectories', - 'FS_createDefaultDevices', - 'FS_createSpecialDirectories', - 'FS_createStandardStreams', - 'FS_staticInit', - 'FS_init', - 'FS_quit', - 'FS_findObject', - 'FS_analyzePath', - 'FS_createFile', - 'FS_createDataFile', - 'FS_forceLoadFile', - 'FS_createLazyFile', - 'MEMFS', - 'TTY', - 'PIPEFS', - 'SOCKFS', - 'tempFixedLengthArray', - 'miniTempWebGLFloatBuffers', - 'miniTempWebGLIntBuffers', - 'GL', - 'AL', - 'GLUT', - 'EGL', - 'GLEW', - 'IDBStore', - 'SDL', - 'SDL_gfx', - 'print', - 'printErr', - 'jstoi_s', -]; -unexportedSymbols.forEach(unexportedRuntimeSymbol); - - // End runtime exports - // Begin JS library exports - // End JS library exports - -// end include: postlibrary.js - -function checkIncomingModuleAPI() { - ignoredModuleProp('fetchSettings'); - ignoredModuleProp('logReadFiles'); - ignoredModuleProp('loadSplitModule'); - ignoredModuleProp('onMalloc'); - ignoredModuleProp('onRealloc'); - ignoredModuleProp('onFree'); - ignoredModuleProp('onSbrkGrow'); -} - -// Imports from the Wasm binary. -var _greet = Module['_greet'] = makeInvalidEarlyAccess('_greet'); -var _fflush = makeInvalidEarlyAccess('_fflush'); -var _emscripten_stack_init = makeInvalidEarlyAccess('_emscripten_stack_init'); -var _emscripten_stack_get_free = makeInvalidEarlyAccess('_emscripten_stack_get_free'); -var _emscripten_stack_get_base = makeInvalidEarlyAccess('_emscripten_stack_get_base'); -var _emscripten_stack_get_end = makeInvalidEarlyAccess('_emscripten_stack_get_end'); -var __emscripten_stack_restore = makeInvalidEarlyAccess('__emscripten_stack_restore'); -var __emscripten_stack_alloc = makeInvalidEarlyAccess('__emscripten_stack_alloc'); -var _emscripten_stack_get_current = makeInvalidEarlyAccess('_emscripten_stack_get_current'); -var memory = makeInvalidEarlyAccess('memory'); -var __indirect_function_table = makeInvalidEarlyAccess('__indirect_function_table'); -var wasmMemory = makeInvalidEarlyAccess('wasmMemory'); - -function assignWasmExports(wasmExports) { - assert(typeof wasmExports['greet'] != 'undefined', 'missing Wasm export: greet'); - assert(typeof wasmExports['fflush'] != 'undefined', 'missing Wasm export: fflush'); - assert(typeof wasmExports['emscripten_stack_init'] != 'undefined', 'missing Wasm export: emscripten_stack_init'); - assert(typeof wasmExports['emscripten_stack_get_free'] != 'undefined', 'missing Wasm export: emscripten_stack_get_free'); - assert(typeof wasmExports['emscripten_stack_get_base'] != 'undefined', 'missing Wasm export: emscripten_stack_get_base'); - assert(typeof wasmExports['emscripten_stack_get_end'] != 'undefined', 'missing Wasm export: emscripten_stack_get_end'); - assert(typeof wasmExports['_emscripten_stack_restore'] != 'undefined', 'missing Wasm export: _emscripten_stack_restore'); - assert(typeof wasmExports['_emscripten_stack_alloc'] != 'undefined', 'missing Wasm export: _emscripten_stack_alloc'); - assert(typeof wasmExports['emscripten_stack_get_current'] != 'undefined', 'missing Wasm export: emscripten_stack_get_current'); - assert(typeof wasmExports['memory'] != 'undefined', 'missing Wasm export: memory'); - assert(typeof wasmExports['__indirect_function_table'] != 'undefined', 'missing Wasm export: __indirect_function_table'); - _greet = Module['_greet'] = createExportWrapper('greet', 0); - _fflush = createExportWrapper('fflush', 1); - _emscripten_stack_init = wasmExports['emscripten_stack_init']; - _emscripten_stack_get_free = wasmExports['emscripten_stack_get_free']; - _emscripten_stack_get_base = wasmExports['emscripten_stack_get_base']; - _emscripten_stack_get_end = wasmExports['emscripten_stack_get_end']; - __emscripten_stack_restore = wasmExports['_emscripten_stack_restore']; - __emscripten_stack_alloc = wasmExports['_emscripten_stack_alloc']; - _emscripten_stack_get_current = wasmExports['emscripten_stack_get_current']; - memory = wasmMemory = wasmExports['memory']; - __indirect_function_table = wasmExports['__indirect_function_table']; -} - -var wasmImports = { - -}; - - -// include: postamble.js -// === Auto-generated postamble setup entry stuff === - -var calledRun; - -function stackCheckInit() { - // This is normally called automatically during __wasm_call_ctors but need to - // get these values before even running any of the ctors so we call it redundantly - // here. - _emscripten_stack_init(); - // TODO(sbc): Move writeStackCookie to native to to avoid this. - writeStackCookie(); -} - -function run() { - - if (runDependencies > 0) { - dependenciesFulfilled = run; - return; - } - - stackCheckInit(); - - preRun(); - - // a preRun added a dependency, run will be called later - if (runDependencies > 0) { - dependenciesFulfilled = run; - return; - } - - function doRun() { - // run may have just been called through dependencies being fulfilled just in this very frame, - // or while the async setStatus time below was happening - assert(!calledRun); - calledRun = true; - Module['calledRun'] = true; - - if (ABORT) return; - - initRuntime(); - - Module['onRuntimeInitialized']?.(); - consumedModuleProp('onRuntimeInitialized'); - - assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'); - - postRun(); - } - - if (Module['setStatus']) { - Module['setStatus']('Running...'); - setTimeout(() => { - setTimeout(() => Module['setStatus'](''), 1); - doRun(); - }, 1); - } else - { - doRun(); - } - checkStackCookie(); -} - -function checkUnflushedContent() { - // Compiler settings do not allow exiting the runtime, so flushing - // the streams is not possible. but in ASSERTIONS mode we check - // if there was something to flush, and if so tell the user they - // should request that the runtime be exitable. - // Normally we would not even include flush() at all, but in ASSERTIONS - // builds we do so just for this check, and here we see if there is any - // content to flush, that is, we check if there would have been - // something a non-ASSERTIONS build would have not seen. - // How we flush the streams depends on whether we are in SYSCALLS_REQUIRE_FILESYSTEM=0 - // mode (which has its own special function for this; otherwise, all - // the code is inside libc) - var oldOut = out; - var oldErr = err; - var has = false; - out = err = (x) => { - has = true; - } - try { // it doesn't matter if it fails - _fflush(0); - } catch(e) {} - out = oldOut; - err = oldErr; - if (has) { - warnOnce('stdio streams had content in them that was not flushed. you should set EXIT_RUNTIME to 1 (see the Emscripten FAQ), or make sure to emit a newline when you printf etc.'); - warnOnce('(this may also be due to not including full filesystem support - try building with -sFORCE_FILESYSTEM)'); - } -} - -var wasmExports; - -// With async instantation wasmExports is assigned asynchronously when the -// instance is received. -createWasm(); - -run(); - -// end include: postamble.js - diff --git a/test/cross_origin_storage/index.wasm b/test/cross_origin_storage/index.wasm deleted file mode 100755 index a7b17d387c1852563c9f194925da15d7892a7698..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 994 zcmaJ=&2H2%5T3EK+hyH0TPV<0m2kIvSSiXCI5tQi-hgA8O}$Il{MB&^y)>ah{2w`R z;J^d$1UwzK(@Kb~kP>_R%{SjnCS!=I3;_Ti^p9!NG}!3vqQQ$}fMd`nY(T%*PkmtE9+770ptC(4WuKx_Sx2yCSP%nUqT8VWpyYsbdla zWOoH$2rZ+Pf!^$!MU?<&=f*gZpFnRnT=7X!>6!(w`>Q_vMMVtj`zt}Fkhqpo|D@mQy4Z1l;*addT>eH-cOl%sCoGgC@7MpI_&UAsM#qf~qb9e%e=*Vuw(E~cvNhX%V_HV`9+-j0W_7ysyDQ+i ze;xdyRNg&v7yRqV_lq+j(;{DZ!>mqKau%hv@Z7An7r|eo#VkrI?@nIiN=9Xw Date: Tue, 9 Jun 2026 18:30:55 +0200 Subject: [PATCH 48/74] test: log COS events to console in miss_then_hit browser test Makes the cache-miss store and cache-hit paths visible in the Chrome console output, which aids debugging and confirms the extension keyed the entry on the correct hash. --- test/test_browser.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/test_browser.py b/test/test_browser.py index c4436aa9854d0..584c22e63335c 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5743,8 +5743,14 @@ def test_cross_origin_storage_miss_then_hit(self): # COS callbacks; otherwise Emscripten aborts on the unknown Module props. create_file('cos_pre.js', ''' var Module = { - onCOSStore: function(hash) { reportResultToServer('stored'); }, - onCOSCacheHit: function(hash) { reportResultToServer('cache-hit'); }, + onCOSStore: function(hash) { + console.log('[COS] stored, SHA-256:', hash); + reportResultToServer('stored'); + }, + onCOSCacheHit: function(hash) { + console.log('[COS] cache-hit, SHA-256:', hash); + reportResultToServer('cache-hit'); + }, }; ''') self.compile_btest('browser_test_hello_world.c', [ From 09ae30def74d2dcd47fd5a5aafd22ccfea8f0b87 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:11:04 +0200 Subject: [PATCH 49/74] style: move [experimental] tag to end of CROSS_ORIGIN_STORAGE comment Per reviewer feedback, [experimental] should be placed at the end of the comment block, adjacent to [link], matching the convention used by all other experimental settings (WASMFS, PURE_WASI, SPLIT_MODULE, etc.). --- src/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings.js b/src/settings.js index ce8b7e7ed16c3..0ac7595d796c2 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2202,7 +2202,6 @@ var GROWABLE_ARRAYBUFFERS = false; // indirectly using `importScripts` var CROSS_ORIGIN = false; -// [experimental] // Enables Cross-Origin Storage (COS) API support for Wasm // loading on the Web target. At link time Emscripten computes the SHA-256 // hash of the final ``.wasm`` binary and embeds it in the generated JS. @@ -2217,6 +2216,7 @@ var CROSS_ORIGIN = false; // See :ref:`CrossOriginStorage` for the full guide. // // [link] +// [experimental] var CROSS_ORIGIN_STORAGE = 0; // Controls which origins may read the Wasm binary from the COS cache. Only From e624ca22948f954ce041d9dc18a432e56bf8633e Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:18:58 +0200 Subject: [PATCH 50/74] fix: remove redundant cosHash?.value guard in COS preamble wasmHash is unconditionally set inside #if CROSS_ORIGIN_STORAGE so the cosHash?.value truthiness check was always true at this point. Per reviewer feedback, simplify to only check for the API presence. --- src/preamble.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/preamble.js b/src/preamble.js index 952e691c0eaf4..5304672e22a7a 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -633,7 +633,7 @@ async function instantiateAsync(binary, binaryFile, imports) { // Any error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. var cosHash = Module['wasmHash']; - if (cosHash?.value && 'crossOriginStorage' in navigator) { + if ('crossOriginStorage' in navigator) { try { var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); // Cache hit — read the Blob and instantiate from its ArrayBuffer. From a1e290c2398b0cff72f2627cdc710c0b3ed8a048 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:28:14 +0200 Subject: [PATCH 51/74] fix: make CROSS_ORIGIN_STORAGE a hard error for non-web environments Previously a warning was emitted when CROSS_ORIGIN_STORAGE was set without a web environment; the preamble.js guard relied on ENVIRONMENT_MAY_BE_WEB to strip the dead COS code. Promote to a hard link-time error so the guard is unnecessary, then drop the outer #if ENVIRONMENT_MAY_BE_WEB wrapper. Per reviewer feedback (r3383088794). --- .../tools_reference/settings_reference.rst | 5 +++-- src/preamble.js | 2 -- src/settings.js | 5 +++-- test/test_other.py | 21 +++++++------------ tools/link.py | 2 +- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 74a7e1314aae0..39dfcd67a07ee 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3366,14 +3366,15 @@ fetched from the shared cross-origin cache on a hit, or stored there after a network fetch on a miss; when the API is absent or errors the runtime falls through to the standard fetch path. -Only meaningful for the Web environment. Incompatible with SINGLE_FILE and +Requires the Web environment; using it without ``-sENVIRONMENT=web`` is a +hard link-time error. Incompatible with SINGLE_FILE and WASM_ASYNC_COMPILATION=0 (both produce hard link-time errors). See :ref:`CrossOriginStorage` for the full guide. .. note:: This is an experimental setting -Default value: 0 +Default value: false .. _cross_origin_storage_origins: diff --git a/src/preamble.js b/src/preamble.js index 5304672e22a7a..fa5c8bcc6a1ef 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -626,7 +626,6 @@ async function instantiateArrayBuffer(binaryFile, imports) { async function instantiateAsync(binary, binaryFile, imports) { #if !SINGLE_FILE -#if ENVIRONMENT_MAY_BE_WEB #if CROSS_ORIGIN_STORAGE // Cross-Origin Storage (COS) progressive enhancement. // https://github.com/WICG/cross-origin-storage @@ -689,7 +688,6 @@ async function instantiateAsync(binary, binaryFile, imports) { } } #endif // CROSS_ORIGIN_STORAGE -#endif // ENVIRONMENT_MAY_BE_WEB if (!binary #if MIN_SAFARI_VERSION < 150000 // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming diff --git a/src/settings.js b/src/settings.js index 0ac7595d796c2..80680960d99b7 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2210,14 +2210,15 @@ var CROSS_ORIGIN = false; // a network fetch on a miss; when the API is absent or errors the runtime // falls through to the standard fetch path. // -// Only meaningful for the Web environment. Incompatible with SINGLE_FILE and +// Requires the Web environment; using it without ``-sENVIRONMENT=web`` is a +// hard link-time error. Incompatible with SINGLE_FILE and // WASM_ASYNC_COMPILATION=0 (both produce hard link-time errors). // // See :ref:`CrossOriginStorage` for the full guide. // // [link] // [experimental] -var CROSS_ORIGIN_STORAGE = 0; +var CROSS_ORIGIN_STORAGE = false; // Controls which origins may read the Wasm binary from the COS cache. Only // meaningful when ``-sCROSS_ORIGIN_STORAGE`` is set. Applied only during the diff --git a/test/test_other.py b/test/test_other.py index c59203bff7f83..74a32623ba099 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15429,20 +15429,13 @@ def test_cross_origin_storage_disabled_by_default(self): js = read_file('hello.js') self.assertNotContained('crossOriginStorage', js) - def test_cross_origin_storage_not_emitted_for_node_target(self): - """COS code must NOT appear when targeting Node.js only, even with the flag set. - - The #if ENVIRONMENT_MAY_BE_WEB guard strips it. A warning must also be - emitted since the flag does nothing in this configuration. - """ - proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=node', - '-o', 'hello.js'], - stderr=PIPE) - self.assertNotContained('crossOriginStorage', read_file('hello.js')) - self.assertContained('CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web', - proc.stderr) + def test_cross_origin_storage_error_for_non_web_target(self): + """CROSS_ORIGIN_STORAGE + non-web environment must be a hard link-time error.""" + self.assert_fail([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE', + '-sENVIRONMENT=node', + '-o', 'hello.js'], + 'CROSS_ORIGIN_STORAGE requires a web environment') def test_cross_origin_storage_error_with_single_file(self): """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" diff --git a/tools/link.py b/tools/link.py index 18f03e5925310..539796722853f 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1212,7 +1212,7 @@ def limit_incoming_module_api(): if settings.CROSS_ORIGIN_STORAGE: if not settings.ENVIRONMENT_MAY_BE_WEB: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web (navigator.crossOriginStorage is not available outside the browser)') + exit_with_error('CROSS_ORIGIN_STORAGE requires a web environment (navigator.crossOriginStorage is not available outside the browser)') if settings.SPLIT_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') if settings.MAIN_MODULE: From 46d948f0091220e37f7f2ea2d564b4b3a57a2aaa Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:32:07 +0200 Subject: [PATCH 52/74] remove: Makefile from COS example directory The README already contains the full build command; the Makefile is redundant and raises cross-platform concerns since make.exe isn't guaranteed. Per reviewer feedback (r3383095225). --- test/cross_origin_storage/Makefile | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 test/cross_origin_storage/Makefile diff --git a/test/cross_origin_storage/Makefile b/test/cross_origin_storage/Makefile deleted file mode 100644 index 024872280c5e6..0000000000000 --- a/test/cross_origin_storage/Makefile +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2026 The Emscripten Authors. All rights reserved. -# Emscripten is available under two separate licenses, the MIT license and the -# University of Illinois/NCSA Open Source License. Both these licenses can be -# found in the LICENSE file. - -EMCC ?= emcc - -.PHONY: all clean - -all: index.js - -index.js: main.cpp - $(EMCC) main.cpp -o index.js \ - -O2 \ - -sCROSS_ORIGIN_STORAGE \ - -sENVIRONMENT=web \ - -sEXPORTED_RUNTIME_METHODS=ccall \ - -sEXPORTED_FUNCTIONS=_greet \ - -sALLOW_MEMORY_GROWTH - -clean: - rm -f index.js index.wasm From 408118d0a03dbe1965b8102ebc0eecb2e3692ddc Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:35:23 +0200 Subject: [PATCH 53/74] style: drop -o flag from COS error-only tests Tests that only check link-time error messages never produce output; the -o flag is unnecessary. Per reviewer feedback (r3383111605). --- test/test_other.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 74a32623ba099..f5ca42231deec 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15433,8 +15433,7 @@ def test_cross_origin_storage_error_for_non_web_target(self): """CROSS_ORIGIN_STORAGE + non-web environment must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=node', - '-o', 'hello.js'], + '-sENVIRONMENT=node'], 'CROSS_ORIGIN_STORAGE requires a web environment') def test_cross_origin_storage_error_with_single_file(self): @@ -15442,8 +15441,7 @@ def test_cross_origin_storage_error_with_single_file(self): self.assert_fail([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sSINGLE_FILE', - '-o', 'hello.js'], + '-sSINGLE_FILE'], 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') def test_cross_origin_storage_error_without_async_compilation(self): @@ -15451,8 +15449,7 @@ def test_cross_origin_storage_error_without_async_compilation(self): self.assert_fail([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sWASM_ASYNC_COMPILATION=0', - '-o', 'hello.js'], + '-sWASM_ASYNC_COMPILATION=0'], 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') def test_cross_origin_storage_warning_with_split_module(self): @@ -15563,8 +15560,7 @@ def test_cross_origin_storage_origins_error_mixed_wildcard(self): [EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]', - '-o', 'hello.js'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]'], "'*' must not be mixed with explicit origins") def test_cross_origin_storage_origins_error_invalid_origin(self): @@ -15573,8 +15569,7 @@ def test_cross_origin_storage_origins_error_invalid_origin(self): [EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]', - '-o', 'hello.js'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]'], 'is not a valid HTTPS origin') def test_cross_origin_storage_origins_error_origin_with_path(self): @@ -15583,8 +15578,7 @@ def test_cross_origin_storage_origins_error_origin_with_path(self): [EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]', - '-o', 'hello.js'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]'], 'is not a valid HTTPS origin') def test_cross_origin_storage_wasm_hash_module_property(self): From ed92f2f8e8437a6e85016d8bb1cd6e23a76a4390 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:37:28 +0200 Subject: [PATCH 54/74] style: prefer hello_world.c over hello_world.cpp in COS tests Per reviewer feedback (r3383113554). --- test/test_other.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index f5ca42231deec..04baf11f2c06d 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15368,7 +15368,7 @@ def test_cross_origin_storage_js_output(self): The embedded hash must be the correct SHA-256 of the compiled .wasm file. """ - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) @@ -15411,7 +15411,7 @@ def test_cross_origin_storage_js_output(self): def test_cross_origin_storage_callbacks_opt_in(self): """COS instrumentation callbacks are emitted only when opted in via INCOMING_MODULE_JS_API.""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sINCOMING_MODULE_JS_API=onCOSCacheHit,onCOSCacheMiss,onCOSStore', @@ -15423,7 +15423,7 @@ def test_cross_origin_storage_callbacks_opt_in(self): def test_cross_origin_storage_disabled_by_default(self): """COS code must NOT appear when the flag is omitted (default off).""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') @@ -15431,14 +15431,14 @@ def test_cross_origin_storage_disabled_by_default(self): def test_cross_origin_storage_error_for_non_web_target(self): """CROSS_ORIGIN_STORAGE + non-web environment must be a hard link-time error.""" - self.assert_fail([EMCC, test_file('hello_world.cpp'), + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=node'], 'CROSS_ORIGIN_STORAGE requires a web environment') def test_cross_origin_storage_error_with_single_file(self): """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" - self.assert_fail([EMCC, test_file('hello_world.cpp'), + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sSINGLE_FILE'], @@ -15446,7 +15446,7 @@ def test_cross_origin_storage_error_with_single_file(self): def test_cross_origin_storage_error_without_async_compilation(self): """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must be a hard link-time error.""" - self.assert_fail([EMCC, test_file('hello_world.cpp'), + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sWASM_ASYNC_COMPILATION=0'], @@ -15454,7 +15454,7 @@ def test_cross_origin_storage_error_without_async_compilation(self): def test_cross_origin_storage_warning_with_split_module(self): """CROSS_ORIGIN_STORAGE + SPLIT_MODULE must warn that secondary files are not covered.""" - proc = self.run_process([EMCC, test_file('hello_world.cpp'), + proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sSPLIT_MODULE', @@ -15465,7 +15465,7 @@ def test_cross_origin_storage_warning_with_split_module(self): def test_cross_origin_storage_warning_with_main_module(self): """CROSS_ORIGIN_STORAGE + MAIN_MODULE must warn that side modules are not covered.""" - proc = self.run_process([EMCC, test_file('hello_world.cpp'), + proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sMAIN_MODULE', @@ -15476,7 +15476,7 @@ def test_cross_origin_storage_warning_with_main_module(self): def test_cross_origin_storage_warning_with_side_module(self): """CROSS_ORIGIN_STORAGE + SIDE_MODULE must warn: no JS glue is emitted.""" - proc = self.run_process([EMCC, test_file('hello_world.cpp'), + proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sSIDE_MODULE', '-o', 'hello.wasm'], @@ -15486,7 +15486,7 @@ def test_cross_origin_storage_warning_with_side_module(self): def test_cross_origin_storage_hash_changes_with_content(self): """Two different programs must produce different embedded hashes.""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) @@ -15512,7 +15512,7 @@ def test_cross_origin_storage_origins_default_is_global(self): Globally available; the user only needs -sCROSS_ORIGIN_STORAGE. """ - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) @@ -15523,7 +15523,7 @@ def test_cross_origin_storage_origins_explicit_wildcard(self): This matches the implicit default. """ - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', "-sCROSS_ORIGIN_STORAGE_ORIGINS=['*']", @@ -15532,7 +15532,7 @@ def test_cross_origin_storage_origins_explicit_wildcard(self): def test_cross_origin_storage_origins_explicit_list(self): """An explicit origins list must be emitted as a JS array.""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com', @@ -15544,7 +15544,7 @@ def test_cross_origin_storage_origins_explicit_list(self): def test_cross_origin_storage_origins_same_site(self): """Empty origins list must omit the origins key entirely (same-site only).""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=[]', @@ -15557,7 +15557,7 @@ def test_cross_origin_storage_origins_same_site(self): def test_cross_origin_storage_origins_error_mixed_wildcard(self): """Mixing '*' with explicit origins must be a link-time error.""" self.assert_fail( - [EMCC, test_file('hello_world.cpp'), + [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]'], @@ -15566,7 +15566,7 @@ def test_cross_origin_storage_origins_error_mixed_wildcard(self): def test_cross_origin_storage_origins_error_invalid_origin(self): """A non-HTTPS or malformed origin must be a link-time error.""" self.assert_fail( - [EMCC, test_file('hello_world.cpp'), + [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]'], @@ -15575,7 +15575,7 @@ def test_cross_origin_storage_origins_error_invalid_origin(self): def test_cross_origin_storage_origins_error_origin_with_path(self): """An origin with a path component must be a link-time error.""" self.assert_fail( - [EMCC, test_file('hello_world.cpp'), + [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]'], @@ -15587,7 +15587,7 @@ def test_cross_origin_storage_wasm_hash_module_property(self): Custom Module['instantiateWasm'] implementations bypass instantiateAsync() and can read Module['wasmHash'] to get the build-time hash object. """ - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) @@ -15605,7 +15605,7 @@ def test_cross_origin_storage_wasm_hash_module_property(self): def test_cross_origin_storage_wasm_hash_absent_without_flag(self): """Module['wasmHash'] must NOT appear when CROSS_ORIGIN_STORAGE is off.""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') From 278eb7f79abe2b6aabacb1d30d99d33da796ece4 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:41:10 +0200 Subject: [PATCH 55/74] style: drop docstrings and comments from COS tests Test names already convey intent; per reviewer feedback (r3383119508). --- test/test_other.py | 64 ---------------------------------------------- 1 file changed, 64 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 04baf11f2c06d..cba69e8a1e247 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15358,59 +15358,31 @@ def test_logReadFiles(self): output = self.do_runf('checksummer.c', args=['test.txt'], cflags=['--pre-js=pre.js']) self.assertContained('read file: /test.txt', output) - # --------------------------------------------------------------------------- - # Tests for CROSS_ORIGIN_STORAGE (-sCROSS_ORIGIN_STORAGE) - # https://github.com/WICG/cross-origin-storage - # --------------------------------------------------------------------------- - def test_cross_origin_storage_js_output(self): - """COS code is present in JS when the feature is enabled for the web target. - - The embedded hash must be the correct SHA-256 of the compiled .wasm file. - """ self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') - - # Feature-detect pattern from the WICG explainer. self.assertContained("'crossOriginStorage' in navigator", js) - - # Correct API call. self.assertContained('navigator.crossOriginStorage.requestFileHandles', js) - - # Hash object shape required by the spec. self.assertContained("algorithm: 'SHA-256'", js) - - # Globally-available flag appropriate for a public Wasm module. self.assertContained("origins: '*'", js) - - # Cache-hit and cache-miss path markers. self.assertContained('getFile()', js) self.assertContained('createWritable()', js) - - # Error name discrimination. self.assertContained("'NotFoundError'", js) self.assertContained("'NotAllowedError'", js) - - # Callbacks are opt-in; must be absent from a default build. self.assertNotContained("Module['onCOSCacheHit']", js) self.assertNotContained("Module['onCOSCacheMiss']", js) self.assertNotContained("Module['onCOSStore']", js) - - # The hash embedded in the JS must be a 64-char lowercase hex string … m = re.search(r"value:\s*'([0-9a-f]{64})'", js) self.assertTrue(m, 'could not find a 64-char hex hash value in JS output') embedded_hash = m.group(1) - - # … and must exactly match the SHA-256 of the emitted .wasm file. expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() self.assertEqual(embedded_hash, expected_hash, 'embedded wasm hash does not match actual .wasm SHA-256') def test_cross_origin_storage_callbacks_opt_in(self): - """COS instrumentation callbacks are emitted only when opted in via INCOMING_MODULE_JS_API.""" self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15422,7 +15394,6 @@ def test_cross_origin_storage_callbacks_opt_in(self): self.assertContained("Module['onCOSStore']", js) def test_cross_origin_storage_disabled_by_default(self): - """COS code must NOT appear when the flag is omitted (default off).""" self.run_process([EMCC, test_file('hello_world.c'), '-sENVIRONMENT=web', '-o', 'hello.js']) @@ -15430,14 +15401,12 @@ def test_cross_origin_storage_disabled_by_default(self): self.assertNotContained('crossOriginStorage', js) def test_cross_origin_storage_error_for_non_web_target(self): - """CROSS_ORIGIN_STORAGE + non-web environment must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=node'], 'CROSS_ORIGIN_STORAGE requires a web environment') def test_cross_origin_storage_error_with_single_file(self): - """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15445,7 +15414,6 @@ def test_cross_origin_storage_error_with_single_file(self): 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') def test_cross_origin_storage_error_without_async_compilation(self): - """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15453,7 +15421,6 @@ def test_cross_origin_storage_error_without_async_compilation(self): 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') def test_cross_origin_storage_warning_with_split_module(self): - """CROSS_ORIGIN_STORAGE + SPLIT_MODULE must warn that secondary files are not covered.""" proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15464,7 +15431,6 @@ def test_cross_origin_storage_warning_with_split_module(self): proc.stderr) def test_cross_origin_storage_warning_with_main_module(self): - """CROSS_ORIGIN_STORAGE + MAIN_MODULE must warn that side modules are not covered.""" proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15475,7 +15441,6 @@ def test_cross_origin_storage_warning_with_main_module(self): proc.stderr) def test_cross_origin_storage_warning_with_side_module(self): - """CROSS_ORIGIN_STORAGE + SIDE_MODULE must warn: no JS glue is emitted.""" proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sSIDE_MODULE', @@ -15485,7 +15450,6 @@ def test_cross_origin_storage_warning_with_side_module(self): proc.stderr) def test_cross_origin_storage_hash_changes_with_content(self): - """Two different programs must produce different embedded hashes.""" self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15503,15 +15467,7 @@ def test_cross_origin_storage_hash_changes_with_content(self): self.assertNotEqual(hash_a, hash_b, 'different programs should produce different embedded wasm hashes') - # --------------------------------------------------------------------------- - # Tests for CROSS_ORIGIN_STORAGE_ORIGINS - # --------------------------------------------------------------------------- - def test_cross_origin_storage_origins_default_is_global(self): - """Without -sCROSS_ORIGIN_STORAGE_ORIGINS, the default must be origins:'*'. - - Globally available; the user only needs -sCROSS_ORIGIN_STORAGE. - """ self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15519,10 +15475,6 @@ def test_cross_origin_storage_origins_default_is_global(self): self.assertContained("origins: '*'", read_file('hello.js')) def test_cross_origin_storage_origins_explicit_wildcard(self): - """Explicitly passing -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] must emit origins:'*'. - - This matches the implicit default. - """ self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15531,7 +15483,6 @@ def test_cross_origin_storage_origins_explicit_wildcard(self): self.assertContained("origins: '*'", read_file('hello.js')) def test_cross_origin_storage_origins_explicit_list(self): - """An explicit origins list must be emitted as a JS array.""" self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15543,19 +15494,16 @@ def test_cross_origin_storage_origins_explicit_list(self): self.assertNotContained("origins: '*'", js) def test_cross_origin_storage_origins_same_site(self): - """Empty origins list must omit the origins key entirely (same-site only).""" self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=[]', '-o', 'hello.js']) js = read_file('hello.js') - # { create: true } with no origins field self.assertContained('{ create: true }', js) self.assertNotContained('origins:', js) def test_cross_origin_storage_origins_error_mixed_wildcard(self): - """Mixing '*' with explicit origins must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', @@ -15564,7 +15512,6 @@ def test_cross_origin_storage_origins_error_mixed_wildcard(self): "'*' must not be mixed with explicit origins") def test_cross_origin_storage_origins_error_invalid_origin(self): - """A non-HTTPS or malformed origin must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', @@ -15573,7 +15520,6 @@ def test_cross_origin_storage_origins_error_invalid_origin(self): 'is not a valid HTTPS origin') def test_cross_origin_storage_origins_error_origin_with_path(self): - """An origin with a path component must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', @@ -15582,29 +15528,19 @@ def test_cross_origin_storage_origins_error_origin_with_path(self): 'is not a valid HTTPS origin') def test_cross_origin_storage_wasm_hash_module_property(self): - """Module['wasmHash'] must be set in the JS output and match the .wasm hash. - - Custom Module['instantiateWasm'] implementations bypass instantiateAsync() - and can read Module['wasmHash'] to get the build-time hash object. - """ self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') - - # The property must be present with a 64-char hex value. m = re.search(r"Module\['wasmHash'\]\s*=\s*\{[^}]*value:\s*'([0-9a-f]{64})'", js) self.assertTrue(m, "Module['wasmHash'] not found in JS output") embedded_hash = m.group(1) - - # It must equal the SHA-256 of the actual .wasm file. expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() self.assertEqual(embedded_hash, expected_hash, "Module['wasmHash'] does not match the actual .wasm SHA-256") def test_cross_origin_storage_wasm_hash_absent_without_flag(self): - """Module['wasmHash'] must NOT appear when CROSS_ORIGIN_STORAGE is off.""" self.run_process([EMCC, test_file('hello_world.c'), '-sENVIRONMENT=web', '-o', 'hello.js']) From f2c9029583ae0b77c796c421f23baa174b5a59d3 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:43:19 +0200 Subject: [PATCH 56/74] refactor: extract setup_cross_origin_storage() from phase_linker_setup Per reviewer feedback (r3383144746) to help reduce the size of the already large phase_linker_setup function. --- tools/link.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/tools/link.py b/tools/link.py index 539796722853f..55ff1984a4d88 100644 --- a/tools/link.py +++ b/tools/link.py @@ -795,6 +795,25 @@ def get_dylibs(options, linker_args): return dylibs +def setup_cross_origin_storage(): + if not settings.ENVIRONMENT_MAY_BE_WEB: + exit_with_error('CROSS_ORIGIN_STORAGE requires a web environment (navigator.crossOriginStorage is not available outside the browser)') + if settings.SPLIT_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') + if settings.MAIN_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') + if settings.SIDE_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') + origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS + if '*' in origins and len(origins) > 1: + exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") + for o in origins: + if o == '*': + continue + if not re.fullmatch(r'https://[^/]+(:\d+)?', o): + exit_with_error(f"CROSS_ORIGIN_STORAGE_ORIGINS: {o!r} is not a valid HTTPS origin (expected 'https://host' or 'https://host:port')") + + @ToolchainProfiler.profile_block('linker_setup') def phase_linker_setup(options, linker_args): # noqa: C901, PLR0912, PLR0915 """Future modifications should consider refactoring to reduce complexity. @@ -1211,24 +1230,7 @@ def limit_incoming_module_api(): exit_with_error('cannot have both WASM=2 and SINGLE_FILE enabled at the same time') if settings.CROSS_ORIGIN_STORAGE: - if not settings.ENVIRONMENT_MAY_BE_WEB: - exit_with_error('CROSS_ORIGIN_STORAGE requires a web environment (navigator.crossOriginStorage is not available outside the browser)') - if settings.SPLIT_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') - if settings.MAIN_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') - if settings.SIDE_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') - origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS - if '*' in origins and len(origins) > 1: - exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") - for o in origins: - if o == '*': - continue - # Each explicit origin must be a valid serialized HTTPS origin: - # scheme "https://", host, optional ":port", no path/query/fragment. - if not re.fullmatch(r'https://[^/]+(:\d+)?', o): - exit_with_error(f"CROSS_ORIGIN_STORAGE_ORIGINS: {o!r} is not a valid HTTPS origin (expected 'https://host' or 'https://host:port')") + setup_cross_origin_storage() if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML: exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output') From 8d399faa0b5817ebe19d7e1fe5c7a169f8d5dee3 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:47:00 +0200 Subject: [PATCH 57/74] remove: COS example build artifact entries from .gitignore No test builds into test/cross_origin_storage/ and the Makefile has been removed, so these entries are no longer needed. Per reviewer feedback (r3383036408). --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 33e2179a20b3a..234517cc97797 100644 --- a/.gitignore +++ b/.gitignore @@ -30,10 +30,6 @@ coverage.xml # Test output /out/ -# COS example build artifacts -/test/cross_origin_storage/index.js -/test/cross_origin_storage/index.wasm - # When updating the website we check it out here. /site/emscripten-site/ From b09b640e526e4de7a8d78e24865ea152277c5e78 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:53:45 +0200 Subject: [PATCH 58/74] refactor: consolidate 19 COS unit tests into 5 - test_cross_origin_storage: core feature + disabled-by-default checks - test_cross_origin_storage_callbacks: opt-in via INCOMING_MODULE_JS_API - test_cross_origin_storage_warnings: SPLIT_MODULE + MAIN_MODULE + SIDE_MODULE - test_cross_origin_storage_errors: non-web + SINGLE_FILE + WASM_ASYNC=0 - test_cross_origin_storage_origins: explicit list + same-site + origin errors Drops redundant duplicate tests (origins_default, explicit_wildcard, wasm_hash_property, hash_changes_with_content). Per reviewer feedback. --- test/test_other.py | 110 +++++++++------------------------------------ 1 file changed, 20 insertions(+), 90 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index cba69e8a1e247..852a035080feb 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15358,7 +15358,7 @@ def test_logReadFiles(self): output = self.do_runf('checksummer.c', args=['test.txt'], cflags=['--pre-js=pre.js']) self.assertContained('read file: /test.txt', output) - def test_cross_origin_storage_js_output(self): + def test_cross_origin_storage(self): self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15381,8 +15381,14 @@ def test_cross_origin_storage_js_output(self): expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() self.assertEqual(embedded_hash, expected_hash, 'embedded wasm hash does not match actual .wasm SHA-256') + self.run_process([EMCC, test_file('hello_world.c'), + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertNotContained('crossOriginStorage', js) + self.assertNotContained("Module['wasmHash']", js) - def test_cross_origin_storage_callbacks_opt_in(self): + def test_cross_origin_storage_callbacks(self): self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15393,34 +15399,7 @@ def test_cross_origin_storage_callbacks_opt_in(self): self.assertContained("Module['onCOSCacheMiss']", js) self.assertContained("Module['onCOSStore']", js) - def test_cross_origin_storage_disabled_by_default(self): - self.run_process([EMCC, test_file('hello_world.c'), - '-sENVIRONMENT=web', - '-o', 'hello.js']) - js = read_file('hello.js') - self.assertNotContained('crossOriginStorage', js) - - def test_cross_origin_storage_error_for_non_web_target(self): - self.assert_fail([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=node'], - 'CROSS_ORIGIN_STORAGE requires a web environment') - - def test_cross_origin_storage_error_with_single_file(self): - self.assert_fail([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-sSINGLE_FILE'], - 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') - - def test_cross_origin_storage_error_without_async_compilation(self): - self.assert_fail([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-sWASM_ASYNC_COMPILATION=0'], - 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') - - def test_cross_origin_storage_warning_with_split_module(self): + def test_cross_origin_storage_warnings(self): proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15429,8 +15408,6 @@ def test_cross_origin_storage_warning_with_split_module(self): stderr=PIPE) self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', proc.stderr) - - def test_cross_origin_storage_warning_with_main_module(self): proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15439,8 +15416,6 @@ def test_cross_origin_storage_warning_with_main_module(self): stderr=PIPE) self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', proc.stderr) - - def test_cross_origin_storage_warning_with_side_module(self): proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sSIDE_MODULE', @@ -15449,40 +15424,23 @@ def test_cross_origin_storage_warning_with_side_module(self): self.assertContained('CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds', proc.stderr) - def test_cross_origin_storage_hash_changes_with_content(self): - self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-o', 'hello.js']) - js_a = read_file('hello.js') - hash_a = re.search(r"value:\s*'([0-9a-f]{64})'", js_a).group(1) - - self.run_process([EMCC, test_file('hello_world_small.c'), + def test_cross_origin_storage_errors(self): + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-o', 'small.js']) - js_b = read_file('small.js') - hash_b = re.search(r"value:\s*'([0-9a-f]{64})'", js_b).group(1) - - self.assertNotEqual(hash_a, hash_b, - 'different programs should produce different embedded wasm hashes') - - def test_cross_origin_storage_origins_default_is_global(self): - self.run_process([EMCC, test_file('hello_world.c'), + '-sENVIRONMENT=node'], + 'CROSS_ORIGIN_STORAGE requires a web environment') + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-o', 'hello.js']) - self.assertContained("origins: '*'", read_file('hello.js')) - - def test_cross_origin_storage_origins_explicit_wildcard(self): - self.run_process([EMCC, test_file('hello_world.c'), + '-sSINGLE_FILE'], + 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - "-sCROSS_ORIGIN_STORAGE_ORIGINS=['*']", - '-o', 'hello.js']) - self.assertContained("origins: '*'", read_file('hello.js')) + '-sWASM_ASYNC_COMPILATION=0'], + 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') - def test_cross_origin_storage_origins_explicit_list(self): + def test_cross_origin_storage_origins(self): self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15492,8 +15450,6 @@ def test_cross_origin_storage_origins_explicit_list(self): self.assertContained('"https://app.example.com"', js) self.assertContained('"https://api.example.com"', js) self.assertNotContained("origins: '*'", js) - - def test_cross_origin_storage_origins_same_site(self): self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15502,24 +15458,18 @@ def test_cross_origin_storage_origins_same_site(self): js = read_file('hello.js') self.assertContained('{ create: true }', js) self.assertNotContained('origins:', js) - - def test_cross_origin_storage_origins_error_mixed_wildcard(self): self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]'], "'*' must not be mixed with explicit origins") - - def test_cross_origin_storage_origins_error_invalid_origin(self): self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]'], 'is not a valid HTTPS origin') - - def test_cross_origin_storage_origins_error_origin_with_path(self): self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', @@ -15527,26 +15477,6 @@ def test_cross_origin_storage_origins_error_origin_with_path(self): '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]'], 'is not a valid HTTPS origin') - def test_cross_origin_storage_wasm_hash_module_property(self): - self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-o', 'hello.js']) - js = read_file('hello.js') - m = re.search(r"Module\['wasmHash'\]\s*=\s*\{[^}]*value:\s*'([0-9a-f]{64})'", js) - self.assertTrue(m, "Module['wasmHash'] not found in JS output") - embedded_hash = m.group(1) - expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() - self.assertEqual(embedded_hash, expected_hash, - "Module['wasmHash'] does not match the actual .wasm SHA-256") - - def test_cross_origin_storage_wasm_hash_absent_without_flag(self): - self.run_process([EMCC, test_file('hello_world.c'), - '-sENVIRONMENT=web', - '-o', 'hello.js']) - js = read_file('hello.js') - self.assertNotContained("Module['wasmHash']", js) - def test_deprecated_settings(self): err = self.run_process([EMCC, '-sMEMORY64', test_file('hello_world.c')], stderr=PIPE).stderr self.assertContained('emcc: warning: MEMORY64 is deprecated (prefer the standard -m64 or --target=wasm64 flags). Please open a bug if you have a continuing need for this setting [-Wdeprecated]', err) From 9465e49ef1edca44f66ef7a32ae8ad2a7fb09dac Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:37:59 +0200 Subject: [PATCH 59/74] style: shorten COS ChangeLog entry to 3 lines --- ChangeLog.md | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 82e1acbff400d..ab203cbdc5bb3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,23 +20,10 @@ See docs/process.md for more on how version tagging works. 6.0.1 (in development) ---------------------- -- New experimental ``-sCROSS_ORIGIN_STORAGE`` linker flag that integrates - the proposed `Cross-Origin Storage browser API - `_ into the Wasm loading path - as a progressive enhancement (web target only). At build time Emscripten - computes the SHA-256 hash of the final ``.wasm`` binary and embeds it in the - generated JS glue. At runtime, if the browser exposes - ``navigator.crossOriginStorage``, the runtime first attempts to retrieve the - module from the shared cross-origin cache (cache hit); on a miss it fetches - normally and stores the binary in COS for future use by any origin. Falls - back transparently to the standard fetch path when the API is unavailable. - Three optional ``Module`` callbacks are available for instrumentation: - ``Module['onCOSCacheHit'](hash)``, ``Module['onCOSCacheMiss'](hash, url)``, - and ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` - and ``-sWASM_ASYNC_COMPILATION=0`` (both produce hard link-time errors). - The companion ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` setting controls which - origins may read the cached file: ``['*']`` (default, globally available), - an explicit HTTPS origin list (restricted), or ``[]`` (same-site only). +- New experimental ``-sCROSS_ORIGIN_STORAGE`` linker flag integrating the + proposed `Cross-Origin Storage browser API `_ + as a progressive enhancement for Wasm loading on the web target. See + :ref:`CrossOriginStorage` for details. - The `-sUSE_PTHREADS` and `-sMEMORY64` flags have been deprecated in favor of the more standard `-pthread` and `-m64` (or `--target=wasm64`) flags. (#27025) - Adds wasm-bindgen support. When `-sWASM_BINDGEN` is set, Emscripten will call From f7ce0fdc8e3b8528627319d7b924ed6066bb2c8b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:40:51 +0200 Subject: [PATCH 60/74] remove: test_cross_origin_storage_callbacks from test_other.py Browser functional tests cover this adequately. Per reviewer feedback. --- test/test_other.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 66a9cb00e8147..c12c830630571 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15404,17 +15404,6 @@ def test_cross_origin_storage(self): self.assertNotContained('crossOriginStorage', js) self.assertNotContained("Module['wasmHash']", js) - def test_cross_origin_storage_callbacks(self): - self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-sINCOMING_MODULE_JS_API=onCOSCacheHit,onCOSCacheMiss,onCOSStore', - '-o', 'hello.js']) - js = read_file('hello.js') - self.assertContained("Module['onCOSCacheHit']", js) - self.assertContained("Module['onCOSCacheMiss']", js) - self.assertContained("Module['onCOSStore']", js) - def test_cross_origin_storage_warnings(self): proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', From a77d2fbaec0e16f8c8a291f086cc06bdade3b831 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:41:17 +0200 Subject: [PATCH 61/74] fix: copyright year and remove redundant build comment from main.cpp Build instructions belong in README.md, not the source file. Copyright year corrected to 2026. Per reviewer feedback. --- test/cross_origin_storage/main.cpp | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/test/cross_origin_storage/main.cpp b/test/cross_origin_storage/main.cpp index 946e88d7d9ed0..587f84682498e 100644 --- a/test/cross_origin_storage/main.cpp +++ b/test/cross_origin_storage/main.cpp @@ -1,22 +1,8 @@ -// Copyright 2025 The Emscripten Authors. All rights reserved. +// Copyright 2026 The Emscripten Authors. All rights reserved. // Emscripten is available under two separate licenses, the MIT license and the // University of Illinois/NCSA Open Source License. Both these licenses can be // found in the LICENSE file. -// Example: Cross-Origin Storage (COS) integration -// -// Build with: -// emcc main.cpp -o index.js \ -// -O2 \ -// -sCROSS_ORIGIN_STORAGE \ -// -sENVIRONMENT=web \ -// -sEXPORTED_RUNTIME_METHODS=ccall \ -// -sEXPORTED_FUNCTIONS=_greet \ -// -sALLOW_MEMORY_GROWTH -// -// Serve the directory over HTTP (e.g. `emrun .` or `python3 -m http.server`) -// and open index.html in a browser that has the COS extension installed. - #include #include From 4a038e4cdaaccb89304dc164d6920cda0c90edb6 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:42:45 +0200 Subject: [PATCH 62/74] remove: test/cross_origin_storage/ example directory Not used by any automated test; browser tests use browser_test_hello_world.c directly. The directory caused repeated confusion and build artifact issues. Feature documentation lives in site/source/docs/compiling/CrossOriginStorage.rst. Per reviewer feedback. --- test/cross_origin_storage/README.md | 67 --------------------- test/cross_origin_storage/index.html | 89 ---------------------------- test/cross_origin_storage/main.cpp | 17 ------ 3 files changed, 173 deletions(-) delete mode 100644 test/cross_origin_storage/README.md delete mode 100644 test/cross_origin_storage/index.html delete mode 100644 test/cross_origin_storage/main.cpp diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md deleted file mode 100644 index b289998c20a46..0000000000000 --- a/test/cross_origin_storage/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Cross-Origin Storage (COS) example - -This example demonstrates Emscripten's experimental -`-sCROSS_ORIGIN_STORAGE=1` flag, which integrates the -[Cross-Origin Storage browser API](https://github.com/WICG/cross-origin-storage) -into the standard Wasm loading path. - -## What it does - -COS is a **progressive enhancement**: when the browser exposes the -`navigator.crossOriginStorage` API, loading takes one of two paths: - -- **Cache miss** (first load): the `.wasm` module is fetched over the network - and stored in the cross-origin cache, keyed by its SHA-256 hash. -- **Cache hit** (subsequent loads, same or any other origin): the module is - retrieved from the cache without a network request for the binary. - -When the browser does not expose the COS API, or when an unexpected error -occurs, the runtime falls back to the standard `fetch()` / -`WebAssembly.instantiateStreaming()` path — the page always loads. - -The page reports which path was taken and, where applicable, the SHA-256 hash -of the Wasm resource and the URL it was fetched from. - -## Build - -```bash -emcc main.cpp -o index.js \ - -O2 \ - -sCROSS_ORIGIN_STORAGE \ - -sENVIRONMENT=web \ - -sEXPORTED_RUNTIME_METHODS=ccall \ - -sEXPORTED_FUNCTIONS=_greet \ - -sALLOW_MEMORY_GROWTH -``` - -This produces `index.js` and `index.wasm`. The SHA-256 hash of `index.wasm` -is embedded in `index.js` at build time — you can verify they match: - -```bash -sha256sum index.wasm -grep -oP "value: '\K[0-9a-f]{64}" index.js -``` - -## Run - -Serve the directory over HTTP (the `file://` protocol does not support -`fetch()`): - -```bash -emrun . -# or -python3 -m http.server -``` - -Open the page in a browser with the Cross-Origin Storage API available. - -The page will report: - -- whether the COS API is active -- on a cache miss: the URL the Wasm was fetched from, and confirmation once it has been stored in COS with its hash -- on a cache hit: the SHA-256 hash of the Wasm resource served from COS - -## See also - -- [COS Emscripten docs](../../site/source/docs/compiling/CrossOriginStorage.rst) -- [WICG explainer](https://github.com/WICG/cross-origin-storage) diff --git a/test/cross_origin_storage/index.html b/test/cross_origin_storage/index.html deleted file mode 100644 index 0ca1b7989453e..0000000000000 --- a/test/cross_origin_storage/index.html +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - Emscripten — Cross-Origin Storage example - - - -

Emscripten — Cross-Origin Storage example

-

Reload the page after the first load to observe the cache-hit path.

-
Loading…
- - - - - - - diff --git a/test/cross_origin_storage/main.cpp b/test/cross_origin_storage/main.cpp deleted file mode 100644 index 587f84682498e..0000000000000 --- a/test/cross_origin_storage/main.cpp +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2026 The Emscripten Authors. All rights reserved. -// Emscripten is available under two separate licenses, the MIT license and the -// University of Illinois/NCSA Open Source License. Both these licenses can be -// found in the LICENSE file. - -#include -#include - -// Called from JavaScript after the module loads. -extern "C" { - -EMSCRIPTEN_KEEPALIVE -const char* greet() { - return "Hello from WebAssembly!"; -} - -} // extern "C" From 7fe5ed94545bdd516aa05701e8a44d563b07cee8 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:51:50 +0200 Subject: [PATCH 63/74] Make feature detection rock-solid --- site/source/docs/compiling/CrossOriginStorage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 9a6dc4dfc7881..1498191479abf 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -328,7 +328,7 @@ via a reference to that config object: // `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 && 'crossOriginStorage' in navigator) { + if (cosHash?.value && 'navigator' in self && 'crossOriginStorage' in navigator) { navigator.crossOriginStorage.requestFileHandles([cosHash]) .then(handles => handles[0].getFile()) .then(f => f.arrayBuffer()) From 2f214113a0e4c7b595953000f87387c349e2888b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:55:33 +0200 Subject: [PATCH 64/74] Make feature detection compact --- site/source/docs/compiling/CrossOriginStorage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 1498191479abf..7e204a31b83f7 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -328,7 +328,7 @@ via a reference to that config object: // `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 && 'navigator' in self && 'crossOriginStorage' in navigator) { + if (cosHash?.value && globalThis.navigator?.crossOriginStorage) { navigator.crossOriginStorage.requestFileHandles([cosHash]) .then(handles => handles[0].getFile()) .then(f => f.arrayBuffer()) From bc048fb656c3b7cdb20840f84ffde1b9d9d5091e Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:58:24 +0200 Subject: [PATCH 65/74] fix: use Markdown syntax in ChangeLog, not RST :ref: --- ChangeLog.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index ab203cbdc5bb3..8ed6c7946b214 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,10 +20,10 @@ See docs/process.md for more on how version tagging works. 6.0.1 (in development) ---------------------- -- New experimental ``-sCROSS_ORIGIN_STORAGE`` linker flag integrating the - proposed `Cross-Origin Storage browser API `_ +- New experimental `-sCROSS_ORIGIN_STORAGE` linker flag integrating the + proposed [Cross-Origin Storage browser API](https://github.com/WICG/cross-origin-storage) as a progressive enhancement for Wasm loading on the web target. See - :ref:`CrossOriginStorage` for details. + `docs/compiling/CrossOriginStorage.rst` for details. - The `-sUSE_PTHREADS` and `-sMEMORY64` flags have been deprecated in favor of the more standard `-pthread` and `-m64` (or `--target=wasm64`) flags. (#27025) - Adds wasm-bindgen support. When `-sWASM_BINDGEN` is set, Emscripten will call From 146cf3917e129784dd3fa0f5d364f7ec5247fdef Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 19:02:13 +0200 Subject: [PATCH 66/74] refactor: move COS incompatibilities to INCOMPATIBLE_SETTINGS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SIDE_MODULE is now a hard error (no JS glue emitted — genuinely incompatible). SPLIT_MODULE and MAIN_MODULE partial-coverage warnings are dropped; they are not true incompatibilities and add noise for an experimental feature. Per reviewer feedback (r3383144746). --- test/test_other.py | 29 ++++------------------------- tools/link.py | 6 ------ tools/settings.py | 1 + 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index c12c830630571..8f7d0429f6ae2 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15404,31 +15404,6 @@ def test_cross_origin_storage(self): self.assertNotContained('crossOriginStorage', js) self.assertNotContained("Module['wasmHash']", js) - def test_cross_origin_storage_warnings(self): - proc = self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-sSPLIT_MODULE', - '-o', 'hello.js'], - stderr=PIPE) - self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', - proc.stderr) - proc = self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-sMAIN_MODULE', - '-o', 'hello.js'], - stderr=PIPE) - self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', - proc.stderr) - proc = self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sSIDE_MODULE', - '-o', 'hello.wasm'], - stderr=PIPE) - self.assertContained('CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds', - proc.stderr) - def test_cross_origin_storage_errors(self): self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', @@ -15444,6 +15419,10 @@ def test_cross_origin_storage_errors(self): '-sENVIRONMENT=web', '-sWASM_ASYNC_COMPILATION=0'], 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') + self.assert_fail([EMCC, test_file('hello_world.c'), + '-sCROSS_ORIGIN_STORAGE', + '-sSIDE_MODULE'], + 'CROSS_ORIGIN_STORAGE is not compatible with SIDE_MODULE') def test_cross_origin_storage_origins(self): self.run_process([EMCC, test_file('hello_world.c'), diff --git a/tools/link.py b/tools/link.py index 9eb8c1b99d43a..ee7bd01b604f4 100644 --- a/tools/link.py +++ b/tools/link.py @@ -799,12 +799,6 @@ def get_dylibs(options, linker_args): def setup_cross_origin_storage(): if not settings.ENVIRONMENT_MAY_BE_WEB: exit_with_error('CROSS_ORIGIN_STORAGE requires a web environment (navigator.crossOriginStorage is not available outside the browser)') - if settings.SPLIT_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') - if settings.MAIN_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') - if settings.SIDE_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS if '*' in origins and len(origins) > 1: exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") diff --git a/tools/settings.py b/tools/settings.py index 1c5e15c8bf5f9..026ddbc653f29 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -150,6 +150,7 @@ ('CROSS_ORIGIN', 'NO_PTHREADS', None), ('CROSS_ORIGIN_STORAGE', 'SINGLE_FILE', 'the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on'), ('CROSS_ORIGIN_STORAGE', 'NO_WASM_ASYNC_COMPILATION', 'synchronous instantiation does not use the COS fetch path'), + ('CROSS_ORIGIN_STORAGE', 'SIDE_MODULE', 'no JS glue is emitted to carry the hash or perform the COS lookup'), ] EXPERIMENTAL_SETTINGS = { From d0571325f378bc50ef7e2f48937c91d6b624de5d Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 19:03:28 +0200 Subject: [PATCH 67/74] style: clean up COS browser tests - Drop -sENVIRONMENT=web (web is included by default) - Drop inline comment and docstrings - Drop section banner comment Per reviewer feedback. --- test/test_browser.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/test/test_browser.py b/test/test_browser.py index 584c22e63335c..3be485c39c4b8 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5697,27 +5697,13 @@ def test_binary_encode(self, extra): def test_shell_minimal(self, args): self.btest_exit('browser_test_hello_world.c', cflags=['--shell-file', path_from_root('html/shell_minimal.html')] + args) - # --------------------------------------------------------------------------- - # Cross-Origin Storage (COS) browser tests - # --------------------------------------------------------------------------- - def test_cross_origin_storage_fallback(self): - """COS flag: page loads via the normal fetch path when COS API is absent.""" - # -sENVIRONMENT=web requires a real browser (not node). if not is_chrome(): self.skipTest('cross-origin storage tests require a Chromium-based browser') self.btest_exit('browser_test_hello_world.c', - cflags=['-O2', '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-Wno-experimental']) + cflags=['-O2', '-sCROSS_ORIGIN_STORAGE', '-Wno-experimental']) def test_cross_origin_storage_miss_then_hit(self): - """COS flag: first load triggers a cache-miss store; second load is a hit. - - Requires EMTEST_COS_EXTENSION_PATH to point to an unpacked copy of the - Cross-Origin Storage Chrome extension (manifest.json directory), which - polyfills navigator.crossOriginStorage. - - See: https://github.com/web-ai-community/cross-origin-storage-extension - """ if not is_chrome(): self.skipTest('cross-origin storage tests require a Chromium-based browser') if not EMTEST_COS_EXTENSION_PATH: @@ -5756,7 +5742,6 @@ def test_cross_origin_storage_miss_then_hit(self): self.compile_btest('browser_test_hello_world.c', [ '-O2', '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', '-Wno-experimental', '-sINCOMING_MODULE_JS_API=onAbort,onExit,onCOSStore,onCOSCacheHit', '--pre-js', 'cos_pre.js', From d12b027d2b1af0aa163c054f239a0c9bfb26bb86 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 19:49:22 +0200 Subject: [PATCH 68/74] style: add PR number to COS ChangeLog entry --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 8ed6c7946b214..a31bc24019665 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -23,7 +23,7 @@ See docs/process.md for more on how version tagging works. - New experimental `-sCROSS_ORIGIN_STORAGE` linker flag integrating the proposed [Cross-Origin Storage browser API](https://github.com/WICG/cross-origin-storage) as a progressive enhancement for Wasm loading on the web target. See - `docs/compiling/CrossOriginStorage.rst` for details. + `docs/compiling/CrossOriginStorage.rst` for details. (#27066) - The `-sUSE_PTHREADS` and `-sMEMORY64` flags have been deprecated in favor of the more standard `-pthread` and `-m64` (or `--target=wasm64`) flags. (#27025) - Adds wasm-bindgen support. When `-sWASM_BINDGEN` is set, Emscripten will call From 48345103140bdecd8fe7806b481a4b8e803a88bd Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 19:52:12 +0200 Subject: [PATCH 69/74] refactor: clean up COS preamble and hash injection - Use globalThis.navigator?.crossOriginStorage (matches codebase pattern) - Move var cosHash inside the if block - Hardcode 'SHA-256' in the template; drop <<< WASM_HASH_ALGORITHM >>> placeholder Per reviewer feedback. --- src/preamble.js | 6 +++--- test/test_other.py | 2 +- tools/link.py | 15 +++++---------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/preamble.js b/src/preamble.js index 1a4cb48d157cb..7c66305524fbb 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -631,8 +631,8 @@ async function instantiateAsync(binary, binaryFile, imports) { // https://github.com/WICG/cross-origin-storage // Any error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. - var cosHash = Module['wasmHash']; - if ('crossOriginStorage' in navigator) { + if (globalThis.navigator?.crossOriginStorage) { + var cosHash = Module['wasmHash']; try { var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); // Cache hit — read the Blob and instantiate from its ArrayBuffer. @@ -890,7 +890,7 @@ function getWasmImports() { #if CROSS_ORIGIN_STORAGE // Expose the build-time hash so that custom Module['instantiateWasm'] // callbacks can implement their own COS-aware loading path. - Module['wasmHash'] = { algorithm: '<<< WASM_HASH_ALGORITHM >>>', value: '<<< WASM_HASH_VALUE >>>' }; + Module['wasmHash'] = { algorithm: 'SHA-256', value: '<<< WASM_HASH_VALUE >>>' }; #endif #if expectToReceiveOnModule('instantiateWasm') diff --git a/test/test_other.py b/test/test_other.py index 8f7d0429f6ae2..bce87e743f070 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15380,7 +15380,7 @@ def test_cross_origin_storage(self): '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') - self.assertContained("'crossOriginStorage' in navigator", js) + self.assertContained('globalThis.navigator?.crossOriginStorage', js) self.assertContained('navigator.crossOriginStorage.requestFileHandles', js) self.assertContained("algorithm: 'SHA-256'", js) self.assertContained("origins: '*'", js) diff --git a/tools/link.py b/tools/link.py index ee7bd01b604f4..5add64aeda873 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1921,18 +1921,13 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat phase_binaryen(target, options, wasm_target) - # Compute the hash of the final wasm (after binaryen) and substitute the - # <<< WASM_HASH_ALGORITHM >>> / <<< WASM_HASH_VALUE >>> placeholders that - # preamble.js left in the generated JS. + # Compute the SHA-256 hash of the final wasm (after binaryen) and substitute + # the <<< WASM_HASH_VALUE >>> placeholder that preamble.js left in the JS. if final_js and settings.CROSS_ORIGIN_STORAGE: if os.path.exists(wasm_target): - wasm_bytes = utils.read_binary(wasm_target) - wasm_hash_algorithm = 'SHA-256' - wasm_hash_value = hashlib.sha256(wasm_bytes).hexdigest() - logger.debug(f'CROSS_ORIGIN_STORAGE: wasm {wasm_hash_algorithm} = {wasm_hash_value}') - js_content = read_file(final_js) - js_content = do_replace(js_content, '<<< WASM_HASH_ALGORITHM >>>', wasm_hash_algorithm) - js_content = do_replace(js_content, '<<< WASM_HASH_VALUE >>>', wasm_hash_value) + wasm_hash_value = hashlib.sha256(utils.read_binary(wasm_target)).hexdigest() + logger.debug(f'CROSS_ORIGIN_STORAGE: wasm SHA-256 = {wasm_hash_value}') + js_content = do_replace(read_file(final_js), '<<< WASM_HASH_VALUE >>>', wasm_hash_value) write_file(final_js, js_content) # If we are not emitting any JS then we are all done now From c9bc25f14b312efaa2f7431e89292650c5212589 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 22:40:23 +0200 Subject: [PATCH 70/74] =?UTF-8?q?test:=20simplify=20COS=20assertions=20?= =?UTF-8?q?=E2=80=94=20use=20tighter=20algorithm+value=20regex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_other.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index bce87e743f070..b7506ee25ff46 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15380,18 +15380,7 @@ def test_cross_origin_storage(self): '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') - self.assertContained('globalThis.navigator?.crossOriginStorage', js) - self.assertContained('navigator.crossOriginStorage.requestFileHandles', js) - self.assertContained("algorithm: 'SHA-256'", js) - self.assertContained("origins: '*'", js) - self.assertContained('getFile()', js) - self.assertContained('createWritable()', js) - self.assertContained("'NotFoundError'", js) - self.assertContained("'NotAllowedError'", js) - self.assertNotContained("Module['onCOSCacheHit']", js) - self.assertNotContained("Module['onCOSCacheMiss']", js) - self.assertNotContained("Module['onCOSStore']", js) - m = re.search(r"value:\s*'([0-9a-f]{64})'", js) + m = re.search(r"algorithm:\s*'SHA-256',\s*value:\s*'([0-9a-f]{64})'", js) self.assertTrue(m, 'could not find a 64-char hex hash value in JS output') embedded_hash = m.group(1) expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() From 33e236931fcfccc5a1a5f25951461998002eecc6 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 22:41:37 +0200 Subject: [PATCH 71/74] test: use simplified CROSS_ORIGIN_STORAGE_ORIGINS syntax in test --- test/test_other.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index b7506ee25ff46..b054e5e441252 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15435,19 +15435,19 @@ def test_cross_origin_storage_origins(self): [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=*,https://example.com'], "'*' must not be mixed with explicit origins") self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=http://example.com'], 'is not a valid HTTPS origin') self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=https://example.com/path'], 'is not a valid HTTPS origin') def test_deprecated_settings(self): From 64eb6095bfd90578c6c351699ee434d0167d233f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 22:43:18 +0200 Subject: [PATCH 72/74] test: drop redundant -sENVIRONMENT=web from COS error tests (web is default) --- test/test_other.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index b054e5e441252..3c0f6b9ac9d5f 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15400,12 +15400,10 @@ def test_cross_origin_storage_errors(self): 'CROSS_ORIGIN_STORAGE requires a web environment') self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', '-sSINGLE_FILE'], 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', '-sWASM_ASYNC_COMPILATION=0'], 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') self.assert_fail([EMCC, test_file('hello_world.c'), From 12c24a0f8cb08bd5bf25864c88052bc9897e2a3f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 23:00:04 +0200 Subject: [PATCH 73/74] test: add TODO to remove COS extension once Chromium ships the feature natively --- test/browser_common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/browser_common.py b/test/browser_common.py index 522bc3cf42a23..7ce6694aaaf8a 100644 --- a/test/browser_common.py +++ b/test/browser_common.py @@ -72,6 +72,8 @@ # Point this at a local clone of: # https://github.com/web-ai-community/cross-origin-storage-extension # (the directory that contains manifest.json). +# TODO: Remove this once Chromium ships COS natively (even behind a flag), +# and update the browser test to use that flag instead. EMTEST_COS_EXTENSION_PATH = os.getenv('EMTEST_COS_EXTENSION_PATH', '') # Triggers the browser to restart after every given number of tests. From 1466998449fa3db5de73a1b997ef6d59909c9b0f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Thu, 11 Jun 2026 13:06:09 +0200 Subject: [PATCH 74/74] Rebaseline codesize test to fix CI --- test/codesize/test_unoptimized_code_size.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/codesize/test_unoptimized_code_size.json b/test/codesize/test_unoptimized_code_size.json index dba4f70894f1b..f1b2ab32e7e61 100644 --- a/test/codesize/test_unoptimized_code_size.json +++ b/test/codesize/test_unoptimized_code_size.json @@ -10,7 +10,7 @@ "strict.js": 52659, "strict.js.gz": 16513, "strict.wasm": 15115, - "strict.wasm.gz": 7461, + "strict.wasm.gz": 7457, "total": 175923, - "total_gz": 63493 + "total_gz": 63489 }