From 0c761b22e2d57d0d648824f0e7361f85bc625d6b Mon Sep 17 00:00:00 2001 From: piandtheradians-glitch Date: Fri, 23 Oct 2026 05:05:55 -0600 Subject: [PATCH 1/2] SINGLE_FILE_BINARY_ENCODE: report a non-UTF-8 read clearly under ASSERTIONS With `-sSINGLE_FILE` and the default `SINGLE_FILE_BINARY_ENCODE=1`, a `.js` that is read in an encoding other than UTF-8 (for example from a page with no `` loaded from `file://`) mangles the embedded Wasm binary, and fails with an error about the Wasm rather than the encoding, such as `CompileError: WebAssembly.instantiate(): unknown type form: 85`. As discussed in #28048, under `ASSERTIONS` the expected length of the embedded binary is now passed to `binaryDecode()`, which checks it once before decoding. Read as UTF-8, each byte of the binary is one character of the string, so a different length means the file was read in another encoding. It then aborts with: > The embedded Wasm binary is 16265 characters long, but should be 14895: this file was not read as UTF-8 (SINGLE_FILE_BINARY_ENCODE). Serve it with charset=utf-8, add to the page, or build with -sSINGLE_FILE_BINARY_ENCODE=0 Builds without `ASSERTIONS` are unchanged (byte-identical output checked for `-O0 -sASSERTIONS=0`, `-O2` and `-sMINIMAL_RUNTIME -O2`). Tests: - `other.test_single_file_not_utf8`: re-encodes the output as Latin-1 (what a Windows-1252 read amounts to) and expects the message instead of a `CompileError`. On `main` it fails with the `CompileError`. - `browser.test_single_file_not_utf8`: loads the build from a `windows-1252` page. On `main` it fails with the `CompileError`; with this change it reports the message (checked in headless Chromium). - `other.test_single_file_instantiate_failure`: updated for the new `binaryDecode` signature under `ASSERTIONS`. Also run locally, with the emsdk version in `test/emsdk_version.txt`: `other.test_single_file*` (all pass except the two `d8` shell tests, as no `d8` here), the browser `test_single_file*` tests, and the code size tests touching `SINGLE_FILE` (`test_codesize_hello_single_file`, `test_minimal_runtime_code_size*`, `test_unoptimized_code_size`, `test_binary_encode_is_smaller_than_base64_encode`). Checked by hand with `-O2`, `--closure=1`, `-sMINIMAL_RUNTIME` and `-sMODULARIZE -sEXPORT_ES6`. Fixes #28048 --- src/binaryDecode.js | 9 +++++++++ src/postamble_minimal.js | 4 ++++ src/preamble.js | 4 ++++ test/test_browser.py | 12 ++++++++++++ test/test_other.py | 13 ++++++++++++- tools/link.py | 2 ++ 6 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/binaryDecode.js b/src/binaryDecode.js index f5ef77313422b..6aed8308e1fc8 100644 --- a/src/binaryDecode.js +++ b/src/binaryDecode.js @@ -2,7 +2,16 @@ // Closure may analyze through the WASM_BINARY_DATA placeholder string into this // function, leading into incorrect results. /** @noinline */ +#if ASSERTIONS +function binaryDecode(bin, expectedLength) { + // Each byte of the binary is one character of the string, so if the lengths + // differ, the file was not read as UTF-8 (for example a page without + // loading it from file://), and the bytes are wrong. + // Say so here, rather than let it fail as a confusing wasm CompileError. + assert(bin.length == expectedLength, `The embedded Wasm binary is ${bin.length} characters long, but should be ${expectedLength}: this file was not read as UTF-8 (SINGLE_FILE_BINARY_ENCODE). Serve it with charset=utf-8, add to the page, or build with -sSINGLE_FILE_BINARY_ENCODE=0`); +#else function binaryDecode(bin) { +#endif for (var i = 0, l = bin.length, o = new Uint8Array(l), c; i < l; ++i) { c = bin.charCodeAt(i); o[i] = ~c >> 8 & c; // Recover the null byte in a manner that is compatible with https://crbug.com/453961758 diff --git a/src/postamble_minimal.js b/src/postamble_minimal.js index a721f98a42259..61537bdfc6391 100644 --- a/src/postamble_minimal.js +++ b/src/postamble_minimal.js @@ -154,7 +154,11 @@ var instantiatePromise; #endif #if SINGLE_FILE && SINGLE_FILE_BINARY_ENCODE && !WASM2JS +#if ASSERTIONS +Module['wasm'] = binaryDecode("<<< WASM_BINARY_DATA >>>", "<<< WASM_BINARY_SIZE >>>"); +#else Module['wasm'] = binaryDecode("<<< WASM_BINARY_DATA >>>"); +#endif #elif SINGLE_FILE && WASM == 1 && !WASM2JS Module['wasm'] = base64Decode('<<< WASM_BINARY_DATA >>>'); #endif diff --git a/src/preamble.js b/src/preamble.js index c6656a977b60f..3d3aac9f40be4 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -418,7 +418,11 @@ function getWasmBinary(file) {} function findWasmBinary() { #if SINGLE_FILE && SINGLE_FILE_BINARY_ENCODE && !WASM2JS +#if ASSERTIONS + return binaryDecode("<<< WASM_BINARY_DATA >>>", "<<< WASM_BINARY_SIZE >>>"); +#else return binaryDecode("<<< WASM_BINARY_DATA >>>"); +#endif #elif SINGLE_FILE return base64Decode('<<< WASM_BINARY_DATA >>>'); #elif AUDIO_WORKLET || !EXPORT_ES6 // For an Audio Worklet, we cannot use `new URL()`. diff --git a/test/test_browser.py b/test/test_browser.py index 5b2da81f03563..9182c10ac385b 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -4727,6 +4727,18 @@ def test_single_file_locate_file(self): self.run_browser('test.html', '/report_result?0') + # Tests that a SINGLE_FILE build loaded from a page whose charset is not UTF-8 + # reports that, rather than failing to compile the mangled Wasm binary. + def test_single_file_not_utf8(self): + self.compile_btest('browser_test_hello_world.c', ['-o', 'test.js', '-sSINGLE_FILE', '-sASSERTIONS']) + + create_file('test.html', ''' + + + ''') + + self.run_browser('test.html', 'this file was not read as UTF-8') + # Tests that SINGLE_FILE works as intended in a Worker in JS output def test_single_file_worker_js(self): self.btest_exit('browser_test_hello_world.c', cflags=['-sSINGLE_FILE'], run_in_worker=True) diff --git a/test/test_other.py b/test/test_other.py index f4162deb73fad..47318cd892831 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -9445,12 +9445,23 @@ def test_single_file_instantiate_failure(self): self.run_process([EMCC, test_file('hello_world.c'), '-sSINGLE_FILE', '-sASSERTIONS=1', '-o', 'out.js']) code = read_file('out.js') # Stub out binaryDecode so that it returns an invalid wasm binary. - code = code.replace('function binaryDecode(bin) {', 'function binaryDecode(bin) { return new Uint8Array([0]);') + code = code.replace('function binaryDecode(bin, expectedLength) {', 'function binaryDecode(bin, expectedLength) { return new Uint8Array([0]);') write_file('out_bad.js', code) out = self.run_js('out_bad.js', assert_returncode=1) self.assertContained('Aborted(CompileError', out) self.assertNotContained('startsWith is not a function', out) + def test_single_file_not_utf8(self): + # A SINGLE_FILE_BINARY_ENCODE build read in an encoding other than UTF-8 + # (as a browser does from a page with no charset) gets a mangled Wasm + # binary. With ASSERTIONS it says so instead of failing to compile it. + self.run_process([EMCC, test_file('hello_world.c'), '-sSINGLE_FILE', '-sASSERTIONS', '-o', 'out.js']) + # Each byte read as one Latin-1 character, as a Windows-1252 read does. + write_file('out_latin1.js', read_binary('out.js').decode('latin-1')) + out = self.run_js('out_latin1.js', assert_returncode=1) + self.assertContained('this file was not read as UTF-8', out) + self.assertNotContained('CompileError', out) + def test_single_file_disables_source_map(self): cmd = [EMCC, test_file('hello_world.c'), '-sSINGLE_FILE', '-gsource-map'] stderr = self.run_process(cmd, stderr=PIPE).stderr diff --git a/tools/link.py b/tools/link.py index 062816b97464c..7bbf9a2009e77 100644 --- a/tools/link.py +++ b/tools/link.py @@ -2453,6 +2453,8 @@ def phase_binaryen(target, wasm_target): if settings.SINGLE_FILE_BINARY_ENCODE: js = do_replace(js, '"<<< WASM_BINARY_DATA >>>"', binary_encode(wasm_target)) + if settings.ASSERTIONS: + js = do_replace(js, '"<<< WASM_BINARY_SIZE >>>"', str(os.path.getsize(wasm_target))) else: js = do_replace(js, '<<< WASM_BINARY_DATA >>>', base64_encode(wasm_target)) delete_file(wasm_target) From b2ca763139f80f29d80c7a860a8c2028713fdd88 Mon Sep 17 00:00:00 2001 From: piandtheradians-glitch Date: Fri, 23 Oct 2026 21:37:21 -0600 Subject: [PATCH 2/2] Use strict equality in the binaryDecode length assert --- src/binaryDecode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/binaryDecode.js b/src/binaryDecode.js index 6aed8308e1fc8..4572a4ff2d18e 100644 --- a/src/binaryDecode.js +++ b/src/binaryDecode.js @@ -8,7 +8,7 @@ function binaryDecode(bin, expectedLength) { // differ, the file was not read as UTF-8 (for example a page without // loading it from file://), and the bytes are wrong. // Say so here, rather than let it fail as a confusing wasm CompileError. - assert(bin.length == expectedLength, `The embedded Wasm binary is ${bin.length} characters long, but should be ${expectedLength}: this file was not read as UTF-8 (SINGLE_FILE_BINARY_ENCODE). Serve it with charset=utf-8, add to the page, or build with -sSINGLE_FILE_BINARY_ENCODE=0`); + assert(bin.length === expectedLength, `The embedded Wasm binary is ${bin.length} characters long, but should be ${expectedLength}: this file was not read as UTF-8 (SINGLE_FILE_BINARY_ENCODE). Serve it with charset=utf-8, add to the page, or build with -sSINGLE_FILE_BINARY_ENCODE=0`); #else function binaryDecode(bin) { #endif