[browser] Fix Debug-only startup failures: stack-first assert, incoming Module API, preRun function form - #134527
pavelsavara wants to merge 7 commits into
Conversation
Emscripten lets preInit/preRun/postRun be either an array of callbacks or a single callback, and normalizes them itself. The browser host prepends its own hooks by spreading the user value, which throws "(Module.preRun || []) is not iterable" when a single function was supplied. Mono already normalizes this in configureEmscriptenStartup. WasmBasicTestApp passes preRun as a function, which is how Wasm.Build.Tests hit this once it moved to the standard workload. EmscriptenModuleInternal declared these as arrays only, which is what allowed the assumption in the first place, so widen them to match Mono's EmscriptenModule. postRun needs no normalization here because the host never prepends to it. Also apply npm run format, which reflows an arrow body in http.ts that was failing the brace-style lint rule. Fixes dotnet#132555
|
Azure Pipelines: Successfully started running 4 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara |
The earlier list only named the properties the loaders themselves set, which rejected 16 properties emscripten accepts by default. That is reachable from user code: Blazor's prepareRuntimeConfig spreads window['Module'] into the module config, so an app author can put any emscripten property there and it would abort a debug build. Declare the default set on both flavors instead, plus wasmMemory for Mono with threads, which is the one property we set that is not a default. The setting stays explicit so an undeclared property still fails at link rather than silently, but nothing that works today changes. Also fix the ordering in BrowserWasmApp.CoreCLR.targets: the PropertyGroup computing _EmccIncomingModuleJSAPI ran before the ItemGroup defining the items, and MSBuild evaluates a target's children in document order, so the list expanded to empty and rejected every Module property on the workload relink path.
Emscripten serializes dotnetInitializeModule independently of its bundle closure. Keep the callback normalization helper nested so Rollup's minified name remains available in dotnet.native.js. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
A Debug CoreCLR workload/native-relink test is needed before approval.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
What changed in this PR
Fixes Debug-only WebAssembly browser startup failures across Mono, CoreCLR, and JavaScript interop.
Changes:
- Handles stack-first WASM stack bounds.
- Declares Emscripten incoming Module APIs.
- Supports function-form startup callbacks.
- Corrects CoreCLR MSBuild evaluation order.
| File | Summary |
|---|---|
src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/http.ts |
Formatting cleanup. |
src/native/libs/System.Native.Browser/native/index.ts |
Normalizes callback values. |
src/native/libs/Common/JavaScript/types/internal.ts |
Widens callback type definitions. |
src/native/corehost/browserhost/CMakeLists.txt |
Adds the explicit Module API allowlist. |
src/mono/mono/utils/mono-threads-wasm.c |
Allows a null stack base for stack-first builds. |
src/mono/browser/build/BrowserWasmApp.targets |
Adds Mono linker API configuration; duplicated allowlist remains a nit. |
src/mono/browser/build/BrowserWasmApp.CoreCLR.targets |
Adds CoreCLR relink API configuration; requires a Debug workload/native-relink test. |
src/mono/browser/browser.proj |
Adds Mono browser API configuration. |
|
|
||
| <PropertyGroup> | ||
| <_EmccExportedRuntimeMethods>"[BROWSER_HOST,@(EmccExportedRuntimeMethod -> '%27%(Identity)%27', ',')]"</_EmccExportedRuntimeMethods> | ||
| <_EmccIncomingModuleJSAPI>"[@(EmccIncomingModuleJSAPI -> '%27%(Identity)%27', ',')]"</_EmccIncomingModuleJSAPI> |
There was a problem hiding this comment.
Structurally addressed: the items now come from the GenerateEmccExports dependency, so the ItemGroup-before-PropertyGroup ordering hazard this flags is gone. The CoreCLR in-tree browserhost link was rebuilt and its checkIncomingModuleAPI is unchanged (rejects wasmMemory/wasmBinary, accepts the 26 defaults). A full Wasm.Build.Tests/xharness Debug startup run is still pending, so I'm leaving this open until that runs.
Note: written with GitHub Copilot.
| g_error ("%s: pthread_attr_destroy failed with \"%s\" (%d)", __func__, g_strerror (res), res); | ||
|
|
||
| g_assert (*staddr != NULL); | ||
| // emscripten links with --stack-first at -O0, which puts the stack at the start of the linear memory, so staddr can legitimately be NULL |
There was a problem hiding this comment.
we should probably backport this to 11.0 (we could argue it's a test-only change since it happens in Debug only) since it will get lost in main soon
There was a problem hiding this comment.
Nobody in the world ever builds mono+MT+wasm+debug. And I'm not going to do it any time soon again.
The explicit emscripten default INCOMING_MODULE_JS_API list was hand-copied into four files. Collapse it to a single source per flavor, matching how EXPORTED_FUNCTIONS/RUNTIME_METHODS already flow. CoreCLR: define the list in GenerateEmccExports (eng/native.wasm.targets plus its fallback in BrowserWasmApp.CoreCLR.targets). The in-tree browserhost link now consumes a CMAKE_EMCC_INCOMING_MODULE_JS_API cmake arg instead of a hardcoded string, and _CoreCLRWriteLinkRsp reads the items from its GenerateEmccExports dependency. This also removes the earlier ItemGroup-before-PropertyGroup ordering hazard, since the items are populated by a dependency target. Mono: browser.proj remains the single source and now emits EmccDefaultIncomingModuleJSAPI into wasm-props.json (incl. wasmMemory for the threads pack variant). ReadWasmProps surfaces it and the app relink in BrowserWasmApp.targets reads it instead of re-listing. CoreCLR browserhost dotnet.native.js checkIncomingModuleAPI is unchanged (rejects wasmMemory/wasmBinary, accepts the 26 defaults). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| // emscripten links with --stack-first at -O0, which puts the stack at the start of the linear memory, so staddr can legitimately be NULL | ||
| g_assert (*stsize != (size_t)-1); |


Three unrelated-looking failures that all only reproduce in Debug browser builds, plus one follow-on cleanup.
1. Mono MT asserts on the stack bounds at startup
At
-O0emscripten links with--stack-first, which puts the stack at the start of linear memory, so__stack_low == 0.pthread_getattr_npreportsstack = emscripten_stack_get_base()andstack_size = base - end, andpthread_attr_getstackreturnsstack - stack_size, i.e.emscripten_stack_get_end()— legitimatelyNULLunder this layout. Emscripten also comments out musl'sif (!a->_a_stackaddr) return EINVAL, so the call succeeds and the assert is what fires.Mono already accounts for this everywhere else:
register_threadhas an explicit#ifndef TARGET_WASMaroundg_assert (staddr)with the comment "for wasm, the stack can be placed at the start of the linear memory", and the non-pthreads branch ofmono_threads_platform_get_stack_boundshas no NULL assert at all — which is why Debug single-threaded works. Only the__EMSCRIPTEN_PTHREADS__branch was missed.The
*stsize != (size_t)-1assert stays, so a genuinely uninitialized main pthread (stack = 0, stack_size = 0) is still caught byg_assert (stsize)inregister_thread.2.
Module.wasmMemory was supplied but wasmMemory not included in INCOMING_MODULE_JS_APIWith assertions on, emscripten aborts when a Module property is supplied that isn't declared. Mono's MT worker sets
Module.wasmMemorywhen it receives the mono config, andwasmMemoryis not in emscripten's default list — so Debug + MT + Mono aborts during startup.Both flavors now declare
INCOMING_MODULE_JS_APIexplicitly: emscripten's full default set, pluswasmMemoryfor Mono when threads are enabled. Nothing that works today changes, and an undeclared property still fails loudly at link instead of silently.An earlier revision of this PR listed only the properties the loaders themselves set, which rejected 16 properties emscripten accepts by default. That turned out to be reachable from user code — Blazor's
prepareRuntimeConfigdoes...(window['Module'] || {})into the module config, so an app author can put any emscripten property onwindow.Moduleand it would have aborted a debug build. Hence the full default set.Adding
wasmMemoryis behaviourally neutral:initMemory()returns early on a pthread before reaching theModule['wasmMemory']branch, and the main thread never sets it.corerunis deliberately left on the default list — its JS injects no Module properties, andcorerun.htmlassignsmonitorRunDependencies.3.
TypeError: (Module.preRun || []) is not iterableEmscripten lets
preInit/preRun/postRunbe an array or a single callback, and normalizes them itself. The browser host prepends its own hooks by spreading the user value, so a single function throws. Mono already normalizes this inconfigureEmscriptenStartup.WasmBasicTestApppassespreRunas a function, which is how Wasm.Build.Tests hit it after moving to the standard workload.EmscriptenModuleInternaldeclared these as arrays only, which is what allowed the assumption — widened to match Mono'sEmscriptenModule.postRunneeds no normalization because the host never prepends to it.Verification
-subset mono -c Debug /p:WasmEnableThreads=truebuilds; in the regenerateddotnet.native.js,checkIncomingModuleAPIrejects exactly what stock emscripten rejects minuswasmMemory.-subset clr+host -c Debugbuilds; the browserhostdotnet.native.jsrejects exactly what stock emscripten rejects.npm run rollup:debugandnpm run lintare clean undersrc/native.Not runtime-tested: no Wasm.Build.Tests or xharness run yet. That gap is what let the
BrowserWasmApp.CoreCLR.targetsordering bug above go unnoticed until a second review pass, since the workload relink targets are only exercised by those tests — worth running before this leaves draft.Resolves #132555
Note
This pull request description was generated with GitHub Copilot.