Coreclr wasi interpreter - #126959

Closed
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter
Closed

Coreclr wasi interpreter#126959
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter

Conversation

@lewing

Copy link
Copy Markdown
Member

No description provided.

lewingand others added 17 commits April 9, 2026 14:33
Enable building the CoreCLR WASM interpreter as a WASI component.
The resulting corerun.wasm (4.8MB stripped) runs on wasmtime and nesm,
with help output in ~150ms. GC initialization succeeds; runtime startup
proceeds to assembly loading.
Build system:
- Add TargetsWasi to CoreCLR supported OS (eng/Subsets.props)
- Set CLR_CMAKE_TARGET_UNIX and CLR_CMAKE_HOST_UNIX for WASI
- Add TARGET_WASI compile definition inside UNIX block
- Add WASI SDK acquisition to runtime.proj
- Import AcquireWasiSdk.targets in native.wasm.targets
- Set WASI emulation flags (_WASI_EMULATED_SIGNAL, _MMAN, etc.)
- Add CI pipeline (eng/pipelines/common/templates/wasi-coreclr-build.yml)
- Disable FEATURE_EVENT_TRACE, watchdog, superpmi, jitinterface for WASI
- Fix libunwind fallthrough for WASM in PAL CMakeLists
- Guard empty PTHREAD_LIBRARY cmake checks
WASI stub headers (pal/src/include/pal/wasi/):
- signal.h: Full POSIX signal types (siginfo_t, sigaction, SA_*, FPE_*)
- pthread.h: Unlock pthread_exit, scheduling declarations
- pwd.h, link.h, dlfcn.h, unistd.h: Missing POSIX stubs
- sys/wait.h, sys/vfs.h, sys/resource.h, ucontext.h: Missing system stubs
PAL stubs (pal/src/arch/wasm/stubs.cpp):
- DebugBreak, OutputDebugString, RaiseException
- PAL_VirtualUnwind, PAL_ProbeMemory, PAL_FreeExceptionRecords
- SEHInitializeSignals, SEHCleanupSignals, UnmaskActivationSignal
- FlushInstructionCache, GetThreadContext
- __cxa_allocate_exception, __cxa_throw, __cxa_thread_atexit
- shm_open, shm_unlink, pthread_getschedparam
WASI mmap (pal/src/arch/wasm/mmap-wasi.c):
- Custom mmap/munmap/mprotect using malloc with allocation tracking
- Replaces wasi-emulated-mman which can't handle PROT_NONE reservations
- Enables GC to allocate segments via linear memory growth
Minimal source changes:
- cgencpu.h: __builtin_frame_address fallback for WASI
- PAL stubs.cpp: Guard emscripten APIs with __EMSCRIPTEN__
- PAL thread.cpp: WASI fallback for GetStackBase/GetStackLimit
- PAL context.h: native_context_t stub for WASI (no ucontext)
- PAL file.cpp: Skip fcntl F_DUPFD_CLOEXEC on WASI
- GC gcenv.unix.cpp: __wasi__ path for memory sizing
- GC events.cpp: _WASI_EMULATED_PROCESS_CLOCKS for timed wait
- finalizerthread.cpp: Synchronous WASI finalizer path
- minipal/mutex.c: Skip recursive mutex attribute on WASI
- Minimal WASI corerun host (corerun-wasi.cpp)
Build: ./build.sh clr.native+clr.corelib+clr.nativecorelib -os wasi -c Release
Test: wasmtime run --dir /::/ corerun.wasm --help
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enable CoreLib loading on WASI by fixing several PAL and host issues:
- eng/native/functions.cmake: Fix install_clr for WASI executables.
WASI SDK produces a single binary, not Emscripten's .js+.wasm pair.
- corerun-wasi.cpp: Resolve CORE_ROOT to absolute path using getcwd
(not realpath which returns '/' on WASI). The assembly binder rejects
relative TPA paths, so corerun must provide absolute paths.
- clrhost.cpp: On WASI (TARGET_WASI), use CORE_ROOT env var to
determine the system directory. The runtime is statically linked
so GetClrModulePathName can't discover the module path normally.
- file.cpp: Bypass realpath() in InternalCanonicalizeRealPath on WASI.
WASI preopen paths are virtual directories that realpath cannot
resolve. Pass paths through directly instead.
- map.cpp: Skip fcntl(F_DUPFD_CLOEXEC) in CreateFileMapping on WASI.
Reuse the fd directly since WASI has no fork/exec (CLOEXEC is moot).
With these fixes, corerun.wasm successfully:
- Opens System.Private.CoreLib.dll (5MB) via WASI preopened dirs
- Maps it into memory via the malloc-based mmap
- Completes coreclr_initialize (host->Start() returns S_OK)
- Begins managed code execution in CreateAppDomainWithManager
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The malloc-based mmap caused memory corruption: the GC reserves and
releases large anonymous regions via mmap/munmap, and malloc reuses
freed addresses. File-backed mappings (assembly loading) then get
addresses within previously-freed GC regions. When the GC later
initializes those pages, it zeroes out the assembly data.
Fix: use a bump allocator backed by a 256MB arena for anonymous
mappings. This prevents address reuse, matching real mmap semantics
where munmap'd pages aren't returned to other callers. File-backed
mappings continue using malloc since they're long-lived and isolated.
With this fix, coreclr_initialize completes successfully on WASI:
- GC heap initializes in the arena
- CoreLib (5MB) loads via malloc into non-overlapping memory
- AppContext.Setup compiles and executes through the interpreter
- CreateAppDomainWithManager returns S_OK
- coreclr_execute_assembly is reached
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 64KB TPA buffer overflowed when scanning directories with many
framework assemblies. Increase to 256KB.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove redundant AcquireWasiSdk.targets import from build-native.proj.
It's already imported through native.wasm.targets at line 3, causing
MSB4011 when building libs for WASI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WASI SDK 32 ships LLVM 22.1.0 which is within the supported version
range (8-23), but init-compiler.sh fails to find it because the WASI
SDK clang is not on PATH. The WASI SDK cmake toolchain file
(wasi-sdk-p2.cmake) already sets CMAKE_C_COMPILER and
CMAKE_CXX_COMPILER to the SDK's clang, so init-compiler.sh is
unnecessary.
Follow the same pattern used for Android builds: set __Compiler to
'default' for WASI targets so gen-buildsys.sh skips init-compiler.sh
and lets the toolchain file handle compiler selection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bump WASI SDK from 25 (LLVM 19) to 32 (LLVM 22). This gives us:
- Native WASI P2 component output from wasm-ld (no wasm-tools wrapper needed)
- Path toward -fwasm-exceptions (requires wasi-sdk-33+ with exceptions sysroot)
- LLVM 22 with better codegen
Fix LLVM 22 strictness warnings promoted to errors:
- pal_icushim_static.c: explicit UErrorCode casts, char* cast, declaration reorder
- pal_networking.c: move declaration before goto label
- stubs.cpp: remove __cxa_thread_atexit stub (now provided by SDK 32 libc++abi)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch from sjlj (-fexceptions) to native WASM exceptions
(-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false) for WASI.
sjlj exceptions cannot handle throw-from-catch patterns needed by
the interpreter's ResumeAfterCatch mechanism. Native WASM exceptions
resolve this — throw/catch now connects properly.
Changes:
- configureplatform.cmake: -fwasm-exceptions + -wasm-use-legacy-eh=false
- CMakeLists.txt: disable -mllvm -wasm-enable-sjlj for WASI
- corerun CMakeLists.txt: link -lunwind instead of -lsetjmp,
pass -fwasm-exceptions at link time
- stubs.cpp: remove __cxa_throw/__cxa_allocate_exception stubs
(now provided by EH-enabled libc++abi/libunwind)
Requires EH-enabled sysroot libraries (libc++.a, libc++abi.a,
libunwind.a) built from wasi-sdk main with -DWASI_SDK_EXCEPTIONS=ON.
These will ship in wasi-sdk-33+. For now, build from source:
cd wasi-sdk && cmake -G Ninja -B build -DWASI_SDK_EXCEPTIONS=ON
ninja -C build
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On WASM, RtlCaptureContext/ClrCaptureContext zeros the CONTEXT since
there are no CPU registers to capture. This leaves the exception
dispatch with IP=0, SP=0 which breaks the managed stack frame iterator.
Fix: In DispatchManagedException, populate the CONTEXT from the current
interpreter frame (InterpreterFrame::SetContextToInterpMethodContextFrame)
before passing it to the EH dispatch. Also record the current interpreter
IP in the frame before throwing (INTOP_THROW sets pFrame->ip).
This provides valid IP/SP to the EH dispatch, but the managed exception
handling still needs additional work in SfiInitWorker to properly walk
interpreter frames on WASM (tracked in dotnet#113694).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The arena bump allocator in mmap-wasi.c aligned the offset within the
arena rather than the absolute address. When malloc returned a base
address that was not 64K-aligned, subsequent 64K-aligned allocations
(e.g., GC handle table segments) were misaligned. The handle table uses
pointer masking (handle & ~0xFFFF) to locate segment headers, so
misaligned segments caused NULL pHandleTable lookups and crashes during
exception handling cleanup (DestroyHandle in CallCatchFunclet).
Also enable recursive mutexes on WASI to match other platforms, since
the runtime may re-enter locks during exception handling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The stack walker finds interpreter frames through the explicit frame
chain (Thread::GetFrame()), not through the CONTEXT IP/SP. The zeroed
context from RtlCaptureContext on WASM is fine because the EH dispatch
starts on an explicit frame. Browser WASM works the same way without
this patch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Report available memory as max (4GB wasm32 limit) minus current usage,
matching Emscripten's approach. The previous code reported current memory
size as available, which is incorrect — it shrinks as memory grows and
doesn't reflect room to grow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The main corerun.cpp already has TARGET_WASM and TARGET_WASI ifdefs
that handle the WASI differences (no dlopen, static exports, etc.).
The separate corerun-wasi.cpp reimplemented TPA building, arg parsing,
and the main loop unnecessarily.
Use the unified corerun.cpp for all platforms. The only WASI-specific
addition is providing HAVE_GETAUXVAL/HAVE_DIRENT_D_TYPE defines directly
since configure.cmake can't run for cross-compilation targets.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The inner `if(NOT CLR_CMAKE_TARGET_WASI)` could never execute since
it was nested inside `if(CLR_CMAKE_TARGET_WASI)`. The sjlj flag is
not needed — WASI uses -fwasm-exceptions instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 15, 2026 17:51
@github-actionsgithub-actionsBot added the area-PAL-coreclr only for closed issues label Apr 15, 2026
@lewinglewing closed this Apr 15, 2026

CopilotAI 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.

Pull request overview

Enable building/running CoreCLR for the WASI target by wiring up WASI SDK acquisition, adjusting CoreCLR/PAL platform conditionals, and adding WASI-specific stubs and build/pipeline support.

Changes:

  • Add/extend WASI build plumbing (SDK acquisition, cmake/msbuild targets, subsets, pipelines).
  • Introduce WASI-specific PAL headers/stubs and WASI mmap implementation, plus various WASI feature guards across CoreCLR/PAL/GC/corerun.
  • Adjust interpreter/browser/wasm conditionals to better separate Emscripten vs WASI behavior.

Reviewed changes

Copilot reviewed 50 out of 50 changed files in this pull request and generated 9 comments.

Show a summary per file
FileDescription
src/native/libs/build-native.projRemoves standalone WASI SDK import; relies on shared wasm targets.
src/native/libs/System.Native/pal_networking.cMinor variable initialization tweak.
src/native/libs/System.Globalization.Native/pal_icushim_static.cICU status init and malloc cast cleanup.
src/coreclr/vm/wasm/cgencpu.hGuard Emscripten stack include; WASI fallback SP logic.
src/coreclr/vm/prestub.cppRefine interpreter ETW handling with portable entrypoints.
src/coreclr/vm/finalizerthread.cppWASI synchronous finalization path under wasm.
src/coreclr/utilcode/clrhost.cppWASI module path derived from CORE_ROOT.
src/coreclr/tools/CMakeLists.txtSkip superpmi for WASI/browser builds.
src/coreclr/runtime.projAdd AcquireWasiSdk dependency for WASI runtime build.
src/coreclr/pal/src/thread/thread.cppWASI stack base/limit handling adjustments.
src/coreclr/pal/src/thread/context.cppDisable remote register capture on WASI.
src/coreclr/pal/src/map/map.cppWASI fcntl/dup workaround for file mappings.
src/coreclr/pal/src/include/pal/wasi/unistd.hWASI unistd wrapper with stubbed POSIX APIs.
src/coreclr/pal/src/include/pal/wasi/ucontext.hWASI ucontext stub header.
src/coreclr/pal/src/include/pal/wasi/sys/wait.hWASI sys/wait stub header.
src/coreclr/pal/src/include/pal/wasi/sys/vfs.hWASI sys/vfs stub header.
src/coreclr/pal/src/include/pal/wasi/sys/resource.hWASI sys/resource wrapper for rlimit.
src/coreclr/pal/src/include/pal/wasi/signal.hWASI signal wrapper defining missing POSIX types/APIs.
src/coreclr/pal/src/include/pal/wasi/pwd.hWASI pwd stub header.
src/coreclr/pal/src/include/pal/wasi/pthread.hWASI pthread wrapper for hidden decls.
src/coreclr/pal/src/include/pal/wasi/link.hWASI link stub for dl_iterate_phdr types.
src/coreclr/pal/src/include/pal/wasi/dlfcn.hWASI dlfcn wrapper for Dl_info/dladdr.
src/coreclr/pal/src/include/pal/context.hAdd WASI native_context_t fallback.
src/coreclr/pal/src/file/file.cppWASI fd duplication workaround for std handles/realpath.
src/coreclr/pal/src/exception/seh.cppDisable seh-unwind.cpp inclusion on WASI.
src/coreclr/pal/src/configure.cmakeMake pthread feature checks conditional; add WASI sched flags.
src/coreclr/pal/src/arch/wasm/stubs.cppEmscripten guards + WASI stubs for missing APIs.
src/coreclr/pal/src/arch/wasm/mmap-wasi.cNew WASI mmap/munmap implementation.
src/coreclr/pal/src/CMakeLists.txtWASI source selection + avoid unwind/dac pieces on wasm.
src/coreclr/hosts/corerun/dotenv.cppDisable corerun dotenv self-test on WASI.
src/coreclr/hosts/corerun/corerun.hppAvoid config.h for wasm; define minimal feature macros.
src/coreclr/hosts/corerun/corerun.cppDisable corerun self-test path on WASI.
src/coreclr/hosts/corerun/CMakeLists.txtWASI link options/libs; avoid dl/rdynamic on wasm.
src/coreclr/gc/unix/gcenv.unix.cppWASI mman define handling + available memory logic.
src/coreclr/gc/unix/events.cppAllow process-clock emulation define for waits.
src/coreclr/gc/unix/configure.cmakeGuard pthread checks on presence of pthread library.
src/coreclr/debug/debug-pal/unix/twowaypipe.cppWASI mkfifo stub for debug transport.
src/coreclr/debug/debug-pal/CMakeLists.txtDisable diagnostic server reference on WASI.
src/coreclr/clrfeatures.cmakeDisable event trace default for WASI.
src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csprojInclude Unix/browser interop stubs for WASI too.
src/coreclr/CMakeLists.txtWASI compile defs + include stub headers; skip tools/watchdog.
eng/pipelines/runtime.ymlAdd WASI CoreCLR build job template.
eng/pipelines/common/templates/wasi-coreclr-build.ymlNew pipeline template for WASI CoreCLR build-only job.
eng/native/functions.cmakeInstall rule adjustment for WASI wasm executables.
eng/native/configureplatform.cmakeTreat WASI as UNIX; set WASI target flags.
eng/native/configurecompiler.cmakeEmit TARGET_WASI compile definition.
eng/native/build-commons.shAvoid overriding CC/CXX for WASI builds.
eng/native.wasm.targetsImport AcquireWasiSdk + add WASI CoreCLR cmake args.
eng/Subsets.propsEnable CoreCLR subsets and defaults for WASI.
eng/AcquireWasiSdk.targetsUpdate runtime-local WASI SDK version.

Comment on lines +2360 to +2364
#elif defined(__EMSCRIPTEN__)
stackBase = (void*)emscripten_stack_get_base();
#endif // TARGET_BROWSER
#else // WASI
stackBase = __builtin_frame_address(0);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

In the WASI path, GetStackBase returns __builtin_frame_address(0), which is the current frame address, not the thread’s stack base. Components that rely on PAL_GetStackBase() for stack scanning/overflow checks will get an incorrect range. Consider using toolchain/linker-provided stack bound symbols (e.g., __stack_high/__stack_low or equivalent for wasi-sdk) or another reliable WASI-specific mechanism rather than the current frame address.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good catch. __builtin_frame_address(0) is an approximation — WASI/wasm32 doesn't expose linker-provided stack bound symbols like __stack_high for wasip2. Added a comment explaining this is an approximation. The value is cached on first call and used for the lifetime of the (single) thread, so it's close to the initial stack top.

Comment on lines +469 to +477
#ifdef TARGET_WASI
// WASI's fcntl may not support F_DUPFD_CLOEXEC at runtime.
// CLOEXEC is irrelevant on WASI (no exec). Use fd_fdstat_get + path_open
// workaround: just reuse the fd directly without duplication since
// WASI doesn't support fork/exec anyway.
UnixFd = pFileLocalData->unix_fd;
#else
UnixFd = fcntl(pFileLocalData->unix_fd, F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

On WASI, assigning UnixFd = pFileLocalData->unix_fd means the file mapping now owns the original file descriptor. FileMappingCleanupRoutine later close()s pLocalData->UnixFd, which will unexpectedly close the underlying handle used by hFile and any other users. Use a real dup operation on WASI (e.g., dup()/dup3() if available) or track ownership so the mapping cleanup doesn’t close a borrowed descriptor.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Valid concern. WASI's fcntl doesn't implement F_DUPFD_CLOEXEC (or F_DUPFD) at runtime despite the constant being defined at compile time, and dup() isn't available in wasip2 headers. The fd sharing is safe here because WASI is single-threaded with no fork/exec, so CLOEXEC is irrelevant and the file mapping lifetime matches the source handle. Updated the comment to explain this rationale.

Comment on lines 2438 to +2445
/* duplicate the FILE *, so that we can fclose() in FILECloseHandle without
closing the original */
#ifdef TARGET_WASI
// WASI: fcntl F_DUPFD_CLOEXEC not supported; reuse fd directly (single-threaded, no exec)
new_fd = fileno(stream);
#else
new_fd = fcntl(fileno(stream), F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

The WASI path sets new_fd = fileno(stream) instead of duplicating the descriptor. This makes the PAL file handle for stdin/stdout/stderr own and potentially close() the original stdio fd (e.g., on failure paths and when the handle is later closed), which can break subsequent I/O. Use an actual descriptor duplication mechanism on WASI, or ensure the PAL handle is marked non-owning so it won’t close the original fd.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Same situation as the map.cpp case — fcntl F_DUPFD* and dup() aren't available on wasip2. The stdio fds (0/1/2) are never closed by the PAL on WASI, so sharing is safe. Updated comment to explain the safety rationale.

<Project>
<PropertyGroup>
<_WasiSdkVersion>25.0</_WasiSdkVersion>
<_WasiSdkVersion>32.0</_WasiSdkVersion>

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

_WasiSdkVersion was updated to 32.0, but src/mono/wasi/build/WasiApp.targets still hard-codes an expected WASI SDK version of 25.0. This will cause Mono WASI builds (and any tooling that validates WASI_SDK_PATH/VERSION) to fail when using the runtime-local SDK acquired here. Please update the other expected-version checks (or centralize the version) so all WASI build paths agree on a single SDK version.

Suggested change
<_WasiSdkVersion>32.0</_WasiSdkVersion>
<WasiSdkVersionCondition="'$(WasiSdkVersion)' == ''">32.0</WasiSdkVersion>
<_WasiSdkVersion>$(WasiSdkVersion)</_WasiSdkVersion>

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

The Mono WASI build uses its own SDK acquisition path and isn't affected by this CoreCLR-specific version bump. We have a separate PR (dotnet/wasi-sdk#239) to update the dotnet fork of wasi-sdk to upstream v33 which will unify SDK versions across both runtimes.

Comment on lines +2415 to +2417
#else // WASI
stackLimit = (void*)sizeof(size_t);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

For WASI, GetStackLimit() sets stackLimit to (void*)sizeof(size_t), which is just a small constant cast to a pointer and not an actual stack boundary. This can break stack range calculations and stack overflow detection. Please compute a real lower stack bound for WASI (or, if unavailable, adjust callers to use SP-based scanning and return a clearly defined sentinel that won’t be treated as an address).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now computes stack limit as frame_address - 8MB (matching the -Wl,-z,stack-size=8388608 set at link time) with a floor at sizeof(size_t) to avoid wrapping to zero.

Comment on lines 485 to 516
@@ -509,6 +510,7 @@ BOOL CONTEXT_GetRegisters(DWORD processId, LPCONTEXT lpContext)
#undef ASSIGN_REG

CONTEXTFromNativeContext(&registers, lpContext, lpContext->ContextFlags);
#endif // !TARGET_WASI
}

bRet = TRUE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

When processId != GetCurrentProcessId() on WASI, the ucontext_t registers block is compiled out, but the function still falls through and returns TRUE without populating lpContext. This results in callers observing success with uninitialized/incorrect register state. Return FALSE (and set an appropriate last error) or explicitly zero/initialize lpContext similarly to the CONTEXT_GetThreadContext fallback path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now returns FALSE immediately for non-current process on WASI.

Comment on lines +31 to +34
};

static inline int dl_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data) { (void)callback; (void)data; return 0; }

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

This header uses size_t in the dl_iterate_phdr callback signature but doesn’t include a header that defines it. Add an include like <stddef.h> (or <sys/types.h>) to ensure size_t is available for both C and C++ compilation.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Added #include <stddef.h>.

