emscripten-core / emscripten-core/emscripten
-sSINGLE_FILE: embedded wasm string decodes to 1 extra byte (0x20), producing invalid wasm (large binaries)
- Dominant language
- C++
- Stars
- 27.6k
- Forks
- 3.6k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 105
Description
### Version
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 6.0.6 (ce75e06884093bcefb86a6b8fd56a5d62a4cc245)
clang version 24.0.0git (llvm-project ff6d537b14d737719d6377789784d04ff9565f65)
Target: wasm32-unknown-emscripten
Installed via emsdk "latest" on Linux x86_64 (Devuan).
### Problem
Building with `-sSINGLE_FILE` produces an .html whose embedded wasm fails to
instantiate in Firefox 140 ESR:
CompileError: wasm validation error: at offset 8421340: invalid data initializer-kind field
The same project built **without** `-sSINGLE_FILE` produces a .wasm that
`wasm-opt --all-features` validates and that runs correctly in the same browser.
This emcc version embeds the wasm as a raw JS string literal decoded by:
function binaryDecode(f){for(var b,v=0,n=f.length,t=new Uint8Array(n);v>8&b;return t}
(not base64 as older versions did).
### What I found
- Reference (multi-file) wasm: **15,022,632 bytes**, valid.
- Saving the result of `findWasmBinary()` from the browser on the single-file
build gives **15,022,633 bytes** — one extra byte.
- Byte-level comparison: a single `0x20` (space) is present at decoded offset
**8,417,950** that does not exist in the reference wasm. Every byte before it
is identical; every byte after is identical but shifted by one. The validation
error at offset 8,421,340 is the parser failing on the shifted data a few KB later.
- The spurious space physically exists in the emitted .html: inside the embedded
string literal, between a literal `>` character and a `\f` escape sequence.
The surrounding emitted text (escapes shown as written in the file):
2\r%> \f%3
decodes to reference bytes `32 0d 25 3e | 0c 25 33` — the space between `>`
and `\f` corresponds to nothing in the reference binary. So this is an
**encode-side** defect (the emitted string already contains the extra byte),
not a browser/decoding problem.
- Possibly relevant observation: escaping in the emitted string is inconsistent —
e.g. byte 0x0C appears in some places as a raw control character and in other
places as `\f` — which may suggest a post-processing pass (HTML/JS
minification?) rewriting whitespace-class characters inside the string literal
after emission.
- Counter-test: embedding the **identical** wasm bytes as a base64 `data:` URI
in the same .html loads and runs perfectly.
- Size dependence: small programs (hello-world size) built with `-sSINGLE_FILE`
work fine. Two separate large projects fail the same way: a ~15 MB wasm
(extra byte at 8,417,950) and a ~7.5 MB wasm (validation error at 7,490,125).
### Link flags used
-sSINGLE_FILE -sASYNCIFY -sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=1073741824
-sEXPORTED_RUNTIME_METHODS=stringToNewUTF8 --shell-file -Oz
The C source is generated by the FreeBASIC compiler's emscripten target
(fbc -target js-asmjs), which drives emcc for compile and link.
### Reproduction
Happens consistently for me with multi-MB wasm binaries. The failing .html is
~18 MB — happy to provide it, the matching reference .wasm, and exact byte
offsets on request, or to test candidate fixes / run with EMCC_DEBUG=1.
Contributor guide
Research direction
Start by tracing the -sSINGLE_FILE path that emits the raw JavaScript string, including findWasmBinary() and binaryDecode(), and use EMCC_DEBUG=1 while reproducing with a multi-MB wasm binary. Compare the emitted string and decoded bytes around offset 8,417,950; done means the generated HTML decodes to the same valid bytes as the reference .wasm.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, javascript, wasm
- Domain
- compilers, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100