Skip to content

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

Merged
pavelsavara merged 11 commits into
dotnet:mainfrom
pavelsavara:browser_fix_debug
Sep 30, 2026
Merged

pavelsavara merged 11 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.

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 in BrowserWasmApp.CoreCLR.targets is removed. Mono defines it once in browser.proj, emits it into wasm-props.json, and the app relink reads it back through ReadWasmProps — the same way EXPORTED_FUNCTIONS/EXPORTED_RUNTIME_METHODS already flow.

The -s INCOMING_MODULE_JS_API flag 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 browserhost CMakeLists.txt if(DEBUG) block, next to ASSERTIONS=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 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.

The normalization helper lives inside dotnetInitializeModule: emscripten serializes that function with Function.toString(), so a bundle-scope helper would be dropped and its minified name (t) left dangling — the ReferenceError: t is not defined startup failure. Keeping it in the serialized scope avoids that.

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+libs+host -c Debug builds; the browserhost dotnet.native.js checkIncomingModuleAPI accepts the 26 emscripten defaults and rejects wasmMemory/wasmBinary, unchanged across the dedup and rename.
  • npm run rollup:debug and npm run lint are clean under src/native.

Still not runtime-tested (no Wasm.Build.Tests or xharness run). The BrowserWasmApp.CoreCLR.targets evaluation-order hazard from an earlier revision is gone: the list now comes from the GenerateEmccExports dependency instead of an ItemGroup that had to precede the PropertyGroup.

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.

Comment thread src/mono/browser/build/BrowserWasmApp.CoreCLR.targets Outdated
Comment thread src/mono/browser/build/BrowserWasmApp.CoreCLR.targets Outdated
Comment thread src/mono/mono/utils/mono-threads-wasm.c
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 thread src/mono/mono/utils/mono-threads-wasm.c
Comment thread src/mono/browser/build/BrowserWasmApp.CoreCLR.targets Outdated
pavelsavara and others added 2 commits September 29, 2026 12:26
# 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>
Comment thread src/mono/browser/build/BrowserWasmApp.targets Outdated
pavelsavara and others added 2 commits September 29, 2026 18:25
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>
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