#ifdef TARGET_WASI
extern "C" int pthread_getschedparam(pthread_t, int *policy, struct sched_param *param)
{
if (policy) *policy = 0;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

pthread_getschedparam returns success but leaves *param uninitialized. Callers expecting sched_param fields to be set may read garbage. Either populate *param with sensible defaults (e.g., zero priority) or return an error code (e.g., ENOTSUP) to signal the API isn’t implemented on WASI.

Suggested change
if (policy) *policy = 0;
if (policy) *policy = 0;
if (param) *param = sched_param{};

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now zero-initializes *param with memset.

Comment on lines +21 to +58
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#define ANON_ARENA_SIZE (256 * 1024 * 1024)

static char *s_arena_base;
static size_t s_arena_offset;

static void ensure_arena(void) {
if (s_arena_base == NULL) {
s_arena_base = (char *)malloc(ANON_ARENA_SIZE);
if (s_arena_base == NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset = 0;
}
}

// Allocate from the bump arena with alignment. Never reuses freed addresses.
static void *arena_alloc(size_t length, size_t align) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_t base = (uintptr_t)s_arena_base;
uintptr_t current = base + s_arena_offset;
uintptr_t aligned_addr = (current + align - 1) & ~(align - 1);
size_t aligned_offset = aligned_addr - base;
if (aligned_offset + length > ANON_ARENA_SIZE) {
return NULL;
}
void *ptr = (void *)aligned_addr;
s_arena_offset = aligned_offset + length;
return ptr;
}

static int is_arena_ptr(void *addr) {
if (s_arena_base == NULL) return 0;
return (char *)addr >= s_arena_base &&
(char *)addr < s_arena_base + ANON_ARENA_SIZE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

ensure_arena() allocates a fixed 256MB arena eagerly via malloc(ANON_ARENA_SIZE). On wasm32/WASI this directly consumes linear memory and can cause large, immediate memory growth or OOM even for small apps. Consider allocating the arena lazily in smaller chunks (or growing it) and/or sizing it based on configured limits rather than a hard-coded 256MB constant.

Suggested change
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
staticchar*s_arena_base;
staticsize_ts_arena_offset;
staticvoidensure_arena(void) {
if (s_arena_base==NULL) {
s_arena_base= (char*)malloc(ANON_ARENA_SIZE);
if (s_arena_base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset=0;
}
}
// Allocate from the bump arena with alignment. Never reuses freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_tbase= (uintptr_t)s_arena_base;
uintptr_tcurrent=base+s_arena_offset;
uintptr_taligned_addr= (current+align-1) & ~(align-1);
size_taligned_offset=aligned_addr-base;
if (aligned_offset+length>ANON_ARENA_SIZE) {
returnNULL;
}
void*ptr= (void*)aligned_addr;
s_arena_offset=aligned_offset+length;
returnptr;
}
staticintis_arena_ptr(void*addr) {
if (s_arena_base==NULL) return0;
return (char*)addr >= s_arena_base&&
(char*)addr<s_arena_base+ANON_ARENA_SIZE;
// Cap anonymous mappings at 256MB total, but allocate backing storage lazily
// in small chunks so small WASI apps do not eagerly consume large amounts of
// linear memory on the first anonymous mapping.
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
#defineANON_ARENA_CHUNK_SIZE (64 * 1024)
#defineANON_ARENA_MAX_CHUNKS (ANON_ARENA_SIZE / ANON_ARENA_CHUNK_SIZE)
typedefstruct
{
char*base;
size_tsize;
size_toffset;
} anon_arena_chunk;
staticanon_arena_chunks_arena_chunks[ANON_ARENA_MAX_CHUNKS];
staticsize_ts_arena_chunk_count;
staticsize_ts_arena_total_size;
staticvoidensure_arena(void) {
// Chunks are allocated lazily by arena_alloc().
}
staticanon_arena_chunk*arena_alloc_chunk(size_tmin_length, size_talign) {
size_tchunk_size;
anon_arena_chunk*chunk;
if (s_arena_chunk_count >= ANON_ARENA_MAX_CHUNKS) {
returnNULL;
}
chunk_size=ANON_ARENA_CHUNK_SIZE;
if (min_length>chunk_size||align>1) {
size_trequired=min_length+align-1;
if (required>chunk_size) {
chunk_size=required;
}
}
if (chunk_size>ANON_ARENA_SIZE-s_arena_total_size) {
returnNULL;
}
chunk=&s_arena_chunks[s_arena_chunk_count];
chunk->base= (char*)malloc(chunk_size);
if (chunk->base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %zu byte arena chunk\n", chunk_size);
abort();
}
chunk->size=chunk_size;
chunk->offset=0;
s_arena_chunk_count++;
s_arena_total_size+=chunk_size;
returnchunk;
}
// Allocate from lazily created bump arena chunks with alignment. Never reuses
// freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
anon_arena_chunk*chunk;
uintptr_tbase;
uintptr_tcurrent;
uintptr_taligned_addr;
size_taligned_offset;
ensure_arena();
if (align==0) {
align=1;
}
if (s_arena_chunk_count!=0) {
chunk=&s_arena_chunks[s_arena_chunk_count-1];
base= (uintptr_t)chunk->base;
current=base+chunk->offset;
aligned_addr= (current+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length <= chunk->size) {
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
}
chunk=arena_alloc_chunk(length, align);
if (chunk==NULL) {
returnNULL;
}
base= (uintptr_t)chunk->base;
aligned_addr= (base+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length>chunk->size) {
returnNULL;
}
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
staticintis_arena_ptr(void*addr) {
size_ti;
for (i=0; i<s_arena_chunk_count; i++) {
char*base=s_arena_chunks[i].base;
if ((char*)addr >= base&& (char*)addr<base+s_arena_chunks[i].size) {
return1;
}
}
return0;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good suggestion. On wasm32, malloc of 256MB doesn't immediately consume physical memory — it triggers memory.grow lazily as pages are touched. The arena is bumped incrementally so only the used portion is backed by real pages. That said, a lazy chunked approach would be cleaner and we'll consider it as a follow-up optimization.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Correction to my previous reply: wasm memory.grow commits pages immediately — there's no lazy page fault mechanism in wasm linear memory. So the 256MB malloc does grow linear memory upfront. A chunked approach that grows the arena in smaller increments as needed would save real memory for smaller apps. Filed as a follow-up improvement.

- context.cpp: Return FALSE for non-current process on WASI instead of
falling through with uninitialized context
- thread.cpp: Improve GetStackBase/GetStackLimit comments; compute stack
limit from frame address minus configured stack size (8MB)
- file.cpp, map.cpp: Improve fd-sharing comments explaining why reuse is
safe on WASI (no fork/exec, no CLOEXEC needed)
- link.h: Add missing stddef.h include for size_t
- stubs.cpp: Zero-initialize sched_param in pthread_getschedparam stub
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lewinglewing reopened this Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-PAL-coreclronly for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Coreclr wasi interpreter - #126959

Closed
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter
Closed

Coreclr wasi interpreter#126959
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter

Conversation

@lewing

Copy link
Copy Markdown
Member

No description provided.

lewingand others added 17 commits April 9, 2026 14:33
Enable building the CoreCLR WASM interpreter as a WASI component.
The resulting corerun.wasm (4.8MB stripped) runs on wasmtime and nesm,
with help output in ~150ms. GC initialization succeeds; runtime startup
proceeds to assembly loading.
Build system:
- Add TargetsWasi to CoreCLR supported OS (eng/Subsets.props)
- Set CLR_CMAKE_TARGET_UNIX and CLR_CMAKE_HOST_UNIX for WASI
- Add TARGET_WASI compile definition inside UNIX block
- Add WASI SDK acquisition to runtime.proj
- Import AcquireWasiSdk.targets in native.wasm.targets
- Set WASI emulation flags (_WASI_EMULATED_SIGNAL, _MMAN, etc.)
- Add CI pipeline (eng/pipelines/common/templates/wasi-coreclr-build.yml)
- Disable FEATURE_EVENT_TRACE, watchdog, superpmi, jitinterface for WASI
- Fix libunwind fallthrough for WASM in PAL CMakeLists
- Guard empty PTHREAD_LIBRARY cmake checks
WASI stub headers (pal/src/include/pal/wasi/):
- signal.h: Full POSIX signal types (siginfo_t, sigaction, SA_*, FPE_*)
- pthread.h: Unlock pthread_exit, scheduling declarations
- pwd.h, link.h, dlfcn.h, unistd.h: Missing POSIX stubs
- sys/wait.h, sys/vfs.h, sys/resource.h, ucontext.h: Missing system stubs
PAL stubs (pal/src/arch/wasm/stubs.cpp):
- DebugBreak, OutputDebugString, RaiseException
- PAL_VirtualUnwind, PAL_ProbeMemory, PAL_FreeExceptionRecords
- SEHInitializeSignals, SEHCleanupSignals, UnmaskActivationSignal
- FlushInstructionCache, GetThreadContext
- __cxa_allocate_exception, __cxa_throw, __cxa_thread_atexit
- shm_open, shm_unlink, pthread_getschedparam
WASI mmap (pal/src/arch/wasm/mmap-wasi.c):
- Custom mmap/munmap/mprotect using malloc with allocation tracking
- Replaces wasi-emulated-mman which can't handle PROT_NONE reservations
- Enables GC to allocate segments via linear memory growth
Minimal source changes:
- cgencpu.h: __builtin_frame_address fallback for WASI
- PAL stubs.cpp: Guard emscripten APIs with __EMSCRIPTEN__
- PAL thread.cpp: WASI fallback for GetStackBase/GetStackLimit
- PAL context.h: native_context_t stub for WASI (no ucontext)
- PAL file.cpp: Skip fcntl F_DUPFD_CLOEXEC on WASI
- GC gcenv.unix.cpp: __wasi__ path for memory sizing
- GC events.cpp: _WASI_EMULATED_PROCESS_CLOCKS for timed wait
- finalizerthread.cpp: Synchronous WASI finalizer path
- minipal/mutex.c: Skip recursive mutex attribute on WASI
- Minimal WASI corerun host (corerun-wasi.cpp)
Build: ./build.sh clr.native+clr.corelib+clr.nativecorelib -os wasi -c Release
Test: wasmtime run --dir /::/ corerun.wasm --help
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enable CoreLib loading on WASI by fixing several PAL and host issues:
- eng/native/functions.cmake: Fix install_clr for WASI executables.
WASI SDK produces a single binary, not Emscripten's .js+.wasm pair.
- corerun-wasi.cpp: Resolve CORE_ROOT to absolute path using getcwd
(not realpath which returns '/' on WASI). The assembly binder rejects
relative TPA paths, so corerun must provide absolute paths.
- clrhost.cpp: On WASI (TARGET_WASI), use CORE_ROOT env var to
determine the system directory. The runtime is statically linked
so GetClrModulePathName can't discover the module path normally.
- file.cpp: Bypass realpath() in InternalCanonicalizeRealPath on WASI.
WASI preopen paths are virtual directories that realpath cannot
resolve. Pass paths through directly instead.
- map.cpp: Skip fcntl(F_DUPFD_CLOEXEC) in CreateFileMapping on WASI.
Reuse the fd directly since WASI has no fork/exec (CLOEXEC is moot).
With these fixes, corerun.wasm successfully:
- Opens System.Private.CoreLib.dll (5MB) via WASI preopened dirs
- Maps it into memory via the malloc-based mmap
- Completes coreclr_initialize (host->Start() returns S_OK)
- Begins managed code execution in CreateAppDomainWithManager
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The malloc-based mmap caused memory corruption: the GC reserves and
releases large anonymous regions via mmap/munmap, and malloc reuses
freed addresses. File-backed mappings (assembly loading) then get
addresses within previously-freed GC regions. When the GC later
initializes those pages, it zeroes out the assembly data.
Fix: use a bump allocator backed by a 256MB arena for anonymous
mappings. This prevents address reuse, matching real mmap semantics
where munmap'd pages aren't returned to other callers. File-backed
mappings continue using malloc since they're long-lived and isolated.
With this fix, coreclr_initialize completes successfully on WASI:
- GC heap initializes in the arena
- CoreLib (5MB) loads via malloc into non-overlapping memory
- AppContext.Setup compiles and executes through the interpreter
- CreateAppDomainWithManager returns S_OK
- coreclr_execute_assembly is reached
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 64KB TPA buffer overflowed when scanning directories with many
framework assemblies. Increase to 256KB.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove redundant AcquireWasiSdk.targets import from build-native.proj.
It's already imported through native.wasm.targets at line 3, causing
MSB4011 when building libs for WASI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WASI SDK 32 ships LLVM 22.1.0 which is within the supported version
range (8-23), but init-compiler.sh fails to find it because the WASI
SDK clang is not on PATH. The WASI SDK cmake toolchain file
(wasi-sdk-p2.cmake) already sets CMAKE_C_COMPILER and
CMAKE_CXX_COMPILER to the SDK's clang, so init-compiler.sh is
unnecessary.
Follow the same pattern used for Android builds: set __Compiler to
'default' for WASI targets so gen-buildsys.sh skips init-compiler.sh
and lets the toolchain file handle compiler selection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bump WASI SDK from 25 (LLVM 19) to 32 (LLVM 22). This gives us:
- Native WASI P2 component output from wasm-ld (no wasm-tools wrapper needed)
- Path toward -fwasm-exceptions (requires wasi-sdk-33+ with exceptions sysroot)
- LLVM 22 with better codegen
Fix LLVM 22 strictness warnings promoted to errors:
- pal_icushim_static.c: explicit UErrorCode casts, char* cast, declaration reorder
- pal_networking.c: move declaration before goto label
- stubs.cpp: remove __cxa_thread_atexit stub (now provided by SDK 32 libc++abi)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch from sjlj (-fexceptions) to native WASM exceptions
(-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false) for WASI.
sjlj exceptions cannot handle throw-from-catch patterns needed by
the interpreter's ResumeAfterCatch mechanism. Native WASM exceptions
resolve this — throw/catch now connects properly.
Changes:
- configureplatform.cmake: -fwasm-exceptions + -wasm-use-legacy-eh=false
- CMakeLists.txt: disable -mllvm -wasm-enable-sjlj for WASI
- corerun CMakeLists.txt: link -lunwind instead of -lsetjmp,
pass -fwasm-exceptions at link time
- stubs.cpp: remove __cxa_throw/__cxa_allocate_exception stubs
(now provided by EH-enabled libc++abi/libunwind)
Requires EH-enabled sysroot libraries (libc++.a, libc++abi.a,
libunwind.a) built from wasi-sdk main with -DWASI_SDK_EXCEPTIONS=ON.
These will ship in wasi-sdk-33+. For now, build from source:
cd wasi-sdk && cmake -G Ninja -B build -DWASI_SDK_EXCEPTIONS=ON
ninja -C build
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On WASM, RtlCaptureContext/ClrCaptureContext zeros the CONTEXT since
there are no CPU registers to capture. This leaves the exception
dispatch with IP=0, SP=0 which breaks the managed stack frame iterator.
Fix: In DispatchManagedException, populate the CONTEXT from the current
interpreter frame (InterpreterFrame::SetContextToInterpMethodContextFrame)
before passing it to the EH dispatch. Also record the current interpreter
IP in the frame before throwing (INTOP_THROW sets pFrame->ip).
This provides valid IP/SP to the EH dispatch, but the managed exception
handling still needs additional work in SfiInitWorker to properly walk
interpreter frames on WASM (tracked in dotnet#113694).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The arena bump allocator in mmap-wasi.c aligned the offset within the
arena rather than the absolute address. When malloc returned a base
address that was not 64K-aligned, subsequent 64K-aligned allocations
(e.g., GC handle table segments) were misaligned. The handle table uses
pointer masking (handle & ~0xFFFF) to locate segment headers, so
misaligned segments caused NULL pHandleTable lookups and crashes during
exception handling cleanup (DestroyHandle in CallCatchFunclet).
Also enable recursive mutexes on WASI to match other platforms, since
the runtime may re-enter locks during exception handling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The stack walker finds interpreter frames through the explicit frame
chain (Thread::GetFrame()), not through the CONTEXT IP/SP. The zeroed
context from RtlCaptureContext on WASM is fine because the EH dispatch
starts on an explicit frame. Browser WASM works the same way without
this patch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Report available memory as max (4GB wasm32 limit) minus current usage,
matching Emscripten's approach. The previous code reported current memory
size as available, which is incorrect — it shrinks as memory grows and
doesn't reflect room to grow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The main corerun.cpp already has TARGET_WASM and TARGET_WASI ifdefs
that handle the WASI differences (no dlopen, static exports, etc.).
The separate corerun-wasi.cpp reimplemented TPA building, arg parsing,
and the main loop unnecessarily.
Use the unified corerun.cpp for all platforms. The only WASI-specific
addition is providing HAVE_GETAUXVAL/HAVE_DIRENT_D_TYPE defines directly
since configure.cmake can't run for cross-compilation targets.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The inner `if(NOT CLR_CMAKE_TARGET_WASI)` could never execute since
it was nested inside `if(CLR_CMAKE_TARGET_WASI)`. The sjlj flag is
not needed — WASI uses -fwasm-exceptions instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 15, 2026 17:51
@github-actionsgithub-actionsBot added the area-PAL-coreclr only for closed issues label Apr 15, 2026
@lewinglewing closed this Apr 15, 2026

CopilotAI 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.

Pull request overview

Enable building/running CoreCLR for the WASI target by wiring up WASI SDK acquisition, adjusting CoreCLR/PAL platform conditionals, and adding WASI-specific stubs and build/pipeline support.

Changes:

  • Add/extend WASI build plumbing (SDK acquisition, cmake/msbuild targets, subsets, pipelines).
  • Introduce WASI-specific PAL headers/stubs and WASI mmap implementation, plus various WASI feature guards across CoreCLR/PAL/GC/corerun.
  • Adjust interpreter/browser/wasm conditionals to better separate Emscripten vs WASI behavior.

Reviewed changes

Copilot reviewed 50 out of 50 changed files in this pull request and generated 9 comments.

Show a summary per file
FileDescription
src/native/libs/build-native.projRemoves standalone WASI SDK import; relies on shared wasm targets.
src/native/libs/System.Native/pal_networking.cMinor variable initialization tweak.
src/native/libs/System.Globalization.Native/pal_icushim_static.cICU status init and malloc cast cleanup.
src/coreclr/vm/wasm/cgencpu.hGuard Emscripten stack include; WASI fallback SP logic.
src/coreclr/vm/prestub.cppRefine interpreter ETW handling with portable entrypoints.
src/coreclr/vm/finalizerthread.cppWASI synchronous finalization path under wasm.
src/coreclr/utilcode/clrhost.cppWASI module path derived from CORE_ROOT.
src/coreclr/tools/CMakeLists.txtSkip superpmi for WASI/browser builds.
src/coreclr/runtime.projAdd AcquireWasiSdk dependency for WASI runtime build.
src/coreclr/pal/src/thread/thread.cppWASI stack base/limit handling adjustments.
src/coreclr/pal/src/thread/context.cppDisable remote register capture on WASI.
src/coreclr/pal/src/map/map.cppWASI fcntl/dup workaround for file mappings.
src/coreclr/pal/src/include/pal/wasi/unistd.hWASI unistd wrapper with stubbed POSIX APIs.
src/coreclr/pal/src/include/pal/wasi/ucontext.hWASI ucontext stub header.
src/coreclr/pal/src/include/pal/wasi/sys/wait.hWASI sys/wait stub header.
src/coreclr/pal/src/include/pal/wasi/sys/vfs.hWASI sys/vfs stub header.
src/coreclr/pal/src/include/pal/wasi/sys/resource.hWASI sys/resource wrapper for rlimit.
src/coreclr/pal/src/include/pal/wasi/signal.hWASI signal wrapper defining missing POSIX types/APIs.
src/coreclr/pal/src/include/pal/wasi/pwd.hWASI pwd stub header.
src/coreclr/pal/src/include/pal/wasi/pthread.hWASI pthread wrapper for hidden decls.
src/coreclr/pal/src/include/pal/wasi/link.hWASI link stub for dl_iterate_phdr types.
src/coreclr/pal/src/include/pal/wasi/dlfcn.hWASI dlfcn wrapper for Dl_info/dladdr.
src/coreclr/pal/src/include/pal/context.hAdd WASI native_context_t fallback.
src/coreclr/pal/src/file/file.cppWASI fd duplication workaround for std handles/realpath.
src/coreclr/pal/src/exception/seh.cppDisable seh-unwind.cpp inclusion on WASI.
src/coreclr/pal/src/configure.cmakeMake pthread feature checks conditional; add WASI sched flags.
src/coreclr/pal/src/arch/wasm/stubs.cppEmscripten guards + WASI stubs for missing APIs.
src/coreclr/pal/src/arch/wasm/mmap-wasi.cNew WASI mmap/munmap implementation.
src/coreclr/pal/src/CMakeLists.txtWASI source selection + avoid unwind/dac pieces on wasm.
src/coreclr/hosts/corerun/dotenv.cppDisable corerun dotenv self-test on WASI.
src/coreclr/hosts/corerun/corerun.hppAvoid config.h for wasm; define minimal feature macros.
src/coreclr/hosts/corerun/corerun.cppDisable corerun self-test path on WASI.
src/coreclr/hosts/corerun/CMakeLists.txtWASI link options/libs; avoid dl/rdynamic on wasm.
src/coreclr/gc/unix/gcenv.unix.cppWASI mman define handling + available memory logic.
src/coreclr/gc/unix/events.cppAllow process-clock emulation define for waits.
src/coreclr/gc/unix/configure.cmakeGuard pthread checks on presence of pthread library.
src/coreclr/debug/debug-pal/unix/twowaypipe.cppWASI mkfifo stub for debug transport.
src/coreclr/debug/debug-pal/CMakeLists.txtDisable diagnostic server reference on WASI.
src/coreclr/clrfeatures.cmakeDisable event trace default for WASI.
src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csprojInclude Unix/browser interop stubs for WASI too.
src/coreclr/CMakeLists.txtWASI compile defs + include stub headers; skip tools/watchdog.
eng/pipelines/runtime.ymlAdd WASI CoreCLR build job template.
eng/pipelines/common/templates/wasi-coreclr-build.ymlNew pipeline template for WASI CoreCLR build-only job.
eng/native/functions.cmakeInstall rule adjustment for WASI wasm executables.
eng/native/configureplatform.cmakeTreat WASI as UNIX; set WASI target flags.
eng/native/configurecompiler.cmakeEmit TARGET_WASI compile definition.
eng/native/build-commons.shAvoid overriding CC/CXX for WASI builds.
eng/native.wasm.targetsImport AcquireWasiSdk + add WASI CoreCLR cmake args.
eng/Subsets.propsEnable CoreCLR subsets and defaults for WASI.
eng/AcquireWasiSdk.targetsUpdate runtime-local WASI SDK version.

Comment on lines +2360 to +2364
#elif defined(__EMSCRIPTEN__)
stackBase = (void*)emscripten_stack_get_base();
#endif // TARGET_BROWSER
#else // WASI
stackBase = __builtin_frame_address(0);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

In the WASI path, GetStackBase returns __builtin_frame_address(0), which is the current frame address, not the thread’s stack base. Components that rely on PAL_GetStackBase() for stack scanning/overflow checks will get an incorrect range. Consider using toolchain/linker-provided stack bound symbols (e.g., __stack_high/__stack_low or equivalent for wasi-sdk) or another reliable WASI-specific mechanism rather than the current frame address.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good catch. __builtin_frame_address(0) is an approximation — WASI/wasm32 doesn't expose linker-provided stack bound symbols like __stack_high for wasip2. Added a comment explaining this is an approximation. The value is cached on first call and used for the lifetime of the (single) thread, so it's close to the initial stack top.

Comment on lines +469 to +477
#ifdef TARGET_WASI
// WASI's fcntl may not support F_DUPFD_CLOEXEC at runtime.
// CLOEXEC is irrelevant on WASI (no exec). Use fd_fdstat_get + path_open
// workaround: just reuse the fd directly without duplication since
// WASI doesn't support fork/exec anyway.
UnixFd = pFileLocalData->unix_fd;
#else
UnixFd = fcntl(pFileLocalData->unix_fd, F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

On WASI, assigning UnixFd = pFileLocalData->unix_fd means the file mapping now owns the original file descriptor. FileMappingCleanupRoutine later close()s pLocalData->UnixFd, which will unexpectedly close the underlying handle used by hFile and any other users. Use a real dup operation on WASI (e.g., dup()/dup3() if available) or track ownership so the mapping cleanup doesn’t close a borrowed descriptor.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Valid concern. WASI's fcntl doesn't implement F_DUPFD_CLOEXEC (or F_DUPFD) at runtime despite the constant being defined at compile time, and dup() isn't available in wasip2 headers. The fd sharing is safe here because WASI is single-threaded with no fork/exec, so CLOEXEC is irrelevant and the file mapping lifetime matches the source handle. Updated the comment to explain this rationale.

Comment on lines 2438 to +2445
/* duplicate the FILE *, so that we can fclose() in FILECloseHandle without
closing the original */
#ifdef TARGET_WASI
// WASI: fcntl F_DUPFD_CLOEXEC not supported; reuse fd directly (single-threaded, no exec)
new_fd = fileno(stream);
#else
new_fd = fcntl(fileno(stream), F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

The WASI path sets new_fd = fileno(stream) instead of duplicating the descriptor. This makes the PAL file handle for stdin/stdout/stderr own and potentially close() the original stdio fd (e.g., on failure paths and when the handle is later closed), which can break subsequent I/O. Use an actual descriptor duplication mechanism on WASI, or ensure the PAL handle is marked non-owning so it won’t close the original fd.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Same situation as the map.cpp case — fcntl F_DUPFD* and dup() aren't available on wasip2. The stdio fds (0/1/2) are never closed by the PAL on WASI, so sharing is safe. Updated comment to explain the safety rationale.

<Project>
<PropertyGroup>
<_WasiSdkVersion>25.0</_WasiSdkVersion>
<_WasiSdkVersion>32.0</_WasiSdkVersion>

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

_WasiSdkVersion was updated to 32.0, but src/mono/wasi/build/WasiApp.targets still hard-codes an expected WASI SDK version of 25.0. This will cause Mono WASI builds (and any tooling that validates WASI_SDK_PATH/VERSION) to fail when using the runtime-local SDK acquired here. Please update the other expected-version checks (or centralize the version) so all WASI build paths agree on a single SDK version.

Suggested change
<_WasiSdkVersion>32.0</_WasiSdkVersion>
<WasiSdkVersionCondition="'$(WasiSdkVersion)' == ''">32.0</WasiSdkVersion>
<_WasiSdkVersion>$(WasiSdkVersion)</_WasiSdkVersion>

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

The Mono WASI build uses its own SDK acquisition path and isn't affected by this CoreCLR-specific version bump. We have a separate PR (dotnet/wasi-sdk#239) to update the dotnet fork of wasi-sdk to upstream v33 which will unify SDK versions across both runtimes.

Comment on lines +2415 to +2417
#else // WASI
stackLimit = (void*)sizeof(size_t);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

For WASI, GetStackLimit() sets stackLimit to (void*)sizeof(size_t), which is just a small constant cast to a pointer and not an actual stack boundary. This can break stack range calculations and stack overflow detection. Please compute a real lower stack bound for WASI (or, if unavailable, adjust callers to use SP-based scanning and return a clearly defined sentinel that won’t be treated as an address).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now computes stack limit as frame_address - 8MB (matching the -Wl,-z,stack-size=8388608 set at link time) with a floor at sizeof(size_t) to avoid wrapping to zero.

Comment on lines 485 to 516
@@ -509,6 +510,7 @@ BOOL CONTEXT_GetRegisters(DWORD processId, LPCONTEXT lpContext)
#undef ASSIGN_REG

CONTEXTFromNativeContext(&registers, lpContext, lpContext->ContextFlags);
#endif // !TARGET_WASI
}

bRet = TRUE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

When processId != GetCurrentProcessId() on WASI, the ucontext_t registers block is compiled out, but the function still falls through and returns TRUE without populating lpContext. This results in callers observing success with uninitialized/incorrect register state. Return FALSE (and set an appropriate last error) or explicitly zero/initialize lpContext similarly to the CONTEXT_GetThreadContext fallback path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now returns FALSE immediately for non-current process on WASI.

Comment on lines +31 to +34
};

static inline int dl_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data) { (void)callback; (void)data; return 0; }

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

This header uses size_t in the dl_iterate_phdr callback signature but doesn’t include a header that defines it. Add an include like <stddef.h> (or <sys/types.h>) to ensure size_t is available for both C and C++ compilation.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Added #include <stddef.h>.

#ifdef TARGET_WASI
extern "C" int pthread_getschedparam(pthread_t, int *policy, struct sched_param *param)
{
if (policy) *policy = 0;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

pthread_getschedparam returns success but leaves *param uninitialized. Callers expecting sched_param fields to be set may read garbage. Either populate *param with sensible defaults (e.g., zero priority) or return an error code (e.g., ENOTSUP) to signal the API isn’t implemented on WASI.

Suggested change
if (policy) *policy = 0;
if (policy) *policy = 0;
if (param) *param = sched_param{};

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now zero-initializes *param with memset.

Comment on lines +21 to +58
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#define ANON_ARENA_SIZE (256 * 1024 * 1024)

static char *s_arena_base;
static size_t s_arena_offset;

static void ensure_arena(void) {
if (s_arena_base == NULL) {
s_arena_base = (char *)malloc(ANON_ARENA_SIZE);
if (s_arena_base == NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset = 0;
}
}

// Allocate from the bump arena with alignment. Never reuses freed addresses.
static void *arena_alloc(size_t length, size_t align) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_t base = (uintptr_t)s_arena_base;
uintptr_t current = base + s_arena_offset;
uintptr_t aligned_addr = (current + align - 1) & ~(align - 1);
size_t aligned_offset = aligned_addr - base;
if (aligned_offset + length > ANON_ARENA_SIZE) {
return NULL;
}
void *ptr = (void *)aligned_addr;
s_arena_offset = aligned_offset + length;
return ptr;
}

static int is_arena_ptr(void *addr) {
if (s_arena_base == NULL) return 0;
return (char *)addr >= s_arena_base &&
(char *)addr < s_arena_base + ANON_ARENA_SIZE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

ensure_arena() allocates a fixed 256MB arena eagerly via malloc(ANON_ARENA_SIZE). On wasm32/WASI this directly consumes linear memory and can cause large, immediate memory growth or OOM even for small apps. Consider allocating the arena lazily in smaller chunks (or growing it) and/or sizing it based on configured limits rather than a hard-coded 256MB constant.

Suggested change
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
staticchar*s_arena_base;
staticsize_ts_arena_offset;
staticvoidensure_arena(void) {
if (s_arena_base==NULL) {
s_arena_base= (char*)malloc(ANON_ARENA_SIZE);
if (s_arena_base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset=0;
}
}
// Allocate from the bump arena with alignment. Never reuses freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_tbase= (uintptr_t)s_arena_base;
uintptr_tcurrent=base+s_arena_offset;
uintptr_taligned_addr= (current+align-1) & ~(align-1);
size_taligned_offset=aligned_addr-base;
if (aligned_offset+length>ANON_ARENA_SIZE) {
returnNULL;
}
void*ptr= (void*)aligned_addr;
s_arena_offset=aligned_offset+length;
returnptr;
}
staticintis_arena_ptr(void*addr) {
if (s_arena_base==NULL) return0;
return (char*)addr >= s_arena_base&&
(char*)addr<s_arena_base+ANON_ARENA_SIZE;
// Cap anonymous mappings at 256MB total, but allocate backing storage lazily
// in small chunks so small WASI apps do not eagerly consume large amounts of
// linear memory on the first anonymous mapping.
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
#defineANON_ARENA_CHUNK_SIZE (64 * 1024)
#defineANON_ARENA_MAX_CHUNKS (ANON_ARENA_SIZE / ANON_ARENA_CHUNK_SIZE)
typedefstruct
{
char*base;
size_tsize;
size_toffset;
} anon_arena_chunk;
staticanon_arena_chunks_arena_chunks[ANON_ARENA_MAX_CHUNKS];
staticsize_ts_arena_chunk_count;
staticsize_ts_arena_total_size;
staticvoidensure_arena(void) {
// Chunks are allocated lazily by arena_alloc().
}
staticanon_arena_chunk*arena_alloc_chunk(size_tmin_length, size_talign) {
size_tchunk_size;
anon_arena_chunk*chunk;
if (s_arena_chunk_count >= ANON_ARENA_MAX_CHUNKS) {
returnNULL;
}
chunk_size=ANON_ARENA_CHUNK_SIZE;
if (min_length>chunk_size||align>1) {
size_trequired=min_length+align-1;
if (required>chunk_size) {
chunk_size=required;
}
}
if (chunk_size>ANON_ARENA_SIZE-s_arena_total_size) {
returnNULL;
}
chunk=&s_arena_chunks[s_arena_chunk_count];
chunk->base= (char*)malloc(chunk_size);
if (chunk->base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %zu byte arena chunk\n", chunk_size);
abort();
}
chunk->size=chunk_size;
chunk->offset=0;
s_arena_chunk_count++;
s_arena_total_size+=chunk_size;
returnchunk;
}
// Allocate from lazily created bump arena chunks with alignment. Never reuses
// freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
anon_arena_chunk*chunk;
uintptr_tbase;
uintptr_tcurrent;
uintptr_taligned_addr;
size_taligned_offset;
ensure_arena();
if (align==0) {
align=1;
}
if (s_arena_chunk_count!=0) {
chunk=&s_arena_chunks[s_arena_chunk_count-1];
base= (uintptr_t)chunk->base;
current=base+chunk->offset;
aligned_addr= (current+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length <= chunk->size) {
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
}
chunk=arena_alloc_chunk(length, align);
if (chunk==NULL) {
returnNULL;
}
base= (uintptr_t)chunk->base;
aligned_addr= (base+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length>chunk->size) {
returnNULL;
}
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
staticintis_arena_ptr(void*addr) {
size_ti;
for (i=0; i<s_arena_chunk_count; i++) {
char*base=s_arena_chunks[i].base;
if ((char*)addr >= base&& (char*)addr<base+s_arena_chunks[i].size) {
return1;
}
}
return0;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good suggestion. On wasm32, malloc of 256MB doesn't immediately consume physical memory — it triggers memory.grow lazily as pages are touched. The arena is bumped incrementally so only the used portion is backed by real pages. That said, a lazy chunked approach would be cleaner and we'll consider it as a follow-up optimization.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Correction to my previous reply: wasm memory.grow commits pages immediately — there's no lazy page fault mechanism in wasm linear memory. So the 256MB malloc does grow linear memory upfront. A chunked approach that grows the arena in smaller increments as needed would save real memory for smaller apps. Filed as a follow-up improvement.

- context.cpp: Return FALSE for non-current process on WASI instead of
falling through with uninitialized context
- thread.cpp: Improve GetStackBase/GetStackLimit comments; compute stack
limit from frame address minus configured stack size (8MB)
- file.cpp, map.cpp: Improve fd-sharing comments explaining why reuse is
safe on WASI (no fork/exec, no CLOEXEC needed)
- link.h: Add missing stddef.h include for size_t
- stubs.cpp: Zero-initialize sched_param in pthread_getschedparam stub
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lewinglewing reopened this Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-PAL-coreclronly for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Coreclr wasi interpreter - #126959

Closed
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter
Closed

Coreclr wasi interpreter#126959
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter

Conversation

@lewing

Copy link
Copy Markdown
Member

No description provided.

lewingand others added 17 commits April 9, 2026 14:33
Enable building the CoreCLR WASM interpreter as a WASI component.
The resulting corerun.wasm (4.8MB stripped) runs on wasmtime and nesm,
with help output in ~150ms. GC initialization succeeds; runtime startup
proceeds to assembly loading.
Build system:
- Add TargetsWasi to CoreCLR supported OS (eng/Subsets.props)
- Set CLR_CMAKE_TARGET_UNIX and CLR_CMAKE_HOST_UNIX for WASI
- Add TARGET_WASI compile definition inside UNIX block
- Add WASI SDK acquisition to runtime.proj
- Import AcquireWasiSdk.targets in native.wasm.targets
- Set WASI emulation flags (_WASI_EMULATED_SIGNAL, _MMAN, etc.)
- Add CI pipeline (eng/pipelines/common/templates/wasi-coreclr-build.yml)
- Disable FEATURE_EVENT_TRACE, watchdog, superpmi, jitinterface for WASI
- Fix libunwind fallthrough for WASM in PAL CMakeLists
- Guard empty PTHREAD_LIBRARY cmake checks
WASI stub headers (pal/src/include/pal/wasi/):
- signal.h: Full POSIX signal types (siginfo_t, sigaction, SA_*, FPE_*)
- pthread.h: Unlock pthread_exit, scheduling declarations
- pwd.h, link.h, dlfcn.h, unistd.h: Missing POSIX stubs
- sys/wait.h, sys/vfs.h, sys/resource.h, ucontext.h: Missing system stubs
PAL stubs (pal/src/arch/wasm/stubs.cpp):
- DebugBreak, OutputDebugString, RaiseException
- PAL_VirtualUnwind, PAL_ProbeMemory, PAL_FreeExceptionRecords
- SEHInitializeSignals, SEHCleanupSignals, UnmaskActivationSignal
- FlushInstructionCache, GetThreadContext
- __cxa_allocate_exception, __cxa_throw, __cxa_thread_atexit
- shm_open, shm_unlink, pthread_getschedparam
WASI mmap (pal/src/arch/wasm/mmap-wasi.c):
- Custom mmap/munmap/mprotect using malloc with allocation tracking
- Replaces wasi-emulated-mman which can't handle PROT_NONE reservations
- Enables GC to allocate segments via linear memory growth
Minimal source changes:
- cgencpu.h: __builtin_frame_address fallback for WASI
- PAL stubs.cpp: Guard emscripten APIs with __EMSCRIPTEN__
- PAL thread.cpp: WASI fallback for GetStackBase/GetStackLimit
- PAL context.h: native_context_t stub for WASI (no ucontext)
- PAL file.cpp: Skip fcntl F_DUPFD_CLOEXEC on WASI
- GC gcenv.unix.cpp: __wasi__ path for memory sizing
- GC events.cpp: _WASI_EMULATED_PROCESS_CLOCKS for timed wait
- finalizerthread.cpp: Synchronous WASI finalizer path
- minipal/mutex.c: Skip recursive mutex attribute on WASI
- Minimal WASI corerun host (corerun-wasi.cpp)
Build: ./build.sh clr.native+clr.corelib+clr.nativecorelib -os wasi -c Release
Test: wasmtime run --dir /::/ corerun.wasm --help
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enable CoreLib loading on WASI by fixing several PAL and host issues:
- eng/native/functions.cmake: Fix install_clr for WASI executables.
WASI SDK produces a single binary, not Emscripten's .js+.wasm pair.
- corerun-wasi.cpp: Resolve CORE_ROOT to absolute path using getcwd
(not realpath which returns '/' on WASI). The assembly binder rejects
relative TPA paths, so corerun must provide absolute paths.
- clrhost.cpp: On WASI (TARGET_WASI), use CORE_ROOT env var to
determine the system directory. The runtime is statically linked
so GetClrModulePathName can't discover the module path normally.
- file.cpp: Bypass realpath() in InternalCanonicalizeRealPath on WASI.
WASI preopen paths are virtual directories that realpath cannot
resolve. Pass paths through directly instead.
- map.cpp: Skip fcntl(F_DUPFD_CLOEXEC) in CreateFileMapping on WASI.
Reuse the fd directly since WASI has no fork/exec (CLOEXEC is moot).
With these fixes, corerun.wasm successfully:
- Opens System.Private.CoreLib.dll (5MB) via WASI preopened dirs
- Maps it into memory via the malloc-based mmap
- Completes coreclr_initialize (host->Start() returns S_OK)
- Begins managed code execution in CreateAppDomainWithManager
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The malloc-based mmap caused memory corruption: the GC reserves and
releases large anonymous regions via mmap/munmap, and malloc reuses
freed addresses. File-backed mappings (assembly loading) then get
addresses within previously-freed GC regions. When the GC later
initializes those pages, it zeroes out the assembly data.
Fix: use a bump allocator backed by a 256MB arena for anonymous
mappings. This prevents address reuse, matching real mmap semantics
where munmap'd pages aren't returned to other callers. File-backed
mappings continue using malloc since they're long-lived and isolated.
With this fix, coreclr_initialize completes successfully on WASI:
- GC heap initializes in the arena
- CoreLib (5MB) loads via malloc into non-overlapping memory
- AppContext.Setup compiles and executes through the interpreter
- CreateAppDomainWithManager returns S_OK
- coreclr_execute_assembly is reached
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 64KB TPA buffer overflowed when scanning directories with many
framework assemblies. Increase to 256KB.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove redundant AcquireWasiSdk.targets import from build-native.proj.
It's already imported through native.wasm.targets at line 3, causing
MSB4011 when building libs for WASI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WASI SDK 32 ships LLVM 22.1.0 which is within the supported version
range (8-23), but init-compiler.sh fails to find it because the WASI
SDK clang is not on PATH. The WASI SDK cmake toolchain file
(wasi-sdk-p2.cmake) already sets CMAKE_C_COMPILER and
CMAKE_CXX_COMPILER to the SDK's clang, so init-compiler.sh is
unnecessary.
Follow the same pattern used for Android builds: set __Compiler to
'default' for WASI targets so gen-buildsys.sh skips init-compiler.sh
and lets the toolchain file handle compiler selection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bump WASI SDK from 25 (LLVM 19) to 32 (LLVM 22). This gives us:
- Native WASI P2 component output from wasm-ld (no wasm-tools wrapper needed)
- Path toward -fwasm-exceptions (requires wasi-sdk-33+ with exceptions sysroot)
- LLVM 22 with better codegen
Fix LLVM 22 strictness warnings promoted to errors:
- pal_icushim_static.c: explicit UErrorCode casts, char* cast, declaration reorder
- pal_networking.c: move declaration before goto label
- stubs.cpp: remove __cxa_thread_atexit stub (now provided by SDK 32 libc++abi)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch from sjlj (-fexceptions) to native WASM exceptions
(-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false) for WASI.
sjlj exceptions cannot handle throw-from-catch patterns needed by
the interpreter's ResumeAfterCatch mechanism. Native WASM exceptions
resolve this — throw/catch now connects properly.
Changes:
- configureplatform.cmake: -fwasm-exceptions + -wasm-use-legacy-eh=false
- CMakeLists.txt: disable -mllvm -wasm-enable-sjlj for WASI
- corerun CMakeLists.txt: link -lunwind instead of -lsetjmp,
pass -fwasm-exceptions at link time
- stubs.cpp: remove __cxa_throw/__cxa_allocate_exception stubs
(now provided by EH-enabled libc++abi/libunwind)
Requires EH-enabled sysroot libraries (libc++.a, libc++abi.a,
libunwind.a) built from wasi-sdk main with -DWASI_SDK_EXCEPTIONS=ON.
These will ship in wasi-sdk-33+. For now, build from source:
cd wasi-sdk && cmake -G Ninja -B build -DWASI_SDK_EXCEPTIONS=ON
ninja -C build
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On WASM, RtlCaptureContext/ClrCaptureContext zeros the CONTEXT since
there are no CPU registers to capture. This leaves the exception
dispatch with IP=0, SP=0 which breaks the managed stack frame iterator.
Fix: In DispatchManagedException, populate the CONTEXT from the current
interpreter frame (InterpreterFrame::SetContextToInterpMethodContextFrame)
before passing it to the EH dispatch. Also record the current interpreter
IP in the frame before throwing (INTOP_THROW sets pFrame->ip).
This provides valid IP/SP to the EH dispatch, but the managed exception
handling still needs additional work in SfiInitWorker to properly walk
interpreter frames on WASM (tracked in dotnet#113694).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The arena bump allocator in mmap-wasi.c aligned the offset within the
arena rather than the absolute address. When malloc returned a base
address that was not 64K-aligned, subsequent 64K-aligned allocations
(e.g., GC handle table segments) were misaligned. The handle table uses
pointer masking (handle & ~0xFFFF) to locate segment headers, so
misaligned segments caused NULL pHandleTable lookups and crashes during
exception handling cleanup (DestroyHandle in CallCatchFunclet).
Also enable recursive mutexes on WASI to match other platforms, since
the runtime may re-enter locks during exception handling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The stack walker finds interpreter frames through the explicit frame
chain (Thread::GetFrame()), not through the CONTEXT IP/SP. The zeroed
context from RtlCaptureContext on WASM is fine because the EH dispatch
starts on an explicit frame. Browser WASM works the same way without
this patch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Report available memory as max (4GB wasm32 limit) minus current usage,
matching Emscripten's approach. The previous code reported current memory
size as available, which is incorrect — it shrinks as memory grows and
doesn't reflect room to grow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The main corerun.cpp already has TARGET_WASM and TARGET_WASI ifdefs
that handle the WASI differences (no dlopen, static exports, etc.).
The separate corerun-wasi.cpp reimplemented TPA building, arg parsing,
and the main loop unnecessarily.
Use the unified corerun.cpp for all platforms. The only WASI-specific
addition is providing HAVE_GETAUXVAL/HAVE_DIRENT_D_TYPE defines directly
since configure.cmake can't run for cross-compilation targets.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The inner `if(NOT CLR_CMAKE_TARGET_WASI)` could never execute since
it was nested inside `if(CLR_CMAKE_TARGET_WASI)`. The sjlj flag is
not needed — WASI uses -fwasm-exceptions instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 15, 2026 17:51
@github-actionsgithub-actionsBot added the area-PAL-coreclr only for closed issues label Apr 15, 2026
@lewinglewing closed this Apr 15, 2026

CopilotAI 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.

Pull request overview

Enable building/running CoreCLR for the WASI target by wiring up WASI SDK acquisition, adjusting CoreCLR/PAL platform conditionals, and adding WASI-specific stubs and build/pipeline support.

Changes:

  • Add/extend WASI build plumbing (SDK acquisition, cmake/msbuild targets, subsets, pipelines).
  • Introduce WASI-specific PAL headers/stubs and WASI mmap implementation, plus various WASI feature guards across CoreCLR/PAL/GC/corerun.
  • Adjust interpreter/browser/wasm conditionals to better separate Emscripten vs WASI behavior.

Reviewed changes

Copilot reviewed 50 out of 50 changed files in this pull request and generated 9 comments.

Show a summary per file
FileDescription
src/native/libs/build-native.projRemoves standalone WASI SDK import; relies on shared wasm targets.
src/native/libs/System.Native/pal_networking.cMinor variable initialization tweak.
src/native/libs/System.Globalization.Native/pal_icushim_static.cICU status init and malloc cast cleanup.
src/coreclr/vm/wasm/cgencpu.hGuard Emscripten stack include; WASI fallback SP logic.
src/coreclr/vm/prestub.cppRefine interpreter ETW handling with portable entrypoints.
src/coreclr/vm/finalizerthread.cppWASI synchronous finalization path under wasm.
src/coreclr/utilcode/clrhost.cppWASI module path derived from CORE_ROOT.
src/coreclr/tools/CMakeLists.txtSkip superpmi for WASI/browser builds.
src/coreclr/runtime.projAdd AcquireWasiSdk dependency for WASI runtime build.
src/coreclr/pal/src/thread/thread.cppWASI stack base/limit handling adjustments.
src/coreclr/pal/src/thread/context.cppDisable remote register capture on WASI.
src/coreclr/pal/src/map/map.cppWASI fcntl/dup workaround for file mappings.
src/coreclr/pal/src/include/pal/wasi/unistd.hWASI unistd wrapper with stubbed POSIX APIs.
src/coreclr/pal/src/include/pal/wasi/ucontext.hWASI ucontext stub header.
src/coreclr/pal/src/include/pal/wasi/sys/wait.hWASI sys/wait stub header.
src/coreclr/pal/src/include/pal/wasi/sys/vfs.hWASI sys/vfs stub header.
src/coreclr/pal/src/include/pal/wasi/sys/resource.hWASI sys/resource wrapper for rlimit.
src/coreclr/pal/src/include/pal/wasi/signal.hWASI signal wrapper defining missing POSIX types/APIs.
src/coreclr/pal/src/include/pal/wasi/pwd.hWASI pwd stub header.
src/coreclr/pal/src/include/pal/wasi/pthread.hWASI pthread wrapper for hidden decls.
src/coreclr/pal/src/include/pal/wasi/link.hWASI link stub for dl_iterate_phdr types.
src/coreclr/pal/src/include/pal/wasi/dlfcn.hWASI dlfcn wrapper for Dl_info/dladdr.
src/coreclr/pal/src/include/pal/context.hAdd WASI native_context_t fallback.
src/coreclr/pal/src/file/file.cppWASI fd duplication workaround for std handles/realpath.
src/coreclr/pal/src/exception/seh.cppDisable seh-unwind.cpp inclusion on WASI.
src/coreclr/pal/src/configure.cmakeMake pthread feature checks conditional; add WASI sched flags.
src/coreclr/pal/src/arch/wasm/stubs.cppEmscripten guards + WASI stubs for missing APIs.
src/coreclr/pal/src/arch/wasm/mmap-wasi.cNew WASI mmap/munmap implementation.
src/coreclr/pal/src/CMakeLists.txtWASI source selection + avoid unwind/dac pieces on wasm.
src/coreclr/hosts/corerun/dotenv.cppDisable corerun dotenv self-test on WASI.
src/coreclr/hosts/corerun/corerun.hppAvoid config.h for wasm; define minimal feature macros.
src/coreclr/hosts/corerun/corerun.cppDisable corerun self-test path on WASI.
src/coreclr/hosts/corerun/CMakeLists.txtWASI link options/libs; avoid dl/rdynamic on wasm.
src/coreclr/gc/unix/gcenv.unix.cppWASI mman define handling + available memory logic.
src/coreclr/gc/unix/events.cppAllow process-clock emulation define for waits.
src/coreclr/gc/unix/configure.cmakeGuard pthread checks on presence of pthread library.
src/coreclr/debug/debug-pal/unix/twowaypipe.cppWASI mkfifo stub for debug transport.
src/coreclr/debug/debug-pal/CMakeLists.txtDisable diagnostic server reference on WASI.
src/coreclr/clrfeatures.cmakeDisable event trace default for WASI.
src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csprojInclude Unix/browser interop stubs for WASI too.
src/coreclr/CMakeLists.txtWASI compile defs + include stub headers; skip tools/watchdog.
eng/pipelines/runtime.ymlAdd WASI CoreCLR build job template.
eng/pipelines/common/templates/wasi-coreclr-build.ymlNew pipeline template for WASI CoreCLR build-only job.
eng/native/functions.cmakeInstall rule adjustment for WASI wasm executables.
eng/native/configureplatform.cmakeTreat WASI as UNIX; set WASI target flags.
eng/native/configurecompiler.cmakeEmit TARGET_WASI compile definition.
eng/native/build-commons.shAvoid overriding CC/CXX for WASI builds.
eng/native.wasm.targetsImport AcquireWasiSdk + add WASI CoreCLR cmake args.
eng/Subsets.propsEnable CoreCLR subsets and defaults for WASI.
eng/AcquireWasiSdk.targetsUpdate runtime-local WASI SDK version.

Comment on lines +2360 to +2364
#elif defined(__EMSCRIPTEN__)
stackBase = (void*)emscripten_stack_get_base();
#endif // TARGET_BROWSER
#else // WASI
stackBase = __builtin_frame_address(0);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

In the WASI path, GetStackBase returns __builtin_frame_address(0), which is the current frame address, not the thread’s stack base. Components that rely on PAL_GetStackBase() for stack scanning/overflow checks will get an incorrect range. Consider using toolchain/linker-provided stack bound symbols (e.g., __stack_high/__stack_low or equivalent for wasi-sdk) or another reliable WASI-specific mechanism rather than the current frame address.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good catch. __builtin_frame_address(0) is an approximation — WASI/wasm32 doesn't expose linker-provided stack bound symbols like __stack_high for wasip2. Added a comment explaining this is an approximation. The value is cached on first call and used for the lifetime of the (single) thread, so it's close to the initial stack top.

Comment on lines +469 to +477
#ifdef TARGET_WASI
// WASI's fcntl may not support F_DUPFD_CLOEXEC at runtime.
// CLOEXEC is irrelevant on WASI (no exec). Use fd_fdstat_get + path_open
// workaround: just reuse the fd directly without duplication since
// WASI doesn't support fork/exec anyway.
UnixFd = pFileLocalData->unix_fd;
#else
UnixFd = fcntl(pFileLocalData->unix_fd, F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

On WASI, assigning UnixFd = pFileLocalData->unix_fd means the file mapping now owns the original file descriptor. FileMappingCleanupRoutine later close()s pLocalData->UnixFd, which will unexpectedly close the underlying handle used by hFile and any other users. Use a real dup operation on WASI (e.g., dup()/dup3() if available) or track ownership so the mapping cleanup doesn’t close a borrowed descriptor.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Valid concern. WASI's fcntl doesn't implement F_DUPFD_CLOEXEC (or F_DUPFD) at runtime despite the constant being defined at compile time, and dup() isn't available in wasip2 headers. The fd sharing is safe here because WASI is single-threaded with no fork/exec, so CLOEXEC is irrelevant and the file mapping lifetime matches the source handle. Updated the comment to explain this rationale.

Comment on lines 2438 to +2445
/* duplicate the FILE *, so that we can fclose() in FILECloseHandle without
closing the original */
#ifdef TARGET_WASI
// WASI: fcntl F_DUPFD_CLOEXEC not supported; reuse fd directly (single-threaded, no exec)
new_fd = fileno(stream);
#else
new_fd = fcntl(fileno(stream), F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

The WASI path sets new_fd = fileno(stream) instead of duplicating the descriptor. This makes the PAL file handle for stdin/stdout/stderr own and potentially close() the original stdio fd (e.g., on failure paths and when the handle is later closed), which can break subsequent I/O. Use an actual descriptor duplication mechanism on WASI, or ensure the PAL handle is marked non-owning so it won’t close the original fd.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Same situation as the map.cpp case — fcntl F_DUPFD* and dup() aren't available on wasip2. The stdio fds (0/1/2) are never closed by the PAL on WASI, so sharing is safe. Updated comment to explain the safety rationale.

<Project>
<PropertyGroup>
<_WasiSdkVersion>25.0</_WasiSdkVersion>
<_WasiSdkVersion>32.0</_WasiSdkVersion>

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

_WasiSdkVersion was updated to 32.0, but src/mono/wasi/build/WasiApp.targets still hard-codes an expected WASI SDK version of 25.0. This will cause Mono WASI builds (and any tooling that validates WASI_SDK_PATH/VERSION) to fail when using the runtime-local SDK acquired here. Please update the other expected-version checks (or centralize the version) so all WASI build paths agree on a single SDK version.

Suggested change
<_WasiSdkVersion>32.0</_WasiSdkVersion>
<WasiSdkVersionCondition="'$(WasiSdkVersion)' == ''">32.0</WasiSdkVersion>
<_WasiSdkVersion>$(WasiSdkVersion)</_WasiSdkVersion>

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

The Mono WASI build uses its own SDK acquisition path and isn't affected by this CoreCLR-specific version bump. We have a separate PR (dotnet/wasi-sdk#239) to update the dotnet fork of wasi-sdk to upstream v33 which will unify SDK versions across both runtimes.

Comment on lines +2415 to +2417
#else // WASI
stackLimit = (void*)sizeof(size_t);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

For WASI, GetStackLimit() sets stackLimit to (void*)sizeof(size_t), which is just a small constant cast to a pointer and not an actual stack boundary. This can break stack range calculations and stack overflow detection. Please compute a real lower stack bound for WASI (or, if unavailable, adjust callers to use SP-based scanning and return a clearly defined sentinel that won’t be treated as an address).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now computes stack limit as frame_address - 8MB (matching the -Wl,-z,stack-size=8388608 set at link time) with a floor at sizeof(size_t) to avoid wrapping to zero.

Comment on lines 485 to 516
@@ -509,6 +510,7 @@ BOOL CONTEXT_GetRegisters(DWORD processId, LPCONTEXT lpContext)
#undef ASSIGN_REG

CONTEXTFromNativeContext(&registers, lpContext, lpContext->ContextFlags);
#endif // !TARGET_WASI
}

bRet = TRUE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

When processId != GetCurrentProcessId() on WASI, the ucontext_t registers block is compiled out, but the function still falls through and returns TRUE without populating lpContext. This results in callers observing success with uninitialized/incorrect register state. Return FALSE (and set an appropriate last error) or explicitly zero/initialize lpContext similarly to the CONTEXT_GetThreadContext fallback path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now returns FALSE immediately for non-current process on WASI.

Comment on lines +31 to +34
};

static inline int dl_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data) { (void)callback; (void)data; return 0; }

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

This header uses size_t in the dl_iterate_phdr callback signature but doesn’t include a header that defines it. Add an include like <stddef.h> (or <sys/types.h>) to ensure size_t is available for both C and C++ compilation.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Added #include <stddef.h>.

#ifdef TARGET_WASI
extern "C" int pthread_getschedparam(pthread_t, int *policy, struct sched_param *param)
{
if (policy) *policy = 0;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

pthread_getschedparam returns success but leaves *param uninitialized. Callers expecting sched_param fields to be set may read garbage. Either populate *param with sensible defaults (e.g., zero priority) or return an error code (e.g., ENOTSUP) to signal the API isn’t implemented on WASI.

Suggested change
if (policy) *policy = 0;
if (policy) *policy = 0;
if (param) *param = sched_param{};

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now zero-initializes *param with memset.

Comment on lines +21 to +58
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#define ANON_ARENA_SIZE (256 * 1024 * 1024)

static char *s_arena_base;
static size_t s_arena_offset;

static void ensure_arena(void) {
if (s_arena_base == NULL) {
s_arena_base = (char *)malloc(ANON_ARENA_SIZE);
if (s_arena_base == NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset = 0;
}
}

// Allocate from the bump arena with alignment. Never reuses freed addresses.
static void *arena_alloc(size_t length, size_t align) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_t base = (uintptr_t)s_arena_base;
uintptr_t current = base + s_arena_offset;
uintptr_t aligned_addr = (current + align - 1) & ~(align - 1);
size_t aligned_offset = aligned_addr - base;
if (aligned_offset + length > ANON_ARENA_SIZE) {
return NULL;
}
void *ptr = (void *)aligned_addr;
s_arena_offset = aligned_offset + length;
return ptr;
}

static int is_arena_ptr(void *addr) {
if (s_arena_base == NULL) return 0;
return (char *)addr >= s_arena_base &&
(char *)addr < s_arena_base + ANON_ARENA_SIZE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

ensure_arena() allocates a fixed 256MB arena eagerly via malloc(ANON_ARENA_SIZE). On wasm32/WASI this directly consumes linear memory and can cause large, immediate memory growth or OOM even for small apps. Consider allocating the arena lazily in smaller chunks (or growing it) and/or sizing it based on configured limits rather than a hard-coded 256MB constant.

Suggested change
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
staticchar*s_arena_base;
staticsize_ts_arena_offset;
staticvoidensure_arena(void) {
if (s_arena_base==NULL) {
s_arena_base= (char*)malloc(ANON_ARENA_SIZE);
if (s_arena_base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset=0;
}
}
// Allocate from the bump arena with alignment. Never reuses freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_tbase= (uintptr_t)s_arena_base;
uintptr_tcurrent=base+s_arena_offset;
uintptr_taligned_addr= (current+align-1) & ~(align-1);
size_taligned_offset=aligned_addr-base;
if (aligned_offset+length>ANON_ARENA_SIZE) {
returnNULL;
}
void*ptr= (void*)aligned_addr;
s_arena_offset=aligned_offset+length;
returnptr;
}
staticintis_arena_ptr(void*addr) {
if (s_arena_base==NULL) return0;
return (char*)addr >= s_arena_base&&
(char*)addr<s_arena_base+ANON_ARENA_SIZE;
// Cap anonymous mappings at 256MB total, but allocate backing storage lazily
// in small chunks so small WASI apps do not eagerly consume large amounts of
// linear memory on the first anonymous mapping.
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
#defineANON_ARENA_CHUNK_SIZE (64 * 1024)
#defineANON_ARENA_MAX_CHUNKS (ANON_ARENA_SIZE / ANON_ARENA_CHUNK_SIZE)
typedefstruct
{
char*base;
size_tsize;
size_toffset;
} anon_arena_chunk;
staticanon_arena_chunks_arena_chunks[ANON_ARENA_MAX_CHUNKS];
staticsize_ts_arena_chunk_count;
staticsize_ts_arena_total_size;
staticvoidensure_arena(void) {
// Chunks are allocated lazily by arena_alloc().
}
staticanon_arena_chunk*arena_alloc_chunk(size_tmin_length, size_talign) {
size_tchunk_size;
anon_arena_chunk*chunk;
if (s_arena_chunk_count >= ANON_ARENA_MAX_CHUNKS) {
returnNULL;
}
chunk_size=ANON_ARENA_CHUNK_SIZE;
if (min_length>chunk_size||align>1) {
size_trequired=min_length+align-1;
if (required>chunk_size) {
chunk_size=required;
}
}
if (chunk_size>ANON_ARENA_SIZE-s_arena_total_size) {
returnNULL;
}
chunk=&s_arena_chunks[s_arena_chunk_count];
chunk->base= (char*)malloc(chunk_size);
if (chunk->base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %zu byte arena chunk\n", chunk_size);
abort();
}
chunk->size=chunk_size;
chunk->offset=0;
s_arena_chunk_count++;
s_arena_total_size+=chunk_size;
returnchunk;
}
// Allocate from lazily created bump arena chunks with alignment. Never reuses
// freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
anon_arena_chunk*chunk;
uintptr_tbase;
uintptr_tcurrent;
uintptr_taligned_addr;
size_taligned_offset;
ensure_arena();
if (align==0) {
align=1;
}
if (s_arena_chunk_count!=0) {
chunk=&s_arena_chunks[s_arena_chunk_count-1];
base= (uintptr_t)chunk->base;
current=base+chunk->offset;
aligned_addr= (current+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length <= chunk->size) {
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
}
chunk=arena_alloc_chunk(length, align);
if (chunk==NULL) {
returnNULL;
}
base= (uintptr_t)chunk->base;
aligned_addr= (base+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length>chunk->size) {
returnNULL;
}
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
staticintis_arena_ptr(void*addr) {
size_ti;
for (i=0; i<s_arena_chunk_count; i++) {
char*base=s_arena_chunks[i].base;
if ((char*)addr >= base&& (char*)addr<base+s_arena_chunks[i].size) {
return1;
}
}
return0;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good suggestion. On wasm32, malloc of 256MB doesn't immediately consume physical memory — it triggers memory.grow lazily as pages are touched. The arena is bumped incrementally so only the used portion is backed by real pages. That said, a lazy chunked approach would be cleaner and we'll consider it as a follow-up optimization.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Correction to my previous reply: wasm memory.grow commits pages immediately — there's no lazy page fault mechanism in wasm linear memory. So the 256MB malloc does grow linear memory upfront. A chunked approach that grows the arena in smaller increments as needed would save real memory for smaller apps. Filed as a follow-up improvement.

- context.cpp: Return FALSE for non-current process on WASI instead of
falling through with uninitialized context
- thread.cpp: Improve GetStackBase/GetStackLimit comments; compute stack
limit from frame address minus configured stack size (8MB)
- file.cpp, map.cpp: Improve fd-sharing comments explaining why reuse is
safe on WASI (no fork/exec, no CLOEXEC needed)
- link.h: Add missing stddef.h include for size_t
- stubs.cpp: Zero-initialize sched_param in pthread_getschedparam stub
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lewinglewing reopened this Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-PAL-coreclronly for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Coreclr wasi interpreter - #126959

Closed
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter
Closed

Coreclr wasi interpreter#126959
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter

Conversation

@lewing

Copy link
Copy Markdown
Member

No description provided.

lewingand others added 17 commits April 9, 2026 14:33
Enable building the CoreCLR WASM interpreter as a WASI component.
The resulting corerun.wasm (4.8MB stripped) runs on wasmtime and nesm,
with help output in ~150ms. GC initialization succeeds; runtime startup
proceeds to assembly loading.
Build system:
- Add TargetsWasi to CoreCLR supported OS (eng/Subsets.props)
- Set CLR_CMAKE_TARGET_UNIX and CLR_CMAKE_HOST_UNIX for WASI
- Add TARGET_WASI compile definition inside UNIX block
- Add WASI SDK acquisition to runtime.proj
- Import AcquireWasiSdk.targets in native.wasm.targets
- Set WASI emulation flags (_WASI_EMULATED_SIGNAL, _MMAN, etc.)
- Add CI pipeline (eng/pipelines/common/templates/wasi-coreclr-build.yml)
- Disable FEATURE_EVENT_TRACE, watchdog, superpmi, jitinterface for WASI
- Fix libunwind fallthrough for WASM in PAL CMakeLists
- Guard empty PTHREAD_LIBRARY cmake checks
WASI stub headers (pal/src/include/pal/wasi/):
- signal.h: Full POSIX signal types (siginfo_t, sigaction, SA_*, FPE_*)
- pthread.h: Unlock pthread_exit, scheduling declarations
- pwd.h, link.h, dlfcn.h, unistd.h: Missing POSIX stubs
- sys/wait.h, sys/vfs.h, sys/resource.h, ucontext.h: Missing system stubs
PAL stubs (pal/src/arch/wasm/stubs.cpp):
- DebugBreak, OutputDebugString, RaiseException
- PAL_VirtualUnwind, PAL_ProbeMemory, PAL_FreeExceptionRecords
- SEHInitializeSignals, SEHCleanupSignals, UnmaskActivationSignal
- FlushInstructionCache, GetThreadContext
- __cxa_allocate_exception, __cxa_throw, __cxa_thread_atexit
- shm_open, shm_unlink, pthread_getschedparam
WASI mmap (pal/src/arch/wasm/mmap-wasi.c):
- Custom mmap/munmap/mprotect using malloc with allocation tracking
- Replaces wasi-emulated-mman which can't handle PROT_NONE reservations
- Enables GC to allocate segments via linear memory growth
Minimal source changes:
- cgencpu.h: __builtin_frame_address fallback for WASI
- PAL stubs.cpp: Guard emscripten APIs with __EMSCRIPTEN__
- PAL thread.cpp: WASI fallback for GetStackBase/GetStackLimit
- PAL context.h: native_context_t stub for WASI (no ucontext)
- PAL file.cpp: Skip fcntl F_DUPFD_CLOEXEC on WASI
- GC gcenv.unix.cpp: __wasi__ path for memory sizing
- GC events.cpp: _WASI_EMULATED_PROCESS_CLOCKS for timed wait
- finalizerthread.cpp: Synchronous WASI finalizer path
- minipal/mutex.c: Skip recursive mutex attribute on WASI
- Minimal WASI corerun host (corerun-wasi.cpp)
Build: ./build.sh clr.native+clr.corelib+clr.nativecorelib -os wasi -c Release
Test: wasmtime run --dir /::/ corerun.wasm --help
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enable CoreLib loading on WASI by fixing several PAL and host issues:
- eng/native/functions.cmake: Fix install_clr for WASI executables.
WASI SDK produces a single binary, not Emscripten's .js+.wasm pair.
- corerun-wasi.cpp: Resolve CORE_ROOT to absolute path using getcwd
(not realpath which returns '/' on WASI). The assembly binder rejects
relative TPA paths, so corerun must provide absolute paths.
- clrhost.cpp: On WASI (TARGET_WASI), use CORE_ROOT env var to
determine the system directory. The runtime is statically linked
so GetClrModulePathName can't discover the module path normally.
- file.cpp: Bypass realpath() in InternalCanonicalizeRealPath on WASI.
WASI preopen paths are virtual directories that realpath cannot
resolve. Pass paths through directly instead.
- map.cpp: Skip fcntl(F_DUPFD_CLOEXEC) in CreateFileMapping on WASI.
Reuse the fd directly since WASI has no fork/exec (CLOEXEC is moot).
With these fixes, corerun.wasm successfully:
- Opens System.Private.CoreLib.dll (5MB) via WASI preopened dirs
- Maps it into memory via the malloc-based mmap
- Completes coreclr_initialize (host->Start() returns S_OK)
- Begins managed code execution in CreateAppDomainWithManager
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The malloc-based mmap caused memory corruption: the GC reserves and
releases large anonymous regions via mmap/munmap, and malloc reuses
freed addresses. File-backed mappings (assembly loading) then get
addresses within previously-freed GC regions. When the GC later
initializes those pages, it zeroes out the assembly data.
Fix: use a bump allocator backed by a 256MB arena for anonymous
mappings. This prevents address reuse, matching real mmap semantics
where munmap'd pages aren't returned to other callers. File-backed
mappings continue using malloc since they're long-lived and isolated.
With this fix, coreclr_initialize completes successfully on WASI:
- GC heap initializes in the arena
- CoreLib (5MB) loads via malloc into non-overlapping memory
- AppContext.Setup compiles and executes through the interpreter
- CreateAppDomainWithManager returns S_OK
- coreclr_execute_assembly is reached
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 64KB TPA buffer overflowed when scanning directories with many
framework assemblies. Increase to 256KB.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove redundant AcquireWasiSdk.targets import from build-native.proj.
It's already imported through native.wasm.targets at line 3, causing
MSB4011 when building libs for WASI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WASI SDK 32 ships LLVM 22.1.0 which is within the supported version
range (8-23), but init-compiler.sh fails to find it because the WASI
SDK clang is not on PATH. The WASI SDK cmake toolchain file
(wasi-sdk-p2.cmake) already sets CMAKE_C_COMPILER and
CMAKE_CXX_COMPILER to the SDK's clang, so init-compiler.sh is
unnecessary.
Follow the same pattern used for Android builds: set __Compiler to
'default' for WASI targets so gen-buildsys.sh skips init-compiler.sh
and lets the toolchain file handle compiler selection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bump WASI SDK from 25 (LLVM 19) to 32 (LLVM 22). This gives us:
- Native WASI P2 component output from wasm-ld (no wasm-tools wrapper needed)
- Path toward -fwasm-exceptions (requires wasi-sdk-33+ with exceptions sysroot)
- LLVM 22 with better codegen
Fix LLVM 22 strictness warnings promoted to errors:
- pal_icushim_static.c: explicit UErrorCode casts, char* cast, declaration reorder
- pal_networking.c: move declaration before goto label
- stubs.cpp: remove __cxa_thread_atexit stub (now provided by SDK 32 libc++abi)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch from sjlj (-fexceptions) to native WASM exceptions
(-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false) for WASI.
sjlj exceptions cannot handle throw-from-catch patterns needed by
the interpreter's ResumeAfterCatch mechanism. Native WASM exceptions
resolve this — throw/catch now connects properly.
Changes:
- configureplatform.cmake: -fwasm-exceptions + -wasm-use-legacy-eh=false
- CMakeLists.txt: disable -mllvm -wasm-enable-sjlj for WASI
- corerun CMakeLists.txt: link -lunwind instead of -lsetjmp,
pass -fwasm-exceptions at link time
- stubs.cpp: remove __cxa_throw/__cxa_allocate_exception stubs
(now provided by EH-enabled libc++abi/libunwind)
Requires EH-enabled sysroot libraries (libc++.a, libc++abi.a,
libunwind.a) built from wasi-sdk main with -DWASI_SDK_EXCEPTIONS=ON.
These will ship in wasi-sdk-33+. For now, build from source:
cd wasi-sdk && cmake -G Ninja -B build -DWASI_SDK_EXCEPTIONS=ON
ninja -C build
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On WASM, RtlCaptureContext/ClrCaptureContext zeros the CONTEXT since
there are no CPU registers to capture. This leaves the exception
dispatch with IP=0, SP=0 which breaks the managed stack frame iterator.
Fix: In DispatchManagedException, populate the CONTEXT from the current
interpreter frame (InterpreterFrame::SetContextToInterpMethodContextFrame)
before passing it to the EH dispatch. Also record the current interpreter
IP in the frame before throwing (INTOP_THROW sets pFrame->ip).
This provides valid IP/SP to the EH dispatch, but the managed exception
handling still needs additional work in SfiInitWorker to properly walk
interpreter frames on WASM (tracked in dotnet#113694).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The arena bump allocator in mmap-wasi.c aligned the offset within the
arena rather than the absolute address. When malloc returned a base
address that was not 64K-aligned, subsequent 64K-aligned allocations
(e.g., GC handle table segments) were misaligned. The handle table uses
pointer masking (handle & ~0xFFFF) to locate segment headers, so
misaligned segments caused NULL pHandleTable lookups and crashes during
exception handling cleanup (DestroyHandle in CallCatchFunclet).
Also enable recursive mutexes on WASI to match other platforms, since
the runtime may re-enter locks during exception handling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The stack walker finds interpreter frames through the explicit frame
chain (Thread::GetFrame()), not through the CONTEXT IP/SP. The zeroed
context from RtlCaptureContext on WASM is fine because the EH dispatch
starts on an explicit frame. Browser WASM works the same way without
this patch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Report available memory as max (4GB wasm32 limit) minus current usage,
matching Emscripten's approach. The previous code reported current memory
size as available, which is incorrect — it shrinks as memory grows and
doesn't reflect room to grow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The main corerun.cpp already has TARGET_WASM and TARGET_WASI ifdefs
that handle the WASI differences (no dlopen, static exports, etc.).
The separate corerun-wasi.cpp reimplemented TPA building, arg parsing,
and the main loop unnecessarily.
Use the unified corerun.cpp for all platforms. The only WASI-specific
addition is providing HAVE_GETAUXVAL/HAVE_DIRENT_D_TYPE defines directly
since configure.cmake can't run for cross-compilation targets.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The inner `if(NOT CLR_CMAKE_TARGET_WASI)` could never execute since
it was nested inside `if(CLR_CMAKE_TARGET_WASI)`. The sjlj flag is
not needed — WASI uses -fwasm-exceptions instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 15, 2026 17:51
@github-actionsgithub-actionsBot added the area-PAL-coreclr only for closed issues label Apr 15, 2026
@lewinglewing closed this Apr 15, 2026

CopilotAI 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.

Pull request overview

Enable building/running CoreCLR for the WASI target by wiring up WASI SDK acquisition, adjusting CoreCLR/PAL platform conditionals, and adding WASI-specific stubs and build/pipeline support.

Changes:

  • Add/extend WASI build plumbing (SDK acquisition, cmake/msbuild targets, subsets, pipelines).
  • Introduce WASI-specific PAL headers/stubs and WASI mmap implementation, plus various WASI feature guards across CoreCLR/PAL/GC/corerun.
  • Adjust interpreter/browser/wasm conditionals to better separate Emscripten vs WASI behavior.

Reviewed changes

Copilot reviewed 50 out of 50 changed files in this pull request and generated 9 comments.

Show a summary per file
FileDescription
src/native/libs/build-native.projRemoves standalone WASI SDK import; relies on shared wasm targets.
src/native/libs/System.Native/pal_networking.cMinor variable initialization tweak.
src/native/libs/System.Globalization.Native/pal_icushim_static.cICU status init and malloc cast cleanup.
src/coreclr/vm/wasm/cgencpu.hGuard Emscripten stack include; WASI fallback SP logic.
src/coreclr/vm/prestub.cppRefine interpreter ETW handling with portable entrypoints.
src/coreclr/vm/finalizerthread.cppWASI synchronous finalization path under wasm.
src/coreclr/utilcode/clrhost.cppWASI module path derived from CORE_ROOT.
src/coreclr/tools/CMakeLists.txtSkip superpmi for WASI/browser builds.
src/coreclr/runtime.projAdd AcquireWasiSdk dependency for WASI runtime build.
src/coreclr/pal/src/thread/thread.cppWASI stack base/limit handling adjustments.
src/coreclr/pal/src/thread/context.cppDisable remote register capture on WASI.
src/coreclr/pal/src/map/map.cppWASI fcntl/dup workaround for file mappings.
src/coreclr/pal/src/include/pal/wasi/unistd.hWASI unistd wrapper with stubbed POSIX APIs.
src/coreclr/pal/src/include/pal/wasi/ucontext.hWASI ucontext stub header.
src/coreclr/pal/src/include/pal/wasi/sys/wait.hWASI sys/wait stub header.
src/coreclr/pal/src/include/pal/wasi/sys/vfs.hWASI sys/vfs stub header.
src/coreclr/pal/src/include/pal/wasi/sys/resource.hWASI sys/resource wrapper for rlimit.
src/coreclr/pal/src/include/pal/wasi/signal.hWASI signal wrapper defining missing POSIX types/APIs.
src/coreclr/pal/src/include/pal/wasi/pwd.hWASI pwd stub header.
src/coreclr/pal/src/include/pal/wasi/pthread.hWASI pthread wrapper for hidden decls.
src/coreclr/pal/src/include/pal/wasi/link.hWASI link stub for dl_iterate_phdr types.
src/coreclr/pal/src/include/pal/wasi/dlfcn.hWASI dlfcn wrapper for Dl_info/dladdr.
src/coreclr/pal/src/include/pal/context.hAdd WASI native_context_t fallback.
src/coreclr/pal/src/file/file.cppWASI fd duplication workaround for std handles/realpath.
src/coreclr/pal/src/exception/seh.cppDisable seh-unwind.cpp inclusion on WASI.
src/coreclr/pal/src/configure.cmakeMake pthread feature checks conditional; add WASI sched flags.
src/coreclr/pal/src/arch/wasm/stubs.cppEmscripten guards + WASI stubs for missing APIs.
src/coreclr/pal/src/arch/wasm/mmap-wasi.cNew WASI mmap/munmap implementation.
src/coreclr/pal/src/CMakeLists.txtWASI source selection + avoid unwind/dac pieces on wasm.
src/coreclr/hosts/corerun/dotenv.cppDisable corerun dotenv self-test on WASI.
src/coreclr/hosts/corerun/corerun.hppAvoid config.h for wasm; define minimal feature macros.
src/coreclr/hosts/corerun/corerun.cppDisable corerun self-test path on WASI.
src/coreclr/hosts/corerun/CMakeLists.txtWASI link options/libs; avoid dl/rdynamic on wasm.
src/coreclr/gc/unix/gcenv.unix.cppWASI mman define handling + available memory logic.
src/coreclr/gc/unix/events.cppAllow process-clock emulation define for waits.
src/coreclr/gc/unix/configure.cmakeGuard pthread checks on presence of pthread library.
src/coreclr/debug/debug-pal/unix/twowaypipe.cppWASI mkfifo stub for debug transport.
src/coreclr/debug/debug-pal/CMakeLists.txtDisable diagnostic server reference on WASI.
src/coreclr/clrfeatures.cmakeDisable event trace default for WASI.
src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csprojInclude Unix/browser interop stubs for WASI too.
src/coreclr/CMakeLists.txtWASI compile defs + include stub headers; skip tools/watchdog.
eng/pipelines/runtime.ymlAdd WASI CoreCLR build job template.
eng/pipelines/common/templates/wasi-coreclr-build.ymlNew pipeline template for WASI CoreCLR build-only job.
eng/native/functions.cmakeInstall rule adjustment for WASI wasm executables.
eng/native/configureplatform.cmakeTreat WASI as UNIX; set WASI target flags.
eng/native/configurecompiler.cmakeEmit TARGET_WASI compile definition.
eng/native/build-commons.shAvoid overriding CC/CXX for WASI builds.
eng/native.wasm.targetsImport AcquireWasiSdk + add WASI CoreCLR cmake args.
eng/Subsets.propsEnable CoreCLR subsets and defaults for WASI.
eng/AcquireWasiSdk.targetsUpdate runtime-local WASI SDK version.

Comment on lines +2360 to +2364
#elif defined(__EMSCRIPTEN__)
stackBase = (void*)emscripten_stack_get_base();
#endif // TARGET_BROWSER
#else // WASI
stackBase = __builtin_frame_address(0);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

In the WASI path, GetStackBase returns __builtin_frame_address(0), which is the current frame address, not the thread’s stack base. Components that rely on PAL_GetStackBase() for stack scanning/overflow checks will get an incorrect range. Consider using toolchain/linker-provided stack bound symbols (e.g., __stack_high/__stack_low or equivalent for wasi-sdk) or another reliable WASI-specific mechanism rather than the current frame address.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good catch. __builtin_frame_address(0) is an approximation — WASI/wasm32 doesn't expose linker-provided stack bound symbols like __stack_high for wasip2. Added a comment explaining this is an approximation. The value is cached on first call and used for the lifetime of the (single) thread, so it's close to the initial stack top.

Comment on lines +469 to +477
#ifdef TARGET_WASI
// WASI's fcntl may not support F_DUPFD_CLOEXEC at runtime.
// CLOEXEC is irrelevant on WASI (no exec). Use fd_fdstat_get + path_open
// workaround: just reuse the fd directly without duplication since
// WASI doesn't support fork/exec anyway.
UnixFd = pFileLocalData->unix_fd;
#else
UnixFd = fcntl(pFileLocalData->unix_fd, F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

On WASI, assigning UnixFd = pFileLocalData->unix_fd means the file mapping now owns the original file descriptor. FileMappingCleanupRoutine later close()s pLocalData->UnixFd, which will unexpectedly close the underlying handle used by hFile and any other users. Use a real dup operation on WASI (e.g., dup()/dup3() if available) or track ownership so the mapping cleanup doesn’t close a borrowed descriptor.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Valid concern. WASI's fcntl doesn't implement F_DUPFD_CLOEXEC (or F_DUPFD) at runtime despite the constant being defined at compile time, and dup() isn't available in wasip2 headers. The fd sharing is safe here because WASI is single-threaded with no fork/exec, so CLOEXEC is irrelevant and the file mapping lifetime matches the source handle. Updated the comment to explain this rationale.

Comment on lines 2438 to +2445
/* duplicate the FILE *, so that we can fclose() in FILECloseHandle without
closing the original */
#ifdef TARGET_WASI
// WASI: fcntl F_DUPFD_CLOEXEC not supported; reuse fd directly (single-threaded, no exec)
new_fd = fileno(stream);
#else
new_fd = fcntl(fileno(stream), F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

The WASI path sets new_fd = fileno(stream) instead of duplicating the descriptor. This makes the PAL file handle for stdin/stdout/stderr own and potentially close() the original stdio fd (e.g., on failure paths and when the handle is later closed), which can break subsequent I/O. Use an actual descriptor duplication mechanism on WASI, or ensure the PAL handle is marked non-owning so it won’t close the original fd.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Same situation as the map.cpp case — fcntl F_DUPFD* and dup() aren't available on wasip2. The stdio fds (0/1/2) are never closed by the PAL on WASI, so sharing is safe. Updated comment to explain the safety rationale.

<Project>
<PropertyGroup>
<_WasiSdkVersion>25.0</_WasiSdkVersion>
<_WasiSdkVersion>32.0</_WasiSdkVersion>

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

_WasiSdkVersion was updated to 32.0, but src/mono/wasi/build/WasiApp.targets still hard-codes an expected WASI SDK version of 25.0. This will cause Mono WASI builds (and any tooling that validates WASI_SDK_PATH/VERSION) to fail when using the runtime-local SDK acquired here. Please update the other expected-version checks (or centralize the version) so all WASI build paths agree on a single SDK version.

Suggested change
<_WasiSdkVersion>32.0</_WasiSdkVersion>
<WasiSdkVersionCondition="'$(WasiSdkVersion)' == ''">32.0</WasiSdkVersion>
<_WasiSdkVersion>$(WasiSdkVersion)</_WasiSdkVersion>

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

The Mono WASI build uses its own SDK acquisition path and isn't affected by this CoreCLR-specific version bump. We have a separate PR (dotnet/wasi-sdk#239) to update the dotnet fork of wasi-sdk to upstream v33 which will unify SDK versions across both runtimes.

Comment on lines +2415 to +2417
#else // WASI
stackLimit = (void*)sizeof(size_t);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

For WASI, GetStackLimit() sets stackLimit to (void*)sizeof(size_t), which is just a small constant cast to a pointer and not an actual stack boundary. This can break stack range calculations and stack overflow detection. Please compute a real lower stack bound for WASI (or, if unavailable, adjust callers to use SP-based scanning and return a clearly defined sentinel that won’t be treated as an address).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now computes stack limit as frame_address - 8MB (matching the -Wl,-z,stack-size=8388608 set at link time) with a floor at sizeof(size_t) to avoid wrapping to zero.

Comment on lines 485 to 516
@@ -509,6 +510,7 @@ BOOL CONTEXT_GetRegisters(DWORD processId, LPCONTEXT lpContext)
#undef ASSIGN_REG

CONTEXTFromNativeContext(&registers, lpContext, lpContext->ContextFlags);
#endif // !TARGET_WASI
}

bRet = TRUE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

When processId != GetCurrentProcessId() on WASI, the ucontext_t registers block is compiled out, but the function still falls through and returns TRUE without populating lpContext. This results in callers observing success with uninitialized/incorrect register state. Return FALSE (and set an appropriate last error) or explicitly zero/initialize lpContext similarly to the CONTEXT_GetThreadContext fallback path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now returns FALSE immediately for non-current process on WASI.

Comment on lines +31 to +34
};

static inline int dl_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data) { (void)callback; (void)data; return 0; }

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

This header uses size_t in the dl_iterate_phdr callback signature but doesn’t include a header that defines it. Add an include like <stddef.h> (or <sys/types.h>) to ensure size_t is available for both C and C++ compilation.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Added #include <stddef.h>.

#ifdef TARGET_WASI
extern "C" int pthread_getschedparam(pthread_t, int *policy, struct sched_param *param)
{
if (policy) *policy = 0;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

pthread_getschedparam returns success but leaves *param uninitialized. Callers expecting sched_param fields to be set may read garbage. Either populate *param with sensible defaults (e.g., zero priority) or return an error code (e.g., ENOTSUP) to signal the API isn’t implemented on WASI.

Suggested change
if (policy) *policy = 0;
if (policy) *policy = 0;
if (param) *param = sched_param{};

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now zero-initializes *param with memset.

Comment on lines +21 to +58
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#define ANON_ARENA_SIZE (256 * 1024 * 1024)

static char *s_arena_base;
static size_t s_arena_offset;

static void ensure_arena(void) {
if (s_arena_base == NULL) {
s_arena_base = (char *)malloc(ANON_ARENA_SIZE);
if (s_arena_base == NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset = 0;
}
}

// Allocate from the bump arena with alignment. Never reuses freed addresses.
static void *arena_alloc(size_t length, size_t align) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_t base = (uintptr_t)s_arena_base;
uintptr_t current = base + s_arena_offset;
uintptr_t aligned_addr = (current + align - 1) & ~(align - 1);
size_t aligned_offset = aligned_addr - base;
if (aligned_offset + length > ANON_ARENA_SIZE) {
return NULL;
}
void *ptr = (void *)aligned_addr;
s_arena_offset = aligned_offset + length;
return ptr;
}

static int is_arena_ptr(void *addr) {
if (s_arena_base == NULL) return 0;
return (char *)addr >= s_arena_base &&
(char *)addr < s_arena_base + ANON_ARENA_SIZE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

ensure_arena() allocates a fixed 256MB arena eagerly via malloc(ANON_ARENA_SIZE). On wasm32/WASI this directly consumes linear memory and can cause large, immediate memory growth or OOM even for small apps. Consider allocating the arena lazily in smaller chunks (or growing it) and/or sizing it based on configured limits rather than a hard-coded 256MB constant.

Suggested change
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
staticchar*s_arena_base;
staticsize_ts_arena_offset;
staticvoidensure_arena(void) {
if (s_arena_base==NULL) {
s_arena_base= (char*)malloc(ANON_ARENA_SIZE);
if (s_arena_base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset=0;
}
}
// Allocate from the bump arena with alignment. Never reuses freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_tbase= (uintptr_t)s_arena_base;
uintptr_tcurrent=base+s_arena_offset;
uintptr_taligned_addr= (current+align-1) & ~(align-1);
size_taligned_offset=aligned_addr-base;
if (aligned_offset+length>ANON_ARENA_SIZE) {
returnNULL;
}
void*ptr= (void*)aligned_addr;
s_arena_offset=aligned_offset+length;
returnptr;
}
staticintis_arena_ptr(void*addr) {
if (s_arena_base==NULL) return0;
return (char*)addr >= s_arena_base&&
(char*)addr<s_arena_base+ANON_ARENA_SIZE;
// Cap anonymous mappings at 256MB total, but allocate backing storage lazily
// in small chunks so small WASI apps do not eagerly consume large amounts of
// linear memory on the first anonymous mapping.
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
#defineANON_ARENA_CHUNK_SIZE (64 * 1024)
#defineANON_ARENA_MAX_CHUNKS (ANON_ARENA_SIZE / ANON_ARENA_CHUNK_SIZE)
typedefstruct
{
char*base;
size_tsize;
size_toffset;
} anon_arena_chunk;
staticanon_arena_chunks_arena_chunks[ANON_ARENA_MAX_CHUNKS];
staticsize_ts_arena_chunk_count;
staticsize_ts_arena_total_size;
staticvoidensure_arena(void) {
// Chunks are allocated lazily by arena_alloc().
}
staticanon_arena_chunk*arena_alloc_chunk(size_tmin_length, size_talign) {
size_tchunk_size;
anon_arena_chunk*chunk;
if (s_arena_chunk_count >= ANON_ARENA_MAX_CHUNKS) {
returnNULL;
}
chunk_size=ANON_ARENA_CHUNK_SIZE;
if (min_length>chunk_size||align>1) {
size_trequired=min_length+align-1;
if (required>chunk_size) {
chunk_size=required;
}
}
if (chunk_size>ANON_ARENA_SIZE-s_arena_total_size) {
returnNULL;
}
chunk=&s_arena_chunks[s_arena_chunk_count];
chunk->base= (char*)malloc(chunk_size);
if (chunk->base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %zu byte arena chunk\n", chunk_size);
abort();
}
chunk->size=chunk_size;
chunk->offset=0;
s_arena_chunk_count++;
s_arena_total_size+=chunk_size;
returnchunk;
}
// Allocate from lazily created bump arena chunks with alignment. Never reuses
// freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
anon_arena_chunk*chunk;
uintptr_tbase;
uintptr_tcurrent;
uintptr_taligned_addr;
size_taligned_offset;
ensure_arena();
if (align==0) {
align=1;
}
if (s_arena_chunk_count!=0) {
chunk=&s_arena_chunks[s_arena_chunk_count-1];
base= (uintptr_t)chunk->base;
current=base+chunk->offset;
aligned_addr= (current+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length <= chunk->size) {
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
}
chunk=arena_alloc_chunk(length, align);
if (chunk==NULL) {
returnNULL;
}
base= (uintptr_t)chunk->base;
aligned_addr= (base+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length>chunk->size) {
returnNULL;
}
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
staticintis_arena_ptr(void*addr) {
size_ti;
for (i=0; i<s_arena_chunk_count; i++) {
char*base=s_arena_chunks[i].base;
if ((char*)addr >= base&& (char*)addr<base+s_arena_chunks[i].size) {
return1;
}
}
return0;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good suggestion. On wasm32, malloc of 256MB doesn't immediately consume physical memory — it triggers memory.grow lazily as pages are touched. The arena is bumped incrementally so only the used portion is backed by real pages. That said, a lazy chunked approach would be cleaner and we'll consider it as a follow-up optimization.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Correction to my previous reply: wasm memory.grow commits pages immediately — there's no lazy page fault mechanism in wasm linear memory. So the 256MB malloc does grow linear memory upfront. A chunked approach that grows the arena in smaller increments as needed would save real memory for smaller apps. Filed as a follow-up improvement.

- context.cpp: Return FALSE for non-current process on WASI instead of
falling through with uninitialized context
- thread.cpp: Improve GetStackBase/GetStackLimit comments; compute stack
limit from frame address minus configured stack size (8MB)
- file.cpp, map.cpp: Improve fd-sharing comments explaining why reuse is
safe on WASI (no fork/exec, no CLOEXEC needed)
- link.h: Add missing stddef.h include for size_t
- stubs.cpp: Zero-initialize sched_param in pthread_getschedparam stub
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lewinglewing reopened this Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-PAL-coreclronly for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Coreclr wasi interpreter - #126959

Closed
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter
Closed

Coreclr wasi interpreter#126959
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter

Conversation

@lewing

Copy link
Copy Markdown
Member

No description provided.

lewingand others added 17 commits April 9, 2026 14:33
Enable building the CoreCLR WASM interpreter as a WASI component.
The resulting corerun.wasm (4.8MB stripped) runs on wasmtime and nesm,
with help output in ~150ms. GC initialization succeeds; runtime startup
proceeds to assembly loading.
Build system:
- Add TargetsWasi to CoreCLR supported OS (eng/Subsets.props)
- Set CLR_CMAKE_TARGET_UNIX and CLR_CMAKE_HOST_UNIX for WASI
- Add TARGET_WASI compile definition inside UNIX block
- Add WASI SDK acquisition to runtime.proj
- Import AcquireWasiSdk.targets in native.wasm.targets
- Set WASI emulation flags (_WASI_EMULATED_SIGNAL, _MMAN, etc.)
- Add CI pipeline (eng/pipelines/common/templates/wasi-coreclr-build.yml)
- Disable FEATURE_EVENT_TRACE, watchdog, superpmi, jitinterface for WASI
- Fix libunwind fallthrough for WASM in PAL CMakeLists
- Guard empty PTHREAD_LIBRARY cmake checks
WASI stub headers (pal/src/include/pal/wasi/):
- signal.h: Full POSIX signal types (siginfo_t, sigaction, SA_*, FPE_*)
- pthread.h: Unlock pthread_exit, scheduling declarations
- pwd.h, link.h, dlfcn.h, unistd.h: Missing POSIX stubs
- sys/wait.h, sys/vfs.h, sys/resource.h, ucontext.h: Missing system stubs
PAL stubs (pal/src/arch/wasm/stubs.cpp):
- DebugBreak, OutputDebugString, RaiseException
- PAL_VirtualUnwind, PAL_ProbeMemory, PAL_FreeExceptionRecords
- SEHInitializeSignals, SEHCleanupSignals, UnmaskActivationSignal
- FlushInstructionCache, GetThreadContext
- __cxa_allocate_exception, __cxa_throw, __cxa_thread_atexit
- shm_open, shm_unlink, pthread_getschedparam
WASI mmap (pal/src/arch/wasm/mmap-wasi.c):
- Custom mmap/munmap/mprotect using malloc with allocation tracking
- Replaces wasi-emulated-mman which can't handle PROT_NONE reservations
- Enables GC to allocate segments via linear memory growth
Minimal source changes:
- cgencpu.h: __builtin_frame_address fallback for WASI
- PAL stubs.cpp: Guard emscripten APIs with __EMSCRIPTEN__
- PAL thread.cpp: WASI fallback for GetStackBase/GetStackLimit
- PAL context.h: native_context_t stub for WASI (no ucontext)
- PAL file.cpp: Skip fcntl F_DUPFD_CLOEXEC on WASI
- GC gcenv.unix.cpp: __wasi__ path for memory sizing
- GC events.cpp: _WASI_EMULATED_PROCESS_CLOCKS for timed wait
- finalizerthread.cpp: Synchronous WASI finalizer path
- minipal/mutex.c: Skip recursive mutex attribute on WASI
- Minimal WASI corerun host (corerun-wasi.cpp)
Build: ./build.sh clr.native+clr.corelib+clr.nativecorelib -os wasi -c Release
Test: wasmtime run --dir /::/ corerun.wasm --help
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enable CoreLib loading on WASI by fixing several PAL and host issues:
- eng/native/functions.cmake: Fix install_clr for WASI executables.
WASI SDK produces a single binary, not Emscripten's .js+.wasm pair.
- corerun-wasi.cpp: Resolve CORE_ROOT to absolute path using getcwd
(not realpath which returns '/' on WASI). The assembly binder rejects
relative TPA paths, so corerun must provide absolute paths.
- clrhost.cpp: On WASI (TARGET_WASI), use CORE_ROOT env var to
determine the system directory. The runtime is statically linked
so GetClrModulePathName can't discover the module path normally.
- file.cpp: Bypass realpath() in InternalCanonicalizeRealPath on WASI.
WASI preopen paths are virtual directories that realpath cannot
resolve. Pass paths through directly instead.
- map.cpp: Skip fcntl(F_DUPFD_CLOEXEC) in CreateFileMapping on WASI.
Reuse the fd directly since WASI has no fork/exec (CLOEXEC is moot).
With these fixes, corerun.wasm successfully:
- Opens System.Private.CoreLib.dll (5MB) via WASI preopened dirs
- Maps it into memory via the malloc-based mmap
- Completes coreclr_initialize (host->Start() returns S_OK)
- Begins managed code execution in CreateAppDomainWithManager
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The malloc-based mmap caused memory corruption: the GC reserves and
releases large anonymous regions via mmap/munmap, and malloc reuses
freed addresses. File-backed mappings (assembly loading) then get
addresses within previously-freed GC regions. When the GC later
initializes those pages, it zeroes out the assembly data.
Fix: use a bump allocator backed by a 256MB arena for anonymous
mappings. This prevents address reuse, matching real mmap semantics
where munmap'd pages aren't returned to other callers. File-backed
mappings continue using malloc since they're long-lived and isolated.
With this fix, coreclr_initialize completes successfully on WASI:
- GC heap initializes in the arena
- CoreLib (5MB) loads via malloc into non-overlapping memory
- AppContext.Setup compiles and executes through the interpreter
- CreateAppDomainWithManager returns S_OK
- coreclr_execute_assembly is reached
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 64KB TPA buffer overflowed when scanning directories with many
framework assemblies. Increase to 256KB.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove redundant AcquireWasiSdk.targets import from build-native.proj.
It's already imported through native.wasm.targets at line 3, causing
MSB4011 when building libs for WASI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WASI SDK 32 ships LLVM 22.1.0 which is within the supported version
range (8-23), but init-compiler.sh fails to find it because the WASI
SDK clang is not on PATH. The WASI SDK cmake toolchain file
(wasi-sdk-p2.cmake) already sets CMAKE_C_COMPILER and
CMAKE_CXX_COMPILER to the SDK's clang, so init-compiler.sh is
unnecessary.
Follow the same pattern used for Android builds: set __Compiler to
'default' for WASI targets so gen-buildsys.sh skips init-compiler.sh
and lets the toolchain file handle compiler selection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bump WASI SDK from 25 (LLVM 19) to 32 (LLVM 22). This gives us:
- Native WASI P2 component output from wasm-ld (no wasm-tools wrapper needed)
- Path toward -fwasm-exceptions (requires wasi-sdk-33+ with exceptions sysroot)
- LLVM 22 with better codegen
Fix LLVM 22 strictness warnings promoted to errors:
- pal_icushim_static.c: explicit UErrorCode casts, char* cast, declaration reorder
- pal_networking.c: move declaration before goto label
- stubs.cpp: remove __cxa_thread_atexit stub (now provided by SDK 32 libc++abi)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch from sjlj (-fexceptions) to native WASM exceptions
(-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false) for WASI.
sjlj exceptions cannot handle throw-from-catch patterns needed by
the interpreter's ResumeAfterCatch mechanism. Native WASM exceptions
resolve this — throw/catch now connects properly.
Changes:
- configureplatform.cmake: -fwasm-exceptions + -wasm-use-legacy-eh=false
- CMakeLists.txt: disable -mllvm -wasm-enable-sjlj for WASI
- corerun CMakeLists.txt: link -lunwind instead of -lsetjmp,
pass -fwasm-exceptions at link time
- stubs.cpp: remove __cxa_throw/__cxa_allocate_exception stubs
(now provided by EH-enabled libc++abi/libunwind)
Requires EH-enabled sysroot libraries (libc++.a, libc++abi.a,
libunwind.a) built from wasi-sdk main with -DWASI_SDK_EXCEPTIONS=ON.
These will ship in wasi-sdk-33+. For now, build from source:
cd wasi-sdk && cmake -G Ninja -B build -DWASI_SDK_EXCEPTIONS=ON
ninja -C build
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On WASM, RtlCaptureContext/ClrCaptureContext zeros the CONTEXT since
there are no CPU registers to capture. This leaves the exception
dispatch with IP=0, SP=0 which breaks the managed stack frame iterator.
Fix: In DispatchManagedException, populate the CONTEXT from the current
interpreter frame (InterpreterFrame::SetContextToInterpMethodContextFrame)
before passing it to the EH dispatch. Also record the current interpreter
IP in the frame before throwing (INTOP_THROW sets pFrame->ip).
This provides valid IP/SP to the EH dispatch, but the managed exception
handling still needs additional work in SfiInitWorker to properly walk
interpreter frames on WASM (tracked in dotnet#113694).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The arena bump allocator in mmap-wasi.c aligned the offset within the
arena rather than the absolute address. When malloc returned a base
address that was not 64K-aligned, subsequent 64K-aligned allocations
(e.g., GC handle table segments) were misaligned. The handle table uses
pointer masking (handle & ~0xFFFF) to locate segment headers, so
misaligned segments caused NULL pHandleTable lookups and crashes during
exception handling cleanup (DestroyHandle in CallCatchFunclet).
Also enable recursive mutexes on WASI to match other platforms, since
the runtime may re-enter locks during exception handling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The stack walker finds interpreter frames through the explicit frame
chain (Thread::GetFrame()), not through the CONTEXT IP/SP. The zeroed
context from RtlCaptureContext on WASM is fine because the EH dispatch
starts on an explicit frame. Browser WASM works the same way without
this patch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Report available memory as max (4GB wasm32 limit) minus current usage,
matching Emscripten's approach. The previous code reported current memory
size as available, which is incorrect — it shrinks as memory grows and
doesn't reflect room to grow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The main corerun.cpp already has TARGET_WASM and TARGET_WASI ifdefs
that handle the WASI differences (no dlopen, static exports, etc.).
The separate corerun-wasi.cpp reimplemented TPA building, arg parsing,
and the main loop unnecessarily.
Use the unified corerun.cpp for all platforms. The only WASI-specific
addition is providing HAVE_GETAUXVAL/HAVE_DIRENT_D_TYPE defines directly
since configure.cmake can't run for cross-compilation targets.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The inner `if(NOT CLR_CMAKE_TARGET_WASI)` could never execute since
it was nested inside `if(CLR_CMAKE_TARGET_WASI)`. The sjlj flag is
not needed — WASI uses -fwasm-exceptions instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 15, 2026 17:51
@github-actionsgithub-actionsBot added the area-PAL-coreclr only for closed issues label Apr 15, 2026
@lewinglewing closed this Apr 15, 2026

CopilotAI 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.

Pull request overview

Enable building/running CoreCLR for the WASI target by wiring up WASI SDK acquisition, adjusting CoreCLR/PAL platform conditionals, and adding WASI-specific stubs and build/pipeline support.

Changes:

  • Add/extend WASI build plumbing (SDK acquisition, cmake/msbuild targets, subsets, pipelines).
  • Introduce WASI-specific PAL headers/stubs and WASI mmap implementation, plus various WASI feature guards across CoreCLR/PAL/GC/corerun.
  • Adjust interpreter/browser/wasm conditionals to better separate Emscripten vs WASI behavior.

Reviewed changes

Copilot reviewed 50 out of 50 changed files in this pull request and generated 9 comments.

Show a summary per file
FileDescription
src/native/libs/build-native.projRemoves standalone WASI SDK import; relies on shared wasm targets.
src/native/libs/System.Native/pal_networking.cMinor variable initialization tweak.
src/native/libs/System.Globalization.Native/pal_icushim_static.cICU status init and malloc cast cleanup.
src/coreclr/vm/wasm/cgencpu.hGuard Emscripten stack include; WASI fallback SP logic.
src/coreclr/vm/prestub.cppRefine interpreter ETW handling with portable entrypoints.
src/coreclr/vm/finalizerthread.cppWASI synchronous finalization path under wasm.
src/coreclr/utilcode/clrhost.cppWASI module path derived from CORE_ROOT.
src/coreclr/tools/CMakeLists.txtSkip superpmi for WASI/browser builds.
src/coreclr/runtime.projAdd AcquireWasiSdk dependency for WASI runtime build.
src/coreclr/pal/src/thread/thread.cppWASI stack base/limit handling adjustments.
src/coreclr/pal/src/thread/context.cppDisable remote register capture on WASI.
src/coreclr/pal/src/map/map.cppWASI fcntl/dup workaround for file mappings.
src/coreclr/pal/src/include/pal/wasi/unistd.hWASI unistd wrapper with stubbed POSIX APIs.
src/coreclr/pal/src/include/pal/wasi/ucontext.hWASI ucontext stub header.
src/coreclr/pal/src/include/pal/wasi/sys/wait.hWASI sys/wait stub header.
src/coreclr/pal/src/include/pal/wasi/sys/vfs.hWASI sys/vfs stub header.
src/coreclr/pal/src/include/pal/wasi/sys/resource.hWASI sys/resource wrapper for rlimit.
src/coreclr/pal/src/include/pal/wasi/signal.hWASI signal wrapper defining missing POSIX types/APIs.
src/coreclr/pal/src/include/pal/wasi/pwd.hWASI pwd stub header.
src/coreclr/pal/src/include/pal/wasi/pthread.hWASI pthread wrapper for hidden decls.
src/coreclr/pal/src/include/pal/wasi/link.hWASI link stub for dl_iterate_phdr types.
src/coreclr/pal/src/include/pal/wasi/dlfcn.hWASI dlfcn wrapper for Dl_info/dladdr.
src/coreclr/pal/src/include/pal/context.hAdd WASI native_context_t fallback.
src/coreclr/pal/src/file/file.cppWASI fd duplication workaround for std handles/realpath.
src/coreclr/pal/src/exception/seh.cppDisable seh-unwind.cpp inclusion on WASI.
src/coreclr/pal/src/configure.cmakeMake pthread feature checks conditional; add WASI sched flags.
src/coreclr/pal/src/arch/wasm/stubs.cppEmscripten guards + WASI stubs for missing APIs.
src/coreclr/pal/src/arch/wasm/mmap-wasi.cNew WASI mmap/munmap implementation.
src/coreclr/pal/src/CMakeLists.txtWASI source selection + avoid unwind/dac pieces on wasm.
src/coreclr/hosts/corerun/dotenv.cppDisable corerun dotenv self-test on WASI.
src/coreclr/hosts/corerun/corerun.hppAvoid config.h for wasm; define minimal feature macros.
src/coreclr/hosts/corerun/corerun.cppDisable corerun self-test path on WASI.
src/coreclr/hosts/corerun/CMakeLists.txtWASI link options/libs; avoid dl/rdynamic on wasm.
src/coreclr/gc/unix/gcenv.unix.cppWASI mman define handling + available memory logic.
src/coreclr/gc/unix/events.cppAllow process-clock emulation define for waits.
src/coreclr/gc/unix/configure.cmakeGuard pthread checks on presence of pthread library.
src/coreclr/debug/debug-pal/unix/twowaypipe.cppWASI mkfifo stub for debug transport.
src/coreclr/debug/debug-pal/CMakeLists.txtDisable diagnostic server reference on WASI.
src/coreclr/clrfeatures.cmakeDisable event trace default for WASI.
src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csprojInclude Unix/browser interop stubs for WASI too.
src/coreclr/CMakeLists.txtWASI compile defs + include stub headers; skip tools/watchdog.
eng/pipelines/runtime.ymlAdd WASI CoreCLR build job template.
eng/pipelines/common/templates/wasi-coreclr-build.ymlNew pipeline template for WASI CoreCLR build-only job.
eng/native/functions.cmakeInstall rule adjustment for WASI wasm executables.
eng/native/configureplatform.cmakeTreat WASI as UNIX; set WASI target flags.
eng/native/configurecompiler.cmakeEmit TARGET_WASI compile definition.
eng/native/build-commons.shAvoid overriding CC/CXX for WASI builds.
eng/native.wasm.targetsImport AcquireWasiSdk + add WASI CoreCLR cmake args.
eng/Subsets.propsEnable CoreCLR subsets and defaults for WASI.
eng/AcquireWasiSdk.targetsUpdate runtime-local WASI SDK version.

Comment on lines +2360 to +2364
#elif defined(__EMSCRIPTEN__)
stackBase = (void*)emscripten_stack_get_base();
#endif // TARGET_BROWSER
#else // WASI
stackBase = __builtin_frame_address(0);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

In the WASI path, GetStackBase returns __builtin_frame_address(0), which is the current frame address, not the thread’s stack base. Components that rely on PAL_GetStackBase() for stack scanning/overflow checks will get an incorrect range. Consider using toolchain/linker-provided stack bound symbols (e.g., __stack_high/__stack_low or equivalent for wasi-sdk) or another reliable WASI-specific mechanism rather than the current frame address.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good catch. __builtin_frame_address(0) is an approximation — WASI/wasm32 doesn't expose linker-provided stack bound symbols like __stack_high for wasip2. Added a comment explaining this is an approximation. The value is cached on first call and used for the lifetime of the (single) thread, so it's close to the initial stack top.

Comment on lines +469 to +477
#ifdef TARGET_WASI
// WASI's fcntl may not support F_DUPFD_CLOEXEC at runtime.
// CLOEXEC is irrelevant on WASI (no exec). Use fd_fdstat_get + path_open
// workaround: just reuse the fd directly without duplication since
// WASI doesn't support fork/exec anyway.
UnixFd = pFileLocalData->unix_fd;
#else
UnixFd = fcntl(pFileLocalData->unix_fd, F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

On WASI, assigning UnixFd = pFileLocalData->unix_fd means the file mapping now owns the original file descriptor. FileMappingCleanupRoutine later close()s pLocalData->UnixFd, which will unexpectedly close the underlying handle used by hFile and any other users. Use a real dup operation on WASI (e.g., dup()/dup3() if available) or track ownership so the mapping cleanup doesn’t close a borrowed descriptor.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Valid concern. WASI's fcntl doesn't implement F_DUPFD_CLOEXEC (or F_DUPFD) at runtime despite the constant being defined at compile time, and dup() isn't available in wasip2 headers. The fd sharing is safe here because WASI is single-threaded with no fork/exec, so CLOEXEC is irrelevant and the file mapping lifetime matches the source handle. Updated the comment to explain this rationale.

Comment on lines 2438 to +2445
/* duplicate the FILE *, so that we can fclose() in FILECloseHandle without
closing the original */
#ifdef TARGET_WASI
// WASI: fcntl F_DUPFD_CLOEXEC not supported; reuse fd directly (single-threaded, no exec)
new_fd = fileno(stream);
#else
new_fd = fcntl(fileno(stream), F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

The WASI path sets new_fd = fileno(stream) instead of duplicating the descriptor. This makes the PAL file handle for stdin/stdout/stderr own and potentially close() the original stdio fd (e.g., on failure paths and when the handle is later closed), which can break subsequent I/O. Use an actual descriptor duplication mechanism on WASI, or ensure the PAL handle is marked non-owning so it won’t close the original fd.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Same situation as the map.cpp case — fcntl F_DUPFD* and dup() aren't available on wasip2. The stdio fds (0/1/2) are never closed by the PAL on WASI, so sharing is safe. Updated comment to explain the safety rationale.

<Project>
<PropertyGroup>
<_WasiSdkVersion>25.0</_WasiSdkVersion>
<_WasiSdkVersion>32.0</_WasiSdkVersion>

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

_WasiSdkVersion was updated to 32.0, but src/mono/wasi/build/WasiApp.targets still hard-codes an expected WASI SDK version of 25.0. This will cause Mono WASI builds (and any tooling that validates WASI_SDK_PATH/VERSION) to fail when using the runtime-local SDK acquired here. Please update the other expected-version checks (or centralize the version) so all WASI build paths agree on a single SDK version.

Suggested change
<_WasiSdkVersion>32.0</_WasiSdkVersion>
<WasiSdkVersionCondition="'$(WasiSdkVersion)' == ''">32.0</WasiSdkVersion>
<_WasiSdkVersion>$(WasiSdkVersion)</_WasiSdkVersion>

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

The Mono WASI build uses its own SDK acquisition path and isn't affected by this CoreCLR-specific version bump. We have a separate PR (dotnet/wasi-sdk#239) to update the dotnet fork of wasi-sdk to upstream v33 which will unify SDK versions across both runtimes.

Comment on lines +2415 to +2417
#else // WASI
stackLimit = (void*)sizeof(size_t);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

For WASI, GetStackLimit() sets stackLimit to (void*)sizeof(size_t), which is just a small constant cast to a pointer and not an actual stack boundary. This can break stack range calculations and stack overflow detection. Please compute a real lower stack bound for WASI (or, if unavailable, adjust callers to use SP-based scanning and return a clearly defined sentinel that won’t be treated as an address).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now computes stack limit as frame_address - 8MB (matching the -Wl,-z,stack-size=8388608 set at link time) with a floor at sizeof(size_t) to avoid wrapping to zero.

Comment on lines 485 to 516
@@ -509,6 +510,7 @@ BOOL CONTEXT_GetRegisters(DWORD processId, LPCONTEXT lpContext)
#undef ASSIGN_REG

CONTEXTFromNativeContext(&registers, lpContext, lpContext->ContextFlags);
#endif // !TARGET_WASI
}

bRet = TRUE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

When processId != GetCurrentProcessId() on WASI, the ucontext_t registers block is compiled out, but the function still falls through and returns TRUE without populating lpContext. This results in callers observing success with uninitialized/incorrect register state. Return FALSE (and set an appropriate last error) or explicitly zero/initialize lpContext similarly to the CONTEXT_GetThreadContext fallback path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now returns FALSE immediately for non-current process on WASI.

Comment on lines +31 to +34
};

static inline int dl_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data) { (void)callback; (void)data; return 0; }

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

This header uses size_t in the dl_iterate_phdr callback signature but doesn’t include a header that defines it. Add an include like <stddef.h> (or <sys/types.h>) to ensure size_t is available for both C and C++ compilation.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Added #include <stddef.h>.

#ifdef TARGET_WASI
extern "C" int pthread_getschedparam(pthread_t, int *policy, struct sched_param *param)
{
if (policy) *policy = 0;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

pthread_getschedparam returns success but leaves *param uninitialized. Callers expecting sched_param fields to be set may read garbage. Either populate *param with sensible defaults (e.g., zero priority) or return an error code (e.g., ENOTSUP) to signal the API isn’t implemented on WASI.

Suggested change
if (policy) *policy = 0;
if (policy) *policy = 0;
if (param) *param = sched_param{};

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now zero-initializes *param with memset.

Comment on lines +21 to +58
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#define ANON_ARENA_SIZE (256 * 1024 * 1024)

static char *s_arena_base;
static size_t s_arena_offset;

static void ensure_arena(void) {
if (s_arena_base == NULL) {
s_arena_base = (char *)malloc(ANON_ARENA_SIZE);
if (s_arena_base == NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset = 0;
}
}

// Allocate from the bump arena with alignment. Never reuses freed addresses.
static void *arena_alloc(size_t length, size_t align) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_t base = (uintptr_t)s_arena_base;
uintptr_t current = base + s_arena_offset;
uintptr_t aligned_addr = (current + align - 1) & ~(align - 1);
size_t aligned_offset = aligned_addr - base;
if (aligned_offset + length > ANON_ARENA_SIZE) {
return NULL;
}
void *ptr = (void *)aligned_addr;
s_arena_offset = aligned_offset + length;
return ptr;
}

static int is_arena_ptr(void *addr) {
if (s_arena_base == NULL) return 0;
return (char *)addr >= s_arena_base &&
(char *)addr < s_arena_base + ANON_ARENA_SIZE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

ensure_arena() allocates a fixed 256MB arena eagerly via malloc(ANON_ARENA_SIZE). On wasm32/WASI this directly consumes linear memory and can cause large, immediate memory growth or OOM even for small apps. Consider allocating the arena lazily in smaller chunks (or growing it) and/or sizing it based on configured limits rather than a hard-coded 256MB constant.

Suggested change
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
staticchar*s_arena_base;
staticsize_ts_arena_offset;
staticvoidensure_arena(void) {
if (s_arena_base==NULL) {
s_arena_base= (char*)malloc(ANON_ARENA_SIZE);
if (s_arena_base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset=0;
}
}
// Allocate from the bump arena with alignment. Never reuses freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_tbase= (uintptr_t)s_arena_base;
uintptr_tcurrent=base+s_arena_offset;
uintptr_taligned_addr= (current+align-1) & ~(align-1);
size_taligned_offset=aligned_addr-base;
if (aligned_offset+length>ANON_ARENA_SIZE) {
returnNULL;
}
void*ptr= (void*)aligned_addr;
s_arena_offset=aligned_offset+length;
returnptr;
}
staticintis_arena_ptr(void*addr) {
if (s_arena_base==NULL) return0;
return (char*)addr >= s_arena_base&&
(char*)addr<s_arena_base+ANON_ARENA_SIZE;
// Cap anonymous mappings at 256MB total, but allocate backing storage lazily
// in small chunks so small WASI apps do not eagerly consume large amounts of
// linear memory on the first anonymous mapping.
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
#defineANON_ARENA_CHUNK_SIZE (64 * 1024)
#defineANON_ARENA_MAX_CHUNKS (ANON_ARENA_SIZE / ANON_ARENA_CHUNK_SIZE)
typedefstruct
{
char*base;
size_tsize;
size_toffset;
} anon_arena_chunk;
staticanon_arena_chunks_arena_chunks[ANON_ARENA_MAX_CHUNKS];
staticsize_ts_arena_chunk_count;
staticsize_ts_arena_total_size;
staticvoidensure_arena(void) {
// Chunks are allocated lazily by arena_alloc().
}
staticanon_arena_chunk*arena_alloc_chunk(size_tmin_length, size_talign) {
size_tchunk_size;
anon_arena_chunk*chunk;
if (s_arena_chunk_count >= ANON_ARENA_MAX_CHUNKS) {
returnNULL;
}
chunk_size=ANON_ARENA_CHUNK_SIZE;
if (min_length>chunk_size||align>1) {
size_trequired=min_length+align-1;
if (required>chunk_size) {
chunk_size=required;
}
}
if (chunk_size>ANON_ARENA_SIZE-s_arena_total_size) {
returnNULL;
}
chunk=&s_arena_chunks[s_arena_chunk_count];
chunk->base= (char*)malloc(chunk_size);
if (chunk->base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %zu byte arena chunk\n", chunk_size);
abort();
}
chunk->size=chunk_size;
chunk->offset=0;
s_arena_chunk_count++;
s_arena_total_size+=chunk_size;
returnchunk;
}
// Allocate from lazily created bump arena chunks with alignment. Never reuses
// freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
anon_arena_chunk*chunk;
uintptr_tbase;
uintptr_tcurrent;
uintptr_taligned_addr;
size_taligned_offset;
ensure_arena();
if (align==0) {
align=1;
}
if (s_arena_chunk_count!=0) {
chunk=&s_arena_chunks[s_arena_chunk_count-1];
base= (uintptr_t)chunk->base;
current=base+chunk->offset;
aligned_addr= (current+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length <= chunk->size) {
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
}
chunk=arena_alloc_chunk(length, align);
if (chunk==NULL) {
returnNULL;
}
base= (uintptr_t)chunk->base;
aligned_addr= (base+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length>chunk->size) {
returnNULL;
}
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
staticintis_arena_ptr(void*addr) {
size_ti;
for (i=0; i<s_arena_chunk_count; i++) {
char*base=s_arena_chunks[i].base;
if ((char*)addr >= base&& (char*)addr<base+s_arena_chunks[i].size) {
return1;
}
}
return0;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good suggestion. On wasm32, malloc of 256MB doesn't immediately consume physical memory — it triggers memory.grow lazily as pages are touched. The arena is bumped incrementally so only the used portion is backed by real pages. That said, a lazy chunked approach would be cleaner and we'll consider it as a follow-up optimization.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Correction to my previous reply: wasm memory.grow commits pages immediately — there's no lazy page fault mechanism in wasm linear memory. So the 256MB malloc does grow linear memory upfront. A chunked approach that grows the arena in smaller increments as needed would save real memory for smaller apps. Filed as a follow-up improvement.

- context.cpp: Return FALSE for non-current process on WASI instead of
falling through with uninitialized context
- thread.cpp: Improve GetStackBase/GetStackLimit comments; compute stack
limit from frame address minus configured stack size (8MB)
- file.cpp, map.cpp: Improve fd-sharing comments explaining why reuse is
safe on WASI (no fork/exec, no CLOEXEC needed)
- link.h: Add missing stddef.h include for size_t
- stubs.cpp: Zero-initialize sched_param in pthread_getschedparam stub
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lewinglewing reopened this Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-PAL-coreclronly for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Coreclr wasi interpreter - #126959

Closed
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter
Closed

Coreclr wasi interpreter#126959
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter

Conversation

@lewing

Copy link
Copy Markdown
Member

No description provided.

lewingand others added 17 commits April 9, 2026 14:33
Enable building the CoreCLR WASM interpreter as a WASI component.
The resulting corerun.wasm (4.8MB stripped) runs on wasmtime and nesm,
with help output in ~150ms. GC initialization succeeds; runtime startup
proceeds to assembly loading.
Build system:
- Add TargetsWasi to CoreCLR supported OS (eng/Subsets.props)
- Set CLR_CMAKE_TARGET_UNIX and CLR_CMAKE_HOST_UNIX for WASI
- Add TARGET_WASI compile definition inside UNIX block
- Add WASI SDK acquisition to runtime.proj
- Import AcquireWasiSdk.targets in native.wasm.targets
- Set WASI emulation flags (_WASI_EMULATED_SIGNAL, _MMAN, etc.)
- Add CI pipeline (eng/pipelines/common/templates/wasi-coreclr-build.yml)
- Disable FEATURE_EVENT_TRACE, watchdog, superpmi, jitinterface for WASI
- Fix libunwind fallthrough for WASM in PAL CMakeLists
- Guard empty PTHREAD_LIBRARY cmake checks
WASI stub headers (pal/src/include/pal/wasi/):
- signal.h: Full POSIX signal types (siginfo_t, sigaction, SA_*, FPE_*)
- pthread.h: Unlock pthread_exit, scheduling declarations
- pwd.h, link.h, dlfcn.h, unistd.h: Missing POSIX stubs
- sys/wait.h, sys/vfs.h, sys/resource.h, ucontext.h: Missing system stubs
PAL stubs (pal/src/arch/wasm/stubs.cpp):
- DebugBreak, OutputDebugString, RaiseException
- PAL_VirtualUnwind, PAL_ProbeMemory, PAL_FreeExceptionRecords
- SEHInitializeSignals, SEHCleanupSignals, UnmaskActivationSignal
- FlushInstructionCache, GetThreadContext
- __cxa_allocate_exception, __cxa_throw, __cxa_thread_atexit
- shm_open, shm_unlink, pthread_getschedparam
WASI mmap (pal/src/arch/wasm/mmap-wasi.c):
- Custom mmap/munmap/mprotect using malloc with allocation tracking
- Replaces wasi-emulated-mman which can't handle PROT_NONE reservations
- Enables GC to allocate segments via linear memory growth
Minimal source changes:
- cgencpu.h: __builtin_frame_address fallback for WASI
- PAL stubs.cpp: Guard emscripten APIs with __EMSCRIPTEN__
- PAL thread.cpp: WASI fallback for GetStackBase/GetStackLimit
- PAL context.h: native_context_t stub for WASI (no ucontext)
- PAL file.cpp: Skip fcntl F_DUPFD_CLOEXEC on WASI
- GC gcenv.unix.cpp: __wasi__ path for memory sizing
- GC events.cpp: _WASI_EMULATED_PROCESS_CLOCKS for timed wait
- finalizerthread.cpp: Synchronous WASI finalizer path
- minipal/mutex.c: Skip recursive mutex attribute on WASI
- Minimal WASI corerun host (corerun-wasi.cpp)
Build: ./build.sh clr.native+clr.corelib+clr.nativecorelib -os wasi -c Release
Test: wasmtime run --dir /::/ corerun.wasm --help
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enable CoreLib loading on WASI by fixing several PAL and host issues:
- eng/native/functions.cmake: Fix install_clr for WASI executables.
WASI SDK produces a single binary, not Emscripten's .js+.wasm pair.
- corerun-wasi.cpp: Resolve CORE_ROOT to absolute path using getcwd
(not realpath which returns '/' on WASI). The assembly binder rejects
relative TPA paths, so corerun must provide absolute paths.
- clrhost.cpp: On WASI (TARGET_WASI), use CORE_ROOT env var to
determine the system directory. The runtime is statically linked
so GetClrModulePathName can't discover the module path normally.
- file.cpp: Bypass realpath() in InternalCanonicalizeRealPath on WASI.
WASI preopen paths are virtual directories that realpath cannot
resolve. Pass paths through directly instead.
- map.cpp: Skip fcntl(F_DUPFD_CLOEXEC) in CreateFileMapping on WASI.
Reuse the fd directly since WASI has no fork/exec (CLOEXEC is moot).
With these fixes, corerun.wasm successfully:
- Opens System.Private.CoreLib.dll (5MB) via WASI preopened dirs
- Maps it into memory via the malloc-based mmap
- Completes coreclr_initialize (host->Start() returns S_OK)
- Begins managed code execution in CreateAppDomainWithManager
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The malloc-based mmap caused memory corruption: the GC reserves and
releases large anonymous regions via mmap/munmap, and malloc reuses
freed addresses. File-backed mappings (assembly loading) then get
addresses within previously-freed GC regions. When the GC later
initializes those pages, it zeroes out the assembly data.
Fix: use a bump allocator backed by a 256MB arena for anonymous
mappings. This prevents address reuse, matching real mmap semantics
where munmap'd pages aren't returned to other callers. File-backed
mappings continue using malloc since they're long-lived and isolated.
With this fix, coreclr_initialize completes successfully on WASI:
- GC heap initializes in the arena
- CoreLib (5MB) loads via malloc into non-overlapping memory
- AppContext.Setup compiles and executes through the interpreter
- CreateAppDomainWithManager returns S_OK
- coreclr_execute_assembly is reached
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 64KB TPA buffer overflowed when scanning directories with many
framework assemblies. Increase to 256KB.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove redundant AcquireWasiSdk.targets import from build-native.proj.
It's already imported through native.wasm.targets at line 3, causing
MSB4011 when building libs for WASI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WASI SDK 32 ships LLVM 22.1.0 which is within the supported version
range (8-23), but init-compiler.sh fails to find it because the WASI
SDK clang is not on PATH. The WASI SDK cmake toolchain file
(wasi-sdk-p2.cmake) already sets CMAKE_C_COMPILER and
CMAKE_CXX_COMPILER to the SDK's clang, so init-compiler.sh is
unnecessary.
Follow the same pattern used for Android builds: set __Compiler to
'default' for WASI targets so gen-buildsys.sh skips init-compiler.sh
and lets the toolchain file handle compiler selection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bump WASI SDK from 25 (LLVM 19) to 32 (LLVM 22). This gives us:
- Native WASI P2 component output from wasm-ld (no wasm-tools wrapper needed)
- Path toward -fwasm-exceptions (requires wasi-sdk-33+ with exceptions sysroot)
- LLVM 22 with better codegen
Fix LLVM 22 strictness warnings promoted to errors:
- pal_icushim_static.c: explicit UErrorCode casts, char* cast, declaration reorder
- pal_networking.c: move declaration before goto label
- stubs.cpp: remove __cxa_thread_atexit stub (now provided by SDK 32 libc++abi)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch from sjlj (-fexceptions) to native WASM exceptions
(-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false) for WASI.
sjlj exceptions cannot handle throw-from-catch patterns needed by
the interpreter's ResumeAfterCatch mechanism. Native WASM exceptions
resolve this — throw/catch now connects properly.
Changes:
- configureplatform.cmake: -fwasm-exceptions + -wasm-use-legacy-eh=false
- CMakeLists.txt: disable -mllvm -wasm-enable-sjlj for WASI
- corerun CMakeLists.txt: link -lunwind instead of -lsetjmp,
pass -fwasm-exceptions at link time
- stubs.cpp: remove __cxa_throw/__cxa_allocate_exception stubs
(now provided by EH-enabled libc++abi/libunwind)
Requires EH-enabled sysroot libraries (libc++.a, libc++abi.a,
libunwind.a) built from wasi-sdk main with -DWASI_SDK_EXCEPTIONS=ON.
These will ship in wasi-sdk-33+. For now, build from source:
cd wasi-sdk && cmake -G Ninja -B build -DWASI_SDK_EXCEPTIONS=ON
ninja -C build
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On WASM, RtlCaptureContext/ClrCaptureContext zeros the CONTEXT since
there are no CPU registers to capture. This leaves the exception
dispatch with IP=0, SP=0 which breaks the managed stack frame iterator.
Fix: In DispatchManagedException, populate the CONTEXT from the current
interpreter frame (InterpreterFrame::SetContextToInterpMethodContextFrame)
before passing it to the EH dispatch. Also record the current interpreter
IP in the frame before throwing (INTOP_THROW sets pFrame->ip).
This provides valid IP/SP to the EH dispatch, but the managed exception
handling still needs additional work in SfiInitWorker to properly walk
interpreter frames on WASM (tracked in dotnet#113694).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The arena bump allocator in mmap-wasi.c aligned the offset within the
arena rather than the absolute address. When malloc returned a base
address that was not 64K-aligned, subsequent 64K-aligned allocations
(e.g., GC handle table segments) were misaligned. The handle table uses
pointer masking (handle & ~0xFFFF) to locate segment headers, so
misaligned segments caused NULL pHandleTable lookups and crashes during
exception handling cleanup (DestroyHandle in CallCatchFunclet).
Also enable recursive mutexes on WASI to match other platforms, since
the runtime may re-enter locks during exception handling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The stack walker finds interpreter frames through the explicit frame
chain (Thread::GetFrame()), not through the CONTEXT IP/SP. The zeroed
context from RtlCaptureContext on WASM is fine because the EH dispatch
starts on an explicit frame. Browser WASM works the same way without
this patch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Report available memory as max (4GB wasm32 limit) minus current usage,
matching Emscripten's approach. The previous code reported current memory
size as available, which is incorrect — it shrinks as memory grows and
doesn't reflect room to grow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The main corerun.cpp already has TARGET_WASM and TARGET_WASI ifdefs
that handle the WASI differences (no dlopen, static exports, etc.).
The separate corerun-wasi.cpp reimplemented TPA building, arg parsing,
and the main loop unnecessarily.
Use the unified corerun.cpp for all platforms. The only WASI-specific
addition is providing HAVE_GETAUXVAL/HAVE_DIRENT_D_TYPE defines directly
since configure.cmake can't run for cross-compilation targets.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The inner `if(NOT CLR_CMAKE_TARGET_WASI)` could never execute since
it was nested inside `if(CLR_CMAKE_TARGET_WASI)`. The sjlj flag is
not needed — WASI uses -fwasm-exceptions instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 15, 2026 17:51
@github-actionsgithub-actionsBot added the area-PAL-coreclr only for closed issues label Apr 15, 2026
@lewinglewing closed this Apr 15, 2026

CopilotAI 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.

Pull request overview

Enable building/running CoreCLR for the WASI target by wiring up WASI SDK acquisition, adjusting CoreCLR/PAL platform conditionals, and adding WASI-specific stubs and build/pipeline support.

Changes:

  • Add/extend WASI build plumbing (SDK acquisition, cmake/msbuild targets, subsets, pipelines).
  • Introduce WASI-specific PAL headers/stubs and WASI mmap implementation, plus various WASI feature guards across CoreCLR/PAL/GC/corerun.
  • Adjust interpreter/browser/wasm conditionals to better separate Emscripten vs WASI behavior.

Reviewed changes

Copilot reviewed 50 out of 50 changed files in this pull request and generated 9 comments.

Show a summary per file
FileDescription
src/native/libs/build-native.projRemoves standalone WASI SDK import; relies on shared wasm targets.
src/native/libs/System.Native/pal_networking.cMinor variable initialization tweak.
src/native/libs/System.Globalization.Native/pal_icushim_static.cICU status init and malloc cast cleanup.
src/coreclr/vm/wasm/cgencpu.hGuard Emscripten stack include; WASI fallback SP logic.
src/coreclr/vm/prestub.cppRefine interpreter ETW handling with portable entrypoints.
src/coreclr/vm/finalizerthread.cppWASI synchronous finalization path under wasm.
src/coreclr/utilcode/clrhost.cppWASI module path derived from CORE_ROOT.
src/coreclr/tools/CMakeLists.txtSkip superpmi for WASI/browser builds.
src/coreclr/runtime.projAdd AcquireWasiSdk dependency for WASI runtime build.
src/coreclr/pal/src/thread/thread.cppWASI stack base/limit handling adjustments.
src/coreclr/pal/src/thread/context.cppDisable remote register capture on WASI.
src/coreclr/pal/src/map/map.cppWASI fcntl/dup workaround for file mappings.
src/coreclr/pal/src/include/pal/wasi/unistd.hWASI unistd wrapper with stubbed POSIX APIs.
src/coreclr/pal/src/include/pal/wasi/ucontext.hWASI ucontext stub header.
src/coreclr/pal/src/include/pal/wasi/sys/wait.hWASI sys/wait stub header.
src/coreclr/pal/src/include/pal/wasi/sys/vfs.hWASI sys/vfs stub header.
src/coreclr/pal/src/include/pal/wasi/sys/resource.hWASI sys/resource wrapper for rlimit.
src/coreclr/pal/src/include/pal/wasi/signal.hWASI signal wrapper defining missing POSIX types/APIs.
src/coreclr/pal/src/include/pal/wasi/pwd.hWASI pwd stub header.
src/coreclr/pal/src/include/pal/wasi/pthread.hWASI pthread wrapper for hidden decls.
src/coreclr/pal/src/include/pal/wasi/link.hWASI link stub for dl_iterate_phdr types.
src/coreclr/pal/src/include/pal/wasi/dlfcn.hWASI dlfcn wrapper for Dl_info/dladdr.
src/coreclr/pal/src/include/pal/context.hAdd WASI native_context_t fallback.
src/coreclr/pal/src/file/file.cppWASI fd duplication workaround for std handles/realpath.
src/coreclr/pal/src/exception/seh.cppDisable seh-unwind.cpp inclusion on WASI.
src/coreclr/pal/src/configure.cmakeMake pthread feature checks conditional; add WASI sched flags.
src/coreclr/pal/src/arch/wasm/stubs.cppEmscripten guards + WASI stubs for missing APIs.
src/coreclr/pal/src/arch/wasm/mmap-wasi.cNew WASI mmap/munmap implementation.
src/coreclr/pal/src/CMakeLists.txtWASI source selection + avoid unwind/dac pieces on wasm.
src/coreclr/hosts/corerun/dotenv.cppDisable corerun dotenv self-test on WASI.
src/coreclr/hosts/corerun/corerun.hppAvoid config.h for wasm; define minimal feature macros.
src/coreclr/hosts/corerun/corerun.cppDisable corerun self-test path on WASI.
src/coreclr/hosts/corerun/CMakeLists.txtWASI link options/libs; avoid dl/rdynamic on wasm.
src/coreclr/gc/unix/gcenv.unix.cppWASI mman define handling + available memory logic.
src/coreclr/gc/unix/events.cppAllow process-clock emulation define for waits.
src/coreclr/gc/unix/configure.cmakeGuard pthread checks on presence of pthread library.
src/coreclr/debug/debug-pal/unix/twowaypipe.cppWASI mkfifo stub for debug transport.
src/coreclr/debug/debug-pal/CMakeLists.txtDisable diagnostic server reference on WASI.
src/coreclr/clrfeatures.cmakeDisable event trace default for WASI.
src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csprojInclude Unix/browser interop stubs for WASI too.
src/coreclr/CMakeLists.txtWASI compile defs + include stub headers; skip tools/watchdog.
eng/pipelines/runtime.ymlAdd WASI CoreCLR build job template.
eng/pipelines/common/templates/wasi-coreclr-build.ymlNew pipeline template for WASI CoreCLR build-only job.
eng/native/functions.cmakeInstall rule adjustment for WASI wasm executables.
eng/native/configureplatform.cmakeTreat WASI as UNIX; set WASI target flags.
eng/native/configurecompiler.cmakeEmit TARGET_WASI compile definition.
eng/native/build-commons.shAvoid overriding CC/CXX for WASI builds.
eng/native.wasm.targetsImport AcquireWasiSdk + add WASI CoreCLR cmake args.
eng/Subsets.propsEnable CoreCLR subsets and defaults for WASI.
eng/AcquireWasiSdk.targetsUpdate runtime-local WASI SDK version.

Comment on lines +2360 to +2364
#elif defined(__EMSCRIPTEN__)
stackBase = (void*)emscripten_stack_get_base();
#endif // TARGET_BROWSER
#else // WASI
stackBase = __builtin_frame_address(0);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

In the WASI path, GetStackBase returns __builtin_frame_address(0), which is the current frame address, not the thread’s stack base. Components that rely on PAL_GetStackBase() for stack scanning/overflow checks will get an incorrect range. Consider using toolchain/linker-provided stack bound symbols (e.g., __stack_high/__stack_low or equivalent for wasi-sdk) or another reliable WASI-specific mechanism rather than the current frame address.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good catch. __builtin_frame_address(0) is an approximation — WASI/wasm32 doesn't expose linker-provided stack bound symbols like __stack_high for wasip2. Added a comment explaining this is an approximation. The value is cached on first call and used for the lifetime of the (single) thread, so it's close to the initial stack top.

Comment on lines +469 to +477
#ifdef TARGET_WASI
// WASI's fcntl may not support F_DUPFD_CLOEXEC at runtime.
// CLOEXEC is irrelevant on WASI (no exec). Use fd_fdstat_get + path_open
// workaround: just reuse the fd directly without duplication since
// WASI doesn't support fork/exec anyway.
UnixFd = pFileLocalData->unix_fd;
#else
UnixFd = fcntl(pFileLocalData->unix_fd, F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

On WASI, assigning UnixFd = pFileLocalData->unix_fd means the file mapping now owns the original file descriptor. FileMappingCleanupRoutine later close()s pLocalData->UnixFd, which will unexpectedly close the underlying handle used by hFile and any other users. Use a real dup operation on WASI (e.g., dup()/dup3() if available) or track ownership so the mapping cleanup doesn’t close a borrowed descriptor.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Valid concern. WASI's fcntl doesn't implement F_DUPFD_CLOEXEC (or F_DUPFD) at runtime despite the constant being defined at compile time, and dup() isn't available in wasip2 headers. The fd sharing is safe here because WASI is single-threaded with no fork/exec, so CLOEXEC is irrelevant and the file mapping lifetime matches the source handle. Updated the comment to explain this rationale.

Comment on lines 2438 to +2445
/* duplicate the FILE *, so that we can fclose() in FILECloseHandle without
closing the original */
#ifdef TARGET_WASI
// WASI: fcntl F_DUPFD_CLOEXEC not supported; reuse fd directly (single-threaded, no exec)
new_fd = fileno(stream);
#else
new_fd = fcntl(fileno(stream), F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

The WASI path sets new_fd = fileno(stream) instead of duplicating the descriptor. This makes the PAL file handle for stdin/stdout/stderr own and potentially close() the original stdio fd (e.g., on failure paths and when the handle is later closed), which can break subsequent I/O. Use an actual descriptor duplication mechanism on WASI, or ensure the PAL handle is marked non-owning so it won’t close the original fd.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Same situation as the map.cpp case — fcntl F_DUPFD* and dup() aren't available on wasip2. The stdio fds (0/1/2) are never closed by the PAL on WASI, so sharing is safe. Updated comment to explain the safety rationale.

<Project>
<PropertyGroup>
<_WasiSdkVersion>25.0</_WasiSdkVersion>
<_WasiSdkVersion>32.0</_WasiSdkVersion>

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

_WasiSdkVersion was updated to 32.0, but src/mono/wasi/build/WasiApp.targets still hard-codes an expected WASI SDK version of 25.0. This will cause Mono WASI builds (and any tooling that validates WASI_SDK_PATH/VERSION) to fail when using the runtime-local SDK acquired here. Please update the other expected-version checks (or centralize the version) so all WASI build paths agree on a single SDK version.

Suggested change
<_WasiSdkVersion>32.0</_WasiSdkVersion>
<WasiSdkVersionCondition="'$(WasiSdkVersion)' == ''">32.0</WasiSdkVersion>
<_WasiSdkVersion>$(WasiSdkVersion)</_WasiSdkVersion>

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

The Mono WASI build uses its own SDK acquisition path and isn't affected by this CoreCLR-specific version bump. We have a separate PR (dotnet/wasi-sdk#239) to update the dotnet fork of wasi-sdk to upstream v33 which will unify SDK versions across both runtimes.

Comment on lines +2415 to +2417
#else // WASI
stackLimit = (void*)sizeof(size_t);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

For WASI, GetStackLimit() sets stackLimit to (void*)sizeof(size_t), which is just a small constant cast to a pointer and not an actual stack boundary. This can break stack range calculations and stack overflow detection. Please compute a real lower stack bound for WASI (or, if unavailable, adjust callers to use SP-based scanning and return a clearly defined sentinel that won’t be treated as an address).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now computes stack limit as frame_address - 8MB (matching the -Wl,-z,stack-size=8388608 set at link time) with a floor at sizeof(size_t) to avoid wrapping to zero.

Comment on lines 485 to 516
@@ -509,6 +510,7 @@ BOOL CONTEXT_GetRegisters(DWORD processId, LPCONTEXT lpContext)
#undef ASSIGN_REG

CONTEXTFromNativeContext(&registers, lpContext, lpContext->ContextFlags);
#endif // !TARGET_WASI
}

bRet = TRUE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

When processId != GetCurrentProcessId() on WASI, the ucontext_t registers block is compiled out, but the function still falls through and returns TRUE without populating lpContext. This results in callers observing success with uninitialized/incorrect register state. Return FALSE (and set an appropriate last error) or explicitly zero/initialize lpContext similarly to the CONTEXT_GetThreadContext fallback path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now returns FALSE immediately for non-current process on WASI.

Comment on lines +31 to +34
};

static inline int dl_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data) { (void)callback; (void)data; return 0; }

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

This header uses size_t in the dl_iterate_phdr callback signature but doesn’t include a header that defines it. Add an include like <stddef.h> (or <sys/types.h>) to ensure size_t is available for both C and C++ compilation.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Added #include <stddef.h>.

#ifdef TARGET_WASI
extern "C" int pthread_getschedparam(pthread_t, int *policy, struct sched_param *param)
{
if (policy) *policy = 0;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

pthread_getschedparam returns success but leaves *param uninitialized. Callers expecting sched_param fields to be set may read garbage. Either populate *param with sensible defaults (e.g., zero priority) or return an error code (e.g., ENOTSUP) to signal the API isn’t implemented on WASI.

Suggested change
if (policy) *policy = 0;
if (policy) *policy = 0;
if (param) *param = sched_param{};

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now zero-initializes *param with memset.

Comment on lines +21 to +58
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#define ANON_ARENA_SIZE (256 * 1024 * 1024)

static char *s_arena_base;
static size_t s_arena_offset;

static void ensure_arena(void) {
if (s_arena_base == NULL) {
s_arena_base = (char *)malloc(ANON_ARENA_SIZE);
if (s_arena_base == NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset = 0;
}
}

// Allocate from the bump arena with alignment. Never reuses freed addresses.
static void *arena_alloc(size_t length, size_t align) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_t base = (uintptr_t)s_arena_base;
uintptr_t current = base + s_arena_offset;
uintptr_t aligned_addr = (current + align - 1) & ~(align - 1);
size_t aligned_offset = aligned_addr - base;
if (aligned_offset + length > ANON_ARENA_SIZE) {
return NULL;
}
void *ptr = (void *)aligned_addr;
s_arena_offset = aligned_offset + length;
return ptr;
}

static int is_arena_ptr(void *addr) {
if (s_arena_base == NULL) return 0;
return (char *)addr >= s_arena_base &&
(char *)addr < s_arena_base + ANON_ARENA_SIZE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

ensure_arena() allocates a fixed 256MB arena eagerly via malloc(ANON_ARENA_SIZE). On wasm32/WASI this directly consumes linear memory and can cause large, immediate memory growth or OOM even for small apps. Consider allocating the arena lazily in smaller chunks (or growing it) and/or sizing it based on configured limits rather than a hard-coded 256MB constant.

Suggested change
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
staticchar*s_arena_base;
staticsize_ts_arena_offset;
staticvoidensure_arena(void) {
if (s_arena_base==NULL) {
s_arena_base= (char*)malloc(ANON_ARENA_SIZE);
if (s_arena_base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset=0;
}
}
// Allocate from the bump arena with alignment. Never reuses freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_tbase= (uintptr_t)s_arena_base;
uintptr_tcurrent=base+s_arena_offset;
uintptr_taligned_addr= (current+align-1) & ~(align-1);
size_taligned_offset=aligned_addr-base;
if (aligned_offset+length>ANON_ARENA_SIZE) {
returnNULL;
}
void*ptr= (void*)aligned_addr;
s_arena_offset=aligned_offset+length;
returnptr;
}
staticintis_arena_ptr(void*addr) {
if (s_arena_base==NULL) return0;
return (char*)addr >= s_arena_base&&
(char*)addr<s_arena_base+ANON_ARENA_SIZE;
// Cap anonymous mappings at 256MB total, but allocate backing storage lazily
// in small chunks so small WASI apps do not eagerly consume large amounts of
// linear memory on the first anonymous mapping.
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
#defineANON_ARENA_CHUNK_SIZE (64 * 1024)
#defineANON_ARENA_MAX_CHUNKS (ANON_ARENA_SIZE / ANON_ARENA_CHUNK_SIZE)
typedefstruct
{
char*base;
size_tsize;
size_toffset;
} anon_arena_chunk;
staticanon_arena_chunks_arena_chunks[ANON_ARENA_MAX_CHUNKS];
staticsize_ts_arena_chunk_count;
staticsize_ts_arena_total_size;
staticvoidensure_arena(void) {
// Chunks are allocated lazily by arena_alloc().
}
staticanon_arena_chunk*arena_alloc_chunk(size_tmin_length, size_talign) {
size_tchunk_size;
anon_arena_chunk*chunk;
if (s_arena_chunk_count >= ANON_ARENA_MAX_CHUNKS) {
returnNULL;
}
chunk_size=ANON_ARENA_CHUNK_SIZE;
if (min_length>chunk_size||align>1) {
size_trequired=min_length+align-1;
if (required>chunk_size) {
chunk_size=required;
}
}
if (chunk_size>ANON_ARENA_SIZE-s_arena_total_size) {
returnNULL;
}
chunk=&s_arena_chunks[s_arena_chunk_count];
chunk->base= (char*)malloc(chunk_size);
if (chunk->base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %zu byte arena chunk\n", chunk_size);
abort();
}
chunk->size=chunk_size;
chunk->offset=0;
s_arena_chunk_count++;
s_arena_total_size+=chunk_size;
returnchunk;
}
// Allocate from lazily created bump arena chunks with alignment. Never reuses
// freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
anon_arena_chunk*chunk;
uintptr_tbase;
uintptr_tcurrent;
uintptr_taligned_addr;
size_taligned_offset;
ensure_arena();
if (align==0) {
align=1;
}
if (s_arena_chunk_count!=0) {
chunk=&s_arena_chunks[s_arena_chunk_count-1];
base= (uintptr_t)chunk->base;
current=base+chunk->offset;
aligned_addr= (current+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length <= chunk->size) {
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
}
chunk=arena_alloc_chunk(length, align);
if (chunk==NULL) {
returnNULL;
}
base= (uintptr_t)chunk->base;
aligned_addr= (base+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length>chunk->size) {
returnNULL;
}
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
staticintis_arena_ptr(void*addr) {
size_ti;
for (i=0; i<s_arena_chunk_count; i++) {
char*base=s_arena_chunks[i].base;
if ((char*)addr >= base&& (char*)addr<base+s_arena_chunks[i].size) {
return1;
}
}
return0;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good suggestion. On wasm32, malloc of 256MB doesn't immediately consume physical memory — it triggers memory.grow lazily as pages are touched. The arena is bumped incrementally so only the used portion is backed by real pages. That said, a lazy chunked approach would be cleaner and we'll consider it as a follow-up optimization.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Correction to my previous reply: wasm memory.grow commits pages immediately — there's no lazy page fault mechanism in wasm linear memory. So the 256MB malloc does grow linear memory upfront. A chunked approach that grows the arena in smaller increments as needed would save real memory for smaller apps. Filed as a follow-up improvement.

- context.cpp: Return FALSE for non-current process on WASI instead of
falling through with uninitialized context
- thread.cpp: Improve GetStackBase/GetStackLimit comments; compute stack
limit from frame address minus configured stack size (8MB)
- file.cpp, map.cpp: Improve fd-sharing comments explaining why reuse is
safe on WASI (no fork/exec, no CLOEXEC needed)
- link.h: Add missing stddef.h include for size_t
- stubs.cpp: Zero-initialize sched_param in pthread_getschedparam stub
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lewinglewing reopened this Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-PAL-coreclronly for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Coreclr wasi interpreter - #126959

Closed
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter
Closed

Coreclr wasi interpreter#126959
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter

Conversation

@lewing

Copy link
Copy Markdown
Member

No description provided.

lewingand others added 17 commits April 9, 2026 14:33
Enable building the CoreCLR WASM interpreter as a WASI component.
The resulting corerun.wasm (4.8MB stripped) runs on wasmtime and nesm,
with help output in ~150ms. GC initialization succeeds; runtime startup
proceeds to assembly loading.
Build system:
- Add TargetsWasi to CoreCLR supported OS (eng/Subsets.props)
- Set CLR_CMAKE_TARGET_UNIX and CLR_CMAKE_HOST_UNIX for WASI
- Add TARGET_WASI compile definition inside UNIX block
- Add WASI SDK acquisition to runtime.proj
- Import AcquireWasiSdk.targets in native.wasm.targets
- Set WASI emulation flags (_WASI_EMULATED_SIGNAL, _MMAN, etc.)
- Add CI pipeline (eng/pipelines/common/templates/wasi-coreclr-build.yml)
- Disable FEATURE_EVENT_TRACE, watchdog, superpmi, jitinterface for WASI
- Fix libunwind fallthrough for WASM in PAL CMakeLists
- Guard empty PTHREAD_LIBRARY cmake checks
WASI stub headers (pal/src/include/pal/wasi/):
- signal.h: Full POSIX signal types (siginfo_t, sigaction, SA_*, FPE_*)
- pthread.h: Unlock pthread_exit, scheduling declarations
- pwd.h, link.h, dlfcn.h, unistd.h: Missing POSIX stubs
- sys/wait.h, sys/vfs.h, sys/resource.h, ucontext.h: Missing system stubs
PAL stubs (pal/src/arch/wasm/stubs.cpp):
- DebugBreak, OutputDebugString, RaiseException
- PAL_VirtualUnwind, PAL_ProbeMemory, PAL_FreeExceptionRecords
- SEHInitializeSignals, SEHCleanupSignals, UnmaskActivationSignal
- FlushInstructionCache, GetThreadContext
- __cxa_allocate_exception, __cxa_throw, __cxa_thread_atexit
- shm_open, shm_unlink, pthread_getschedparam
WASI mmap (pal/src/arch/wasm/mmap-wasi.c):
- Custom mmap/munmap/mprotect using malloc with allocation tracking
- Replaces wasi-emulated-mman which can't handle PROT_NONE reservations
- Enables GC to allocate segments via linear memory growth
Minimal source changes:
- cgencpu.h: __builtin_frame_address fallback for WASI
- PAL stubs.cpp: Guard emscripten APIs with __EMSCRIPTEN__
- PAL thread.cpp: WASI fallback for GetStackBase/GetStackLimit
- PAL context.h: native_context_t stub for WASI (no ucontext)
- PAL file.cpp: Skip fcntl F_DUPFD_CLOEXEC on WASI
- GC gcenv.unix.cpp: __wasi__ path for memory sizing
- GC events.cpp: _WASI_EMULATED_PROCESS_CLOCKS for timed wait
- finalizerthread.cpp: Synchronous WASI finalizer path
- minipal/mutex.c: Skip recursive mutex attribute on WASI
- Minimal WASI corerun host (corerun-wasi.cpp)
Build: ./build.sh clr.native+clr.corelib+clr.nativecorelib -os wasi -c Release
Test: wasmtime run --dir /::/ corerun.wasm --help
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enable CoreLib loading on WASI by fixing several PAL and host issues:
- eng/native/functions.cmake: Fix install_clr for WASI executables.
WASI SDK produces a single binary, not Emscripten's .js+.wasm pair.
- corerun-wasi.cpp: Resolve CORE_ROOT to absolute path using getcwd
(not realpath which returns '/' on WASI). The assembly binder rejects
relative TPA paths, so corerun must provide absolute paths.
- clrhost.cpp: On WASI (TARGET_WASI), use CORE_ROOT env var to
determine the system directory. The runtime is statically linked
so GetClrModulePathName can't discover the module path normally.
- file.cpp: Bypass realpath() in InternalCanonicalizeRealPath on WASI.
WASI preopen paths are virtual directories that realpath cannot
resolve. Pass paths through directly instead.
- map.cpp: Skip fcntl(F_DUPFD_CLOEXEC) in CreateFileMapping on WASI.
Reuse the fd directly since WASI has no fork/exec (CLOEXEC is moot).
With these fixes, corerun.wasm successfully:
- Opens System.Private.CoreLib.dll (5MB) via WASI preopened dirs
- Maps it into memory via the malloc-based mmap
- Completes coreclr_initialize (host->Start() returns S_OK)
- Begins managed code execution in CreateAppDomainWithManager
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The malloc-based mmap caused memory corruption: the GC reserves and
releases large anonymous regions via mmap/munmap, and malloc reuses
freed addresses. File-backed mappings (assembly loading) then get
addresses within previously-freed GC regions. When the GC later
initializes those pages, it zeroes out the assembly data.
Fix: use a bump allocator backed by a 256MB arena for anonymous
mappings. This prevents address reuse, matching real mmap semantics
where munmap'd pages aren't returned to other callers. File-backed
mappings continue using malloc since they're long-lived and isolated.
With this fix, coreclr_initialize completes successfully on WASI:
- GC heap initializes in the arena
- CoreLib (5MB) loads via malloc into non-overlapping memory
- AppContext.Setup compiles and executes through the interpreter
- CreateAppDomainWithManager returns S_OK
- coreclr_execute_assembly is reached
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 64KB TPA buffer overflowed when scanning directories with many
framework assemblies. Increase to 256KB.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove redundant AcquireWasiSdk.targets import from build-native.proj.
It's already imported through native.wasm.targets at line 3, causing
MSB4011 when building libs for WASI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WASI SDK 32 ships LLVM 22.1.0 which is within the supported version
range (8-23), but init-compiler.sh fails to find it because the WASI
SDK clang is not on PATH. The WASI SDK cmake toolchain file
(wasi-sdk-p2.cmake) already sets CMAKE_C_COMPILER and
CMAKE_CXX_COMPILER to the SDK's clang, so init-compiler.sh is
unnecessary.
Follow the same pattern used for Android builds: set __Compiler to
'default' for WASI targets so gen-buildsys.sh skips init-compiler.sh
and lets the toolchain file handle compiler selection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bump WASI SDK from 25 (LLVM 19) to 32 (LLVM 22). This gives us:
- Native WASI P2 component output from wasm-ld (no wasm-tools wrapper needed)
- Path toward -fwasm-exceptions (requires wasi-sdk-33+ with exceptions sysroot)
- LLVM 22 with better codegen
Fix LLVM 22 strictness warnings promoted to errors:
- pal_icushim_static.c: explicit UErrorCode casts, char* cast, declaration reorder
- pal_networking.c: move declaration before goto label
- stubs.cpp: remove __cxa_thread_atexit stub (now provided by SDK 32 libc++abi)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch from sjlj (-fexceptions) to native WASM exceptions
(-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false) for WASI.
sjlj exceptions cannot handle throw-from-catch patterns needed by
the interpreter's ResumeAfterCatch mechanism. Native WASM exceptions
resolve this — throw/catch now connects properly.
Changes:
- configureplatform.cmake: -fwasm-exceptions + -wasm-use-legacy-eh=false
- CMakeLists.txt: disable -mllvm -wasm-enable-sjlj for WASI
- corerun CMakeLists.txt: link -lunwind instead of -lsetjmp,
pass -fwasm-exceptions at link time
- stubs.cpp: remove __cxa_throw/__cxa_allocate_exception stubs
(now provided by EH-enabled libc++abi/libunwind)
Requires EH-enabled sysroot libraries (libc++.a, libc++abi.a,
libunwind.a) built from wasi-sdk main with -DWASI_SDK_EXCEPTIONS=ON.
These will ship in wasi-sdk-33+. For now, build from source:
cd wasi-sdk && cmake -G Ninja -B build -DWASI_SDK_EXCEPTIONS=ON
ninja -C build
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On WASM, RtlCaptureContext/ClrCaptureContext zeros the CONTEXT since
there are no CPU registers to capture. This leaves the exception
dispatch with IP=0, SP=0 which breaks the managed stack frame iterator.
Fix: In DispatchManagedException, populate the CONTEXT from the current
interpreter frame (InterpreterFrame::SetContextToInterpMethodContextFrame)
before passing it to the EH dispatch. Also record the current interpreter
IP in the frame before throwing (INTOP_THROW sets pFrame->ip).
This provides valid IP/SP to the EH dispatch, but the managed exception
handling still needs additional work in SfiInitWorker to properly walk
interpreter frames on WASM (tracked in dotnet#113694).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The arena bump allocator in mmap-wasi.c aligned the offset within the
arena rather than the absolute address. When malloc returned a base
address that was not 64K-aligned, subsequent 64K-aligned allocations
(e.g., GC handle table segments) were misaligned. The handle table uses
pointer masking (handle & ~0xFFFF) to locate segment headers, so
misaligned segments caused NULL pHandleTable lookups and crashes during
exception handling cleanup (DestroyHandle in CallCatchFunclet).
Also enable recursive mutexes on WASI to match other platforms, since
the runtime may re-enter locks during exception handling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The stack walker finds interpreter frames through the explicit frame
chain (Thread::GetFrame()), not through the CONTEXT IP/SP. The zeroed
context from RtlCaptureContext on WASM is fine because the EH dispatch
starts on an explicit frame. Browser WASM works the same way without
this patch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Report available memory as max (4GB wasm32 limit) minus current usage,
matching Emscripten's approach. The previous code reported current memory
size as available, which is incorrect — it shrinks as memory grows and
doesn't reflect room to grow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The main corerun.cpp already has TARGET_WASM and TARGET_WASI ifdefs
that handle the WASI differences (no dlopen, static exports, etc.).
The separate corerun-wasi.cpp reimplemented TPA building, arg parsing,
and the main loop unnecessarily.
Use the unified corerun.cpp for all platforms. The only WASI-specific
addition is providing HAVE_GETAUXVAL/HAVE_DIRENT_D_TYPE defines directly
since configure.cmake can't run for cross-compilation targets.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The inner `if(NOT CLR_CMAKE_TARGET_WASI)` could never execute since
it was nested inside `if(CLR_CMAKE_TARGET_WASI)`. The sjlj flag is
not needed — WASI uses -fwasm-exceptions instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 15, 2026 17:51
@github-actionsgithub-actionsBot added the area-PAL-coreclr only for closed issues label Apr 15, 2026
@lewinglewing closed this Apr 15, 2026

CopilotAI 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.

Pull request overview

Enable building/running CoreCLR for the WASI target by wiring up WASI SDK acquisition, adjusting CoreCLR/PAL platform conditionals, and adding WASI-specific stubs and build/pipeline support.

Changes:

  • Add/extend WASI build plumbing (SDK acquisition, cmake/msbuild targets, subsets, pipelines).
  • Introduce WASI-specific PAL headers/stubs and WASI mmap implementation, plus various WASI feature guards across CoreCLR/PAL/GC/corerun.
  • Adjust interpreter/browser/wasm conditionals to better separate Emscripten vs WASI behavior.

Reviewed changes

Copilot reviewed 50 out of 50 changed files in this pull request and generated 9 comments.

Show a summary per file
FileDescription
src/native/libs/build-native.projRemoves standalone WASI SDK import; relies on shared wasm targets.
src/native/libs/System.Native/pal_networking.cMinor variable initialization tweak.
src/native/libs/System.Globalization.Native/pal_icushim_static.cICU status init and malloc cast cleanup.
src/coreclr/vm/wasm/cgencpu.hGuard Emscripten stack include; WASI fallback SP logic.
src/coreclr/vm/prestub.cppRefine interpreter ETW handling with portable entrypoints.
src/coreclr/vm/finalizerthread.cppWASI synchronous finalization path under wasm.
src/coreclr/utilcode/clrhost.cppWASI module path derived from CORE_ROOT.
src/coreclr/tools/CMakeLists.txtSkip superpmi for WASI/browser builds.
src/coreclr/runtime.projAdd AcquireWasiSdk dependency for WASI runtime build.
src/coreclr/pal/src/thread/thread.cppWASI stack base/limit handling adjustments.
src/coreclr/pal/src/thread/context.cppDisable remote register capture on WASI.
src/coreclr/pal/src/map/map.cppWASI fcntl/dup workaround for file mappings.
src/coreclr/pal/src/include/pal/wasi/unistd.hWASI unistd wrapper with stubbed POSIX APIs.
src/coreclr/pal/src/include/pal/wasi/ucontext.hWASI ucontext stub header.
src/coreclr/pal/src/include/pal/wasi/sys/wait.hWASI sys/wait stub header.
src/coreclr/pal/src/include/pal/wasi/sys/vfs.hWASI sys/vfs stub header.
src/coreclr/pal/src/include/pal/wasi/sys/resource.hWASI sys/resource wrapper for rlimit.
src/coreclr/pal/src/include/pal/wasi/signal.hWASI signal wrapper defining missing POSIX types/APIs.
src/coreclr/pal/src/include/pal/wasi/pwd.hWASI pwd stub header.
src/coreclr/pal/src/include/pal/wasi/pthread.hWASI pthread wrapper for hidden decls.
src/coreclr/pal/src/include/pal/wasi/link.hWASI link stub for dl_iterate_phdr types.
src/coreclr/pal/src/include/pal/wasi/dlfcn.hWASI dlfcn wrapper for Dl_info/dladdr.
src/coreclr/pal/src/include/pal/context.hAdd WASI native_context_t fallback.
src/coreclr/pal/src/file/file.cppWASI fd duplication workaround for std handles/realpath.
src/coreclr/pal/src/exception/seh.cppDisable seh-unwind.cpp inclusion on WASI.
src/coreclr/pal/src/configure.cmakeMake pthread feature checks conditional; add WASI sched flags.
src/coreclr/pal/src/arch/wasm/stubs.cppEmscripten guards + WASI stubs for missing APIs.
src/coreclr/pal/src/arch/wasm/mmap-wasi.cNew WASI mmap/munmap implementation.
src/coreclr/pal/src/CMakeLists.txtWASI source selection + avoid unwind/dac pieces on wasm.
src/coreclr/hosts/corerun/dotenv.cppDisable corerun dotenv self-test on WASI.
src/coreclr/hosts/corerun/corerun.hppAvoid config.h for wasm; define minimal feature macros.
src/coreclr/hosts/corerun/corerun.cppDisable corerun self-test path on WASI.
src/coreclr/hosts/corerun/CMakeLists.txtWASI link options/libs; avoid dl/rdynamic on wasm.
src/coreclr/gc/unix/gcenv.unix.cppWASI mman define handling + available memory logic.
src/coreclr/gc/unix/events.cppAllow process-clock emulation define for waits.
src/coreclr/gc/unix/configure.cmakeGuard pthread checks on presence of pthread library.
src/coreclr/debug/debug-pal/unix/twowaypipe.cppWASI mkfifo stub for debug transport.
src/coreclr/debug/debug-pal/CMakeLists.txtDisable diagnostic server reference on WASI.
src/coreclr/clrfeatures.cmakeDisable event trace default for WASI.
src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csprojInclude Unix/browser interop stubs for WASI too.
src/coreclr/CMakeLists.txtWASI compile defs + include stub headers; skip tools/watchdog.
eng/pipelines/runtime.ymlAdd WASI CoreCLR build job template.
eng/pipelines/common/templates/wasi-coreclr-build.ymlNew pipeline template for WASI CoreCLR build-only job.
eng/native/functions.cmakeInstall rule adjustment for WASI wasm executables.
eng/native/configureplatform.cmakeTreat WASI as UNIX; set WASI target flags.
eng/native/configurecompiler.cmakeEmit TARGET_WASI compile definition.
eng/native/build-commons.shAvoid overriding CC/CXX for WASI builds.
eng/native.wasm.targetsImport AcquireWasiSdk + add WASI CoreCLR cmake args.
eng/Subsets.propsEnable CoreCLR subsets and defaults for WASI.
eng/AcquireWasiSdk.targetsUpdate runtime-local WASI SDK version.

Comment on lines +2360 to +2364
#elif defined(__EMSCRIPTEN__)
stackBase = (void*)emscripten_stack_get_base();
#endif // TARGET_BROWSER
#else // WASI
stackBase = __builtin_frame_address(0);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

In the WASI path, GetStackBase returns __builtin_frame_address(0), which is the current frame address, not the thread’s stack base. Components that rely on PAL_GetStackBase() for stack scanning/overflow checks will get an incorrect range. Consider using toolchain/linker-provided stack bound symbols (e.g., __stack_high/__stack_low or equivalent for wasi-sdk) or another reliable WASI-specific mechanism rather than the current frame address.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good catch. __builtin_frame_address(0) is an approximation — WASI/wasm32 doesn't expose linker-provided stack bound symbols like __stack_high for wasip2. Added a comment explaining this is an approximation. The value is cached on first call and used for the lifetime of the (single) thread, so it's close to the initial stack top.

Comment on lines +469 to +477
#ifdef TARGET_WASI
// WASI's fcntl may not support F_DUPFD_CLOEXEC at runtime.
// CLOEXEC is irrelevant on WASI (no exec). Use fd_fdstat_get + path_open
// workaround: just reuse the fd directly without duplication since
// WASI doesn't support fork/exec anyway.
UnixFd = pFileLocalData->unix_fd;
#else
UnixFd = fcntl(pFileLocalData->unix_fd, F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

On WASI, assigning UnixFd = pFileLocalData->unix_fd means the file mapping now owns the original file descriptor. FileMappingCleanupRoutine later close()s pLocalData->UnixFd, which will unexpectedly close the underlying handle used by hFile and any other users. Use a real dup operation on WASI (e.g., dup()/dup3() if available) or track ownership so the mapping cleanup doesn’t close a borrowed descriptor.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Valid concern. WASI's fcntl doesn't implement F_DUPFD_CLOEXEC (or F_DUPFD) at runtime despite the constant being defined at compile time, and dup() isn't available in wasip2 headers. The fd sharing is safe here because WASI is single-threaded with no fork/exec, so CLOEXEC is irrelevant and the file mapping lifetime matches the source handle. Updated the comment to explain this rationale.

Comment on lines 2438 to +2445
/* duplicate the FILE *, so that we can fclose() in FILECloseHandle without
closing the original */
#ifdef TARGET_WASI
// WASI: fcntl F_DUPFD_CLOEXEC not supported; reuse fd directly (single-threaded, no exec)
new_fd = fileno(stream);
#else
new_fd = fcntl(fileno(stream), F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

The WASI path sets new_fd = fileno(stream) instead of duplicating the descriptor. This makes the PAL file handle for stdin/stdout/stderr own and potentially close() the original stdio fd (e.g., on failure paths and when the handle is later closed), which can break subsequent I/O. Use an actual descriptor duplication mechanism on WASI, or ensure the PAL handle is marked non-owning so it won’t close the original fd.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Same situation as the map.cpp case — fcntl F_DUPFD* and dup() aren't available on wasip2. The stdio fds (0/1/2) are never closed by the PAL on WASI, so sharing is safe. Updated comment to explain the safety rationale.

<Project>
<PropertyGroup>
<_WasiSdkVersion>25.0</_WasiSdkVersion>
<_WasiSdkVersion>32.0</_WasiSdkVersion>

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

_WasiSdkVersion was updated to 32.0, but src/mono/wasi/build/WasiApp.targets still hard-codes an expected WASI SDK version of 25.0. This will cause Mono WASI builds (and any tooling that validates WASI_SDK_PATH/VERSION) to fail when using the runtime-local SDK acquired here. Please update the other expected-version checks (or centralize the version) so all WASI build paths agree on a single SDK version.

Suggested change
<_WasiSdkVersion>32.0</_WasiSdkVersion>
<WasiSdkVersionCondition="'$(WasiSdkVersion)' == ''">32.0</WasiSdkVersion>
<_WasiSdkVersion>$(WasiSdkVersion)</_WasiSdkVersion>

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

The Mono WASI build uses its own SDK acquisition path and isn't affected by this CoreCLR-specific version bump. We have a separate PR (dotnet/wasi-sdk#239) to update the dotnet fork of wasi-sdk to upstream v33 which will unify SDK versions across both runtimes.

Comment on lines +2415 to +2417
#else // WASI
stackLimit = (void*)sizeof(size_t);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

For WASI, GetStackLimit() sets stackLimit to (void*)sizeof(size_t), which is just a small constant cast to a pointer and not an actual stack boundary. This can break stack range calculations and stack overflow detection. Please compute a real lower stack bound for WASI (or, if unavailable, adjust callers to use SP-based scanning and return a clearly defined sentinel that won’t be treated as an address).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now computes stack limit as frame_address - 8MB (matching the -Wl,-z,stack-size=8388608 set at link time) with a floor at sizeof(size_t) to avoid wrapping to zero.

Comment on lines 485 to 516
@@ -509,6 +510,7 @@ BOOL CONTEXT_GetRegisters(DWORD processId, LPCONTEXT lpContext)
#undef ASSIGN_REG

CONTEXTFromNativeContext(&registers, lpContext, lpContext->ContextFlags);
#endif // !TARGET_WASI
}

bRet = TRUE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

When processId != GetCurrentProcessId() on WASI, the ucontext_t registers block is compiled out, but the function still falls through and returns TRUE without populating lpContext. This results in callers observing success with uninitialized/incorrect register state. Return FALSE (and set an appropriate last error) or explicitly zero/initialize lpContext similarly to the CONTEXT_GetThreadContext fallback path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now returns FALSE immediately for non-current process on WASI.

Comment on lines +31 to +34
};

static inline int dl_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data) { (void)callback; (void)data; return 0; }

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

This header uses size_t in the dl_iterate_phdr callback signature but doesn’t include a header that defines it. Add an include like <stddef.h> (or <sys/types.h>) to ensure size_t is available for both C and C++ compilation.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Added #include <stddef.h>.

#ifdef TARGET_WASI
extern "C" int pthread_getschedparam(pthread_t, int *policy, struct sched_param *param)
{
if (policy) *policy = 0;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

pthread_getschedparam returns success but leaves *param uninitialized. Callers expecting sched_param fields to be set may read garbage. Either populate *param with sensible defaults (e.g., zero priority) or return an error code (e.g., ENOTSUP) to signal the API isn’t implemented on WASI.

Suggested change
if (policy) *policy = 0;
if (policy) *policy = 0;
if (param) *param = sched_param{};

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now zero-initializes *param with memset.

Comment on lines +21 to +58
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#define ANON_ARENA_SIZE (256 * 1024 * 1024)

static char *s_arena_base;
static size_t s_arena_offset;

static void ensure_arena(void) {
if (s_arena_base == NULL) {
s_arena_base = (char *)malloc(ANON_ARENA_SIZE);
if (s_arena_base == NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset = 0;
}
}

// Allocate from the bump arena with alignment. Never reuses freed addresses.
static void *arena_alloc(size_t length, size_t align) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_t base = (uintptr_t)s_arena_base;
uintptr_t current = base + s_arena_offset;
uintptr_t aligned_addr = (current + align - 1) & ~(align - 1);
size_t aligned_offset = aligned_addr - base;
if (aligned_offset + length > ANON_ARENA_SIZE) {
return NULL;
}
void *ptr = (void *)aligned_addr;
s_arena_offset = aligned_offset + length;
return ptr;
}

static int is_arena_ptr(void *addr) {
if (s_arena_base == NULL) return 0;
return (char *)addr >= s_arena_base &&
(char *)addr < s_arena_base + ANON_ARENA_SIZE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

ensure_arena() allocates a fixed 256MB arena eagerly via malloc(ANON_ARENA_SIZE). On wasm32/WASI this directly consumes linear memory and can cause large, immediate memory growth or OOM even for small apps. Consider allocating the arena lazily in smaller chunks (or growing it) and/or sizing it based on configured limits rather than a hard-coded 256MB constant.

Suggested change
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
staticchar*s_arena_base;
staticsize_ts_arena_offset;
staticvoidensure_arena(void) {
if (s_arena_base==NULL) {
s_arena_base= (char*)malloc(ANON_ARENA_SIZE);
if (s_arena_base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset=0;
}
}
// Allocate from the bump arena with alignment. Never reuses freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_tbase= (uintptr_t)s_arena_base;
uintptr_tcurrent=base+s_arena_offset;
uintptr_taligned_addr= (current+align-1) & ~(align-1);
size_taligned_offset=aligned_addr-base;
if (aligned_offset+length>ANON_ARENA_SIZE) {
returnNULL;
}
void*ptr= (void*)aligned_addr;
s_arena_offset=aligned_offset+length;
returnptr;
}
staticintis_arena_ptr(void*addr) {
if (s_arena_base==NULL) return0;
return (char*)addr >= s_arena_base&&
(char*)addr<s_arena_base+ANON_ARENA_SIZE;
// Cap anonymous mappings at 256MB total, but allocate backing storage lazily
// in small chunks so small WASI apps do not eagerly consume large amounts of
// linear memory on the first anonymous mapping.
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
#defineANON_ARENA_CHUNK_SIZE (64 * 1024)
#defineANON_ARENA_MAX_CHUNKS (ANON_ARENA_SIZE / ANON_ARENA_CHUNK_SIZE)
typedefstruct
{
char*base;
size_tsize;
size_toffset;
} anon_arena_chunk;
staticanon_arena_chunks_arena_chunks[ANON_ARENA_MAX_CHUNKS];
staticsize_ts_arena_chunk_count;
staticsize_ts_arena_total_size;
staticvoidensure_arena(void) {
// Chunks are allocated lazily by arena_alloc().
}
staticanon_arena_chunk*arena_alloc_chunk(size_tmin_length, size_talign) {
size_tchunk_size;
anon_arena_chunk*chunk;
if (s_arena_chunk_count >= ANON_ARENA_MAX_CHUNKS) {
returnNULL;
}
chunk_size=ANON_ARENA_CHUNK_SIZE;
if (min_length>chunk_size||align>1) {
size_trequired=min_length+align-1;
if (required>chunk_size) {
chunk_size=required;
}
}
if (chunk_size>ANON_ARENA_SIZE-s_arena_total_size) {
returnNULL;
}
chunk=&s_arena_chunks[s_arena_chunk_count];
chunk->base= (char*)malloc(chunk_size);
if (chunk->base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %zu byte arena chunk\n", chunk_size);
abort();
}
chunk->size=chunk_size;
chunk->offset=0;
s_arena_chunk_count++;
s_arena_total_size+=chunk_size;
returnchunk;
}
// Allocate from lazily created bump arena chunks with alignment. Never reuses
// freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
anon_arena_chunk*chunk;
uintptr_tbase;
uintptr_tcurrent;
uintptr_taligned_addr;
size_taligned_offset;
ensure_arena();
if (align==0) {
align=1;
}
if (s_arena_chunk_count!=0) {
chunk=&s_arena_chunks[s_arena_chunk_count-1];
base= (uintptr_t)chunk->base;
current=base+chunk->offset;
aligned_addr= (current+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length <= chunk->size) {
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
}
chunk=arena_alloc_chunk(length, align);
if (chunk==NULL) {
returnNULL;
}
base= (uintptr_t)chunk->base;
aligned_addr= (base+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length>chunk->size) {
returnNULL;
}
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
staticintis_arena_ptr(void*addr) {
size_ti;
for (i=0; i<s_arena_chunk_count; i++) {
char*base=s_arena_chunks[i].base;
if ((char*)addr >= base&& (char*)addr<base+s_arena_chunks[i].size) {
return1;
}
}
return0;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good suggestion. On wasm32, malloc of 256MB doesn't immediately consume physical memory — it triggers memory.grow lazily as pages are touched. The arena is bumped incrementally so only the used portion is backed by real pages. That said, a lazy chunked approach would be cleaner and we'll consider it as a follow-up optimization.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Correction to my previous reply: wasm memory.grow commits pages immediately — there's no lazy page fault mechanism in wasm linear memory. So the 256MB malloc does grow linear memory upfront. A chunked approach that grows the arena in smaller increments as needed would save real memory for smaller apps. Filed as a follow-up improvement.

- context.cpp: Return FALSE for non-current process on WASI instead of
falling through with uninitialized context
- thread.cpp: Improve GetStackBase/GetStackLimit comments; compute stack
limit from frame address minus configured stack size (8MB)
- file.cpp, map.cpp: Improve fd-sharing comments explaining why reuse is
safe on WASI (no fork/exec, no CLOEXEC needed)
- link.h: Add missing stddef.h include for size_t
- stubs.cpp: Zero-initialize sched_param in pthread_getschedparam stub
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lewinglewing reopened this Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-PAL-coreclronly for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Coreclr wasi interpreter - #126959

Closed
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter
Closed

Coreclr wasi interpreter#126959
lewing wants to merge 18 commits into
dotnet:mainfrom
lewing:coreclr-wasi-interpreter

Conversation

@lewing

Copy link
Copy Markdown
Member

No description provided.

lewingand others added 17 commits April 9, 2026 14:33
Enable building the CoreCLR WASM interpreter as a WASI component.
The resulting corerun.wasm (4.8MB stripped) runs on wasmtime and nesm,
with help output in ~150ms. GC initialization succeeds; runtime startup
proceeds to assembly loading.
Build system:
- Add TargetsWasi to CoreCLR supported OS (eng/Subsets.props)
- Set CLR_CMAKE_TARGET_UNIX and CLR_CMAKE_HOST_UNIX for WASI
- Add TARGET_WASI compile definition inside UNIX block
- Add WASI SDK acquisition to runtime.proj
- Import AcquireWasiSdk.targets in native.wasm.targets
- Set WASI emulation flags (_WASI_EMULATED_SIGNAL, _MMAN, etc.)
- Add CI pipeline (eng/pipelines/common/templates/wasi-coreclr-build.yml)
- Disable FEATURE_EVENT_TRACE, watchdog, superpmi, jitinterface for WASI
- Fix libunwind fallthrough for WASM in PAL CMakeLists
- Guard empty PTHREAD_LIBRARY cmake checks
WASI stub headers (pal/src/include/pal/wasi/):
- signal.h: Full POSIX signal types (siginfo_t, sigaction, SA_*, FPE_*)
- pthread.h: Unlock pthread_exit, scheduling declarations
- pwd.h, link.h, dlfcn.h, unistd.h: Missing POSIX stubs
- sys/wait.h, sys/vfs.h, sys/resource.h, ucontext.h: Missing system stubs
PAL stubs (pal/src/arch/wasm/stubs.cpp):
- DebugBreak, OutputDebugString, RaiseException
- PAL_VirtualUnwind, PAL_ProbeMemory, PAL_FreeExceptionRecords
- SEHInitializeSignals, SEHCleanupSignals, UnmaskActivationSignal
- FlushInstructionCache, GetThreadContext
- __cxa_allocate_exception, __cxa_throw, __cxa_thread_atexit
- shm_open, shm_unlink, pthread_getschedparam
WASI mmap (pal/src/arch/wasm/mmap-wasi.c):
- Custom mmap/munmap/mprotect using malloc with allocation tracking
- Replaces wasi-emulated-mman which can't handle PROT_NONE reservations
- Enables GC to allocate segments via linear memory growth
Minimal source changes:
- cgencpu.h: __builtin_frame_address fallback for WASI
- PAL stubs.cpp: Guard emscripten APIs with __EMSCRIPTEN__
- PAL thread.cpp: WASI fallback for GetStackBase/GetStackLimit
- PAL context.h: native_context_t stub for WASI (no ucontext)
- PAL file.cpp: Skip fcntl F_DUPFD_CLOEXEC on WASI
- GC gcenv.unix.cpp: __wasi__ path for memory sizing
- GC events.cpp: _WASI_EMULATED_PROCESS_CLOCKS for timed wait
- finalizerthread.cpp: Synchronous WASI finalizer path
- minipal/mutex.c: Skip recursive mutex attribute on WASI
- Minimal WASI corerun host (corerun-wasi.cpp)
Build: ./build.sh clr.native+clr.corelib+clr.nativecorelib -os wasi -c Release
Test: wasmtime run --dir /::/ corerun.wasm --help
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enable CoreLib loading on WASI by fixing several PAL and host issues:
- eng/native/functions.cmake: Fix install_clr for WASI executables.
WASI SDK produces a single binary, not Emscripten's .js+.wasm pair.
- corerun-wasi.cpp: Resolve CORE_ROOT to absolute path using getcwd
(not realpath which returns '/' on WASI). The assembly binder rejects
relative TPA paths, so corerun must provide absolute paths.
- clrhost.cpp: On WASI (TARGET_WASI), use CORE_ROOT env var to
determine the system directory. The runtime is statically linked
so GetClrModulePathName can't discover the module path normally.
- file.cpp: Bypass realpath() in InternalCanonicalizeRealPath on WASI.
WASI preopen paths are virtual directories that realpath cannot
resolve. Pass paths through directly instead.
- map.cpp: Skip fcntl(F_DUPFD_CLOEXEC) in CreateFileMapping on WASI.
Reuse the fd directly since WASI has no fork/exec (CLOEXEC is moot).
With these fixes, corerun.wasm successfully:
- Opens System.Private.CoreLib.dll (5MB) via WASI preopened dirs
- Maps it into memory via the malloc-based mmap
- Completes coreclr_initialize (host->Start() returns S_OK)
- Begins managed code execution in CreateAppDomainWithManager
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The malloc-based mmap caused memory corruption: the GC reserves and
releases large anonymous regions via mmap/munmap, and malloc reuses
freed addresses. File-backed mappings (assembly loading) then get
addresses within previously-freed GC regions. When the GC later
initializes those pages, it zeroes out the assembly data.
Fix: use a bump allocator backed by a 256MB arena for anonymous
mappings. This prevents address reuse, matching real mmap semantics
where munmap'd pages aren't returned to other callers. File-backed
mappings continue using malloc since they're long-lived and isolated.
With this fix, coreclr_initialize completes successfully on WASI:
- GC heap initializes in the arena
- CoreLib (5MB) loads via malloc into non-overlapping memory
- AppContext.Setup compiles and executes through the interpreter
- CreateAppDomainWithManager returns S_OK
- coreclr_execute_assembly is reached
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 64KB TPA buffer overflowed when scanning directories with many
framework assemblies. Increase to 256KB.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove redundant AcquireWasiSdk.targets import from build-native.proj.
It's already imported through native.wasm.targets at line 3, causing
MSB4011 when building libs for WASI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WASI SDK 32 ships LLVM 22.1.0 which is within the supported version
range (8-23), but init-compiler.sh fails to find it because the WASI
SDK clang is not on PATH. The WASI SDK cmake toolchain file
(wasi-sdk-p2.cmake) already sets CMAKE_C_COMPILER and
CMAKE_CXX_COMPILER to the SDK's clang, so init-compiler.sh is
unnecessary.
Follow the same pattern used for Android builds: set __Compiler to
'default' for WASI targets so gen-buildsys.sh skips init-compiler.sh
and lets the toolchain file handle compiler selection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bump WASI SDK from 25 (LLVM 19) to 32 (LLVM 22). This gives us:
- Native WASI P2 component output from wasm-ld (no wasm-tools wrapper needed)
- Path toward -fwasm-exceptions (requires wasi-sdk-33+ with exceptions sysroot)
- LLVM 22 with better codegen
Fix LLVM 22 strictness warnings promoted to errors:
- pal_icushim_static.c: explicit UErrorCode casts, char* cast, declaration reorder
- pal_networking.c: move declaration before goto label
- stubs.cpp: remove __cxa_thread_atexit stub (now provided by SDK 32 libc++abi)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch from sjlj (-fexceptions) to native WASM exceptions
(-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false) for WASI.
sjlj exceptions cannot handle throw-from-catch patterns needed by
the interpreter's ResumeAfterCatch mechanism. Native WASM exceptions
resolve this — throw/catch now connects properly.
Changes:
- configureplatform.cmake: -fwasm-exceptions + -wasm-use-legacy-eh=false
- CMakeLists.txt: disable -mllvm -wasm-enable-sjlj for WASI
- corerun CMakeLists.txt: link -lunwind instead of -lsetjmp,
pass -fwasm-exceptions at link time
- stubs.cpp: remove __cxa_throw/__cxa_allocate_exception stubs
(now provided by EH-enabled libc++abi/libunwind)
Requires EH-enabled sysroot libraries (libc++.a, libc++abi.a,
libunwind.a) built from wasi-sdk main with -DWASI_SDK_EXCEPTIONS=ON.
These will ship in wasi-sdk-33+. For now, build from source:
cd wasi-sdk && cmake -G Ninja -B build -DWASI_SDK_EXCEPTIONS=ON
ninja -C build
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On WASM, RtlCaptureContext/ClrCaptureContext zeros the CONTEXT since
there are no CPU registers to capture. This leaves the exception
dispatch with IP=0, SP=0 which breaks the managed stack frame iterator.
Fix: In DispatchManagedException, populate the CONTEXT from the current
interpreter frame (InterpreterFrame::SetContextToInterpMethodContextFrame)
before passing it to the EH dispatch. Also record the current interpreter
IP in the frame before throwing (INTOP_THROW sets pFrame->ip).
This provides valid IP/SP to the EH dispatch, but the managed exception
handling still needs additional work in SfiInitWorker to properly walk
interpreter frames on WASM (tracked in dotnet#113694).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The arena bump allocator in mmap-wasi.c aligned the offset within the
arena rather than the absolute address. When malloc returned a base
address that was not 64K-aligned, subsequent 64K-aligned allocations
(e.g., GC handle table segments) were misaligned. The handle table uses
pointer masking (handle & ~0xFFFF) to locate segment headers, so
misaligned segments caused NULL pHandleTable lookups and crashes during
exception handling cleanup (DestroyHandle in CallCatchFunclet).
Also enable recursive mutexes on WASI to match other platforms, since
the runtime may re-enter locks during exception handling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The stack walker finds interpreter frames through the explicit frame
chain (Thread::GetFrame()), not through the CONTEXT IP/SP. The zeroed
context from RtlCaptureContext on WASM is fine because the EH dispatch
starts on an explicit frame. Browser WASM works the same way without
this patch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Report available memory as max (4GB wasm32 limit) minus current usage,
matching Emscripten's approach. The previous code reported current memory
size as available, which is incorrect — it shrinks as memory grows and
doesn't reflect room to grow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The main corerun.cpp already has TARGET_WASM and TARGET_WASI ifdefs
that handle the WASI differences (no dlopen, static exports, etc.).
The separate corerun-wasi.cpp reimplemented TPA building, arg parsing,
and the main loop unnecessarily.
Use the unified corerun.cpp for all platforms. The only WASI-specific
addition is providing HAVE_GETAUXVAL/HAVE_DIRENT_D_TYPE defines directly
since configure.cmake can't run for cross-compilation targets.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The inner `if(NOT CLR_CMAKE_TARGET_WASI)` could never execute since
it was nested inside `if(CLR_CMAKE_TARGET_WASI)`. The sjlj flag is
not needed — WASI uses -fwasm-exceptions instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 15, 2026 17:51
@github-actionsgithub-actionsBot added the area-PAL-coreclr only for closed issues label Apr 15, 2026
@lewinglewing closed this Apr 15, 2026

CopilotAI 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.

Pull request overview

Enable building/running CoreCLR for the WASI target by wiring up WASI SDK acquisition, adjusting CoreCLR/PAL platform conditionals, and adding WASI-specific stubs and build/pipeline support.

Changes:

  • Add/extend WASI build plumbing (SDK acquisition, cmake/msbuild targets, subsets, pipelines).
  • Introduce WASI-specific PAL headers/stubs and WASI mmap implementation, plus various WASI feature guards across CoreCLR/PAL/GC/corerun.
  • Adjust interpreter/browser/wasm conditionals to better separate Emscripten vs WASI behavior.

Reviewed changes

Copilot reviewed 50 out of 50 changed files in this pull request and generated 9 comments.

Show a summary per file
FileDescription
src/native/libs/build-native.projRemoves standalone WASI SDK import; relies on shared wasm targets.
src/native/libs/System.Native/pal_networking.cMinor variable initialization tweak.
src/native/libs/System.Globalization.Native/pal_icushim_static.cICU status init and malloc cast cleanup.
src/coreclr/vm/wasm/cgencpu.hGuard Emscripten stack include; WASI fallback SP logic.
src/coreclr/vm/prestub.cppRefine interpreter ETW handling with portable entrypoints.
src/coreclr/vm/finalizerthread.cppWASI synchronous finalization path under wasm.
src/coreclr/utilcode/clrhost.cppWASI module path derived from CORE_ROOT.
src/coreclr/tools/CMakeLists.txtSkip superpmi for WASI/browser builds.
src/coreclr/runtime.projAdd AcquireWasiSdk dependency for WASI runtime build.
src/coreclr/pal/src/thread/thread.cppWASI stack base/limit handling adjustments.
src/coreclr/pal/src/thread/context.cppDisable remote register capture on WASI.
src/coreclr/pal/src/map/map.cppWASI fcntl/dup workaround for file mappings.
src/coreclr/pal/src/include/pal/wasi/unistd.hWASI unistd wrapper with stubbed POSIX APIs.
src/coreclr/pal/src/include/pal/wasi/ucontext.hWASI ucontext stub header.
src/coreclr/pal/src/include/pal/wasi/sys/wait.hWASI sys/wait stub header.
src/coreclr/pal/src/include/pal/wasi/sys/vfs.hWASI sys/vfs stub header.
src/coreclr/pal/src/include/pal/wasi/sys/resource.hWASI sys/resource wrapper for rlimit.
src/coreclr/pal/src/include/pal/wasi/signal.hWASI signal wrapper defining missing POSIX types/APIs.
src/coreclr/pal/src/include/pal/wasi/pwd.hWASI pwd stub header.
src/coreclr/pal/src/include/pal/wasi/pthread.hWASI pthread wrapper for hidden decls.
src/coreclr/pal/src/include/pal/wasi/link.hWASI link stub for dl_iterate_phdr types.
src/coreclr/pal/src/include/pal/wasi/dlfcn.hWASI dlfcn wrapper for Dl_info/dladdr.
src/coreclr/pal/src/include/pal/context.hAdd WASI native_context_t fallback.
src/coreclr/pal/src/file/file.cppWASI fd duplication workaround for std handles/realpath.
src/coreclr/pal/src/exception/seh.cppDisable seh-unwind.cpp inclusion on WASI.
src/coreclr/pal/src/configure.cmakeMake pthread feature checks conditional; add WASI sched flags.
src/coreclr/pal/src/arch/wasm/stubs.cppEmscripten guards + WASI stubs for missing APIs.
src/coreclr/pal/src/arch/wasm/mmap-wasi.cNew WASI mmap/munmap implementation.
src/coreclr/pal/src/CMakeLists.txtWASI source selection + avoid unwind/dac pieces on wasm.
src/coreclr/hosts/corerun/dotenv.cppDisable corerun dotenv self-test on WASI.
src/coreclr/hosts/corerun/corerun.hppAvoid config.h for wasm; define minimal feature macros.
src/coreclr/hosts/corerun/corerun.cppDisable corerun self-test path on WASI.
src/coreclr/hosts/corerun/CMakeLists.txtWASI link options/libs; avoid dl/rdynamic on wasm.
src/coreclr/gc/unix/gcenv.unix.cppWASI mman define handling + available memory logic.
src/coreclr/gc/unix/events.cppAllow process-clock emulation define for waits.
src/coreclr/gc/unix/configure.cmakeGuard pthread checks on presence of pthread library.
src/coreclr/debug/debug-pal/unix/twowaypipe.cppWASI mkfifo stub for debug transport.
src/coreclr/debug/debug-pal/CMakeLists.txtDisable diagnostic server reference on WASI.
src/coreclr/clrfeatures.cmakeDisable event trace default for WASI.
src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csprojInclude Unix/browser interop stubs for WASI too.
src/coreclr/CMakeLists.txtWASI compile defs + include stub headers; skip tools/watchdog.
eng/pipelines/runtime.ymlAdd WASI CoreCLR build job template.
eng/pipelines/common/templates/wasi-coreclr-build.ymlNew pipeline template for WASI CoreCLR build-only job.
eng/native/functions.cmakeInstall rule adjustment for WASI wasm executables.
eng/native/configureplatform.cmakeTreat WASI as UNIX; set WASI target flags.
eng/native/configurecompiler.cmakeEmit TARGET_WASI compile definition.
eng/native/build-commons.shAvoid overriding CC/CXX for WASI builds.
eng/native.wasm.targetsImport AcquireWasiSdk + add WASI CoreCLR cmake args.
eng/Subsets.propsEnable CoreCLR subsets and defaults for WASI.
eng/AcquireWasiSdk.targetsUpdate runtime-local WASI SDK version.

Comment on lines +2360 to +2364
#elif defined(__EMSCRIPTEN__)
stackBase = (void*)emscripten_stack_get_base();
#endif // TARGET_BROWSER
#else // WASI
stackBase = __builtin_frame_address(0);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

In the WASI path, GetStackBase returns __builtin_frame_address(0), which is the current frame address, not the thread’s stack base. Components that rely on PAL_GetStackBase() for stack scanning/overflow checks will get an incorrect range. Consider using toolchain/linker-provided stack bound symbols (e.g., __stack_high/__stack_low or equivalent for wasi-sdk) or another reliable WASI-specific mechanism rather than the current frame address.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good catch. __builtin_frame_address(0) is an approximation — WASI/wasm32 doesn't expose linker-provided stack bound symbols like __stack_high for wasip2. Added a comment explaining this is an approximation. The value is cached on first call and used for the lifetime of the (single) thread, so it's close to the initial stack top.

Comment on lines +469 to +477
#ifdef TARGET_WASI
// WASI's fcntl may not support F_DUPFD_CLOEXEC at runtime.
// CLOEXEC is irrelevant on WASI (no exec). Use fd_fdstat_get + path_open
// workaround: just reuse the fd directly without duplication since
// WASI doesn't support fork/exec anyway.
UnixFd = pFileLocalData->unix_fd;
#else
UnixFd = fcntl(pFileLocalData->unix_fd, F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

On WASI, assigning UnixFd = pFileLocalData->unix_fd means the file mapping now owns the original file descriptor. FileMappingCleanupRoutine later close()s pLocalData->UnixFd, which will unexpectedly close the underlying handle used by hFile and any other users. Use a real dup operation on WASI (e.g., dup()/dup3() if available) or track ownership so the mapping cleanup doesn’t close a borrowed descriptor.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Valid concern. WASI's fcntl doesn't implement F_DUPFD_CLOEXEC (or F_DUPFD) at runtime despite the constant being defined at compile time, and dup() isn't available in wasip2 headers. The fd sharing is safe here because WASI is single-threaded with no fork/exec, so CLOEXEC is irrelevant and the file mapping lifetime matches the source handle. Updated the comment to explain this rationale.

Comment on lines 2438 to +2445
/* duplicate the FILE *, so that we can fclose() in FILECloseHandle without
closing the original */
#ifdef TARGET_WASI
// WASI: fcntl F_DUPFD_CLOEXEC not supported; reuse fd directly (single-threaded, no exec)
new_fd = fileno(stream);
#else
new_fd = fcntl(fileno(stream), F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
#endif

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

The WASI path sets new_fd = fileno(stream) instead of duplicating the descriptor. This makes the PAL file handle for stdin/stdout/stderr own and potentially close() the original stdio fd (e.g., on failure paths and when the handle is later closed), which can break subsequent I/O. Use an actual descriptor duplication mechanism on WASI, or ensure the PAL handle is marked non-owning so it won’t close the original fd.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Same situation as the map.cpp case — fcntl F_DUPFD* and dup() aren't available on wasip2. The stdio fds (0/1/2) are never closed by the PAL on WASI, so sharing is safe. Updated comment to explain the safety rationale.

<Project>
<PropertyGroup>
<_WasiSdkVersion>25.0</_WasiSdkVersion>
<_WasiSdkVersion>32.0</_WasiSdkVersion>

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

_WasiSdkVersion was updated to 32.0, but src/mono/wasi/build/WasiApp.targets still hard-codes an expected WASI SDK version of 25.0. This will cause Mono WASI builds (and any tooling that validates WASI_SDK_PATH/VERSION) to fail when using the runtime-local SDK acquired here. Please update the other expected-version checks (or centralize the version) so all WASI build paths agree on a single SDK version.

Suggested change
<_WasiSdkVersion>32.0</_WasiSdkVersion>
<WasiSdkVersionCondition="'$(WasiSdkVersion)' == ''">32.0</WasiSdkVersion>
<_WasiSdkVersion>$(WasiSdkVersion)</_WasiSdkVersion>

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

The Mono WASI build uses its own SDK acquisition path and isn't affected by this CoreCLR-specific version bump. We have a separate PR (dotnet/wasi-sdk#239) to update the dotnet fork of wasi-sdk to upstream v33 which will unify SDK versions across both runtimes.

Comment on lines +2415 to +2417
#else // WASI
stackLimit = (void*)sizeof(size_t);
#endif // TARGET_WASM

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

For WASI, GetStackLimit() sets stackLimit to (void*)sizeof(size_t), which is just a small constant cast to a pointer and not an actual stack boundary. This can break stack range calculations and stack overflow detection. Please compute a real lower stack bound for WASI (or, if unavailable, adjust callers to use SP-based scanning and return a clearly defined sentinel that won’t be treated as an address).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now computes stack limit as frame_address - 8MB (matching the -Wl,-z,stack-size=8388608 set at link time) with a floor at sizeof(size_t) to avoid wrapping to zero.

Comment on lines 485 to 516
@@ -509,6 +510,7 @@ BOOL CONTEXT_GetRegisters(DWORD processId, LPCONTEXT lpContext)
#undef ASSIGN_REG

CONTEXTFromNativeContext(&registers, lpContext, lpContext->ContextFlags);
#endif // !TARGET_WASI
}

bRet = TRUE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

When processId != GetCurrentProcessId() on WASI, the ucontext_t registers block is compiled out, but the function still falls through and returns TRUE without populating lpContext. This results in callers observing success with uninitialized/incorrect register state. Return FALSE (and set an appropriate last error) or explicitly zero/initialize lpContext similarly to the CONTEXT_GetThreadContext fallback path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now returns FALSE immediately for non-current process on WASI.

Comment on lines +31 to +34
};

static inline int dl_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data) { (void)callback; (void)data; return 0; }

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

This header uses size_t in the dl_iterate_phdr callback signature but doesn’t include a header that defines it. Add an include like <stddef.h> (or <sys/types.h>) to ensure size_t is available for both C and C++ compilation.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Added #include <stddef.h>.

#ifdef TARGET_WASI
extern "C" int pthread_getschedparam(pthread_t, int *policy, struct sched_param *param)
{
if (policy) *policy = 0;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

pthread_getschedparam returns success but leaves *param uninitialized. Callers expecting sched_param fields to be set may read garbage. Either populate *param with sensible defaults (e.g., zero priority) or return an error code (e.g., ENOTSUP) to signal the API isn’t implemented on WASI.

Suggested change
if (policy) *policy = 0;
if (policy) *policy = 0;
if (param) *param = sched_param{};

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Fixed. Now zero-initializes *param with memset.

Comment on lines +21 to +58
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#define ANON_ARENA_SIZE (256 * 1024 * 1024)

static char *s_arena_base;
static size_t s_arena_offset;

static void ensure_arena(void) {
if (s_arena_base == NULL) {
s_arena_base = (char *)malloc(ANON_ARENA_SIZE);
if (s_arena_base == NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset = 0;
}
}

// Allocate from the bump arena with alignment. Never reuses freed addresses.
static void *arena_alloc(size_t length, size_t align) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_t base = (uintptr_t)s_arena_base;
uintptr_t current = base + s_arena_offset;
uintptr_t aligned_addr = (current + align - 1) & ~(align - 1);
size_t aligned_offset = aligned_addr - base;
if (aligned_offset + length > ANON_ARENA_SIZE) {
return NULL;
}
void *ptr = (void *)aligned_addr;
s_arena_offset = aligned_offset + length;
return ptr;
}

static int is_arena_ptr(void *addr) {
if (s_arena_base == NULL) return 0;
return (char *)addr >= s_arena_base &&
(char *)addr < s_arena_base + ANON_ARENA_SIZE;

CopilotAIApr 15, 2026

Copy link

Choose a reason for hiding this comment

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

ensure_arena() allocates a fixed 256MB arena eagerly via malloc(ANON_ARENA_SIZE). On wasm32/WASI this directly consumes linear memory and can cause large, immediate memory growth or OOM even for small apps. Consider allocating the arena lazily in smaller chunks (or growing it) and/or sizing it based on configured limits rather than a hard-coded 256MB constant.

Suggested change
// 256MB arena — sufficient for GC heap + runtime structures.
// On WASM this is virtual (backed by memory.grow on demand).
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
staticchar*s_arena_base;
staticsize_ts_arena_offset;
staticvoidensure_arena(void) {
if (s_arena_base==NULL) {
s_arena_base= (char*)malloc(ANON_ARENA_SIZE);
if (s_arena_base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %d byte arena\n", ANON_ARENA_SIZE);
abort();
}
s_arena_offset=0;
}
}
// Allocate from the bump arena with alignment. Never reuses freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
ensure_arena();
// Align the absolute address, not just the offset
uintptr_tbase= (uintptr_t)s_arena_base;
uintptr_tcurrent=base+s_arena_offset;
uintptr_taligned_addr= (current+align-1) & ~(align-1);
size_taligned_offset=aligned_addr-base;
if (aligned_offset+length>ANON_ARENA_SIZE) {
returnNULL;
}
void*ptr= (void*)aligned_addr;
s_arena_offset=aligned_offset+length;
returnptr;
}
staticintis_arena_ptr(void*addr) {
if (s_arena_base==NULL) return0;
return (char*)addr >= s_arena_base&&
(char*)addr<s_arena_base+ANON_ARENA_SIZE;
// Cap anonymous mappings at 256MB total, but allocate backing storage lazily
// in small chunks so small WASI apps do not eagerly consume large amounts of
// linear memory on the first anonymous mapping.
#defineANON_ARENA_SIZE (256 * 1024 * 1024)
#defineANON_ARENA_CHUNK_SIZE (64 * 1024)
#defineANON_ARENA_MAX_CHUNKS (ANON_ARENA_SIZE / ANON_ARENA_CHUNK_SIZE)
typedefstruct
{
char*base;
size_tsize;
size_toffset;
} anon_arena_chunk;
staticanon_arena_chunks_arena_chunks[ANON_ARENA_MAX_CHUNKS];
staticsize_ts_arena_chunk_count;
staticsize_ts_arena_total_size;
staticvoidensure_arena(void) {
// Chunks are allocated lazily by arena_alloc().
}
staticanon_arena_chunk*arena_alloc_chunk(size_tmin_length, size_talign) {
size_tchunk_size;
anon_arena_chunk*chunk;
if (s_arena_chunk_count >= ANON_ARENA_MAX_CHUNKS) {
returnNULL;
}
chunk_size=ANON_ARENA_CHUNK_SIZE;
if (min_length>chunk_size||align>1) {
size_trequired=min_length+align-1;
if (required>chunk_size) {
chunk_size=required;
}
}
if (chunk_size>ANON_ARENA_SIZE-s_arena_total_size) {
returnNULL;
}
chunk=&s_arena_chunks[s_arena_chunk_count];
chunk->base= (char*)malloc(chunk_size);
if (chunk->base==NULL) {
fprintf(stderr, "WASI mmap: failed to allocate %zu byte arena chunk\n", chunk_size);
abort();
}
chunk->size=chunk_size;
chunk->offset=0;
s_arena_chunk_count++;
s_arena_total_size+=chunk_size;
returnchunk;
}
// Allocate from lazily created bump arena chunks with alignment. Never reuses
// freed addresses.
staticvoid*arena_alloc(size_tlength, size_talign) {
anon_arena_chunk*chunk;
uintptr_tbase;
uintptr_tcurrent;
uintptr_taligned_addr;
size_taligned_offset;
ensure_arena();
if (align==0) {
align=1;
}
if (s_arena_chunk_count!=0) {
chunk=&s_arena_chunks[s_arena_chunk_count-1];
base= (uintptr_t)chunk->base;
current=base+chunk->offset;
aligned_addr= (current+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length <= chunk->size) {
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
}
chunk=arena_alloc_chunk(length, align);
if (chunk==NULL) {
returnNULL;
}
base= (uintptr_t)chunk->base;
aligned_addr= (base+align-1) & ~(align-1);
aligned_offset=aligned_addr-base;
if (aligned_offset+length>chunk->size) {
returnNULL;
}
chunk->offset=aligned_offset+length;
return (void*)aligned_addr;
}
staticintis_arena_ptr(void*addr) {
size_ti;
for (i=0; i<s_arena_chunk_count; i++) {
char*base=s_arena_chunks[i].base;
if ((char*)addr >= base&& (char*)addr<base+s_arena_chunks[i].size) {
return1;
}
}
return0;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Good suggestion. On wasm32, malloc of 256MB doesn't immediately consume physical memory — it triggers memory.grow lazily as pages are touched. The arena is bumped incrementally so only the used portion is backed by real pages. That said, a lazy chunked approach would be cleaner and we'll consider it as a follow-up optimization.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Note

This response was generated with assistance from GitHub Copilot.

Correction to my previous reply: wasm memory.grow commits pages immediately — there's no lazy page fault mechanism in wasm linear memory. So the 256MB malloc does grow linear memory upfront. A chunked approach that grows the arena in smaller increments as needed would save real memory for smaller apps. Filed as a follow-up improvement.

- context.cpp: Return FALSE for non-current process on WASI instead of
falling through with uninitialized context
- thread.cpp: Improve GetStackBase/GetStackLimit comments; compute stack
limit from frame address minus configured stack size (8MB)
- file.cpp, map.cpp: Improve fd-sharing comments explaining why reuse is
safe on WASI (no fork/exec, no CLOEXEC needed)
- link.h: Add missing stddef.h include for size_t
- stubs.cpp: Zero-initialize sched_param in pthread_getschedparam stub
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lewinglewing reopened this Apr 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-PAL-coreclronly for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lewing