Conversation
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>
There was a problem hiding this comment.
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
| File | Description |
|---|---|
| src/native/libs/build-native.proj | Removes standalone WASI SDK import; relies on shared wasm targets. |
| src/native/libs/System.Native/pal_networking.c | Minor variable initialization tweak. |
| src/native/libs/System.Globalization.Native/pal_icushim_static.c | ICU status init and malloc cast cleanup. |
| src/coreclr/vm/wasm/cgencpu.h | Guard Emscripten stack include; WASI fallback SP logic. |
| src/coreclr/vm/prestub.cpp | Refine interpreter ETW handling with portable entrypoints. |
| src/coreclr/vm/finalizerthread.cpp | WASI synchronous finalization path under wasm. |
| src/coreclr/utilcode/clrhost.cpp | WASI module path derived from CORE_ROOT. |
| src/coreclr/tools/CMakeLists.txt | Skip superpmi for WASI/browser builds. |
| src/coreclr/runtime.proj | Add AcquireWasiSdk dependency for WASI runtime build. |
| src/coreclr/pal/src/thread/thread.cpp | WASI stack base/limit handling adjustments. |
| src/coreclr/pal/src/thread/context.cpp | Disable remote register capture on WASI. |
| src/coreclr/pal/src/map/map.cpp | WASI fcntl/dup workaround for file mappings. |
| src/coreclr/pal/src/include/pal/wasi/unistd.h | WASI unistd wrapper with stubbed POSIX APIs. |
| src/coreclr/pal/src/include/pal/wasi/ucontext.h | WASI ucontext stub header. |
| src/coreclr/pal/src/include/pal/wasi/sys/wait.h | WASI sys/wait stub header. |
| src/coreclr/pal/src/include/pal/wasi/sys/vfs.h | WASI sys/vfs stub header. |
| src/coreclr/pal/src/include/pal/wasi/sys/resource.h | WASI sys/resource wrapper for rlimit. |
| src/coreclr/pal/src/include/pal/wasi/signal.h | WASI signal wrapper defining missing POSIX types/APIs. |
| src/coreclr/pal/src/include/pal/wasi/pwd.h | WASI pwd stub header. |
| src/coreclr/pal/src/include/pal/wasi/pthread.h | WASI pthread wrapper for hidden decls. |
| src/coreclr/pal/src/include/pal/wasi/link.h | WASI link stub for dl_iterate_phdr types. |
| src/coreclr/pal/src/include/pal/wasi/dlfcn.h | WASI dlfcn wrapper for Dl_info/dladdr. |
| src/coreclr/pal/src/include/pal/context.h | Add WASI native_context_t fallback. |
| src/coreclr/pal/src/file/file.cpp | WASI fd duplication workaround for std handles/realpath. |
| src/coreclr/pal/src/exception/seh.cpp | Disable seh-unwind.cpp inclusion on WASI. |
| src/coreclr/pal/src/configure.cmake | Make pthread feature checks conditional; add WASI sched flags. |
| src/coreclr/pal/src/arch/wasm/stubs.cpp | Emscripten guards + WASI stubs for missing APIs. |
| src/coreclr/pal/src/arch/wasm/mmap-wasi.c | New WASI mmap/munmap implementation. |
| src/coreclr/pal/src/CMakeLists.txt | WASI source selection + avoid unwind/dac pieces on wasm. |
| src/coreclr/hosts/corerun/dotenv.cpp | Disable corerun dotenv self-test on WASI. |
| src/coreclr/hosts/corerun/corerun.hpp | Avoid config.h for wasm; define minimal feature macros. |
| src/coreclr/hosts/corerun/corerun.cpp | Disable corerun self-test path on WASI. |
| src/coreclr/hosts/corerun/CMakeLists.txt | WASI link options/libs; avoid dl/rdynamic on wasm. |
| src/coreclr/gc/unix/gcenv.unix.cpp | WASI mman define handling + available memory logic. |
| src/coreclr/gc/unix/events.cpp | Allow process-clock emulation define for waits. |
| src/coreclr/gc/unix/configure.cmake | Guard pthread checks on presence of pthread library. |
| src/coreclr/debug/debug-pal/unix/twowaypipe.cpp | WASI mkfifo stub for debug transport. |
| src/coreclr/debug/debug-pal/CMakeLists.txt | Disable diagnostic server reference on WASI. |
| src/coreclr/clrfeatures.cmake | Disable event trace default for WASI. |
| src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj | Include Unix/browser interop stubs for WASI too. |
| src/coreclr/CMakeLists.txt | WASI compile defs + include stub headers; skip tools/watchdog. |
| eng/pipelines/runtime.yml | Add WASI CoreCLR build job template. |
| eng/pipelines/common/templates/wasi-coreclr-build.yml | New pipeline template for WASI CoreCLR build-only job. |
| eng/native/functions.cmake | Install rule adjustment for WASI wasm executables. |
| eng/native/configureplatform.cmake | Treat WASI as UNIX; set WASI target flags. |
| eng/native/configurecompiler.cmake | Emit TARGET_WASI compile definition. |
| eng/native/build-commons.sh | Avoid overriding CC/CXX for WASI builds. |
| eng/native.wasm.targets | Import AcquireWasiSdk + add WASI CoreCLR cmake args. |
| eng/Subsets.props | Enable CoreCLR subsets and defaults for WASI. |
| eng/AcquireWasiSdk.targets | Update runtime-local WASI SDK version. |
| #elif defined(__EMSCRIPTEN__) | ||
| stackBase = (void*)emscripten_stack_get_base(); | ||
| #endif // TARGET_BROWSER | ||
| #else // WASI | ||
| stackBase = __builtin_frame_address(0); | ||
| #endif // TARGET_WASM |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| #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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| /* 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
_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.
| <_WasiSdkVersion>32.0</_WasiSdkVersion> | |
| <WasiSdkVersion Condition="'$(WasiSdkVersion)' == ''">32.0</WasiSdkVersion> | |
| <_WasiSdkVersion>$(WasiSdkVersion)</_WasiSdkVersion> |
There was a problem hiding this comment.
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.
| #else // WASI | ||
| stackLimit = (void*)sizeof(size_t); | ||
| #endif // TARGET_WASM |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
| @@ -509,6 +510,7 @@ BOOL CONTEXT_GetRegisters(DWORD processId, LPCONTEXT lpContext) | |||
| #undef ASSIGN_REG | |||
|
|
|||
| CONTEXTFromNativeContext(®isters, lpContext, lpContext->ContextFlags); | |||
| #endif // !TARGET_WASI | |||
| } | |||
|
|
|||
| bRet = TRUE; | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Note
This response was generated with assistance from GitHub Copilot.
Fixed. Now returns FALSE immediately for non-current process on WASI.
| }; | ||
|
|
||
| static inline int dl_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data) { (void)callback; (void)data; return 0; } | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
| if (policy) *policy = 0; | |
| if (policy) *policy = 0; | |
| if (param) *param = sched_param{}; |
There was a problem hiding this comment.
Note
This response was generated with assistance from GitHub Copilot.
Fixed. Now zero-initializes *param with memset.
| // 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; |
There was a problem hiding this comment.
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.
| // 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; | |
| // 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. | |
| #define ANON_ARENA_SIZE (256 * 1024 * 1024) | |
| #define ANON_ARENA_CHUNK_SIZE (64 * 1024) | |
| #define ANON_ARENA_MAX_CHUNKS (ANON_ARENA_SIZE / ANON_ARENA_CHUNK_SIZE) | |
| typedef struct | |
| { | |
| char *base; | |
| size_t size; | |
| size_t offset; | |
| } anon_arena_chunk; | |
| static anon_arena_chunk s_arena_chunks[ANON_ARENA_MAX_CHUNKS]; | |
| static size_t s_arena_chunk_count; | |
| static size_t s_arena_total_size; | |
| static void ensure_arena(void) { | |
| // Chunks are allocated lazily by arena_alloc(). | |
| } | |
| static anon_arena_chunk *arena_alloc_chunk(size_t min_length, size_t align) { | |
| size_t chunk_size; | |
| anon_arena_chunk *chunk; | |
| if (s_arena_chunk_count >= ANON_ARENA_MAX_CHUNKS) { | |
| return NULL; | |
| } | |
| chunk_size = ANON_ARENA_CHUNK_SIZE; | |
| if (min_length > chunk_size || align > 1) { | |
| size_t required = min_length + align - 1; | |
| if (required > chunk_size) { | |
| chunk_size = required; | |
| } | |
| } | |
| if (chunk_size > ANON_ARENA_SIZE - s_arena_total_size) { | |
| return NULL; | |
| } | |
| 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; | |
| return chunk; | |
| } | |
| // Allocate from lazily created bump arena chunks with alignment. Never reuses | |
| // freed addresses. | |
| static void *arena_alloc(size_t length, size_t align) { | |
| anon_arena_chunk *chunk; | |
| uintptr_t base; | |
| uintptr_t current; | |
| uintptr_t aligned_addr; | |
| size_t aligned_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) { | |
| return NULL; | |
| } | |
| base = (uintptr_t)chunk->base; | |
| aligned_addr = (base + align - 1) & ~(align - 1); | |
| aligned_offset = aligned_addr - base; | |
| if (aligned_offset + length > chunk->size) { | |
| return NULL; | |
| } | |
| chunk->offset = aligned_offset + length; | |
| return (void *)aligned_addr; | |
| } | |
| static int is_arena_ptr(void *addr) { | |
| size_t i; | |
| 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) { | |
| return 1; | |
| } | |
| } | |
| return 0; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
No description provided.