[browser] Fix Debug-only startup failures: stack-first assert, incoming Module API, preRun function form - #134527
Merged
Merged
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. |
Contributor
|
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>
pavelsavara
marked this pull request as ready for review
September 25, 2026 10:09
pavelsavara
requested review from
akoeplinger,
lewing,
maraf,
steveisok and
vitek-karas
as code owners
September 25, 2026 10:09
Contributor
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. |
maraf
reviewed
Sep 25, 2026
This was referenced Sep 25, 2026
akoeplinger
reviewed
Sep 25, 2026
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>
maraf
reviewed
Sep 29, 2026
# Conflicts: # src/native/libs/System.Native.Browser/native/index.ts
…targets eng/native.wasm.targets is packaged into the WebAssembly SDK pack alongside this file (Microsoft.NET.Runtime.WebAssembly.Sdk.pkgproj), so both the in-tree import ($(RepositoryEngineeringDir)native.wasm.targets) and the workload import ($(MSBuildThisFileDirectory)native.wasm.targets) resolve to its authoritative GenerateEmccExports. The local fallback was already overridden by last-definition-wins in both cases since dotnet#132715 started packaging the eng file, so it was dead code that had to be kept in sync by hand. Remove it and let the single GenerateEmccExports (exported functions/methods plus the INCOMING_MODULE_JS_API allowlist) be the only definition. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
maraf
approved these changes
Sep 29, 2026
The emscripten INCOMING_MODULE_JS_API allowlist only changes behavior when assertions are on (Debug): its whole purpose is the early "Module.<x> was supplied but not in INCOMING_MODULE_JS_API" abort. In Release there are no assertions, so restricting the list has no benefit. Emit the -s INCOMING_MODULE_JS_API flag only for Debug and let emscripten use its default list otherwise: - browser.proj / BrowserWasmApp.targets / BrowserWasmApp.CoreCLR.targets: add Condition="'$(Configuration)' == 'Debug'" to the link-flag item. - browserhost CMakeLists.txt: move the -sINCOMING_MODULE_JS_API line into the existing if(UPPERCASE_CMAKE_BUILD_TYPE STREQUAL DEBUG) block, next to ASSERTIONS=1. The CMAKE_EMCC_INCOMING_MODULE_JS_API arg is still passed unconditionally (inert data in Release). Name the MSBuild item _EmccIncomingModuleJSAPI (each entry is one API) and the joined property _EmccIncomingModuleJSAPIs (the comma-separated list) so users don't set the item: the underscore marks both internal. The wasm-props.json task parameter keeps the EmccDefaultIncomingModuleJSAPI name to match EmccDefaultExported*. Validated: Debug clr+libs+host browser build succeeds and the generated browserhost checkIncomingModuleAPI is unchanged (rejects wasmMemory/wasmBinary, accepts the 26 defaults). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
maraf
approved these changes
Sep 29, 2026
This was referenced Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


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.One source of the list per flavor
The list is not hand-copied per link step. CoreCLR defines it once in
GenerateEmccExports(eng/native.wasm.targets), which is packaged into the WebAssembly SDK, so both the in-tree browserhost link and the workload relink consume the same target — the earlier fallback copy inBrowserWasmApp.CoreCLR.targetsis removed. Mono defines it once inbrowser.proj, emits it intowasm-props.json, and the app relink reads it back throughReadWasmProps— the same wayEXPORTED_FUNCTIONS/EXPORTED_RUNTIME_METHODSalready flow.The
-s INCOMING_MODULE_JS_APIflag is emitted only for Debug: the allowlist only does anything with emscripten assertions on, so Release keeps emscripten's default list (for CoreCLR the flag moved into the browserhostCMakeLists.txtif(DEBUG)block, next toASSERTIONS=1). The MSBuild item is named_EmccIncomingModuleJSAPI(each entry is one API) and the joined property_EmccIncomingModuleJSAPIs, both underscore-internal so app authors don't set them.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.The normalization helper lives inside
dotnetInitializeModule: emscripten serializes that function withFunction.toString(), so a bundle-scope helper would be dropped and its minified name (t) left dangling — theReferenceError: t is not definedstartup failure. Keeping it in the serialized scope avoids that.Verification
-subset mono -c Debug /p:WasmEnableThreads=truebuilds; in the regenerateddotnet.native.js,checkIncomingModuleAPIrejects exactly what stock emscripten rejects minuswasmMemory.-subset clr+libs+host -c Debugbuilds; the browserhostdotnet.native.jscheckIncomingModuleAPIaccepts the 26 emscripten defaults and rejectswasmMemory/wasmBinary, unchanged across the dedup and rename.npm run rollup:debugandnpm run lintare clean undersrc/native.Still not runtime-tested (no Wasm.Build.Tests or xharness run). The
BrowserWasmApp.CoreCLR.targetsevaluation-order hazard from an earlier revision is gone: the list now comes from theGenerateEmccExportsdependency instead of anItemGroupthat had to precede thePropertyGroup.Resolves #132555
Note
This pull request description was generated with GitHub Copilot.