Skip to content

[browser] Fix Debug-only startup failures: stack-first assert, incoming Module API, preRun function form - #134527

Open
pavelsavara wants to merge 7 commits into
dotnet:mainfrom
pavelsavara:browser_fix_debug
Open

pavelsavara wants to merge 7 commits into
dotnet:mainfrom
pavelsavara:browser_fix_debug

Conversation

@pavelsavara

@pavelsavara pavelsavara commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

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

* Assertion at src/mono/mono/utils/mono-threads-wasm.c:201, condition `*staddr != NULL' not met
  at mono_threads_platform_get_stack_bounds
  at mono_thread_info_get_stack_bounds
  at register_thread
  at mono_wasm_register_ui_thread

At -O0 emscripten links with --stack-first, which puts the stack at the start of linear memory, so __stack_low == 0. pthread_getattr_np reports stack = emscripten_stack_get_base() and stack_size = base - end, and pthread_attr_getstack returns stack - stack_size, i.e. emscripten_stack_get_end() — legitimately NULL under this layout. Emscripten also comments out musl's if (!a->_a_stackaddr) return EINVAL, so the call succeeds and the assert is what fires.

Mono already accounts for this everywhere else: register_thread has an explicit #ifndef TARGET_WASM around g_assert (staddr) with the comment "for wasm, the stack can be placed at the start of the linear memory", and the non-pthreads branch of mono_threads_platform_get_stack_bounds has no NULL assert at all — which is why Debug single-threaded works. Only the __EMSCRIPTEN_PTHREADS__ branch was missed.

The *stsize != (size_t)-1 assert stays, so a genuinely uninitialized main pthread (stack = 0, stack_size = 0) is still caught by g_assert (stsize) in register_thread.

2. Module.wasmMemory was supplied but wasmMemory not included in INCOMING_MODULE_JS_API

With assertions on, emscripten aborts when a Module property is supplied that isn't declared. Mono's MT worker sets Module.wasmMemory when it receives the mono config, and wasmMemory is not in emscripten's default list — so Debug + MT + Mono aborts during startup.

Both flavors now declare INCOMING_MODULE_JS_API explicitly: emscripten's full default set, plus wasmMemory for 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 prepareRuntimeConfig does ...(window['Module'] || {}) into the module config, so an app author can put any emscripten property on window.Module and it would have aborted a debug build. Hence the full default set.

Adding wasmMemory is behaviourally neutral: initMemory() returns early on a pthread before reaching the Module['wasmMemory'] branch, and the main thread never sets it.

corerun is deliberately left on the default list — its JS injects no Module properties, and corerun.html assigns monitorRunDependencies.

3. TypeError: (Module.preRun || []) is not iterable

Emscripten lets preInit/preRun/postRun be 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 in configureEmscriptenStartup.

WasmBasicTestApp passes preRun as a function, which is how Wasm.Build.Tests hit it after moving to the standard workload. EmscriptenModuleInternal declared these as arrays only, which is what allowed the assumption — widened to match Mono's EmscriptenModule. postRun needs no normalization because the host never prepends to it.

Verification

  • Mono -subset mono -c Debug /p:WasmEnableThreads=true builds; in the regenerated dotnet.native.js, checkIncomingModuleAPI rejects exactly what stock emscripten rejects minus wasmMemory.
  • CoreCLR -subset clr+host -c Debug builds; the browserhost dotnet.native.js rejects exactly what stock emscripten rejects.
  • npm run rollup:debug and npm run lint are clean under src/native.

Not runtime-tested: no Wasm.Build.Tests or xharness run yet. That gap is what let the BrowserWasmApp.CoreCLR.targets ordering 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.

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

Copy link
Copy Markdown
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.

@pavelsavara pavelsavara added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Sep 23, 2026
@pavelsavara pavelsavara added this to the 12.0.0 milestone Sep 23, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

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
pavelsavara marked this pull request as ready for review September 25, 2026 10:09
Copilot AI lite review requested due to automatic review settings September 25, 2026 10:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Medium severity

Open (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>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/mono/browser/build/BrowserWasmApp.CoreCLR.targets Outdated
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copilot AI review requested due to automatic review settings September 25, 2026 16:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Null stack-base range checks remain unsafe and must be guarded.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)

Comment on lines +201 to 202
// 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);

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-Build-mono os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[browser][coreclr] TypeError: (Module.preRun || []) is not iterable

4 participants