From 2010bf5dd9156f24035a421418bb4a6797184960 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 27 Apr 2026 18:14:41 +0200 Subject: [PATCH 01/36] rebase + feedback --- src/coreclr/gc/CMakeLists.txt | 6 +- src/coreclr/gc/init.cpp | 26 +- src/coreclr/gc/unix/gcenv.unix.cpp | 27 +- src/coreclr/gc/wasm/CMakeLists.txt | 10 + src/coreclr/gc/wasm/gcenv.cpp | 549 +++++++++++++++++++++++++++ src/coreclr/pal/src/map/virtual.cpp | 105 ++++- src/coreclr/pal/src/misc/sysinfo.cpp | 3 +- src/native/minipal/wasm.h | 25 ++ 8 files changed, 716 insertions(+), 35 deletions(-) create mode 100644 src/coreclr/gc/wasm/CMakeLists.txt create mode 100644 src/coreclr/gc/wasm/gcenv.cpp create mode 100644 src/native/minipal/wasm.h diff --git a/src/coreclr/gc/CMakeLists.txt b/src/coreclr/gc/CMakeLists.txt index e28391221f66ca..9b48e6ddd4ded6 100644 --- a/src/coreclr/gc/CMakeLists.txt +++ b/src/coreclr/gc/CMakeLists.txt @@ -25,7 +25,9 @@ set(GC_SOURCES gcbridge.cpp handletablecache.cpp) -if(CLR_CMAKE_HOST_UNIX) +if(CLR_CMAKE_TARGET_ARCH_WASM) + add_subdirectory(wasm) +elseif(CLR_CMAKE_HOST_UNIX) add_subdirectory(unix) include(unix/configure.cmake) else() @@ -33,7 +35,7 @@ else() set (GC_SOURCES ${GC_SOURCES} windows/Native.rc) -endif(CLR_CMAKE_HOST_UNIX) +endif() if (CLR_CMAKE_TARGET_ARCH_ARM64 OR CLR_CMAKE_TARGET_ARCH_AMD64) add_subdirectory(vxsort) diff --git a/src/coreclr/gc/init.cpp b/src/coreclr/gc/init.cpp index ccf0b35b3d312c..d47284f228ebfb 100644 --- a/src/coreclr/gc/init.cpp +++ b/src/coreclr/gc/init.cpp @@ -914,10 +914,11 @@ HRESULT gc_heap::initialize_gc (size_t soh_segment_size, return E_OUTOFMEMORY; if (use_large_pages_p) { -#ifndef HOST_64BIT +#if !defined(HOST_64BIT) && !defined(HOST_WASM) // Large pages are not supported on 32bit + // except WASM, which uses the large-pages code path to skip decommit assert (false); -#endif //!HOST_64BIT +#endif //!HOST_64BIT && !HOST_WASM if (heap_hard_limit_oh[soh]) { @@ -1280,6 +1281,10 @@ bool gc_heap::compute_hard_limit() #ifdef HOST_64BIT use_large_pages_p = GCConfig::GetGCLargePages(); +#elif defined(HOST_WASM) + // On WASM, reserve == commit (posix_memalign allocates real memory) and there is + // no way to decommit. Enabling the large-pages path makes the GC skip VirtualDecommit. + use_large_pages_p = true; #endif //HOST_64BIT if (heap_hard_limit_oh[soh] || heap_hard_limit_oh[loh] || heap_hard_limit_oh[poh]) @@ -1368,9 +1373,11 @@ bool gc_heap::compute_hard_limit() bool gc_heap::compute_memory_settings(bool is_initialization, uint32_t& nhp, uint32_t nhp_from_config, size_t& seg_size_from_config, size_t new_current_total_committed) { -#ifdef HOST_64BIT +#if defined(HOST_64BIT) || defined(HOST_WASM) // If the hard limit is specified, the user is saying even if the process is already // running in a container, use this limit for the GC heap. + // On WASM, the linear memory has a hard ceiling set in the .wasm file, enforced by + // the engine — semantically equivalent to a container memory limit. if (!hard_limit_config_p) { if (is_restricted_physical_mem) @@ -1387,7 +1394,7 @@ bool gc_heap::compute_memory_settings(bool is_initialization, uint32_t& nhp, uin } } } -#endif //HOST_64BIT +#endif //HOST_64BIT || HOST_WASM if (heap_hard_limit && (heap_hard_limit < new_current_total_committed)) { @@ -1431,6 +1438,17 @@ bool gc_heap::compute_memory_settings(bool is_initialization, uint32_t& nhp, uin // 0 <= soh_segment_size <= 1Gb size_t limit_to_check = (heap_hard_limit_oh[soh] ? heap_hard_limit_oh[soh] : heap_hard_limit); soh_segment_size = max (adjust_segment_size_hard_limit (limit_to_check, nhp), seg_size_from_config); +#ifdef HOST_WASM + // On WASM, VirtualReserve allocates real memory (no virtual memory). + // Cap segment size so all 3 initial segments (SOH + LOH + POH) fit within + // the hard limit with room to grow. On 32-bit without per-OH limits, + // LOH and POH segments equal soh_segment_size, so total = 3 * soh. + { + size_t max_seg = round_down_power2 (heap_hard_limit / (3 * 2 * nhp)); + max_seg = max (max_seg, (size_t)(1024 * 1024)); + soh_segment_size = min (soh_segment_size, max_seg); + } +#endif //HOST_WASM } else { diff --git a/src/coreclr/gc/unix/gcenv.unix.cpp b/src/coreclr/gc/unix/gcenv.unix.cpp index 42b73e0611241a..126483bde3ec96 100644 --- a/src/coreclr/gc/unix/gcenv.unix.cpp +++ b/src/coreclr/gc/unix/gcenv.unix.cpp @@ -108,11 +108,6 @@ typedef cpuset_t cpu_set_t; #define SYSCONF_GET_NUMPROCS _SC_NPROCESSORS_ONLN #endif -#ifdef __EMSCRIPTEN__ -#include -#endif // __EMSCRIPTEN__ - - // The cached total number of CPUs that can be used in the OS. uint32_t g_totalCpuCount = 0; @@ -347,7 +342,7 @@ void GCToOSInterface::Sleep(uint32_t sleepMSec) requested.tv_nsec = (sleepMSec - requested.tv_sec * tccSecondsToMilliSeconds) * tccMilliSecondsToNanoSeconds; timespec remaining; - while (nanosleep(&requested, &remaining) == EINTR) + while (nanosleep(&requested, &remaining) == -1 && errno == EINTR) { requested = remaining; } @@ -405,7 +400,7 @@ static void* VirtualReserveInner(size_t size, size_t alignment, uint32_t flags, } pRetVal = pAlignedRetVal; -#if defined(MADV_DONTDUMP) && !defined(TARGET_WASM) +#if defined(MADV_DONTDUMP) // Do not include reserved uncommitted memory in coredump. if (!committing) { @@ -453,13 +448,9 @@ bool GCToOSInterface::VirtualRelease(void* address, size_t size) // true if it has succeeded, false if it has failed static bool VirtualCommitInner(void* address, size_t size, uint16_t node, bool newMemory) { -#ifndef TARGET_WASM bool success = mprotect(address, size, PROT_WRITE | PROT_READ) == 0; -#else - bool success = true; -#endif // !TARGET_WASM -#if defined(MADV_DONTDUMP) && !defined(TARGET_WASM) +#if defined(MADV_DONTDUMP) if (success && !newMemory) { // Include committed memory in coredump. New memory is included by default. @@ -544,13 +535,13 @@ bool GCToOSInterface::VirtualDecommit(void* address, size_t size) #endif bool bRetVal = mmap(address, size, PROT_NONE, mmapFlags, -1, 0) != MAP_FAILED; -#if defined(MADV_DONTDUMP) && !defined(TARGET_WASM) +#if defined(MADV_DONTDUMP) if (bRetVal) { // Do not include freed memory in coredump. madvise(address, size, MADV_DONTDUMP); } -#endif // defined(MADV_DONTDUMP) && !defined(TARGET_WASM) +#endif // defined(MADV_DONTDUMP) return bRetVal; } @@ -565,9 +556,6 @@ bool GCToOSInterface::VirtualDecommit(void* address, size_t size) // true if it has succeeded, false if it has failed bool GCToOSInterface::VirtualReset(void * address, size_t size, bool unlock) { -#ifdef TARGET_WASM - return true; -#else // !TARGET_WASM int st = EINVAL; #ifdef MADV_DONTDUMP @@ -586,7 +574,6 @@ bool GCToOSInterface::VirtualReset(void * address, size_t size, bool unlock) #endif // MADV_FREE return (st == 0); -#endif // !TARGET_WASM } // Check if the OS supports write watching @@ -836,7 +823,7 @@ static uint64_t GetMemorySizeMultiplier(char units) return 1; } -#if !defined(__APPLE__) && !defined(__HAIKU__) && !defined(__EMSCRIPTEN__) +#if !defined(__APPLE__) && !defined(__HAIKU__) // Try to read the MemAvailable entry from /proc/meminfo. // Return true if the /proc/meminfo existed, the entry was present and we were able to parse it. static bool ReadMemAvailable(uint64_t* memAvailable) @@ -1104,8 +1091,6 @@ uint64_t GetAvailablePhysicalMemory() { available = info.free_memory; } -#elif defined(__EMSCRIPTEN__) - available = emscripten_get_heap_max() - emscripten_get_heap_size(); #else // Linux static volatile bool tryReadMemInfo = true; diff --git a/src/coreclr/gc/wasm/CMakeLists.txt b/src/coreclr/gc/wasm/CMakeLists.txt new file mode 100644 index 00000000000000..f394f677de1ac8 --- /dev/null +++ b/src/coreclr/gc/wasm/CMakeLists.txt @@ -0,0 +1,10 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) +include_directories("../env") +include_directories("..") +include_directories("../unix") + +set(GC_PAL_SOURCES + gcenv.cpp + ../unix/events.cpp) + +add_library(gc_pal OBJECT ${GC_PAL_SOURCES} ${VERSION_FILE_PATH}) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp new file mode 100644 index 00000000000000..a09780f44bba63 --- /dev/null +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -0,0 +1,549 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// WASM-specific GC OS interface implementation. +// Replaces gcenv.unix.cpp when targeting WebAssembly (browser or WASI). + +#include +#include +#include +#include +#include + +#include "config.gc.h" +#include "common.h" + +#include "gcenv.structs.h" +#include "gcenv.base.h" +#include "gcenv.os.h" +#include "gcenv.ee.h" +#include "gcenv.unix.inl" +#include "gcconfig.h" + +#include + +#ifdef FEATURE_MULTITHREADING +#include +#include +#include +#endif + +#include +#include +#include + +#include "globals.h" + +#ifdef TARGET_BROWSER +#include +#endif + +// WASM memory.grow operates in 64KB pages. This is distinct from OS_PAGE_SIZE +// (the GC's page granularity), which we set to 16KB below. +static const size_t WasmPageSize = 64 * 1024; + +// The cached total number of CPUs that can be used in the OS. +// WASM is single-threaded, so this is always 1. +uint32_t g_totalCpuCount = 0; + +uint32_t g_pageSizeUnixInl = 0; + +AffinitySet g_processAffinitySet; + +// NUMA globals - WASM has no NUMA support but these are referenced by the GC. +extern "C" int g_highestNumaNode = 0; +extern "C" bool g_numaAvailable = false; + +static size_t g_RestrictedPhysicalMemoryLimit = 0; + +static int64_t g_totalPhysicalMemSize = 0; + +// Forward declarations +size_t GetRestrictedPhysicalMemoryLimit(); +bool GetPhysicalMemoryUsed(size_t* val); + +// ============================================================================ +// Initialization / Shutdown +// ============================================================================ + +bool GCToOSInterface::Initialize() +{ + g_pageSizeUnixInl = minipal_getpagesize(); + + // WASM is single-threaded + g_totalCpuCount = 1; + + if (!g_processAffinitySet.Initialize(1)) + { + return false; + } + + g_processAffinitySet.Add(0); + + // Get the physical memory size +#ifdef TARGET_BROWSER + g_totalPhysicalMemSize = (int64_t)emscripten_get_heap_max(); +#else // TARGET_WASI + // WASI doesn't have an API to query max memory. + g_totalPhysicalMemSize = 2LL * 1024 * 1024 * 1024; // 2GB +#endif + + assert(g_totalPhysicalMemSize != 0); + + return true; +} + +void GCToOSInterface::Shutdown() +{ +} + +// ============================================================================ +// Thread / Process identification +// ============================================================================ + +uint64_t GCToOSInterface::GetCurrentThreadIdForLogging() +{ + return (uint64_t)minipal_get_current_thread_id(); +} + +uint32_t GCToOSInterface::GetCurrentProcessId() +{ + return getpid(); +} + +bool GCToOSInterface::SetCurrentThreadIdealAffinity(uint16_t srcProcNo, uint16_t dstProcNo) +{ + (void)srcProcNo; + (void)dstProcNo; + return true; +} + +uint32_t GCToOSInterface::GetCurrentProcessorNumber() +{ + return 0; +} + +bool GCToOSInterface::CanGetCurrentProcessorNumber() +{ + return true; +} + +// ============================================================================ +// Debugging / Sleeping / Yielding +// ============================================================================ + +void GCToOSInterface::DebugBreak() +{ +#if __has_builtin(__builtin_debugtrap) + __builtin_debugtrap(); +#else + abort(); +#endif +} + +void GCToOSInterface::Sleep(uint32_t sleepMSec) +{ +#ifdef FEATURE_MULTITHREADING + timespec requested = + { + static_cast(sleepMSec / 1000), + static_cast((sleepMSec % 1000) * 1000000) + }; + + while (nanosleep(&requested, &requested) != 0 && errno == EINTR) + { + } +#else + // On single-threaded WASM, nanosleep is either a no-op or stalls the + // event loop. There are no other threads to wait for, and no signals + // to deliver EINTR. + (void)sleepMSec; +#endif +} + +void GCToOSInterface::YieldThread(uint32_t switchCount) +{ +#ifdef FEATURE_MULTITHREADING + (void)switchCount; + sched_yield(); +#else + // No-op on single-threaded WASM — there are no other threads to yield to. + (void)switchCount; +#endif +} + +// ============================================================================ +// Virtual Memory — WASM-specific (posix_memalign / free) +// ============================================================================ + +// Emscripten does not provide a complete implementation of mmap and munmap: +// munmap cannot unmap partial allocations, mmap(PROT_NONE) still consumes +// linear memory, and MAP_FIXED is broken. +// Emscripten does provide an implementation of posix_memalign which is used here. + +// sbrk optimization: posix_memalign (dlmemalign) obtains memory in one of two ways: +// 1. Recycling a previously free()'d block from the allocator's free list. +// 2. Growing the WASM linear memory via sbrk() → memory.grow. +// +// The WebAssembly spec guarantees that memory.grow zero-initializes new pages, +// so freshly grown memory does not need an explicit memset. Only recycled blocks +// (which may contain stale data from a previous VirtualDecommit→free cycle) must +// be zeroed. +// +// We detect which case occurred by recording sbrk(0) (the current program break) +// before the allocation. If posix_memalign returns a pointer at or above the old +// break, the memory came from heap growth and is already zero. If it falls below, +// it was recycled and must be explicitly zeroed. +// +// This is safe because WASM is single-threaded — no concurrent sbrk calls can +// occur between our sbrk(0) probe and the posix_memalign call. This is the same +// approach used by Mono's WASM mmap implementation (mono-mmap-wasm.c). + +static void* VirtualReserveInner(size_t size, size_t alignment, uint32_t flags) +{ + assert(!(flags & VirtualReserveFlags::WriteWatch) && "WriteWatch not supported on WASM"); + if (alignment < OS_PAGE_SIZE) + { + alignment = OS_PAGE_SIZE; + } + +#ifndef FEATURE_MULTITHREADING + // Capture the program break before allocation to detect heap growth vs recycling. + void* old_brk = sbrk(0); +#endif + + void* pRetVal; + int result = posix_memalign(&pRetVal, alignment, size); + if (result != 0) + { + return nullptr; + } + +#ifndef FEATURE_MULTITHREADING + // Only zero recycled memory. Fresh memory from heap growth (memory.grow) is + // guaranteed to be zero by the WebAssembly spec. + if (pRetVal < old_brk) + { + memset(pRetVal, 0, size); + } +#else + // The sbrk optimization is not safe with multiple threads — another thread + // could call sbrk between our probe and posix_memalign, giving a false + // "fresh memory" result. Fall back to always zeroing. + memset(pRetVal, 0, size); +#endif + + return pRetVal; +} + +void* GCToOSInterface::VirtualReserve(size_t size, size_t alignment, uint32_t flags, uint16_t node) +{ + return VirtualReserveInner(size, alignment, flags); +} + +bool GCToOSInterface::VirtualRelease(void* address, size_t size) +{ + free(address); + return true; +} + +void* GCToOSInterface::VirtualReserveAndCommitLargePages(size_t size, uint16_t node) +{ + // WASM has no large pages — just reserve+commit normally. + return VirtualReserveInner(size, OS_PAGE_SIZE, 0); +} + +bool GCToOSInterface::VirtualCommit(void* address, size_t size, uint16_t node) +{ + // The GC skips this for heap memory when use_large_pages_p is true (which + // it always is on WASM). This is still called for bookkeeping memory. + // If previously decommitted (sentinel at page boundary), zero the range. +#ifdef FEATURE_MULTITHREADING + // Under MT, VirtualDecommit already zeroes the full range on decommit, and + // VirtualReserveInner zeroes on allocation — so commit is a no-op. + (void)address; + (void)size; +#else + if (size && *(uint8_t*)address != 0) + { + memset(address, 0, size); + } +#endif + return true; +} + +bool GCToOSInterface::VirtualDecommit(void* address, size_t size) +{ + // The GC skips this for heap memory when use_large_pages_p is true (which + // it always is on WASM). This is still called for bookkeeping memory. + // On WASM, we cannot return memory to the OS or change page protection. +#ifdef FEATURE_MULTITHREADING + // Under MT, VirtualCommit always zeroes unconditionally, so we just zero + // here immediately — no sentinel trick needed, and no races to worry about. + memset(address, 0, size); +#else + // Instead of zeroing the entire range here (expensive), write a non-zero + // sentinel at each page boundary. VirtualCommit checks the first byte to + // decide whether a zeroing pass is needed. Writing every page guarantees + // the sentinel is visible even if a sub-range is recommitted at an offset. + for (size_t offset = 0; offset < size; offset += OS_PAGE_SIZE) + { + *((uint8_t*)address + offset) = 1; + } +#endif + return true; +} + +bool GCToOSInterface::VirtualReset(void* address, size_t size, bool unlock) +{ + // Return false to indicate reset is not supported. + // This forces the GC to use the decommit+commit fallback path instead: + // VirtualDecommit marks the range with sentinels, and VirtualCommit + // performs the actual zeroing pass when the range is recommitted. + // That behavior is more correct on WASM, where madvise is a no-op. + return false; +} + +// ============================================================================ +// Write Watch (not supported on WASM) +// ============================================================================ + +bool GCToOSInterface::SupportsWriteWatch() +{ + return false; +} + +void GCToOSInterface::ResetWriteWatch(void* address, size_t size) +{ + assert(!"should never call ResetWriteWatch on WASM"); +} + +bool GCToOSInterface::GetWriteWatch(bool resetState, void* address, size_t size, void** pageAddresses, uintptr_t* pageAddressesCount) +{ + assert(!"should never call GetWriteWatch on WASM"); + return false; +} + +// ============================================================================ +// Processor cache +// ============================================================================ + +size_t GCToOSInterface::GetCacheSizePerLogicalCpu(bool trueSize) +{ + // WASM doesn't expose cache topology. + // Return a reasonable default (256 KB). + return 256 * 1024; +} + +// ============================================================================ +// Thread affinity / priority +// ============================================================================ + +bool GCToOSInterface::SetThreadAffinity(uint16_t procNo) +{ + // No thread affinity on WASM + return false; +} + +bool GCToOSInterface::BoostThreadPriority() +{ + // No thread priority on WASM + return false; +} + +const AffinitySet* GCToOSInterface::SetGCThreadsAffinitySet(uintptr_t configAffinityMask, const AffinitySet* configAffinitySet) +{ + (void)configAffinityMask; + (void)configAffinitySet; + return &g_processAffinitySet; +} + +// ============================================================================ +// Virtual / Physical Memory Limits +// ============================================================================ + +static uint64_t GetTotalPhysicalMemory() +{ +#ifdef TARGET_BROWSER + return emscripten_get_heap_max(); +#else // TARGET_WASI + // WASI doesn't have an API to query max memory. + return 2ULL * 1024 * 1024 * 1024; // 2GB +#endif +} + +size_t GCToOSInterface::GetVirtualMemoryLimit() +{ + // On 32-bit WASM, the entire address space is the limit + return (size_t)-1; +} + +size_t GCToOSInterface::GetVirtualMemoryMaxAddress() +{ + return GetTotalPhysicalMemory(); +} + +size_t GetRestrictedPhysicalMemoryLimit() +{ + // WASM linear memory has a hard ceiling set in the .wasm file, enforced by the engine. + // This is semantically equivalent to a container memory limit (cgroups on Linux). + // Returning the total memory here makes is_restricted_physical_mem = true, which + // enables the GC to auto-set heap_hard_limit proportional to available memory. + return GetTotalPhysicalMemory(); +} + +bool GetPhysicalMemoryUsed(size_t* val) +{ + // __builtin_wasm_memory_size(0) returns count of 64KB WASM pages, not GC pages. + *val = __builtin_wasm_memory_size(0) * WasmPageSize; + if (*val == 0) + { + // This overflow can happen when all 4GB of memory are in use. + *val = GetTotalPhysicalMemory(); + } + + return true; +} + +uint64_t GCToOSInterface::GetPhysicalMemoryLimit(bool* is_restricted) +{ + size_t restricted_limit; + if (is_restricted) + *is_restricted = false; + + restricted_limit = GetRestrictedPhysicalMemoryLimit(); + g_RestrictedPhysicalMemoryLimit = restricted_limit; + + if (restricted_limit != 0 && restricted_limit != SIZE_T_MAX) + { + if (is_restricted) + *is_restricted = true; + return restricted_limit; + } + + return g_totalPhysicalMemSize; +} + +static uint64_t GetAvailablePhysicalMemory() +{ +#ifdef TARGET_BROWSER + return emscripten_get_heap_max() - emscripten_get_heap_size(); +#else // TARGET_WASI + // Best approximation: total minus currently used + // __builtin_wasm_memory_size(0) returns count of 64KB WASM pages, not GC pages. + size_t used = __builtin_wasm_memory_size(0) * WasmPageSize; + if (used == 0) + { + // This overflow can happen when all 4GB of memory are in use. + return 0; + } + uint64_t total = GetTotalPhysicalMemory(); + return (total > used) ? (total - used) : 0; +#endif +} + +static uint64_t GetAvailablePageFile() +{ + // No swap on WASM + return 0; +} + +void GCToOSInterface::GetMemoryStatus(uint64_t restricted_limit, uint32_t* memory_load, uint64_t* available_physical, uint64_t* available_page_file) +{ + uint64_t available = 0; + uint32_t load = 0; + + size_t used; + if (restricted_limit != 0) + { + if (GetPhysicalMemoryUsed(&used)) + { + available = restricted_limit > used ? restricted_limit - used : 0; + load = (uint32_t)(((float)used * 100) / (float)restricted_limit); + } + } + else + { + available = GetAvailablePhysicalMemory(); + + if (memory_load != nullptr) + { + uint64_t total = g_totalPhysicalMemSize; + + if (total > available) + { + used = total - available; + load = (uint32_t)(((float)used * 100) / (float)total); + } + } + } + + if (available_physical != nullptr) + *available_physical = available; + + if (memory_load != nullptr) + *memory_load = load; + + if (available_page_file != nullptr) + *available_page_file = GetAvailablePageFile(); +} + +// ============================================================================ +// Time +// ============================================================================ + +int64_t GCToOSInterface::QueryPerformanceCounter() +{ + return minipal_hires_ticks(); +} + +int64_t GCToOSInterface::QueryPerformanceFrequency() +{ + return minipal_hires_tick_frequency(); +} + +uint64_t GCToOSInterface::GetLowPrecisionTimeStamp() +{ + return (uint64_t)minipal_lowres_ticks(); +} + +// ============================================================================ +// Processor count / NUMA / CPU Groups +// ============================================================================ + +uint32_t GCToOSInterface::GetTotalProcessorCount() +{ + return g_totalCpuCount; +} + +uint32_t GCToOSInterface::GetMaxProcessorCount() +{ + return (uint32_t)g_processAffinitySet.MaxCpuCount(); +} + +bool GCToOSInterface::CanEnableGCNumaAware() +{ + return false; +} + +bool GCToOSInterface::CanEnableGCCPUGroups() +{ + return false; +} + +bool GCToOSInterface::GetProcessorForHeap(uint16_t heap_number, uint16_t* proc_no, uint16_t* node_no) +{ + if (heap_number == 0) + { + *proc_no = 0; + *node_no = NUMA_NODE_UNDEFINED; + return true; + } + + return false; +} + +bool GCToOSInterface::ParseGCHeapAffinitizeRangesEntry(const char** config_string, size_t* start_index, size_t* end_index) +{ + return ParseIndexOrRange(config_string, start_index, end_index); +} diff --git a/src/coreclr/pal/src/map/virtual.cpp b/src/coreclr/pal/src/map/virtual.cpp index 043502e916acd1..09d4513b8fa695 100644 --- a/src/coreclr/pal/src/map/virtual.cpp +++ b/src/coreclr/pal/src/map/virtual.cpp @@ -39,6 +39,7 @@ SET_DEFAULT_DEBUG_CHANNEL(VIRTUAL); // some headers have code with asserts, so d #include #include #include +#include #if HAVE_VM_ALLOCATE #include @@ -165,7 +166,7 @@ extern "C" BOOL VIRTUALInitialize(bool initializeExecutableMemoryAllocator) { - s_virtualPageSize = getpagesize(); + s_virtualPageSize = minipal_getpagesize(); TRACE("Initializing the Virtual Critical Sections. \n"); @@ -531,7 +532,11 @@ static LPVOID VIRTUALReserveMemory( { ASSERT( "Unable to store the structure in the list.\n"); pthrCurrent->SetLastError( ERROR_INTERNAL_ERROR ); +#ifdef TARGET_WASM + free( pRetVal ); +#else munmap( pRetVal, MemSize ); +#endif pRetVal = NULL; } } @@ -565,6 +570,49 @@ static LPVOID ReserveVirtualMemory( TRACE( "Reserving the memory now.\n"); +#ifdef TARGET_WASM + if (lpAddress != nullptr) + { + // Address hints (lpAddress) cannot be honored on WASM. + ERROR("Failed due to unsupported address hint on WASM.\n"); + pthrCurrent->SetLastError(ERROR_INVALID_ADDRESS); + return nullptr; + } + (void)fAllocationType; // Large pages / executable flags are N/A on WASM. + + // WASM has no virtual memory — mmap(PROT_NONE) still consumes linear memory, + // munmap of partial ranges doesn't return memory, and MAP_FIXED is broken. + // Use posix_memalign/free instead. + + #ifndef FEATURE_MULTITHREADING + // sbrk optimization: posix_memalign (dlmemalign) either recycles a free()'d + // block or grows the WASM linear memory via sbrk() → memory.grow. The WASM + // spec guarantees that memory.grow zero-initializes new pages, so only + // recycled blocks need explicit zeroing. We detect which case occurred by + // probing sbrk(0) before the allocation — safe because WASM is single-threaded. + void* old_brk = sbrk(0); +#endif + + LPVOID pRetVal = nullptr; + if (posix_memalign(&pRetVal, GetVirtualPageSize(), MemSize) != 0 || pRetVal == nullptr) + { + ERROR( "Failed due to insufficient memory.\n" ); + pthrCurrent->SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return nullptr; + } + +#ifndef FEATURE_MULTITHREADING + // Only zero recycled memory. Fresh pages from memory.grow are guaranteed zero. + if (pRetVal < old_brk) + { + memset(pRetVal, 0, MemSize); + } +#else + // The sbrk optimization is not safe with multiple threads. Fall back to + // always zeroing. + memset(pRetVal, 0, MemSize); +#endif +#else // !TARGET_WASM // Most platforms will only commit memory if it is dirtied, // so this should not consume too much swap space. int mmapFlags = MAP_ANON | MAP_PRIVATE; @@ -627,13 +675,14 @@ static LPVOID ReserveVirtualMemory( } #endif // MMAP_ANON_IGNORES_PROTECTION -#if defined(MADV_DONTDUMP) && !defined(TARGET_WASM) +#if defined(MADV_DONTDUMP) // Do not include reserved uncommitted memory in coredump. if (!(fAllocationType & MEM_COMMIT)) { madvise(pRetVal, MemSize, MADV_DONTDUMP); } #endif +#endif // !TARGET_WASM return pRetVal; } @@ -724,6 +773,20 @@ VIRTUALCommitMemory( ERROR("mprotect() failed! Error(%d)=%s\n", errno, strerror(errno)); goto error; } +#else + // On WASM, reserve == commit. If this range was previously decommitted, + // sentinels were placed at each page boundary. Check the first byte and + // zero the entire range if needed. +#ifdef FEATURE_MULTITHREADING + // Under MT, VirtualDecommit already zeroes the full range on decommit, and + // reserve already zeroes on allocation — so commit is a no-op. + (void)MemSize; +#else + if (MemSize && *(BYTE*)StartBoundary != 0) + { + ZeroMemory((LPVOID) StartBoundary, MemSize); + } +#endif #endif #if defined(MADV_DONTDUMP) && !defined(TARGET_WASM) @@ -738,7 +801,6 @@ VIRTUALCommitMemory( #ifndef TARGET_WASM error: -#endif if ( flAllocationType & MEM_RESERVE || IsLocallyReserved ) { munmap( pRetVal, MemSize ); @@ -753,6 +815,7 @@ VIRTUALCommitMemory( pInformation = NULL; pRetVal = NULL; +#endif // !TARGET_WASM done: LogVaOperation( @@ -1077,11 +1140,22 @@ VirtualFree( goto VirtualFreeExit; } #else // TARGET_WASM - // We can't decommit the mapping (MAP_FIXED doesn't work in emscripten), and we can't - // MADV_DONTNEED it (madvise doesn't work in emscripten), but we can at least zero - // the memory so that if an attempt is made to reuse it later, the memory will be - // empty as PAL tests expect it to be. +#ifdef FEATURE_MULTITHREADING + // Under MT, VirtualCommit always zeroes unconditionally, so just zero + // here immediately — no sentinel trick needed, and no races. ZeroMemory((LPVOID) StartBoundary, MemSize); +#else + // We can't decommit the mapping (MAP_FIXED doesn't work in emscripten), and we can't + // MADV_DONTNEED it (madvise doesn't work in emscripten). Instead of zeroing the + // entire range here, write a non-zero sentinel at each page boundary. The commit + // path checks the first byte to decide whether a zeroing pass is needed. + // Writing every page guarantees the sentinel is visible even if a sub-range is + // recommitted at an offset. + for (SIZE_T offset = 0; offset < MemSize; offset += GetVirtualPageSize()) + { + *((BYTE*)StartBoundary + offset) = 1; + } +#endif // FEATURE_MULTITHREADING #endif // TARGET_WASM } @@ -1108,6 +1182,22 @@ VirtualFree( TRACE( "Releasing the following memory %d to %d.\n", pMemoryToBeReleased->startBoundary, pMemoryToBeReleased->memSize ); +#ifdef TARGET_WASM + // Remove the tracking entry before freeing — if list removal fails, + // the memory is still valid and the caller can retry. + { + UINT_PTR boundary = pMemoryToBeReleased->startBoundary; + if ( VIRTUALReleaseMemory( pMemoryToBeReleased ) == FALSE ) + { + ASSERT( "Unable to remove the PCMI entry from the list.\n" ); + pthrCurrent->SetLastError( ERROR_INTERNAL_ERROR ); + bRetVal = FALSE; + goto VirtualFreeExit; + } + pMemoryToBeReleased = NULL; + free( (LPVOID)boundary ); + } +#else // !TARGET_WASM if ( munmap( (LPVOID)pMemoryToBeReleased->startBoundary, pMemoryToBeReleased->memSize ) == 0 ) { @@ -1127,6 +1217,7 @@ VirtualFree( bRetVal = FALSE; goto VirtualFreeExit; } +#endif // !TARGET_WASM } VirtualFreeExit: diff --git a/src/coreclr/pal/src/misc/sysinfo.cpp b/src/coreclr/pal/src/misc/sysinfo.cpp index df2a319b365488..a813a24828cdcf 100644 --- a/src/coreclr/pal/src/misc/sysinfo.cpp +++ b/src/coreclr/pal/src/misc/sysinfo.cpp @@ -24,6 +24,7 @@ Revision History: #include #include #include +#include #define __STDC_FORMAT_MACROS #include #include @@ -233,7 +234,7 @@ GetSystemInfo( PERF_ENTRY(GetSystemInfo); ENTRY("GetSystemInfo (lpSystemInfo=%p)\n", lpSystemInfo); - pagesize = getpagesize(); + pagesize = minipal_getpagesize(); lpSystemInfo->wProcessorArchitecture_PAL_Undefined = 0; lpSystemInfo->wReserved_PAL_Undefined = 0; diff --git a/src/native/minipal/wasm.h b/src/native/minipal/wasm.h new file mode 100644 index 00000000000000..f592653244ad34 --- /dev/null +++ b/src/native/minipal/wasm.h @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef MINIPAL_WASM_H +#define MINIPAL_WASM_H + +// Cross-platform page size accessor +#ifdef __cplusplus +inline +#else +static inline +#endif +int minipal_getpagesize(void) +{ +#ifdef __wasm__ + // The OS page size used by CoreCLR on WASM (16KB). + // WASM has no hardware pages; getpagesize() returns the 64KB memory.grow granularity, + // which is too coarse for GC alignment and thresholds. + return 16 * 1024; +#else + return getpagesize(); +#endif +} + +#endif // MINIPAL_WASM_H From 26c344660cba1512ee0894cfe5b5eb743930ef38 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 27 Apr 2026 19:06:50 +0200 Subject: [PATCH 02/36] fix --- src/coreclr/gc/init.cpp | 8 ++++++++ src/coreclr/gc/interface.cpp | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/coreclr/gc/init.cpp b/src/coreclr/gc/init.cpp index d47284f228ebfb..25727c7cbe4c3a 100644 --- a/src/coreclr/gc/init.cpp +++ b/src/coreclr/gc/init.cpp @@ -920,6 +920,13 @@ HRESULT gc_heap::initialize_gc (size_t soh_segment_size, assert (false); #endif //!HOST_64BIT && !HOST_WASM +#ifndef HOST_WASM + // On real large-page platforms the OS pre-committed all the memory during + // reserve_initial_memory, so we tighten the hard limit to match the + // actual segment sizes. On WASM, use_large_pages_p is only a decommit- + // skip flag — the original auto-detected hard limit (75% of WASM linear + // memory max) must be preserved so that bookkeeping commits and later + // allocations have room within the limit. if (heap_hard_limit_oh[soh]) { heap_hard_limit_oh[soh] = soh_segment_size * number_of_heaps; @@ -932,6 +939,7 @@ HRESULT gc_heap::initialize_gc (size_t soh_segment_size, assert (heap_hard_limit); heap_hard_limit = (soh_segment_size + loh_segment_size + poh_segment_size) * number_of_heaps; } +#endif //HOST_WASM } #endif //USE_REGIONS diff --git a/src/coreclr/gc/interface.cpp b/src/coreclr/gc/interface.cpp index 5300d34848d22d..6aece90dd75391 100644 --- a/src/coreclr/gc/interface.cpp +++ b/src/coreclr/gc/interface.cpp @@ -352,7 +352,10 @@ HRESULT GCHeap::Initialize() #ifdef HOST_64BIT large_seg_size = gc_heap::use_large_pages_p ? gc_heap::soh_segment_size : gc_heap::soh_segment_size * 2; #else //HOST_64BIT +#ifndef HOST_WASM + // Large pages not supported on 32-bit (except WASM which uses it to skip decommit). assert (!gc_heap::use_large_pages_p); +#endif //HOST_WASM large_seg_size = gc_heap::soh_segment_size; #endif //HOST_64BIT pin_seg_size = large_seg_size; From 5d6d6b5c4a88b83e2105c078a563d081a74f458a Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 27 Apr 2026 19:10:26 +0200 Subject: [PATCH 03/36] dash --- src/coreclr/gc/wasm/gcenv.cpp | 14 +++++++------- src/coreclr/pal/src/map/virtual.cpp | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index a09780f44bba63..f34f9cb2c766f0 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -167,13 +167,13 @@ void GCToOSInterface::YieldThread(uint32_t switchCount) (void)switchCount; sched_yield(); #else - // No-op on single-threaded WASM — there are no other threads to yield to. + // No-op on single-threaded WASM - there are no other threads to yield to. (void)switchCount; #endif } // ============================================================================ -// Virtual Memory — WASM-specific (posix_memalign / free) +// Virtual Memory - WASM-specific (posix_memalign / free) // ============================================================================ // Emscripten does not provide a complete implementation of mmap and munmap: @@ -195,7 +195,7 @@ void GCToOSInterface::YieldThread(uint32_t switchCount) // break, the memory came from heap growth and is already zero. If it falls below, // it was recycled and must be explicitly zeroed. // -// This is safe because WASM is single-threaded — no concurrent sbrk calls can +// This is safe because WASM is single-threaded - no concurrent sbrk calls can // occur between our sbrk(0) probe and the posix_memalign call. This is the same // approach used by Mono's WASM mmap implementation (mono-mmap-wasm.c). @@ -227,7 +227,7 @@ static void* VirtualReserveInner(size_t size, size_t alignment, uint32_t flags) memset(pRetVal, 0, size); } #else - // The sbrk optimization is not safe with multiple threads — another thread + // The sbrk optimization is not safe with multiple threads - another thread // could call sbrk between our probe and posix_memalign, giving a false // "fresh memory" result. Fall back to always zeroing. memset(pRetVal, 0, size); @@ -249,7 +249,7 @@ bool GCToOSInterface::VirtualRelease(void* address, size_t size) void* GCToOSInterface::VirtualReserveAndCommitLargePages(size_t size, uint16_t node) { - // WASM has no large pages — just reserve+commit normally. + // WASM has no large pages - just reserve+commit normally. return VirtualReserveInner(size, OS_PAGE_SIZE, 0); } @@ -260,7 +260,7 @@ bool GCToOSInterface::VirtualCommit(void* address, size_t size, uint16_t node) // If previously decommitted (sentinel at page boundary), zero the range. #ifdef FEATURE_MULTITHREADING // Under MT, VirtualDecommit already zeroes the full range on decommit, and - // VirtualReserveInner zeroes on allocation — so commit is a no-op. + // VirtualReserveInner zeroes on allocation - so commit is a no-op. (void)address; (void)size; #else @@ -279,7 +279,7 @@ bool GCToOSInterface::VirtualDecommit(void* address, size_t size) // On WASM, we cannot return memory to the OS or change page protection. #ifdef FEATURE_MULTITHREADING // Under MT, VirtualCommit always zeroes unconditionally, so we just zero - // here immediately — no sentinel trick needed, and no races to worry about. + // here immediately - no sentinel trick needed, and no races to worry about. memset(address, 0, size); #else // Instead of zeroing the entire range here (expensive), write a non-zero diff --git a/src/coreclr/pal/src/map/virtual.cpp b/src/coreclr/pal/src/map/virtual.cpp index 09d4513b8fa695..6e01047ba44366 100644 --- a/src/coreclr/pal/src/map/virtual.cpp +++ b/src/coreclr/pal/src/map/virtual.cpp @@ -580,7 +580,7 @@ static LPVOID ReserveVirtualMemory( } (void)fAllocationType; // Large pages / executable flags are N/A on WASM. - // WASM has no virtual memory — mmap(PROT_NONE) still consumes linear memory, + // WASM has no virtual memory - mmap(PROT_NONE) still consumes linear memory, // munmap of partial ranges doesn't return memory, and MAP_FIXED is broken. // Use posix_memalign/free instead. @@ -589,7 +589,7 @@ static LPVOID ReserveVirtualMemory( // block or grows the WASM linear memory via sbrk() → memory.grow. The WASM // spec guarantees that memory.grow zero-initializes new pages, so only // recycled blocks need explicit zeroing. We detect which case occurred by - // probing sbrk(0) before the allocation — safe because WASM is single-threaded. + // probing sbrk(0) before the allocation - safe because WASM is single-threaded. void* old_brk = sbrk(0); #endif @@ -779,7 +779,7 @@ VIRTUALCommitMemory( // zero the entire range if needed. #ifdef FEATURE_MULTITHREADING // Under MT, VirtualDecommit already zeroes the full range on decommit, and - // reserve already zeroes on allocation — so commit is a no-op. + // reserve already zeroes on allocation - so commit is a no-op. (void)MemSize; #else if (MemSize && *(BYTE*)StartBoundary != 0) @@ -1142,7 +1142,7 @@ VirtualFree( #else // TARGET_WASM #ifdef FEATURE_MULTITHREADING // Under MT, VirtualCommit always zeroes unconditionally, so just zero - // here immediately — no sentinel trick needed, and no races. + // here immediately - no sentinel trick needed, and no races. ZeroMemory((LPVOID) StartBoundary, MemSize); #else // We can't decommit the mapping (MAP_FIXED doesn't work in emscripten), and we can't @@ -1183,7 +1183,7 @@ VirtualFree( pMemoryToBeReleased->startBoundary, pMemoryToBeReleased->memSize ); #ifdef TARGET_WASM - // Remove the tracking entry before freeing — if list removal fails, + // Remove the tracking entry before freeing - if list removal fails, // the memory is still valid and the caller can retry. { UINT_PTR boundary = pMemoryToBeReleased->startBoundary; From 1b550fff0dd6e03e5fdb02c8fa7b0e4b36b9e97c Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 27 Apr 2026 19:23:14 +0200 Subject: [PATCH 04/36] feedback --- src/coreclr/pal/src/map/virtual.cpp | 36 ++++++++--------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/src/coreclr/pal/src/map/virtual.cpp b/src/coreclr/pal/src/map/virtual.cpp index 6e01047ba44366..a89c0761ddb4d1 100644 --- a/src/coreclr/pal/src/map/virtual.cpp +++ b/src/coreclr/pal/src/map/virtual.cpp @@ -1183,34 +1183,10 @@ VirtualFree( pMemoryToBeReleased->startBoundary, pMemoryToBeReleased->memSize ); #ifdef TARGET_WASM - // Remove the tracking entry before freeing - if list removal fails, - // the memory is still valid and the caller can retry. - { - UINT_PTR boundary = pMemoryToBeReleased->startBoundary; - if ( VIRTUALReleaseMemory( pMemoryToBeReleased ) == FALSE ) - { - ASSERT( "Unable to remove the PCMI entry from the list.\n" ); - pthrCurrent->SetLastError( ERROR_INTERNAL_ERROR ); - bRetVal = FALSE; - goto VirtualFreeExit; - } - pMemoryToBeReleased = NULL; - free( (LPVOID)boundary ); - } + free( (LPVOID)pMemoryToBeReleased->startBoundary ); #else // !TARGET_WASM if ( munmap( (LPVOID)pMemoryToBeReleased->startBoundary, - pMemoryToBeReleased->memSize ) == 0 ) - { - if ( VIRTUALReleaseMemory( pMemoryToBeReleased ) == FALSE ) - { - ASSERT( "Unable to remove the PCMI entry from the list.\n" ); - pthrCurrent->SetLastError( ERROR_INTERNAL_ERROR ); - bRetVal = FALSE; - goto VirtualFreeExit; - } - pMemoryToBeReleased = NULL; - } - else + pMemoryToBeReleased->memSize ) != 0 ) { ASSERT( "Unable to unmap the memory, munmap() returned an abnormal value.\n" ); pthrCurrent->SetLastError( ERROR_INTERNAL_ERROR ); @@ -1218,6 +1194,14 @@ VirtualFree( goto VirtualFreeExit; } #endif // !TARGET_WASM + if ( VIRTUALReleaseMemory( pMemoryToBeReleased ) == FALSE ) + { + ASSERT( "Unable to remove the PCMI entry from the list.\n" ); + pthrCurrent->SetLastError( ERROR_INTERNAL_ERROR ); + bRetVal = FALSE; + goto VirtualFreeExit; + } + pMemoryToBeReleased = NULL; } VirtualFreeExit: From 574c3eb6e527a9906efb340b79b31b30f304528b Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 27 Apr 2026 19:25:10 +0200 Subject: [PATCH 05/36] feedback --- src/coreclr/gc/wasm/gcenv.cpp | 5 ++++- src/coreclr/pal/src/map/virtual.cpp | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index f34f9cb2c766f0..ef4939901c8b40 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -210,6 +210,7 @@ static void* VirtualReserveInner(size_t size, size_t alignment, uint32_t flags) #ifndef FEATURE_MULTITHREADING // Capture the program break before allocation to detect heap growth vs recycling. void* old_brk = sbrk(0); + uintptr_t old_brk_val = (old_brk != (void*)-1) ? (uintptr_t)old_brk : 0; #endif void* pRetVal; @@ -222,7 +223,9 @@ static void* VirtualReserveInner(size_t size, size_t alignment, uint32_t flags) #ifndef FEATURE_MULTITHREADING // Only zero recycled memory. Fresh memory from heap growth (memory.grow) is // guaranteed to be zero by the WebAssembly spec. - if (pRetVal < old_brk) + // Compare as uintptr_t to avoid UB from relational comparison of unrelated pointers. + // When sbrk failed (old_brk_val == 0), fall back to unconditional zeroing. + if (old_brk_val == 0 || (uintptr_t)pRetVal < old_brk_val) { memset(pRetVal, 0, size); } diff --git a/src/coreclr/pal/src/map/virtual.cpp b/src/coreclr/pal/src/map/virtual.cpp index a89c0761ddb4d1..b0a6d26014cc70 100644 --- a/src/coreclr/pal/src/map/virtual.cpp +++ b/src/coreclr/pal/src/map/virtual.cpp @@ -584,13 +584,14 @@ static LPVOID ReserveVirtualMemory( // munmap of partial ranges doesn't return memory, and MAP_FIXED is broken. // Use posix_memalign/free instead. - #ifndef FEATURE_MULTITHREADING +#ifndef FEATURE_MULTITHREADING // sbrk optimization: posix_memalign (dlmemalign) either recycles a free()'d // block or grows the WASM linear memory via sbrk() → memory.grow. The WASM // spec guarantees that memory.grow zero-initializes new pages, so only // recycled blocks need explicit zeroing. We detect which case occurred by // probing sbrk(0) before the allocation - safe because WASM is single-threaded. void* old_brk = sbrk(0); + uintptr_t old_brk_val = (old_brk != (void*)-1) ? (uintptr_t)old_brk : 0; #endif LPVOID pRetVal = nullptr; @@ -603,7 +604,9 @@ static LPVOID ReserveVirtualMemory( #ifndef FEATURE_MULTITHREADING // Only zero recycled memory. Fresh pages from memory.grow are guaranteed zero. - if (pRetVal < old_brk) + // Compare as uintptr_t to avoid UB from relational comparison of unrelated pointers. + // When sbrk failed (old_brk_val == 0), fall back to unconditional zeroing. + if (old_brk_val == 0 || (uintptr_t)pRetVal < old_brk_val) { memset(pRetVal, 0, MemSize); } From 0275ab2b3b12fad7c5c9cddf52ee0d9f6fb9df86 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 27 Apr 2026 19:29:29 +0200 Subject: [PATCH 06/36] feedback --- src/native/minipal/wasm.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/native/minipal/wasm.h b/src/native/minipal/wasm.h index f592653244ad34..c45b385ff5f04b 100644 --- a/src/native/minipal/wasm.h +++ b/src/native/minipal/wasm.h @@ -4,6 +4,10 @@ #ifndef MINIPAL_WASM_H #define MINIPAL_WASM_H +#ifndef __wasm__ +#include +#endif + // Cross-platform page size accessor #ifdef __cplusplus inline From 2ef6b4b1653f5d24af377b06cc66f94014b0508f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 27 Apr 2026 20:21:24 +0200 Subject: [PATCH 07/36] Fix WASM GC build: include configure.cmake to generate config.gc.h --- src/coreclr/gc/wasm/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/coreclr/gc/wasm/CMakeLists.txt b/src/coreclr/gc/wasm/CMakeLists.txt index f394f677de1ac8..60fa95feb92e97 100644 --- a/src/coreclr/gc/wasm/CMakeLists.txt +++ b/src/coreclr/gc/wasm/CMakeLists.txt @@ -3,6 +3,8 @@ include_directories("../env") include_directories("..") include_directories("../unix") +include(../unix/configure.cmake) + set(GC_PAL_SOURCES gcenv.cpp ../unix/events.cpp) From 32ddc90ad97ff4e7fa9fadd221d06bcb402fc557 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 29 Apr 2026 08:52:59 +0200 Subject: [PATCH 08/36] feedback --- src/coreclr/gc/init.cpp | 37 +++++++++++++++--------------- src/coreclr/gc/wasm/CMakeLists.txt | 2 -- src/coreclr/gc/wasm/gcenv.cpp | 32 ++++---------------------- 3 files changed, 24 insertions(+), 47 deletions(-) diff --git a/src/coreclr/gc/init.cpp b/src/coreclr/gc/init.cpp index 0bc2db32692e35..74ef2e9380aeb8 100644 --- a/src/coreclr/gc/init.cpp +++ b/src/coreclr/gc/init.cpp @@ -931,26 +931,25 @@ HRESULT gc_heap::initialize_gc (size_t soh_segment_size, assert (false); #endif //!HOST_64BIT && !HOST_WASM -#ifndef HOST_WASM - // On real large-page platforms the OS pre-committed all the memory during - // reserve_initial_memory, so we tighten the hard limit to match the - // actual segment sizes. On WASM, use_large_pages_p is only a decommit- - // skip flag — the original auto-detected hard limit (75% of WASM linear - // memory max) must be preserved so that bookkeeping commits and later - // allocations have room within the limit. - if (heap_hard_limit_oh[soh]) + // With real OS large pages the entire reservation is pre-committed, so + // tighten the hard limit to match the actual segment sizes. In emulation + // mode (and on WASM) memory is not pre-committed via OS large pages so + // preserve the original hard limit for bookkeeping headroom. + if (!large_pages_emulation_mode_p) { - heap_hard_limit_oh[soh] = soh_segment_size * number_of_heaps; - heap_hard_limit_oh[loh] = loh_segment_size * number_of_heaps; - heap_hard_limit_oh[poh] = poh_segment_size * number_of_heaps; - heap_hard_limit = heap_hard_limit_oh[soh] + heap_hard_limit_oh[loh] + heap_hard_limit_oh[poh]; - } - else - { - assert (heap_hard_limit); - heap_hard_limit = (soh_segment_size + loh_segment_size + poh_segment_size) * number_of_heaps; + if (heap_hard_limit_oh[soh]) + { + heap_hard_limit_oh[soh] = soh_segment_size * number_of_heaps; + heap_hard_limit_oh[loh] = loh_segment_size * number_of_heaps; + heap_hard_limit_oh[poh] = poh_segment_size * number_of_heaps; + heap_hard_limit = heap_hard_limit_oh[soh] + heap_hard_limit_oh[loh] + heap_hard_limit_oh[poh]; + } + else + { + assert (heap_hard_limit); + heap_hard_limit = (soh_segment_size + loh_segment_size + poh_segment_size) * number_of_heaps; + } } -#endif //HOST_WASM } #endif //USE_REGIONS @@ -1305,7 +1304,9 @@ bool gc_heap::compute_hard_limit() #elif defined(HOST_WASM) // On WASM, reserve == commit (posix_memalign allocates real memory) and there is // no way to decommit. Enabling the large-pages path makes the GC skip VirtualDecommit. + // Emulation mode tells the GC that memory is not pre-committed via OS large pages. use_large_pages_p = true; + large_pages_emulation_mode_p = true; #endif //HOST_64BIT if (heap_hard_limit_oh[soh] || heap_hard_limit_oh[loh] || heap_hard_limit_oh[poh]) diff --git a/src/coreclr/gc/wasm/CMakeLists.txt b/src/coreclr/gc/wasm/CMakeLists.txt index 60fa95feb92e97..f394f677de1ac8 100644 --- a/src/coreclr/gc/wasm/CMakeLists.txt +++ b/src/coreclr/gc/wasm/CMakeLists.txt @@ -3,8 +3,6 @@ include_directories("../env") include_directories("..") include_directories("../unix") -include(../unix/configure.cmake) - set(GC_PAL_SOURCES gcenv.cpp ../unix/events.cpp) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index ef4939901c8b40..a9b529a3705f45 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -260,18 +260,10 @@ bool GCToOSInterface::VirtualCommit(void* address, size_t size, uint16_t node) { // The GC skips this for heap memory when use_large_pages_p is true (which // it always is on WASM). This is still called for bookkeeping memory. - // If previously decommitted (sentinel at page boundary), zero the range. -#ifdef FEATURE_MULTITHREADING - // Under MT, VirtualDecommit already zeroes the full range on decommit, and - // VirtualReserveInner zeroes on allocation - so commit is a no-op. + // Memory is always zero here: either from VirtualReserveInner (initial + // allocation) or from VirtualDecommit (which zeroes on decommit). (void)address; (void)size; -#else - if (size && *(uint8_t*)address != 0) - { - memset(address, 0, size); - } -#endif return true; } @@ -280,30 +272,16 @@ bool GCToOSInterface::VirtualDecommit(void* address, size_t size) // The GC skips this for heap memory when use_large_pages_p is true (which // it always is on WASM). This is still called for bookkeeping memory. // On WASM, we cannot return memory to the OS or change page protection. -#ifdef FEATURE_MULTITHREADING - // Under MT, VirtualCommit always zeroes unconditionally, so we just zero - // here immediately - no sentinel trick needed, and no races to worry about. + // Zero the range so it is clean for any future VirtualCommit (which is a no-op). memset(address, 0, size); -#else - // Instead of zeroing the entire range here (expensive), write a non-zero - // sentinel at each page boundary. VirtualCommit checks the first byte to - // decide whether a zeroing pass is needed. Writing every page guarantees - // the sentinel is visible even if a sub-range is recommitted at an offset. - for (size_t offset = 0; offset < size; offset += OS_PAGE_SIZE) - { - *((uint8_t*)address + offset) = 1; - } -#endif return true; } bool GCToOSInterface::VirtualReset(void* address, size_t size, bool unlock) { // Return false to indicate reset is not supported. - // This forces the GC to use the decommit+commit fallback path instead: - // VirtualDecommit marks the range with sentinels, and VirtualCommit - // performs the actual zeroing pass when the range is recommitted. - // That behavior is more correct on WASM, where madvise is a no-op. + // This forces the GC to use the decommit+commit fallback path instead. + // On WASM, madvise is a no-op so reset cannot discard pages. return false; } From adb3817960ccc1b3cc2b4978c4976e70402387a7 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 29 Apr 2026 08:59:42 +0200 Subject: [PATCH 09/36] too verbose --- src/coreclr/gc/init.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/coreclr/gc/init.cpp b/src/coreclr/gc/init.cpp index 74ef2e9380aeb8..f26eddf594fa2f 100644 --- a/src/coreclr/gc/init.cpp +++ b/src/coreclr/gc/init.cpp @@ -931,10 +931,6 @@ HRESULT gc_heap::initialize_gc (size_t soh_segment_size, assert (false); #endif //!HOST_64BIT && !HOST_WASM - // With real OS large pages the entire reservation is pre-committed, so - // tighten the hard limit to match the actual segment sizes. In emulation - // mode (and on WASM) memory is not pre-committed via OS large pages so - // preserve the original hard limit for bookkeeping headroom. if (!large_pages_emulation_mode_p) { if (heap_hard_limit_oh[soh]) From 435518abcc9086ade446678f50b00c10f7885f90 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 29 Apr 2026 09:08:18 +0200 Subject: [PATCH 10/36] cleanup --- src/coreclr/pal/src/map/virtual.cpp | 33 ++++++----------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/src/coreclr/pal/src/map/virtual.cpp b/src/coreclr/pal/src/map/virtual.cpp index b0a6d26014cc70..3501708a3d1fb8 100644 --- a/src/coreclr/pal/src/map/virtual.cpp +++ b/src/coreclr/pal/src/map/virtual.cpp @@ -777,19 +777,10 @@ VIRTUALCommitMemory( goto error; } #else - // On WASM, reserve == commit. If this range was previously decommitted, - // sentinels were placed at each page boundary. Check the first byte and - // zero the entire range if needed. -#ifdef FEATURE_MULTITHREADING - // Under MT, VirtualDecommit already zeroes the full range on decommit, and - // reserve already zeroes on allocation - so commit is a no-op. + // On WASM, reserve == commit — memory is accessible after posix_memalign. + // Memory is always zero here: either from ReserveVirtualMemory (initial + // allocation) or from the MEM_DECOMMIT path (which zeroes on decommit). (void)MemSize; -#else - if (MemSize && *(BYTE*)StartBoundary != 0) - { - ZeroMemory((LPVOID) StartBoundary, MemSize); - } -#endif #endif #if defined(MADV_DONTDUMP) && !defined(TARGET_WASM) @@ -1143,22 +1134,10 @@ VirtualFree( goto VirtualFreeExit; } #else // TARGET_WASM -#ifdef FEATURE_MULTITHREADING - // Under MT, VirtualCommit always zeroes unconditionally, so just zero - // here immediately - no sentinel trick needed, and no races. + // On WASM, we cannot decommit (MAP_FIXED and madvise don't work in + // emscripten). Zero the range so it is clean for any future commit + // (which is a no-op). ZeroMemory((LPVOID) StartBoundary, MemSize); -#else - // We can't decommit the mapping (MAP_FIXED doesn't work in emscripten), and we can't - // MADV_DONTNEED it (madvise doesn't work in emscripten). Instead of zeroing the - // entire range here, write a non-zero sentinel at each page boundary. The commit - // path checks the first byte to decide whether a zeroing pass is needed. - // Writing every page guarantees the sentinel is visible even if a sub-range is - // recommitted at an offset. - for (SIZE_T offset = 0; offset < MemSize; offset += GetVirtualPageSize()) - { - *((BYTE*)StartBoundary + offset) = 1; - } -#endif // FEATURE_MULTITHREADING #endif // TARGET_WASM } From fe828ff97095433f3a9b71e04fd97862ed224567 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Wed, 29 Apr 2026 09:46:34 +0200 Subject: [PATCH 11/36] Update src/coreclr/gc/wasm/gcenv.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/coreclr/gc/wasm/gcenv.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index a9b529a3705f45..589680a824f5f1 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -355,8 +355,9 @@ static uint64_t GetTotalPhysicalMemory() size_t GCToOSInterface::GetVirtualMemoryLimit() { - // On 32-bit WASM, the entire address space is the limit - return (size_t)-1; + // WASM linear memory has a hard engine-enforced ceiling, so report that + // maximum rather than an unbounded virtual address space. + return GetVirtualMemoryMaxAddress(); } size_t GCToOSInterface::GetVirtualMemoryMaxAddress() From f05b837ca47026565b5a8fa49c7157790860e6a1 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 29 Apr 2026 09:53:50 +0200 Subject: [PATCH 12/36] feedback --- src/native/minipal/wasm.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/native/minipal/wasm.h b/src/native/minipal/wasm.h index c45b385ff5f04b..3b407e6b0499ad 100644 --- a/src/native/minipal/wasm.h +++ b/src/native/minipal/wasm.h @@ -22,7 +22,10 @@ int minipal_getpagesize(void) // which is too coarse for GC alignment and thresholds. return 16 * 1024; #else - return getpagesize(); + static int cached_page_size = 0; + if (cached_page_size == 0) + cached_page_size = getpagesize(); + return cached_page_size; #endif } From 47b5e078d4b1966cd15dca4f002cea2431d59844 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 29 Apr 2026 10:26:45 +0200 Subject: [PATCH 13/36] feedback --- src/coreclr/gc/wasm/gcenv.cpp | 9 +++++++++ src/coreclr/pal/src/map/virtual.cpp | 2 ++ src/native/minipal/wasm.h | 18 ++++++++++-------- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 589680a824f5f1..68767126345c91 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -241,17 +241,20 @@ static void* VirtualReserveInner(size_t size, size_t alignment, uint32_t flags) void* GCToOSInterface::VirtualReserve(size_t size, size_t alignment, uint32_t flags, uint16_t node) { + (void)node; return VirtualReserveInner(size, alignment, flags); } bool GCToOSInterface::VirtualRelease(void* address, size_t size) { + (void)size; free(address); return true; } void* GCToOSInterface::VirtualReserveAndCommitLargePages(size_t size, uint16_t node) { + (void)node; // WASM has no large pages - just reserve+commit normally. return VirtualReserveInner(size, OS_PAGE_SIZE, 0); } @@ -264,6 +267,7 @@ bool GCToOSInterface::VirtualCommit(void* address, size_t size, uint16_t node) // allocation) or from VirtualDecommit (which zeroes on decommit). (void)address; (void)size; + (void)node; return true; } @@ -282,6 +286,9 @@ bool GCToOSInterface::VirtualReset(void* address, size_t size, bool unlock) // Return false to indicate reset is not supported. // This forces the GC to use the decommit+commit fallback path instead. // On WASM, madvise is a no-op so reset cannot discard pages. + (void)address; + (void)size; + (void)unlock; return false; } @@ -311,6 +318,7 @@ bool GCToOSInterface::GetWriteWatch(bool resetState, void* address, size_t size, size_t GCToOSInterface::GetCacheSizePerLogicalCpu(bool trueSize) { + (void)trueSize; // WASM doesn't expose cache topology. // Return a reasonable default (256 KB). return 256 * 1024; @@ -322,6 +330,7 @@ size_t GCToOSInterface::GetCacheSizePerLogicalCpu(bool trueSize) bool GCToOSInterface::SetThreadAffinity(uint16_t procNo) { + (void)procNo; // No thread affinity on WASM return false; } diff --git a/src/coreclr/pal/src/map/virtual.cpp b/src/coreclr/pal/src/map/virtual.cpp index 3501708a3d1fb8..f203a2b965d004 100644 --- a/src/coreclr/pal/src/map/virtual.cpp +++ b/src/coreclr/pal/src/map/virtual.cpp @@ -766,7 +766,9 @@ VIRTUALCommitMemory( TRACE( "Committing the memory now..\n"); +#ifndef TARGET_WASM nProtect = W32toUnixAccessControl(flProtect); +#endif pRetVal = (void *) StartBoundary; #ifndef TARGET_WASM diff --git a/src/native/minipal/wasm.h b/src/native/minipal/wasm.h index 3b407e6b0499ad..4c6e4125886758 100644 --- a/src/native/minipal/wasm.h +++ b/src/native/minipal/wasm.h @@ -1,20 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#ifndef MINIPAL_WASM_H -#define MINIPAL_WASM_H +#ifndef HAVE_MINIPAL_WASM_H +#define HAVE_MINIPAL_WASM_H #ifndef __wasm__ #include #endif -// Cross-platform page size accessor #ifdef __cplusplus -inline -#else -static inline +extern "C" { #endif -int minipal_getpagesize(void) + +static inline int minipal_getpagesize(void) { #ifdef __wasm__ // The OS page size used by CoreCLR on WASM (16KB). @@ -29,4 +27,8 @@ int minipal_getpagesize(void) #endif } -#endif // MINIPAL_WASM_H +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // HAVE_MINIPAL_WASM_H From 9b98c13274b98c50ad91f01e8018da0ca009b6df Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 29 Apr 2026 10:34:20 +0200 Subject: [PATCH 14/36] feedback --- src/coreclr/gc/wasm/gcenv.cpp | 2 +- src/coreclr/pal/src/map/virtual.cpp | 2 +- src/coreclr/pal/src/misc/sysinfo.cpp | 2 +- src/native/minipal/CMakeLists.txt | 1 + src/native/minipal/ospagesize.c | 36 ++++++++++++++++++++++++++++ src/native/minipal/utils.h | 11 +++++++++ src/native/minipal/wasm.h | 34 -------------------------- 7 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 src/native/minipal/ospagesize.c delete mode 100644 src/native/minipal/wasm.h diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 68767126345c91..67ec341a26a01e 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -30,7 +30,7 @@ #include #include -#include +#include #include "globals.h" diff --git a/src/coreclr/pal/src/map/virtual.cpp b/src/coreclr/pal/src/map/virtual.cpp index f203a2b965d004..98c6ed527c2a36 100644 --- a/src/coreclr/pal/src/map/virtual.cpp +++ b/src/coreclr/pal/src/map/virtual.cpp @@ -39,7 +39,7 @@ SET_DEFAULT_DEBUG_CHANNEL(VIRTUAL); // some headers have code with asserts, so d #include #include #include -#include +#include #if HAVE_VM_ALLOCATE #include diff --git a/src/coreclr/pal/src/misc/sysinfo.cpp b/src/coreclr/pal/src/misc/sysinfo.cpp index a813a24828cdcf..a8c010408c3bc8 100644 --- a/src/coreclr/pal/src/misc/sysinfo.cpp +++ b/src/coreclr/pal/src/misc/sysinfo.cpp @@ -24,7 +24,7 @@ Revision History: #include #include #include -#include +#include #define __STDC_FORMAT_MACROS #include #include diff --git a/src/native/minipal/CMakeLists.txt b/src/native/minipal/CMakeLists.txt index d7f9ad5e2ab78a..4e9518685f4cb2 100644 --- a/src/native/minipal/CMakeLists.txt +++ b/src/native/minipal/CMakeLists.txt @@ -6,6 +6,7 @@ set(SOURCES memorybarrierprocesswide.c mutex.c guid.c + ospagesize.c random.c debugger.c strings.c diff --git a/src/native/minipal/ospagesize.c b/src/native/minipal/ospagesize.c new file mode 100644 index 00000000000000..3cdf642021b33e --- /dev/null +++ b/src/native/minipal/ospagesize.c @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include + +#ifdef __wasm__ +// The OS page size used by CoreCLR on WASM (16KB). +// WASM has no hardware pages; getpagesize() returns the 64KB memory.grow granularity, +// which is too coarse for GC alignment and thresholds. +int minipal_getpagesize(void) +{ + return 16 * 1024; +} +#elif HOST_WINDOWS +#include +int minipal_getpagesize(void) +{ + static int cached_page_size = 0; + if (cached_page_size == 0) + { + SYSTEM_INFO sysInfo; + GetSystemInfo(&sysInfo); + cached_page_size = (int)sysInfo.dwPageSize; + } + return cached_page_size; +} +#else +#include +int minipal_getpagesize(void) +{ + static int cached_page_size = 0; + if (cached_page_size == 0) + cached_page_size = getpagesize(); + return cached_page_size; +} +#endif diff --git a/src/native/minipal/utils.h b/src/native/minipal/utils.h index 97758ffcf0f8a8..255c895476345b 100644 --- a/src/native/minipal/utils.h +++ b/src/native/minipal/utils.h @@ -146,4 +146,15 @@ #define __asan_handle_no_return() #endif +#ifdef __cplusplus +extern "C" { +#endif + +// Returns the OS page size in bytes. The value is cached after the first call. +int minipal_getpagesize(void); + +#ifdef __cplusplus +} +#endif + #endif // HAVE_MINIPAL_UTILS_H diff --git a/src/native/minipal/wasm.h b/src/native/minipal/wasm.h deleted file mode 100644 index 4c6e4125886758..00000000000000 --- a/src/native/minipal/wasm.h +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#ifndef HAVE_MINIPAL_WASM_H -#define HAVE_MINIPAL_WASM_H - -#ifndef __wasm__ -#include -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -static inline int minipal_getpagesize(void) -{ -#ifdef __wasm__ - // The OS page size used by CoreCLR on WASM (16KB). - // WASM has no hardware pages; getpagesize() returns the 64KB memory.grow granularity, - // which is too coarse for GC alignment and thresholds. - return 16 * 1024; -#else - static int cached_page_size = 0; - if (cached_page_size == 0) - cached_page_size = getpagesize(); - return cached_page_size; -#endif -} - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif // HAVE_MINIPAL_WASM_H From 6555b06e79cf312780e4b4e95a58843991aa8779 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Wed, 29 Apr 2026 10:50:16 +0200 Subject: [PATCH 15/36] Update src/coreclr/gc/wasm/gcenv.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/coreclr/gc/wasm/gcenv.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 67ec341a26a01e..5c46fe5653d815 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -59,8 +59,8 @@ static size_t g_RestrictedPhysicalMemoryLimit = 0; static int64_t g_totalPhysicalMemSize = 0; // Forward declarations -size_t GetRestrictedPhysicalMemoryLimit(); -bool GetPhysicalMemoryUsed(size_t* val); +static size_t GetRestrictedPhysicalMemoryLimit(); +static bool GetPhysicalMemoryUsed(size_t* val); // ============================================================================ // Initialization / Shutdown From d1f1f26ce1ad70d77b787e540a211492242d5464 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 29 Apr 2026 13:19:23 +0200 Subject: [PATCH 16/36] feedback --- src/coreclr/gc/wasm/CMakeLists.txt | 2 ++ src/coreclr/gc/wasm/gcenv.cpp | 26 ++++++++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/coreclr/gc/wasm/CMakeLists.txt b/src/coreclr/gc/wasm/CMakeLists.txt index f394f677de1ac8..60fa95feb92e97 100644 --- a/src/coreclr/gc/wasm/CMakeLists.txt +++ b/src/coreclr/gc/wasm/CMakeLists.txt @@ -3,6 +3,8 @@ include_directories("../env") include_directories("..") include_directories("../unix") +include(../unix/configure.cmake) + set(GC_PAL_SOURCES gcenv.cpp ../unix/events.cpp) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 5c46fe5653d815..5247483877f2d9 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -386,13 +386,23 @@ size_t GetRestrictedPhysicalMemoryLimit() bool GetPhysicalMemoryUsed(size_t* val) { // __builtin_wasm_memory_size(0) returns count of 64KB WASM pages, not GC pages. - *val = __builtin_wasm_memory_size(0) * WasmPageSize; - if (*val == 0) + // Compute in 64 bits so a legitimate 0-page memory is not conflated with wasm32 overflow. + uint64_t pages = static_cast(__builtin_wasm_memory_size(0)); + if (pages > (UINT64_MAX / WasmPageSize)) { - // This overflow can happen when all 4GB of memory are in use. *val = GetTotalPhysicalMemory(); + return true; + } + + uint64_t bytesUsed = pages * static_cast(WasmPageSize); + if (bytesUsed > static_cast(SIZE_MAX)) + { + // Clamp when the byte count cannot be represented in size_t on wasm32. + *val = GetTotalPhysicalMemory(); + return true; } + *val = static_cast(bytesUsed); return true; } @@ -420,14 +430,10 @@ static uint64_t GetAvailablePhysicalMemory() #ifdef TARGET_BROWSER return emscripten_get_heap_max() - emscripten_get_heap_size(); #else // TARGET_WASI - // Best approximation: total minus currently used + // Best approximation: total minus currently used. // __builtin_wasm_memory_size(0) returns count of 64KB WASM pages, not GC pages. - size_t used = __builtin_wasm_memory_size(0) * WasmPageSize; - if (used == 0) - { - // This overflow can happen when all 4GB of memory are in use. - return 0; - } + // Compute in 64 bits so a legitimate 0-page memory is not conflated with wasm32 overflow. + uint64_t used = static_cast(__builtin_wasm_memory_size(0)) * static_cast(WasmPageSize); uint64_t total = GetTotalPhysicalMemory(); return (total > used) ? (total - used) : 0; #endif From 96a9d4d103e035f68a4bd7116281e1ba2f4fd56b Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 29 Apr 2026 15:47:39 +0200 Subject: [PATCH 17/36] feedback --- src/coreclr/gc/wasm/gcenv.cpp | 1 + src/coreclr/pal/src/map/virtual.cpp | 1 + src/coreclr/pal/src/misc/sysinfo.cpp | 1 + src/native/minipal/CMakeLists.txt | 1 - src/native/minipal/ospagesize.c | 27 ++++++++-------- src/native/minipal/ospagesize.h | 48 ++++++++++++++++++++++++++++ src/native/minipal/utils.h | 11 ------- 7 files changed, 64 insertions(+), 26 deletions(-) create mode 100644 src/native/minipal/ospagesize.h diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 5247483877f2d9..a575c43de9d181 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include "globals.h" diff --git a/src/coreclr/pal/src/map/virtual.cpp b/src/coreclr/pal/src/map/virtual.cpp index 98c6ed527c2a36..e9a612b970d19e 100644 --- a/src/coreclr/pal/src/map/virtual.cpp +++ b/src/coreclr/pal/src/map/virtual.cpp @@ -40,6 +40,7 @@ SET_DEFAULT_DEBUG_CHANNEL(VIRTUAL); // some headers have code with asserts, so d #include #include #include +#include #if HAVE_VM_ALLOCATE #include diff --git a/src/coreclr/pal/src/misc/sysinfo.cpp b/src/coreclr/pal/src/misc/sysinfo.cpp index a8c010408c3bc8..63b02d00be88d1 100644 --- a/src/coreclr/pal/src/misc/sysinfo.cpp +++ b/src/coreclr/pal/src/misc/sysinfo.cpp @@ -25,6 +25,7 @@ Revision History: #include #include #include +#include #define __STDC_FORMAT_MACROS #include #include diff --git a/src/native/minipal/CMakeLists.txt b/src/native/minipal/CMakeLists.txt index 4e9518685f4cb2..d7f9ad5e2ab78a 100644 --- a/src/native/minipal/CMakeLists.txt +++ b/src/native/minipal/CMakeLists.txt @@ -6,7 +6,6 @@ set(SOURCES memorybarrierprocesswide.c mutex.c guid.c - ospagesize.c random.c debugger.c strings.c diff --git a/src/native/minipal/ospagesize.c b/src/native/minipal/ospagesize.c index 3cdf642021b33e..0a0a540d1dcef3 100644 --- a/src/native/minipal/ospagesize.c +++ b/src/native/minipal/ospagesize.c @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#include +#include #ifdef __wasm__ // The OS page size used by CoreCLR on WASM (16KB). @@ -12,25 +12,24 @@ int minipal_getpagesize(void) return 16 * 1024; } #elif HOST_WINDOWS -#include int minipal_getpagesize(void) { - static int cached_page_size = 0; - if (cached_page_size == 0) - { - SYSTEM_INFO sysInfo; - GetSystemInfo(&sysInfo); - cached_page_size = (int)sysInfo.dwPageSize; - } - return cached_page_size; + // The page size on Windows is 4KB and is not going to change. + return 4 * 1024; } #else #include int minipal_getpagesize(void) { - static int cached_page_size = 0; - if (cached_page_size == 0) - cached_page_size = getpagesize(); - return cached_page_size; + // Use volatile to prevent the compiler from reordering loads/stores of the cache, + // which could cause callers to observe a zero value after another thread initialized it. + static volatile int cached_page_size = 0; + int page_size = cached_page_size; + if (page_size == 0) + { + page_size = getpagesize(); + cached_page_size = page_size; + } + return page_size; } #endif diff --git a/src/native/minipal/ospagesize.h b/src/native/minipal/ospagesize.h new file mode 100644 index 00000000000000..a7d891f80a5764 --- /dev/null +++ b/src/native/minipal/ospagesize.h @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef HAVE_MINIPAL_OSPAGESIZE_H +#define HAVE_MINIPAL_OSPAGESIZE_H + +#if !defined(__wasm__) && !defined(HOST_WINDOWS) && !defined(_WIN32) +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// Returns the OS page size in bytes. +// +// Defined inline so that callers see a compile-time constant on platforms where +// the page size is fixed (Windows: 4KB, WASM: 16KB). This matters for the GC, +// which expects GetPageSize to fold into a constant for alignment math. +static inline int minipal_getpagesize(void) +{ +#ifdef __wasm__ + // The OS page size used by CoreCLR on WASM (16KB). + // WASM has no hardware pages; getpagesize() returns the 64KB memory.grow granularity, + // which is too coarse for GC alignment and thresholds. + return 16 * 1024; +#elif defined(HOST_WINDOWS) || defined(_WIN32) + // The page size on Windows is 4KB and is not going to change. + return 4 * 1024; +#else + // Use volatile to prevent the compiler from reordering loads/stores of the cache, + // which could cause callers to observe a zero value after another thread initialized it. + static volatile int cached_page_size = 0; + int page_size = cached_page_size; + if (page_size == 0) + { + page_size = getpagesize(); + cached_page_size = page_size; + } + return page_size; +#endif +} + +#ifdef __cplusplus +} +#endif + +#endif // HAVE_MINIPAL_OSPAGESIZE_H diff --git a/src/native/minipal/utils.h b/src/native/minipal/utils.h index 255c895476345b..97758ffcf0f8a8 100644 --- a/src/native/minipal/utils.h +++ b/src/native/minipal/utils.h @@ -146,15 +146,4 @@ #define __asan_handle_no_return() #endif -#ifdef __cplusplus -extern "C" { -#endif - -// Returns the OS page size in bytes. The value is cached after the first call. -int minipal_getpagesize(void); - -#ifdef __cplusplus -} -#endif - #endif // HAVE_MINIPAL_UTILS_H From d70f152b901c9698ba68f06e7d4709ca5e0a3b7e Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 30 Apr 2026 12:11:01 +0200 Subject: [PATCH 18/36] feedback --- src/coreclr/gc/CMakeLists.txt | 1 + src/coreclr/gc/wasm/CMakeLists.txt | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/coreclr/gc/CMakeLists.txt b/src/coreclr/gc/CMakeLists.txt index 9b48e6ddd4ded6..ed681a46808f0b 100644 --- a/src/coreclr/gc/CMakeLists.txt +++ b/src/coreclr/gc/CMakeLists.txt @@ -27,6 +27,7 @@ set(GC_SOURCES if(CLR_CMAKE_TARGET_ARCH_WASM) add_subdirectory(wasm) + include(unix/configure.cmake) elseif(CLR_CMAKE_HOST_UNIX) add_subdirectory(unix) include(unix/configure.cmake) diff --git a/src/coreclr/gc/wasm/CMakeLists.txt b/src/coreclr/gc/wasm/CMakeLists.txt index 60fa95feb92e97..f394f677de1ac8 100644 --- a/src/coreclr/gc/wasm/CMakeLists.txt +++ b/src/coreclr/gc/wasm/CMakeLists.txt @@ -3,8 +3,6 @@ include_directories("../env") include_directories("..") include_directories("../unix") -include(../unix/configure.cmake) - set(GC_PAL_SOURCES gcenv.cpp ../unix/events.cpp) From c91ff1d09d28e602337d7f004939415ce0812d7f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 30 Apr 2026 12:11:13 +0200 Subject: [PATCH 19/36] drop file --- src/native/minipal/ospagesize.c | 35 --------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 src/native/minipal/ospagesize.c diff --git a/src/native/minipal/ospagesize.c b/src/native/minipal/ospagesize.c deleted file mode 100644 index 0a0a540d1dcef3..00000000000000 --- a/src/native/minipal/ospagesize.c +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#include - -#ifdef __wasm__ -// The OS page size used by CoreCLR on WASM (16KB). -// WASM has no hardware pages; getpagesize() returns the 64KB memory.grow granularity, -// which is too coarse for GC alignment and thresholds. -int minipal_getpagesize(void) -{ - return 16 * 1024; -} -#elif HOST_WINDOWS -int minipal_getpagesize(void) -{ - // The page size on Windows is 4KB and is not going to change. - return 4 * 1024; -} -#else -#include -int minipal_getpagesize(void) -{ - // Use volatile to prevent the compiler from reordering loads/stores of the cache, - // which could cause callers to observe a zero value after another thread initialized it. - static volatile int cached_page_size = 0; - int page_size = cached_page_size; - if (page_size == 0) - { - page_size = getpagesize(); - cached_page_size = page_size; - } - return page_size; -} -#endif From b5dc456fad5aeeaf2e0e7d784ec949869db30a03 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 30 Apr 2026 12:14:54 +0200 Subject: [PATCH 20/36] remove sbrk trick --- src/coreclr/gc/wasm/gcenv.cpp | 48 +++++------------------------ src/coreclr/pal/src/map/virtual.cpp | 30 +++--------------- 2 files changed, 13 insertions(+), 65 deletions(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index a575c43de9d181..f13635133fbaee 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -158,19 +158,18 @@ void GCToOSInterface::Sleep(uint32_t sleepMSec) // On single-threaded WASM, nanosleep is either a no-op or stalls the // event loop. There are no other threads to wait for, and no signals // to deliver EINTR. - (void)sleepMSec; #endif + (void)sleepMSec; } void GCToOSInterface::YieldThread(uint32_t switchCount) { #ifdef FEATURE_MULTITHREADING - (void)switchCount; sched_yield(); #else // No-op on single-threaded WASM - there are no other threads to yield to. - (void)switchCount; #endif + (void)switchCount; } // ============================================================================ @@ -181,24 +180,13 @@ void GCToOSInterface::YieldThread(uint32_t switchCount) // munmap cannot unmap partial allocations, mmap(PROT_NONE) still consumes // linear memory, and MAP_FIXED is broken. // Emscripten does provide an implementation of posix_memalign which is used here. - -// sbrk optimization: posix_memalign (dlmemalign) obtains memory in one of two ways: -// 1. Recycling a previously free()'d block from the allocator's free list. -// 2. Growing the WASM linear memory via sbrk() → memory.grow. -// -// The WebAssembly spec guarantees that memory.grow zero-initializes new pages, -// so freshly grown memory does not need an explicit memset. Only recycled blocks -// (which may contain stale data from a previous VirtualDecommit→free cycle) must -// be zeroed. // -// We detect which case occurred by recording sbrk(0) (the current program break) -// before the allocation. If posix_memalign returns a pointer at or above the old -// break, the memory came from heap growth and is already zero. If it falls below, -// it was recycled and must be explicitly zeroed. -// -// This is safe because WASM is single-threaded - no concurrent sbrk calls can -// occur between our sbrk(0) probe and the posix_memalign call. This is the same -// approach used by Mono's WASM mmap implementation (mono-mmap-wasm.c). +// posix_memalign returns either freshly grown linear memory (zero by the WASM +// spec) or a recycled block from the allocator's free list (which may contain +// stale data from a previous VirtualDecommit -> free cycle). Since we cannot +// portably distinguish the two without relying on dlmalloc implementation +// details, we always zero the returned memory to match VirtualReserve's +// "memory starts zeroed" contract. static void* VirtualReserveInner(size_t size, size_t alignment, uint32_t flags) { @@ -208,12 +196,6 @@ static void* VirtualReserveInner(size_t size, size_t alignment, uint32_t flags) alignment = OS_PAGE_SIZE; } -#ifndef FEATURE_MULTITHREADING - // Capture the program break before allocation to detect heap growth vs recycling. - void* old_brk = sbrk(0); - uintptr_t old_brk_val = (old_brk != (void*)-1) ? (uintptr_t)old_brk : 0; -#endif - void* pRetVal; int result = posix_memalign(&pRetVal, alignment, size); if (result != 0) @@ -221,21 +203,7 @@ static void* VirtualReserveInner(size_t size, size_t alignment, uint32_t flags) return nullptr; } -#ifndef FEATURE_MULTITHREADING - // Only zero recycled memory. Fresh memory from heap growth (memory.grow) is - // guaranteed to be zero by the WebAssembly spec. - // Compare as uintptr_t to avoid UB from relational comparison of unrelated pointers. - // When sbrk failed (old_brk_val == 0), fall back to unconditional zeroing. - if (old_brk_val == 0 || (uintptr_t)pRetVal < old_brk_val) - { - memset(pRetVal, 0, size); - } -#else - // The sbrk optimization is not safe with multiple threads - another thread - // could call sbrk between our probe and posix_memalign, giving a false - // "fresh memory" result. Fall back to always zeroing. memset(pRetVal, 0, size); -#endif return pRetVal; } diff --git a/src/coreclr/pal/src/map/virtual.cpp b/src/coreclr/pal/src/map/virtual.cpp index e9a612b970d19e..5e03ee11ded56b 100644 --- a/src/coreclr/pal/src/map/virtual.cpp +++ b/src/coreclr/pal/src/map/virtual.cpp @@ -585,16 +585,6 @@ static LPVOID ReserveVirtualMemory( // munmap of partial ranges doesn't return memory, and MAP_FIXED is broken. // Use posix_memalign/free instead. -#ifndef FEATURE_MULTITHREADING - // sbrk optimization: posix_memalign (dlmemalign) either recycles a free()'d - // block or grows the WASM linear memory via sbrk() → memory.grow. The WASM - // spec guarantees that memory.grow zero-initializes new pages, so only - // recycled blocks need explicit zeroing. We detect which case occurred by - // probing sbrk(0) before the allocation - safe because WASM is single-threaded. - void* old_brk = sbrk(0); - uintptr_t old_brk_val = (old_brk != (void*)-1) ? (uintptr_t)old_brk : 0; -#endif - LPVOID pRetVal = nullptr; if (posix_memalign(&pRetVal, GetVirtualPageSize(), MemSize) != 0 || pRetVal == nullptr) { @@ -603,19 +593,11 @@ static LPVOID ReserveVirtualMemory( return nullptr; } -#ifndef FEATURE_MULTITHREADING - // Only zero recycled memory. Fresh pages from memory.grow are guaranteed zero. - // Compare as uintptr_t to avoid UB from relational comparison of unrelated pointers. - // When sbrk failed (old_brk_val == 0), fall back to unconditional zeroing. - if (old_brk_val == 0 || (uintptr_t)pRetVal < old_brk_val) - { - memset(pRetVal, 0, MemSize); - } -#else - // The sbrk optimization is not safe with multiple threads. Fall back to - // always zeroing. + // posix_memalign may return either freshly grown linear memory (zeroed by the + // WASM spec) or a recycled block from the allocator's free list (which may + // contain stale data from a previous free()). Always zero to match the + // "reserved memory starts zeroed" contract of this function. memset(pRetVal, 0, MemSize); -#endif #else // !TARGET_WASM // Most platforms will only commit memory if it is dirtied, // so this should not consume too much swap space. @@ -767,12 +749,10 @@ VIRTUALCommitMemory( TRACE( "Committing the memory now..\n"); -#ifndef TARGET_WASM - nProtect = W32toUnixAccessControl(flProtect); -#endif pRetVal = (void *) StartBoundary; #ifndef TARGET_WASM + nProtect = W32toUnixAccessControl(flProtect); // Commit the pages if (mprotect((void *) StartBoundary, MemSize, nProtect) != 0) { From bd7a1476e9da0a44ab81eb101b383a2879233685 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 30 Apr 2026 12:43:28 +0200 Subject: [PATCH 21/36] never_decommit_p --- src/coreclr/gc/gc.cpp | 1 + src/coreclr/gc/gcpriv.h | 8 ++++++++ src/coreclr/gc/init.cpp | 18 ++++++++-------- src/coreclr/gc/interface.cpp | 4 +--- src/coreclr/gc/memory.cpp | 32 +++++++++++++++-------------- src/coreclr/gc/regions_segments.cpp | 20 ++++++++++-------- src/coreclr/gc/sweep.cpp | 2 +- src/coreclr/gc/wasm/gcenv.cpp | 4 ++-- 8 files changed, 50 insertions(+), 39 deletions(-) diff --git a/src/coreclr/gc/gc.cpp b/src/coreclr/gc/gc.cpp index 7f715cf7daf57e..13c2e30c8f0002 100644 --- a/src/coreclr/gc/gc.cpp +++ b/src/coreclr/gc/gc.cpp @@ -2620,6 +2620,7 @@ heap_segment* gc_heap::segment_standby_list; #endif //USE_REGIONS bool gc_heap::use_large_pages_p = 0; bool gc_heap::large_pages_emulation_mode_p = 0; +bool gc_heap::never_decommit_p = 0; #ifdef HEAP_BALANCE_INSTRUMENTATION size_t gc_heap::last_gc_end_time_us = 0; #endif //HEAP_BALANCE_INSTRUMENTATION diff --git a/src/coreclr/gc/gcpriv.h b/src/coreclr/gc/gcpriv.h index 18b128049a2e8d..bf257d73c4a155 100644 --- a/src/coreclr/gc/gcpriv.h +++ b/src/coreclr/gc/gcpriv.h @@ -5373,6 +5373,14 @@ class gc_heap PER_HEAP_ISOLATED_FIELD_INIT_ONLY bool use_large_pages_p; PER_HEAP_ISOLATED_FIELD_INIT_ONLY bool large_pages_emulation_mode_p; + // Indicates that the underlying OS does not support decommitting memory. + // Implies that VirtualCommit/VirtualDecommit are no-ops on heap memory and + // that GC code paths that rely on returning memory to the OS must be skipped. + // Set unconditionally on WASM. The use_large_pages_p path also implies this + // (large pages are pre-committed and never decommitted) but additionally + // pulls in OS huge-page-specific behavior that is not desired on WASM. + PER_HEAP_ISOLATED_FIELD_INIT_ONLY bool never_decommit_p; + #ifdef MULTIPLE_HEAPS // Init-ed in gc_heap::initialize_gc PER_HEAP_ISOLATED_FIELD_INIT_ONLY gc_heap** g_heaps; diff --git a/src/coreclr/gc/init.cpp b/src/coreclr/gc/init.cpp index f26eddf594fa2f..92b5b4cf0e8eb0 100644 --- a/src/coreclr/gc/init.cpp +++ b/src/coreclr/gc/init.cpp @@ -925,11 +925,10 @@ HRESULT gc_heap::initialize_gc (size_t soh_segment_size, return E_OUTOFMEMORY; if (use_large_pages_p) { -#if !defined(HOST_64BIT) && !defined(HOST_WASM) +#ifndef HOST_64BIT // Large pages are not supported on 32bit - // except WASM, which uses the large-pages code path to skip decommit assert (false); -#endif //!HOST_64BIT && !HOST_WASM +#endif //!HOST_64BIT if (!large_pages_emulation_mode_p) { @@ -1297,14 +1296,15 @@ bool gc_heap::compute_hard_limit() int64_t large_pages_config = GCConfig::GetGCLargePages(); use_large_pages_p = (large_pages_config != 0); large_pages_emulation_mode_p = (large_pages_config == 2); -#elif defined(HOST_WASM) - // On WASM, reserve == commit (posix_memalign allocates real memory) and there is - // no way to decommit. Enabling the large-pages path makes the GC skip VirtualDecommit. - // Emulation mode tells the GC that memory is not pre-committed via OS large pages. - use_large_pages_p = true; - large_pages_emulation_mode_p = true; #endif //HOST_64BIT +#ifdef HOST_WASM + // On WASM, reserve == commit (posix_memalign allocates real memory) and there is + // no way to give memory back to the engine. Tell the GC to treat decommit as a no-op + // and to never rely on memory shrinking back to the OS. + never_decommit_p = true; +#endif //HOST_WASM + if (heap_hard_limit_oh[soh] || heap_hard_limit_oh[loh] || heap_hard_limit_oh[poh]) { if (!heap_hard_limit_oh[soh]) diff --git a/src/coreclr/gc/interface.cpp b/src/coreclr/gc/interface.cpp index 7c95e1c2d19cd5..66e3641c483709 100644 --- a/src/coreclr/gc/interface.cpp +++ b/src/coreclr/gc/interface.cpp @@ -352,10 +352,8 @@ HRESULT GCHeap::Initialize() #ifdef HOST_64BIT large_seg_size = gc_heap::use_large_pages_p ? gc_heap::soh_segment_size : gc_heap::soh_segment_size * 2; #else //HOST_64BIT -#ifndef HOST_WASM - // Large pages not supported on 32-bit (except WASM which uses it to skip decommit). + // Large pages not supported on 32-bit. assert (!gc_heap::use_large_pages_p); -#endif //HOST_WASM large_seg_size = gc_heap::soh_segment_size; #endif //HOST_64BIT pin_seg_size = large_seg_size; diff --git a/src/coreclr/gc/memory.cpp b/src/coreclr/gc/memory.cpp index 79f2f489fadf40..ea6e6a75b45cc6 100644 --- a/src/coreclr/gc/memory.cpp +++ b/src/coreclr/gc/memory.cpp @@ -98,8 +98,9 @@ bool gc_heap::virtual_commit (void* address, size_t size, int bucket, int h_numb } // If it's a valid heap number it means it's commiting for memory on the GC heap. - // In addition if large pages is enabled, we set commit_succeeded_p to true because memory is already committed. - bool commit_succeeded_p = ((h_number >= 0) ? (use_large_pages_p ? true : + // In addition if large pages or never-decommit is enabled, we set commit_succeeded_p + // to true because memory is already committed (and VirtualCommit would be a no-op). + bool commit_succeeded_p = ((h_number >= 0) ? ((use_large_pages_p || never_decommit_p) ? true : virtual_alloc_commit_for_heap (address, size, h_number)) : GCToOSInterface::VirtualCommit(address, size)); @@ -171,10 +172,11 @@ bool gc_heap::virtual_decommit (void* address, size_t size, int bucket, int h_nu * Case 3: This is for free - the bucket will be recorded_committed_free_bucket, and the h_number will be -1 */ - // With large pages, VirtualDecommit on heap memory is a no-op. All such callers - // should either skip the decommit or handle stale data themselves (decommit_region - // does the latter by calling reduce_committed_bytes directly and clearing memory). - assert (!use_large_pages_p || bucket == recorded_committed_bookkeeping_bucket); + // With large pages or never-decommit, VirtualDecommit on heap memory is a no-op. + // All such callers should either skip the decommit or handle stale data themselves + // (decommit_region does the latter by calling reduce_committed_bytes directly and + // clearing memory). + assert ((!use_large_pages_p && !never_decommit_p) || bucket == recorded_committed_bookkeeping_bucket); bool decommit_succeeded_p = GCToOSInterface::VirtualDecommit (address, size); @@ -202,7 +204,7 @@ void gc_heap::virtual_free (void* add, size_t allocated_size, heap_segment* sg) // distribute_free_regions where we are calling estimate_gen_growth. void gc_heap::decommit_ephemeral_segment_pages() { - if (settings.concurrent || use_large_pages_p || (settings.pause_mode == pause_no_gc)) + if (settings.concurrent || use_large_pages_p || never_decommit_p || (settings.pause_mode == pause_no_gc)) { return; } @@ -315,7 +317,7 @@ bool gc_heap::decommit_step (uint64_t step_milliseconds) } } } - if (use_large_pages_p) + if (use_large_pages_p || never_decommit_p) { return (decommit_size != 0); } @@ -323,7 +325,7 @@ bool gc_heap::decommit_step (uint64_t step_milliseconds) #ifdef MULTIPLE_HEAPS // should never get here for large pages because decommit_ephemeral_segment_pages // will not do anything if use_large_pages_p is true - assert(!use_large_pages_p); + assert(!use_large_pages_p && !never_decommit_p); for (int i = 0; i < n_heaps; i++) { @@ -345,10 +347,10 @@ size_t gc_heap::decommit_region (heap_segment* region, int bucket, int h_number) uint8_t* decommit_end = heap_segment_committed (region); size_t decommit_size = decommit_end - page_start; bool decommit_succeeded_p; - if (use_large_pages_p) + if (use_large_pages_p || never_decommit_p) { - // VirtualDecommit is a no-op for large pages so skip it and update - // committed bookkeeping directly. Memory clearing is handled below. + // VirtualDecommit is a no-op for large pages / never-decommit so skip it and + // update committed bookkeeping directly. Memory clearing is handled below. decommit_succeeded_p = true; reduce_committed_bytes (page_start, decommit_size, bucket, h_number, true); } @@ -356,7 +358,7 @@ size_t gc_heap::decommit_region (heap_segment* region, int bucket, int h_number) { decommit_succeeded_p = virtual_decommit (page_start, decommit_size, bucket, h_number); } - bool require_clearing_memory_p = !decommit_succeeded_p || use_large_pages_p; + bool require_clearing_memory_p = !decommit_succeeded_p || use_large_pages_p || never_decommit_p; dprintf (REGIONS_LOG, ("decommitted region %p(%p-%p) (%zu bytes) - success: %d", region, page_start, @@ -365,7 +367,7 @@ size_t gc_heap::decommit_region (heap_segment* region, int bucket, int h_number) decommit_succeeded_p)); if (require_clearing_memory_p) { - uint8_t* clear_end = use_large_pages_p ? heap_segment_used (region) : heap_segment_committed (region); + uint8_t* clear_end = (use_large_pages_p || never_decommit_p) ? heap_segment_used (region) : heap_segment_committed (region); size_t clear_size = clear_end - page_start; memclr (page_start, clear_size); heap_segment_used (region) = heap_segment_mem (region); @@ -397,7 +399,7 @@ size_t gc_heap::decommit_region (heap_segment* region, int bucket, int h_number) } #endif //BACKGROUND_GC - if (use_large_pages_p) + if (use_large_pages_p || never_decommit_p) { assert (heap_segment_used (region) == heap_segment_mem (region)); } diff --git a/src/coreclr/gc/regions_segments.cpp b/src/coreclr/gc/regions_segments.cpp index e9275063f10d09..daf7d9d0793880 100644 --- a/src/coreclr/gc/regions_segments.cpp +++ b/src/coreclr/gc/regions_segments.cpp @@ -1475,7 +1475,7 @@ void gc_heap::reset_heap_segment_pages (heap_segment* seg) void gc_heap::decommit_heap_segment_pages (heap_segment* seg, size_t extra_space) { - if (use_large_pages_p) + if (use_large_pages_p || never_decommit_p) return; uint8_t* page_start = align_on_page (heap_segment_allocated(seg)); @@ -1493,7 +1493,7 @@ void gc_heap::decommit_heap_segment_pages (heap_segment* seg, size_t gc_heap::decommit_heap_segment_pages_worker (heap_segment* seg, uint8_t* new_committed) { - assert (!use_large_pages_p); + assert (!use_large_pages_p && !never_decommit_p); uint8_t* page_start = align_on_page (new_committed); ptrdiff_t size = heap_segment_committed (seg) - page_start; if (size > 0) @@ -1522,9 +1522,10 @@ size_t gc_heap::decommit_heap_segment_pages_worker (heap_segment* seg, //decommit all pages except one or 2 void gc_heap::decommit_heap_segment (heap_segment* seg) { - // For large pages, VirtualDecommit is a no-op so skip the decommit entirely - // to avoid lowering committed/used bookkeeping while memory retains stale data. - if (use_large_pages_p) + // For large pages or never-decommit, VirtualDecommit is a no-op so skip the + // decommit entirely to avoid lowering committed/used bookkeeping while memory + // retains stale data. + if (use_large_pages_p || never_decommit_p) { return; } @@ -1820,10 +1821,11 @@ void gc_heap::distribute_free_regions() while (decommit_step(DECOMMIT_TIME_STEP_MILLISECONDS)) { } - // For large pages, VirtualDecommit on in-use regions is a no-op so the - // memory is never actually returned to the OS. Skip the tail decommit - // entirely to avoid misleading bookkeeping and unnecessary memclr overhead. - if (!use_large_pages_p) + // For large pages or never-decommit, VirtualDecommit on in-use regions is a + // no-op so the memory is never actually returned to the OS. Skip the tail + // decommit entirely to avoid misleading bookkeeping and unnecessary memclr + // overhead. + if (!use_large_pages_p && !never_decommit_p) { #ifdef MULTIPLE_HEAPS for (int i = 0; i < n_heaps; i++) diff --git a/src/coreclr/gc/sweep.cpp b/src/coreclr/gc/sweep.cpp index 25a1825639eb2d..92f81150401877 100644 --- a/src/coreclr/gc/sweep.cpp +++ b/src/coreclr/gc/sweep.cpp @@ -442,7 +442,7 @@ void gc_heap::clear_unused_array (uint8_t* x, size_t size) void gc_heap::reset_memory (uint8_t* o, size_t sizeo) { - if (gc_heap::use_large_pages_p) + if (gc_heap::use_large_pages_p || gc_heap::never_decommit_p) return; if (sizeo > 128 * 1024) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index f13635133fbaee..de012791311faa 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -230,7 +230,7 @@ void* GCToOSInterface::VirtualReserveAndCommitLargePages(size_t size, uint16_t n bool GCToOSInterface::VirtualCommit(void* address, size_t size, uint16_t node) { - // The GC skips this for heap memory when use_large_pages_p is true (which + // The GC skips this for heap memory when never_decommit_p is true (which // it always is on WASM). This is still called for bookkeeping memory. // Memory is always zero here: either from VirtualReserveInner (initial // allocation) or from VirtualDecommit (which zeroes on decommit). @@ -242,7 +242,7 @@ bool GCToOSInterface::VirtualCommit(void* address, size_t size, uint16_t node) bool GCToOSInterface::VirtualDecommit(void* address, size_t size) { - // The GC skips this for heap memory when use_large_pages_p is true (which + // The GC skips this for heap memory when never_decommit_p is true (which // it always is on WASM). This is still called for bookkeeping memory. // On WASM, we cannot return memory to the OS or change page protection. // Zero the range so it is clean for any future VirtualCommit (which is a no-op). From fba3ee0c9fdf46401f0b0a2875e2c1afe0010c4f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 30 Apr 2026 13:00:29 +0200 Subject: [PATCH 22/36] cleanup --- src/coreclr/gc/init.cpp | 40 +++++++++++------------------------ src/coreclr/gc/interface.cpp | 1 - src/coreclr/gc/wasm/gcenv.cpp | 10 ++++----- 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/src/coreclr/gc/init.cpp b/src/coreclr/gc/init.cpp index 92b5b4cf0e8eb0..9f60b53db3b11d 100644 --- a/src/coreclr/gc/init.cpp +++ b/src/coreclr/gc/init.cpp @@ -930,20 +930,17 @@ HRESULT gc_heap::initialize_gc (size_t soh_segment_size, assert (false); #endif //!HOST_64BIT - if (!large_pages_emulation_mode_p) + if (heap_hard_limit_oh[soh]) { - if (heap_hard_limit_oh[soh]) - { - heap_hard_limit_oh[soh] = soh_segment_size * number_of_heaps; - heap_hard_limit_oh[loh] = loh_segment_size * number_of_heaps; - heap_hard_limit_oh[poh] = poh_segment_size * number_of_heaps; - heap_hard_limit = heap_hard_limit_oh[soh] + heap_hard_limit_oh[loh] + heap_hard_limit_oh[poh]; - } - else - { - assert (heap_hard_limit); - heap_hard_limit = (soh_segment_size + loh_segment_size + poh_segment_size) * number_of_heaps; - } + heap_hard_limit_oh[soh] = soh_segment_size * number_of_heaps; + heap_hard_limit_oh[loh] = loh_segment_size * number_of_heaps; + heap_hard_limit_oh[poh] = poh_segment_size * number_of_heaps; + heap_hard_limit = heap_hard_limit_oh[soh] + heap_hard_limit_oh[loh] + heap_hard_limit_oh[poh]; + } + else + { + assert (heap_hard_limit); + heap_hard_limit = (soh_segment_size + loh_segment_size + poh_segment_size) * number_of_heaps; } } #endif //USE_REGIONS @@ -1391,11 +1388,9 @@ bool gc_heap::compute_hard_limit() bool gc_heap::compute_memory_settings(bool is_initialization, uint32_t& nhp, uint32_t nhp_from_config, size_t& seg_size_from_config, size_t new_current_total_committed) { -#if defined(HOST_64BIT) || defined(HOST_WASM) +#ifdef HOST_64BIT // If the hard limit is specified, the user is saying even if the process is already // running in a container, use this limit for the GC heap. - // On WASM, the linear memory has a hard ceiling set in the .wasm file, enforced by - // the engine — semantically equivalent to a container memory limit. if (!hard_limit_config_p) { if (is_restricted_physical_mem) @@ -1412,7 +1407,7 @@ bool gc_heap::compute_memory_settings(bool is_initialization, uint32_t& nhp, uin } } } -#endif //HOST_64BIT || HOST_WASM +#endif //HOST_64BIT if (heap_hard_limit && (heap_hard_limit < new_current_total_committed)) { @@ -1456,17 +1451,6 @@ bool gc_heap::compute_memory_settings(bool is_initialization, uint32_t& nhp, uin // 0 <= soh_segment_size <= 1Gb size_t limit_to_check = (heap_hard_limit_oh[soh] ? heap_hard_limit_oh[soh] : heap_hard_limit); soh_segment_size = max (adjust_segment_size_hard_limit (limit_to_check, nhp), seg_size_from_config); -#ifdef HOST_WASM - // On WASM, VirtualReserve allocates real memory (no virtual memory). - // Cap segment size so all 3 initial segments (SOH + LOH + POH) fit within - // the hard limit with room to grow. On 32-bit without per-OH limits, - // LOH and POH segments equal soh_segment_size, so total = 3 * soh. - { - size_t max_seg = round_down_power2 (heap_hard_limit / (3 * 2 * nhp)); - max_seg = max (max_seg, (size_t)(1024 * 1024)); - soh_segment_size = min (soh_segment_size, max_seg); - } -#endif //HOST_WASM } else { diff --git a/src/coreclr/gc/interface.cpp b/src/coreclr/gc/interface.cpp index 66e3641c483709..acdae1415cb403 100644 --- a/src/coreclr/gc/interface.cpp +++ b/src/coreclr/gc/interface.cpp @@ -352,7 +352,6 @@ HRESULT GCHeap::Initialize() #ifdef HOST_64BIT large_seg_size = gc_heap::use_large_pages_p ? gc_heap::soh_segment_size : gc_heap::soh_segment_size * 2; #else //HOST_64BIT - // Large pages not supported on 32-bit. assert (!gc_heap::use_large_pages_p); large_seg_size = gc_heap::soh_segment_size; #endif //HOST_64BIT diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index de012791311faa..4f42b0e9b5db7b 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -343,13 +343,11 @@ size_t GCToOSInterface::GetVirtualMemoryMaxAddress() return GetTotalPhysicalMemory(); } -size_t GetRestrictedPhysicalMemoryLimit() +static size_t GetRestrictedPhysicalMemoryLimit() { - // WASM linear memory has a hard ceiling set in the .wasm file, enforced by the engine. - // This is semantically equivalent to a container memory limit (cgroups on Linux). - // Returning the total memory here makes is_restricted_physical_mem = true, which - // enables the GC to auto-set heap_hard_limit proportional to available memory. - return GetTotalPhysicalMemory(); + // No restricted-memory mode on WASM. The linear memory ceiling enforced by the + // engine is the only hard cap; we don't auto-derive a GC heap_hard_limit from it. + return 0; } bool GetPhysicalMemoryUsed(size_t* val) From c446f3b7f437009c203fd33e55d95cdedfc3489f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 30 Apr 2026 13:24:11 +0200 Subject: [PATCH 23/36] feedback + cleanup --- src/coreclr/gc/gcpriv.h | 6 ++--- src/coreclr/gc/init.cpp | 8 ++++--- src/coreclr/gc/memory.cpp | 37 +++++++++++++++-------------- src/coreclr/gc/regions_segments.cpp | 22 ++++++++--------- src/coreclr/gc/sweep.cpp | 2 +- src/coreclr/gc/wasm/gcenv.cpp | 15 +++++------- src/native/minipal/CMakeLists.txt | 1 + src/native/minipal/ospagesize.c | 27 +++++++++++++++++++++ src/native/minipal/ospagesize.h | 29 +++++++++------------- 9 files changed, 84 insertions(+), 63 deletions(-) create mode 100644 src/native/minipal/ospagesize.c diff --git a/src/coreclr/gc/gcpriv.h b/src/coreclr/gc/gcpriv.h index bf257d73c4a155..0a9077d93d92a7 100644 --- a/src/coreclr/gc/gcpriv.h +++ b/src/coreclr/gc/gcpriv.h @@ -5376,9 +5376,9 @@ class gc_heap // Indicates that the underlying OS does not support decommitting memory. // Implies that VirtualCommit/VirtualDecommit are no-ops on heap memory and // that GC code paths that rely on returning memory to the OS must be skipped. - // Set unconditionally on WASM. The use_large_pages_p path also implies this - // (large pages are pre-committed and never decommitted) but additionally - // pulls in OS huge-page-specific behavior that is not desired on WASM. + // Set unconditionally on WASM and whenever use_large_pages_p is set (large pages + // are pre-committed and cannot be decommitted). Code that wants to skip a + // decommit-related path should test this flag rather than use_large_pages_p. PER_HEAP_ISOLATED_FIELD_INIT_ONLY bool never_decommit_p; #ifdef MULTIPLE_HEAPS diff --git a/src/coreclr/gc/init.cpp b/src/coreclr/gc/init.cpp index 9f60b53db3b11d..51aefa8d4c7c93 100644 --- a/src/coreclr/gc/init.cpp +++ b/src/coreclr/gc/init.cpp @@ -1295,11 +1295,13 @@ bool gc_heap::compute_hard_limit() large_pages_emulation_mode_p = (large_pages_config == 2); #endif //HOST_64BIT + // Large pages are pre-committed and cannot be decommitted, so they imply + // never_decommit_p. On WASM, reserve == commit (posix_memalign allocates real + // memory) and there is no way to give memory back to the engine. #ifdef HOST_WASM - // On WASM, reserve == commit (posix_memalign allocates real memory) and there is - // no way to give memory back to the engine. Tell the GC to treat decommit as a no-op - // and to never rely on memory shrinking back to the OS. never_decommit_p = true; +#else + never_decommit_p = use_large_pages_p; #endif //HOST_WASM if (heap_hard_limit_oh[soh] || heap_hard_limit_oh[loh] || heap_hard_limit_oh[poh]) diff --git a/src/coreclr/gc/memory.cpp b/src/coreclr/gc/memory.cpp index ea6e6a75b45cc6..5c073c8f2dcf03 100644 --- a/src/coreclr/gc/memory.cpp +++ b/src/coreclr/gc/memory.cpp @@ -98,9 +98,10 @@ bool gc_heap::virtual_commit (void* address, size_t size, int bucket, int h_numb } // If it's a valid heap number it means it's commiting for memory on the GC heap. - // In addition if large pages or never-decommit is enabled, we set commit_succeeded_p - // to true because memory is already committed (and VirtualCommit would be a no-op). - bool commit_succeeded_p = ((h_number >= 0) ? ((use_large_pages_p || never_decommit_p) ? true : + // In addition if never-decommit is enabled (which is implied by large pages), we + // set commit_succeeded_p to true because memory is already committed (and + // VirtualCommit would be a no-op). + bool commit_succeeded_p = ((h_number >= 0) ? (never_decommit_p ? true : virtual_alloc_commit_for_heap (address, size, h_number)) : GCToOSInterface::VirtualCommit(address, size)); @@ -172,11 +173,11 @@ bool gc_heap::virtual_decommit (void* address, size_t size, int bucket, int h_nu * Case 3: This is for free - the bucket will be recorded_committed_free_bucket, and the h_number will be -1 */ - // With large pages or never-decommit, VirtualDecommit on heap memory is a no-op. - // All such callers should either skip the decommit or handle stale data themselves - // (decommit_region does the latter by calling reduce_committed_bytes directly and - // clearing memory). - assert ((!use_large_pages_p && !never_decommit_p) || bucket == recorded_committed_bookkeeping_bucket); + // With never-decommit (implied by large pages), VirtualDecommit on heap memory is + // a no-op. All such callers should either skip the decommit or handle stale data + // themselves (decommit_region does the latter by calling reduce_committed_bytes + // directly and clearing memory). + assert (!never_decommit_p || bucket == recorded_committed_bookkeeping_bucket); bool decommit_succeeded_p = GCToOSInterface::VirtualDecommit (address, size); @@ -204,7 +205,7 @@ void gc_heap::virtual_free (void* add, size_t allocated_size, heap_segment* sg) // distribute_free_regions where we are calling estimate_gen_growth. void gc_heap::decommit_ephemeral_segment_pages() { - if (settings.concurrent || use_large_pages_p || never_decommit_p || (settings.pause_mode == pause_no_gc)) + if (settings.concurrent || never_decommit_p || (settings.pause_mode == pause_no_gc)) { return; } @@ -317,15 +318,15 @@ bool gc_heap::decommit_step (uint64_t step_milliseconds) } } } - if (use_large_pages_p || never_decommit_p) + if (never_decommit_p) { return (decommit_size != 0); } #endif //USE_REGIONS #ifdef MULTIPLE_HEAPS - // should never get here for large pages because decommit_ephemeral_segment_pages - // will not do anything if use_large_pages_p is true - assert(!use_large_pages_p && !never_decommit_p); + // should never get here for never-decommit because decommit_ephemeral_segment_pages + // will not do anything if never_decommit_p is true + assert(!never_decommit_p); for (int i = 0; i < n_heaps; i++) { @@ -347,9 +348,9 @@ size_t gc_heap::decommit_region (heap_segment* region, int bucket, int h_number) uint8_t* decommit_end = heap_segment_committed (region); size_t decommit_size = decommit_end - page_start; bool decommit_succeeded_p; - if (use_large_pages_p || never_decommit_p) + if (never_decommit_p) { - // VirtualDecommit is a no-op for large pages / never-decommit so skip it and + // VirtualDecommit is a no-op when never_decommit_p is set, so skip it and // update committed bookkeeping directly. Memory clearing is handled below. decommit_succeeded_p = true; reduce_committed_bytes (page_start, decommit_size, bucket, h_number, true); @@ -358,7 +359,7 @@ size_t gc_heap::decommit_region (heap_segment* region, int bucket, int h_number) { decommit_succeeded_p = virtual_decommit (page_start, decommit_size, bucket, h_number); } - bool require_clearing_memory_p = !decommit_succeeded_p || use_large_pages_p || never_decommit_p; + bool require_clearing_memory_p = !decommit_succeeded_p || never_decommit_p; dprintf (REGIONS_LOG, ("decommitted region %p(%p-%p) (%zu bytes) - success: %d", region, page_start, @@ -367,7 +368,7 @@ size_t gc_heap::decommit_region (heap_segment* region, int bucket, int h_number) decommit_succeeded_p)); if (require_clearing_memory_p) { - uint8_t* clear_end = (use_large_pages_p || never_decommit_p) ? heap_segment_used (region) : heap_segment_committed (region); + uint8_t* clear_end = never_decommit_p ? heap_segment_used (region) : heap_segment_committed (region); size_t clear_size = clear_end - page_start; memclr (page_start, clear_size); heap_segment_used (region) = heap_segment_mem (region); @@ -399,7 +400,7 @@ size_t gc_heap::decommit_region (heap_segment* region, int bucket, int h_number) } #endif //BACKGROUND_GC - if (use_large_pages_p || never_decommit_p) + if (never_decommit_p) { assert (heap_segment_used (region) == heap_segment_mem (region)); } diff --git a/src/coreclr/gc/regions_segments.cpp b/src/coreclr/gc/regions_segments.cpp index daf7d9d0793880..2352950498f8cb 100644 --- a/src/coreclr/gc/regions_segments.cpp +++ b/src/coreclr/gc/regions_segments.cpp @@ -1475,7 +1475,7 @@ void gc_heap::reset_heap_segment_pages (heap_segment* seg) void gc_heap::decommit_heap_segment_pages (heap_segment* seg, size_t extra_space) { - if (use_large_pages_p || never_decommit_p) + if (never_decommit_p) return; uint8_t* page_start = align_on_page (heap_segment_allocated(seg)); @@ -1493,7 +1493,7 @@ void gc_heap::decommit_heap_segment_pages (heap_segment* seg, size_t gc_heap::decommit_heap_segment_pages_worker (heap_segment* seg, uint8_t* new_committed) { - assert (!use_large_pages_p && !never_decommit_p); + assert (!never_decommit_p); uint8_t* page_start = align_on_page (new_committed); ptrdiff_t size = heap_segment_committed (seg) - page_start; if (size > 0) @@ -1522,10 +1522,10 @@ size_t gc_heap::decommit_heap_segment_pages_worker (heap_segment* seg, //decommit all pages except one or 2 void gc_heap::decommit_heap_segment (heap_segment* seg) { - // For large pages or never-decommit, VirtualDecommit is a no-op so skip the - // decommit entirely to avoid lowering committed/used bookkeeping while memory - // retains stale data. - if (use_large_pages_p || never_decommit_p) + // For never-decommit (implied by large pages), VirtualDecommit is a no-op so + // skip the decommit entirely to avoid lowering committed/used bookkeeping while + // memory retains stale data. + if (never_decommit_p) { return; } @@ -1821,11 +1821,11 @@ void gc_heap::distribute_free_regions() while (decommit_step(DECOMMIT_TIME_STEP_MILLISECONDS)) { } - // For large pages or never-decommit, VirtualDecommit on in-use regions is a - // no-op so the memory is never actually returned to the OS. Skip the tail - // decommit entirely to avoid misleading bookkeeping and unnecessary memclr - // overhead. - if (!use_large_pages_p && !never_decommit_p) + // For never-decommit (implied by large pages), VirtualDecommit on in-use + // regions is a no-op so the memory is never actually returned to the OS. + // Skip the tail decommit entirely to avoid misleading bookkeeping and + // unnecessary memclr overhead. + if (!never_decommit_p) { #ifdef MULTIPLE_HEAPS for (int i = 0; i < n_heaps; i++) diff --git a/src/coreclr/gc/sweep.cpp b/src/coreclr/gc/sweep.cpp index 92f81150401877..85f804318af8b5 100644 --- a/src/coreclr/gc/sweep.cpp +++ b/src/coreclr/gc/sweep.cpp @@ -442,7 +442,7 @@ void gc_heap::clear_unused_array (uint8_t* x, size_t size) void gc_heap::reset_memory (uint8_t* o, size_t sizeo) { - if (gc_heap::use_large_pages_p || gc_heap::never_decommit_p) + if (gc_heap::never_decommit_p) return; if (sizeo > 128 * 1024) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 4f42b0e9b5db7b..41c25453832a80 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -55,13 +55,12 @@ AffinitySet g_processAffinitySet; extern "C" int g_highestNumaNode = 0; extern "C" bool g_numaAvailable = false; -static size_t g_RestrictedPhysicalMemoryLimit = 0; - static int64_t g_totalPhysicalMemSize = 0; // Forward declarations static size_t GetRestrictedPhysicalMemoryLimit(); static bool GetPhysicalMemoryUsed(size_t* val); +static uint64_t GetAvailablePhysicalMemory(); // ============================================================================ // Initialization / Shutdown @@ -333,13 +332,14 @@ static uint64_t GetTotalPhysicalMemory() size_t GCToOSInterface::GetVirtualMemoryLimit() { - // WASM linear memory has a hard engine-enforced ceiling, so report that - // maximum rather than an unbounded virtual address space. return GetVirtualMemoryMaxAddress(); } size_t GCToOSInterface::GetVirtualMemoryMaxAddress() { + // On WASM, linear-memory ceiling = max addressable. Both APIs return the + // same value because there is no separate per-process virtual address space + // limit beyond what the engine permits the linear memory to grow to. return GetTotalPhysicalMemory(); } @@ -350,7 +350,7 @@ static size_t GetRestrictedPhysicalMemoryLimit() return 0; } -bool GetPhysicalMemoryUsed(size_t* val) +static bool GetPhysicalMemoryUsed(size_t* val) { // __builtin_wasm_memory_size(0) returns count of 64KB WASM pages, not GC pages. // Compute in 64 bits so a legitimate 0-page memory is not conflated with wasm32 overflow. @@ -375,13 +375,10 @@ bool GetPhysicalMemoryUsed(size_t* val) uint64_t GCToOSInterface::GetPhysicalMemoryLimit(bool* is_restricted) { - size_t restricted_limit; if (is_restricted) *is_restricted = false; - restricted_limit = GetRestrictedPhysicalMemoryLimit(); - g_RestrictedPhysicalMemoryLimit = restricted_limit; - + size_t restricted_limit = GetRestrictedPhysicalMemoryLimit(); if (restricted_limit != 0 && restricted_limit != SIZE_T_MAX) { if (is_restricted) diff --git a/src/native/minipal/CMakeLists.txt b/src/native/minipal/CMakeLists.txt index d7f9ad5e2ab78a..f54026d84c2a5d 100644 --- a/src/native/minipal/CMakeLists.txt +++ b/src/native/minipal/CMakeLists.txt @@ -14,6 +14,7 @@ set(SOURCES utf8.c xoshiro128pp.c log.c + ospagesize.c ) # Provide an object library for scenarios where we ship static libraries diff --git a/src/native/minipal/ospagesize.c b/src/native/minipal/ospagesize.c new file mode 100644 index 00000000000000..c2713a3a364a18 --- /dev/null +++ b/src/native/minipal/ospagesize.c @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// On WASM and Windows the page size is a compile-time constant and minipal_getpagesize +// is defined inline in the header. This file provides the POSIX implementation, which +// must call getpagesize() once per process and cache the result. + +#if !defined(__wasm__) && !defined(HOST_WINDOWS) && !defined(_WIN32) + +#include +#include "ospagesize.h" + +int minipal_getpagesize(void) +{ + // Process-wide constant. Any thread that races to initialize the cache writes + // the same value, so no synchronization is required. + static int cached_page_size = 0; + int page_size = cached_page_size; + if (page_size == 0) + { + page_size = getpagesize(); + cached_page_size = page_size; + } + return page_size; +} + +#endif diff --git a/src/native/minipal/ospagesize.h b/src/native/minipal/ospagesize.h index a7d891f80a5764..8a5242fcc95e3d 100644 --- a/src/native/minipal/ospagesize.h +++ b/src/native/minipal/ospagesize.h @@ -4,42 +4,35 @@ #ifndef HAVE_MINIPAL_OSPAGESIZE_H #define HAVE_MINIPAL_OSPAGESIZE_H -#if !defined(__wasm__) && !defined(HOST_WINDOWS) && !defined(_WIN32) -#include -#endif - #ifdef __cplusplus extern "C" { #endif // Returns the OS page size in bytes. // -// Defined inline so that callers see a compile-time constant on platforms where -// the page size is fixed (Windows: 4KB, WASM: 16KB). This matters for the GC, +// On platforms where the page size is fixed (Windows: 4KB, WASM: 16KB) this is +// defined inline so callers see a compile-time constant. This matters for the GC, // which expects GetPageSize to fold into a constant for alignment math. +// +// On other platforms the value is queried from the OS once and cached; the +// definition lives in ospagesize.c so there is exactly one cache per process. +#if defined(__wasm__) static inline int minipal_getpagesize(void) { -#ifdef __wasm__ // The OS page size used by CoreCLR on WASM (16KB). // WASM has no hardware pages; getpagesize() returns the 64KB memory.grow granularity, // which is too coarse for GC alignment and thresholds. return 16 * 1024; +} #elif defined(HOST_WINDOWS) || defined(_WIN32) +static inline int minipal_getpagesize(void) +{ // The page size on Windows is 4KB and is not going to change. return 4 * 1024; +} #else - // Use volatile to prevent the compiler from reordering loads/stores of the cache, - // which could cause callers to observe a zero value after another thread initialized it. - static volatile int cached_page_size = 0; - int page_size = cached_page_size; - if (page_size == 0) - { - page_size = getpagesize(); - cached_page_size = page_size; - } - return page_size; +int minipal_getpagesize(void); #endif -} #ifdef __cplusplus } From 8c5f23cb3daba70b1b3531952f56aee2a98913af Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 30 Apr 2026 13:44:09 +0200 Subject: [PATCH 24/36] feedback --- src/coreclr/gc/init.cpp | 4 ++-- src/coreclr/gc/wasm/gcenv.cpp | 25 +++++++------------------ src/native/minipal/CMakeLists.txt | 8 +++++++- src/native/minipal/ospagesize.c | 19 ++++++++----------- src/native/minipal/ospagesize.h | 8 +++++--- 5 files changed, 29 insertions(+), 35 deletions(-) diff --git a/src/coreclr/gc/init.cpp b/src/coreclr/gc/init.cpp index 51aefa8d4c7c93..113415c2e48636 100644 --- a/src/coreclr/gc/init.cpp +++ b/src/coreclr/gc/init.cpp @@ -1298,11 +1298,11 @@ bool gc_heap::compute_hard_limit() // Large pages are pre-committed and cannot be decommitted, so they imply // never_decommit_p. On WASM, reserve == commit (posix_memalign allocates real // memory) and there is no way to give memory back to the engine. -#ifdef HOST_WASM +#if defined(HOST_WASM) || defined(__wasm__) never_decommit_p = true; #else never_decommit_p = use_large_pages_p; -#endif //HOST_WASM +#endif // defined(HOST_WASM) || defined(__wasm__) if (heap_hard_limit_oh[soh] || heap_hard_limit_oh[loh] || heap_hard_limit_oh[poh]) { diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 41c25453832a80..ff8a891ffc3073 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "config.gc.h" #include "common.h" @@ -68,7 +69,7 @@ static uint64_t GetAvailablePhysicalMemory(); bool GCToOSInterface::Initialize() { - g_pageSizeUnixInl = minipal_getpagesize(); + g_pageSizeUnixInl = (uint32_t)minipal_getpagesize(); // WASM is single-threaded g_totalCpuCount = 1; @@ -353,23 +354,11 @@ static size_t GetRestrictedPhysicalMemoryLimit() static bool GetPhysicalMemoryUsed(size_t* val) { // __builtin_wasm_memory_size(0) returns count of 64KB WASM pages, not GC pages. - // Compute in 64 bits so a legitimate 0-page memory is not conflated with wasm32 overflow. - uint64_t pages = static_cast(__builtin_wasm_memory_size(0)); - if (pages > (UINT64_MAX / WasmPageSize)) - { - *val = GetTotalPhysicalMemory(); - return true; - } - - uint64_t bytesUsed = pages * static_cast(WasmPageSize); - if (bytesUsed > static_cast(SIZE_MAX)) - { - // Clamp when the byte count cannot be represented in size_t on wasm32. - *val = GetTotalPhysicalMemory(); - return true; - } - - *val = static_cast(bytesUsed); + // On wasm32 the spec maximum is 65536 pages = exactly 4GiB, which overflows + // size_t (UINT32_MAX) by one. No engine actually permits allocating the full + // 4GiB, but compute in uint64_t and clamp to be safe. + uint64_t bytesUsed = static_cast(__builtin_wasm_memory_size(0)) * WasmPageSize; + *val = (bytesUsed > SIZE_MAX) ? GetTotalPhysicalMemory() : static_cast(bytesUsed); return true; } diff --git a/src/native/minipal/CMakeLists.txt b/src/native/minipal/CMakeLists.txt index f54026d84c2a5d..19d4181b9bd8d6 100644 --- a/src/native/minipal/CMakeLists.txt +++ b/src/native/minipal/CMakeLists.txt @@ -14,9 +14,15 @@ set(SOURCES utf8.c xoshiro128pp.c log.c - ospagesize.c ) +# ospagesize is provided inline in the header on Windows and WASM; the .c file +# only contains the POSIX implementation. Including it on those platforms would +# produce an empty translation unit (which some toolchains warn/error on). +if(NOT WIN32 AND NOT CLR_CMAKE_TARGET_ARCH_WASM) + list(APPEND SOURCES ospagesize.c) +endif() + # Provide an object library for scenarios where we ship static libraries include_directories(${CLR_SRC_NATIVE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/native/minipal/ospagesize.c b/src/native/minipal/ospagesize.c index c2713a3a364a18..0d171149e25a1c 100644 --- a/src/native/minipal/ospagesize.c +++ b/src/native/minipal/ospagesize.c @@ -1,27 +1,24 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// On WASM and Windows the page size is a compile-time constant and minipal_getpagesize -// is defined inline in the header. This file provides the POSIX implementation, which -// must call getpagesize() once per process and cache the result. - -#if !defined(__wasm__) && !defined(HOST_WINDOWS) && !defined(_WIN32) +// POSIX implementation of minipal_getpagesize. On WASM and Windows the page size +// is a compile-time constant and minipal_getpagesize is defined inline in the +// header; this file is excluded from the build on those platforms by +// src/native/minipal/CMakeLists.txt to avoid an empty translation unit. #include #include "ospagesize.h" -int minipal_getpagesize(void) +size_t minipal_getpagesize(void) { // Process-wide constant. Any thread that races to initialize the cache writes // the same value, so no synchronization is required. - static int cached_page_size = 0; - int page_size = cached_page_size; + static size_t cached_page_size = 0; + size_t page_size = cached_page_size; if (page_size == 0) { - page_size = getpagesize(); + page_size = (size_t)getpagesize(); cached_page_size = page_size; } return page_size; } - -#endif diff --git a/src/native/minipal/ospagesize.h b/src/native/minipal/ospagesize.h index 8a5242fcc95e3d..0cdcf689fd520c 100644 --- a/src/native/minipal/ospagesize.h +++ b/src/native/minipal/ospagesize.h @@ -4,6 +4,8 @@ #ifndef HAVE_MINIPAL_OSPAGESIZE_H #define HAVE_MINIPAL_OSPAGESIZE_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -17,7 +19,7 @@ extern "C" { // On other platforms the value is queried from the OS once and cached; the // definition lives in ospagesize.c so there is exactly one cache per process. #if defined(__wasm__) -static inline int minipal_getpagesize(void) +static inline size_t minipal_getpagesize(void) { // The OS page size used by CoreCLR on WASM (16KB). // WASM has no hardware pages; getpagesize() returns the 64KB memory.grow granularity, @@ -25,13 +27,13 @@ static inline int minipal_getpagesize(void) return 16 * 1024; } #elif defined(HOST_WINDOWS) || defined(_WIN32) -static inline int minipal_getpagesize(void) +static inline size_t minipal_getpagesize(void) { // The page size on Windows is 4KB and is not going to change. return 4 * 1024; } #else -int minipal_getpagesize(void); +size_t minipal_getpagesize(void); #endif #ifdef __cplusplus From 7b913c2e3a0b2544d039c0056470cf19b34d7b91 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 30 Apr 2026 14:36:57 +0200 Subject: [PATCH 25/36] fix --- src/coreclr/gc/wasm/CMakeLists.txt | 4 ++++ src/native/minipal/CMakeLists.txt | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/coreclr/gc/wasm/CMakeLists.txt b/src/coreclr/gc/wasm/CMakeLists.txt index f394f677de1ac8..0173a8e81fdf33 100644 --- a/src/coreclr/gc/wasm/CMakeLists.txt +++ b/src/coreclr/gc/wasm/CMakeLists.txt @@ -3,6 +3,10 @@ include_directories("../env") include_directories("..") include_directories("../unix") +# Generates config.gc.h in this subdirectory's binary dir (picked up via +# CMAKE_INCLUDE_CURRENT_DIR). Mirrors how gc/unix/CMakeLists.txt does it. +include(../unix/configure.cmake) + set(GC_PAL_SOURCES gcenv.cpp ../unix/events.cpp) diff --git a/src/native/minipal/CMakeLists.txt b/src/native/minipal/CMakeLists.txt index 19d4181b9bd8d6..af1990a9909fc4 100644 --- a/src/native/minipal/CMakeLists.txt +++ b/src/native/minipal/CMakeLists.txt @@ -18,8 +18,9 @@ set(SOURCES # ospagesize is provided inline in the header on Windows and WASM; the .c file # only contains the POSIX implementation. Including it on those platforms would -# produce an empty translation unit (which some toolchains warn/error on). -if(NOT WIN32 AND NOT CLR_CMAKE_TARGET_ARCH_WASM) +# produce a redefinition error (mono builds for wasi/browser set HOST_WASM but +# not CLR_CMAKE_TARGET_ARCH_WASM, so check both). +if(NOT WIN32 AND NOT CLR_CMAKE_TARGET_ARCH_WASM AND NOT HOST_WASM) list(APPEND SOURCES ospagesize.c) endif() From b31232bffed954fe937d05b932f61b79bfd30dcc Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Thu, 30 Apr 2026 14:51:36 +0200 Subject: [PATCH 26/36] Update src/coreclr/gc/wasm/gcenv.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/coreclr/gc/wasm/gcenv.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index ff8a891ffc3073..f60ac3f1846b70 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -358,7 +358,7 @@ static bool GetPhysicalMemoryUsed(size_t* val) // size_t (UINT32_MAX) by one. No engine actually permits allocating the full // 4GiB, but compute in uint64_t and clamp to be safe. uint64_t bytesUsed = static_cast(__builtin_wasm_memory_size(0)) * WasmPageSize; - *val = (bytesUsed > SIZE_MAX) ? GetTotalPhysicalMemory() : static_cast(bytesUsed); + *val = (bytesUsed > SIZE_MAX) ? SIZE_MAX : static_cast(bytesUsed); return true; } From 57d0e5cf402cfbb1e850f3ffa1c8bbf2c2369f19 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Fri, 1 May 2026 09:01:20 +0200 Subject: [PATCH 27/36] Update src/coreclr/gc/wasm/gcenv.cpp Co-authored-by: Jan Kotas --- src/coreclr/gc/wasm/gcenv.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index f60ac3f1846b70..7f6ab37c3324d5 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -435,7 +435,7 @@ void GCToOSInterface::GetMemoryStatus(uint64_t restricted_limit, uint32_t* memor *memory_load = load; if (available_page_file != nullptr) - *available_page_file = GetAvailablePageFile(); + *available_page_file = 0; // No page file on wasm } // ============================================================================ From cb2c789c22c2650574992101439795341aeabad6 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Fri, 1 May 2026 09:10:50 +0200 Subject: [PATCH 28/36] Update src/coreclr/gc/wasm/gcenv.cpp Co-authored-by: Jan Kotas --- src/coreclr/gc/wasm/gcenv.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 7f6ab37c3324d5..2b485e5b1cf665 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -46,7 +46,7 @@ static const size_t WasmPageSize = 64 * 1024; // The cached total number of CPUs that can be used in the OS. // WASM is single-threaded, so this is always 1. -uint32_t g_totalCpuCount = 0; +uint32_t g_totalCpuCount = 1; uint32_t g_pageSizeUnixInl = 0; From f716254d2daeb1bc2c0f9a002a3b6e16585022e5 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Fri, 1 May 2026 09:11:20 +0200 Subject: [PATCH 29/36] Update src/coreclr/gc/wasm/gcenv.cpp Co-authored-by: Jan Kotas --- src/coreclr/gc/wasm/gcenv.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 2b485e5b1cf665..f97abeb2ce9090 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -367,14 +367,6 @@ uint64_t GCToOSInterface::GetPhysicalMemoryLimit(bool* is_restricted) if (is_restricted) *is_restricted = false; - size_t restricted_limit = GetRestrictedPhysicalMemoryLimit(); - if (restricted_limit != 0 && restricted_limit != SIZE_T_MAX) - { - if (is_restricted) - *is_restricted = true; - return restricted_limit; - } - return g_totalPhysicalMemSize; } From 07c2288444282ad923f223e2a04e66f77d052403 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Fri, 1 May 2026 09:11:51 +0200 Subject: [PATCH 30/36] Update src/coreclr/gc/wasm/gcenv.cpp Co-authored-by: Jan Kotas --- src/coreclr/gc/wasm/gcenv.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index f97abeb2ce9090..8d746afbe21e68 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -364,6 +364,8 @@ static bool GetPhysicalMemoryUsed(size_t* val) uint64_t GCToOSInterface::GetPhysicalMemoryLimit(bool* is_restricted) { + // No restricted-memory mode on WASM. The linear memory ceiling enforced by the + // engine is the only hard cap; we don't auto-derive a GC heap_hard_limit from it. if (is_restricted) *is_restricted = false; From fd1ddd2bd1f1d64ca1fa7a9af1d0ddc7a1ddceb7 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 1 May 2026 09:45:25 +0200 Subject: [PATCH 31/36] feedback --- src/coreclr/gc/wasm/gcenv.cpp | 64 ++++++++++------------------------- 1 file changed, 18 insertions(+), 46 deletions(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 8d746afbe21e68..93e8b578d5c26d 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -41,17 +41,11 @@ #endif // WASM memory.grow operates in 64KB pages. This is distinct from OS_PAGE_SIZE -// (the GC's page granularity), which we set to 16KB below. +// (the GC's page granularity), which is 16KB on WASM (via minipal_getpagesize). static const size_t WasmPageSize = 64 * 1024; -// The cached total number of CPUs that can be used in the OS. -// WASM is single-threaded, so this is always 1. -uint32_t g_totalCpuCount = 1; - uint32_t g_pageSizeUnixInl = 0; -AffinitySet g_processAffinitySet; - // NUMA globals - WASM has no NUMA support but these are referenced by the GC. extern "C" int g_highestNumaNode = 0; extern "C" bool g_numaAvailable = false; @@ -59,7 +53,6 @@ extern "C" bool g_numaAvailable = false; static int64_t g_totalPhysicalMemSize = 0; // Forward declarations -static size_t GetRestrictedPhysicalMemoryLimit(); static bool GetPhysicalMemoryUsed(size_t* val); static uint64_t GetAvailablePhysicalMemory(); @@ -71,16 +64,6 @@ bool GCToOSInterface::Initialize() { g_pageSizeUnixInl = (uint32_t)minipal_getpagesize(); - // WASM is single-threaded - g_totalCpuCount = 1; - - if (!g_processAffinitySet.Initialize(1)) - { - return false; - } - - g_processAffinitySet.Add(0); - // Get the physical memory size #ifdef TARGET_BROWSER g_totalPhysicalMemSize = (int64_t)emscripten_get_heap_max(); @@ -314,7 +297,7 @@ const AffinitySet* GCToOSInterface::SetGCThreadsAffinitySet(uintptr_t configAffi { (void)configAffinityMask; (void)configAffinitySet; - return &g_processAffinitySet; + return nullptr; // only for multiple heaps } // ============================================================================ @@ -341,23 +324,17 @@ size_t GCToOSInterface::GetVirtualMemoryMaxAddress() // On WASM, linear-memory ceiling = max addressable. Both APIs return the // same value because there is no separate per-process virtual address space // limit beyond what the engine permits the linear memory to grow to. - return GetTotalPhysicalMemory(); -} - -static size_t GetRestrictedPhysicalMemoryLimit() -{ - // No restricted-memory mode on WASM. The linear memory ceiling enforced by the - // engine is the only hard cap; we don't auto-derive a GC heap_hard_limit from it. - return 0; + return g_totalPhysicalMemSize; } static bool GetPhysicalMemoryUsed(size_t* val) { - // __builtin_wasm_memory_size(0) returns count of 64KB WASM pages, not GC pages. - // On wasm32 the spec maximum is 65536 pages = exactly 4GiB, which overflows - // size_t (UINT32_MAX) by one. No engine actually permits allocating the full - // 4GiB, but compute in uint64_t and clamp to be safe. +#ifdef TARGET_BROWSER + uint64_t bytesUsed = static_cast(emscripten_get_heap_size()); +#else // TARGET_WASI uint64_t bytesUsed = static_cast(__builtin_wasm_memory_size(0)) * WasmPageSize; +#endif + *val = (bytesUsed > SIZE_MAX) ? SIZE_MAX : static_cast(bytesUsed); return true; } @@ -375,21 +352,12 @@ uint64_t GCToOSInterface::GetPhysicalMemoryLimit(bool* is_restricted) static uint64_t GetAvailablePhysicalMemory() { #ifdef TARGET_BROWSER - return emscripten_get_heap_max() - emscripten_get_heap_size(); + uint64_t bytesUsed = static_cast(emscripten_get_heap_size()); #else // TARGET_WASI - // Best approximation: total minus currently used. - // __builtin_wasm_memory_size(0) returns count of 64KB WASM pages, not GC pages. - // Compute in 64 bits so a legitimate 0-page memory is not conflated with wasm32 overflow. - uint64_t used = static_cast(__builtin_wasm_memory_size(0)) * static_cast(WasmPageSize); - uint64_t total = GetTotalPhysicalMemory(); - return (total > used) ? (total - used) : 0; + uint64_t bytesUsed = static_cast(__builtin_wasm_memory_size(0)) * WasmPageSize; #endif -} - -static uint64_t GetAvailablePageFile() -{ - // No swap on WASM - return 0; + uint64_t total = g_totalPhysicalMemSize; + return (total > bytesUsed) ? (total - bytesUsed) : 0; } void GCToOSInterface::GetMemoryStatus(uint64_t restricted_limit, uint32_t* memory_load, uint64_t* available_physical, uint64_t* available_page_file) @@ -405,6 +373,10 @@ void GCToOSInterface::GetMemoryStatus(uint64_t restricted_limit, uint32_t* memor available = restricted_limit > used ? restricted_limit - used : 0; load = (uint32_t)(((float)used * 100) / (float)restricted_limit); } + else + { + available = GetAvailablePhysicalMemory(); + } } else { @@ -457,12 +429,12 @@ uint64_t GCToOSInterface::GetLowPrecisionTimeStamp() uint32_t GCToOSInterface::GetTotalProcessorCount() { - return g_totalCpuCount; + return 1; } uint32_t GCToOSInterface::GetMaxProcessorCount() { - return (uint32_t)g_processAffinitySet.MaxCpuCount(); + return 1; } bool GCToOSInterface::CanEnableGCNumaAware() From e2eb693ae45003c9845737669809284e814b7202 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 1 May 2026 10:06:36 +0200 Subject: [PATCH 32/36] fix --- src/coreclr/gc/wasm/gcenv.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 93e8b578d5c26d..a5211db2233815 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -44,6 +44,8 @@ // (the GC's page granularity), which is 16KB on WASM (via minipal_getpagesize). static const size_t WasmPageSize = 64 * 1024; +uint32_t g_totalCpuCount = 1; + uint32_t g_pageSizeUnixInl = 0; // NUMA globals - WASM has no NUMA support but these are referenced by the GC. From d7f4c5029a51e9a07528400522525c0b50cffe3d Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 1 May 2026 10:33:49 +0200 Subject: [PATCH 33/36] inline constant fold --- src/coreclr/gc/env/gcenv.unix.inl | 7 ++++++- src/coreclr/gc/env/gcenv.windows.inl | 3 ++- src/coreclr/gc/wasm/gcenv.cpp | 5 ----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/coreclr/gc/env/gcenv.unix.inl b/src/coreclr/gc/env/gcenv.unix.inl index 0e221e8a0a9596..76c676f60d3bee 100644 --- a/src/coreclr/gc/env/gcenv.unix.inl +++ b/src/coreclr/gc/env/gcenv.unix.inl @@ -6,14 +6,19 @@ #include "gcenv.os.h" -extern uint32_t g_pageSizeUnixInl; +#include #define OS_PAGE_SIZE GCToOSInterface::GetPageSize() #ifndef DACCESS_COMPILE FORCEINLINE size_t GCToOSInterface::GetPageSize() { +#if defined(__wasm__) + return minipal_getpagesize(); +#else + extern uint32_t g_pageSizeUnixInl; return g_pageSizeUnixInl; +#endif // defined(__wasm__) } #endif // DACCESS_COMPILE diff --git a/src/coreclr/gc/env/gcenv.windows.inl b/src/coreclr/gc/env/gcenv.windows.inl index 8ac51a19775076..84bcedb3c500b3 100644 --- a/src/coreclr/gc/env/gcenv.windows.inl +++ b/src/coreclr/gc/env/gcenv.windows.inl @@ -6,12 +6,13 @@ #include "gcenv.os.h" +#include #define OS_PAGE_SIZE GCToOSInterface::GetPageSize() FORCEINLINE size_t GCToOSInterface::GetPageSize() { - return 0x1000; + return minipal_getpagesize(); } #endif // __GCENV_WINDOWS_INL__ diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index a5211db2233815..246609d218d768 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -32,7 +32,6 @@ #include #include #include -#include #include "globals.h" @@ -46,8 +45,6 @@ static const size_t WasmPageSize = 64 * 1024; uint32_t g_totalCpuCount = 1; -uint32_t g_pageSizeUnixInl = 0; - // NUMA globals - WASM has no NUMA support but these are referenced by the GC. extern "C" int g_highestNumaNode = 0; extern "C" bool g_numaAvailable = false; @@ -64,8 +61,6 @@ static uint64_t GetAvailablePhysicalMemory(); bool GCToOSInterface::Initialize() { - g_pageSizeUnixInl = (uint32_t)minipal_getpagesize(); - // Get the physical memory size #ifdef TARGET_BROWSER g_totalPhysicalMemSize = (int64_t)emscripten_get_heap_max(); From b752041dd8df83a7c47e594f40c40453de9acd36 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Fri, 1 May 2026 21:09:17 +0200 Subject: [PATCH 34/36] Update src/coreclr/gc/wasm/gcenv.cpp Co-authored-by: Jan Kotas --- src/coreclr/gc/wasm/gcenv.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 246609d218d768..9c506098651276 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -45,10 +45,6 @@ static const size_t WasmPageSize = 64 * 1024; uint32_t g_totalCpuCount = 1; -// NUMA globals - WASM has no NUMA support but these are referenced by the GC. -extern "C" int g_highestNumaNode = 0; -extern "C" bool g_numaAvailable = false; - static int64_t g_totalPhysicalMemSize = 0; // Forward declarations From 4616dcc0be0de5706e08069dbd2ef5b68ba9f0da Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Fri, 1 May 2026 21:09:59 +0200 Subject: [PATCH 35/36] Update src/coreclr/gc/wasm/gcenv.cpp Co-authored-by: Jan Kotas --- src/coreclr/gc/wasm/gcenv.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp index 9c506098651276..c2c959b140c8da 100644 --- a/src/coreclr/gc/wasm/gcenv.cpp +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -297,16 +297,6 @@ const AffinitySet* GCToOSInterface::SetGCThreadsAffinitySet(uintptr_t configAffi // Virtual / Physical Memory Limits // ============================================================================ -static uint64_t GetTotalPhysicalMemory() -{ -#ifdef TARGET_BROWSER - return emscripten_get_heap_max(); -#else // TARGET_WASI - // WASI doesn't have an API to query max memory. - return 2ULL * 1024 * 1024 * 1024; // 2GB -#endif -} - size_t GCToOSInterface::GetVirtualMemoryLimit() { return GetVirtualMemoryMaxAddress(); From d60a5bbfa3d69519c28e608bbedac2c7ded53114 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Sat, 2 May 2026 09:36:20 +0200 Subject: [PATCH 36/36] Update src/native/minipal/ospagesize.h Co-authored-by: Adeel Mujahid <3840695+am11@users.noreply.github.com> --- src/native/minipal/ospagesize.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/native/minipal/ospagesize.h b/src/native/minipal/ospagesize.h index 0cdcf689fd520c..ffc1db642121bf 100644 --- a/src/native/minipal/ospagesize.h +++ b/src/native/minipal/ospagesize.h @@ -18,7 +18,7 @@ extern "C" { // // On other platforms the value is queried from the OS once and cached; the // definition lives in ospagesize.c so there is exactly one cache per process. -#if defined(__wasm__) +#if defined(TARGET_WASM) static inline size_t minipal_getpagesize(void) { // The OS page size used by CoreCLR on WASM (16KB). @@ -26,7 +26,7 @@ static inline size_t minipal_getpagesize(void) // which is too coarse for GC alignment and thresholds. return 16 * 1024; } -#elif defined(HOST_WINDOWS) || defined(_WIN32) +#elif defined(HOST_WINDOWS) static inline size_t minipal_getpagesize(void) { // The page size on Windows is 4KB and is not going to change.