From ba59eeacaa45b1842e91073c3e40f4589479f933 Mon Sep 17 00:00:00 2001 From: Jan Vorlicek Date: Fri, 17 Jul 2026 20:54:56 +0200 Subject: [PATCH 1/4] Respect VmallocTotal from /proc/meminfo as virtual address space limit Add minipal_get_virtual_address_space_limit() in src/native/minipal/vmlimit.c that computes the effective virtual address space limit by taking the minimum of RLIMIT_AS and VmallocTotal from /proc/meminfo. The result is cached since these values don't change during process lifetime. All existing RLIMIT_AS call sites in coreclr (GC, PAL, minipal doublemapping) now use this single consolidated function instead of duplicating the getrlimit logic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/gc/unix/cgroup.cpp | 13 +--- src/coreclr/gc/unix/gcenv.unix.cpp | 15 ++-- src/coreclr/minipal/Unix/doublemapping.cpp | 17 ++--- src/coreclr/pal/src/map/virtual.cpp | 26 +++---- src/native/minipal/CMakeLists.txt | 2 +- src/native/minipal/vmlimit.c | 87 ++++++++++++++++++++++ src/native/minipal/vmlimit.h | 25 +++++++ 7 files changed, 144 insertions(+), 41 deletions(-) create mode 100644 src/native/minipal/vmlimit.c create mode 100644 src/native/minipal/vmlimit.h diff --git a/src/coreclr/gc/unix/cgroup.cpp b/src/coreclr/gc/unix/cgroup.cpp index 6ab2ba7cd28ef2..a5df67ac1a22b1 100644 --- a/src/coreclr/gc/unix/cgroup.cpp +++ b/src/coreclr/gc/unix/cgroup.cpp @@ -21,10 +21,10 @@ Module Name: #include #include #include -#include #include #include #include +#include #include "config.gc.h" @@ -579,14 +579,9 @@ size_t GetRestrictedPhysicalMemoryLimit() return 0; } - struct rlimit curr_rlimit; - size_t rlimit_soft_limit = (size_t)RLIM_INFINITY; - if (getrlimit(RLIMIT_AS, &curr_rlimit) == 0) - { - rlimit_soft_limit = curr_rlimit.rlim_cur; - } - physical_memory_limit = (physical_memory_limit < rlimit_soft_limit) ? - physical_memory_limit : rlimit_soft_limit; + size_t vm_limit = minipal_get_virtual_address_space_limit(); + physical_memory_limit = (physical_memory_limit < vm_limit) ? + physical_memory_limit : vm_limit; // Ensure that limit is not greater than real memory size long pages = sysconf(_SC_PHYS_PAGES); diff --git a/src/coreclr/gc/unix/gcenv.unix.cpp b/src/coreclr/gc/unix/gcenv.unix.cpp index 6689618c705512..31594da6d1757d 100644 --- a/src/coreclr/gc/unix/gcenv.unix.cpp +++ b/src/coreclr/gc/unix/gcenv.unix.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #if HAVE_SWAPCTL @@ -1007,13 +1008,11 @@ static size_t GetCurrentVirtualMemorySize() // non zero if it has succeeded, GetVirtualMemoryMaxAddress() if not available size_t GCToOSInterface::GetVirtualMemoryLimit() { -#ifdef RLIMIT_AS - rlimit addressSpaceLimit; - if ((getrlimit(RLIMIT_AS, &addressSpaceLimit) == 0) && (addressSpaceLimit.rlim_cur != RLIM_INFINITY)) + size_t addressSpaceLimit = minipal_get_virtual_address_space_limit(); + if (addressSpaceLimit != SIZE_MAX) { - return addressSpaceLimit.rlim_cur; + return addressSpaceLimit; } -#endif // RLIMIT_AS // No virtual memory limit return GetVirtualMemoryMaxAddress(); @@ -1239,15 +1238,15 @@ void GCToOSInterface::GetMemoryStatus(uint64_t restricted_limit, uint32_t* memor } #if HAVE_PROCFS_STATM - rlimit addressSpaceLimit; - if ((getrlimit(RLIMIT_AS, &addressSpaceLimit) == 0) && (addressSpaceLimit.rlim_cur != RLIM_INFINITY)) + size_t virtualAddressSpaceLimit = minipal_get_virtual_address_space_limit(); + if (virtualAddressSpaceLimit != SIZE_MAX) { // If there is virtual address space limit set, compute virtual memory load and change // the load to this one in case it is higher than the physical memory load size_t used_virtual = GetCurrentVirtualMemorySize(); if (used_virtual != (size_t)-1) { - uint32_t load_virtual = (uint32_t)(((float)used_virtual * 100) / (float)addressSpaceLimit.rlim_cur); + uint32_t load_virtual = (uint32_t)(((float)used_virtual * 100) / (float)virtualAddressSpaceLimit); if (load_virtual > load) { load = load_virtual; diff --git a/src/coreclr/minipal/Unix/doublemapping.cpp b/src/coreclr/minipal/Unix/doublemapping.cpp index 997d7a523e81c8..52da050caaf613 100644 --- a/src/coreclr/minipal/Unix/doublemapping.cpp +++ b/src/coreclr/minipal/Unix/doublemapping.cpp @@ -25,6 +25,7 @@ #endif // TARGET_LINUX && !MFD_CLOEXEC #include "minipal.h" #include "minipal/cpufeatures.h" +#include #ifndef TARGET_APPLE #if !defined(TARGET_WASI) @@ -109,19 +110,15 @@ bool VMToOSInterface::CreateDoubleMemoryMapper(void** pHandle, size_t *pMaxExecu // Clip the maximum double mapped memory size to 1/4 of the virtual address space limit. // When such a limit is set, GC reserves 1/2 of it, so we need to leave something // for the rest of the process. -#ifdef RLIMIT_AS - // OpenBSD has no address-space rlimit (RLIMIT_AS), so this clipping is skipped there. - // WASI also has no RLIMIT_AS. - struct rlimit virtualAddressSpaceLimit; - if ((getrlimit(RLIMIT_AS, &virtualAddressSpaceLimit) == 0) && (virtualAddressSpaceLimit.rlim_cur != RLIM_INFINITY)) - { - virtualAddressSpaceLimit.rlim_cur /= 4; - if (maxDoubleMappedMemorySize > virtualAddressSpaceLimit.rlim_cur) + size_t virtualAddressSpaceLimit = minipal_get_virtual_address_space_limit(); + if (virtualAddressSpaceLimit != SIZE_MAX) + { + virtualAddressSpaceLimit /= 4; + if (maxDoubleMappedMemorySize > virtualAddressSpaceLimit) { - maxDoubleMappedMemorySize = virtualAddressSpaceLimit.rlim_cur; + maxDoubleMappedMemorySize = virtualAddressSpaceLimit; } } -#endif // RLIMIT_AS // Clip the maximum double mapped memory size to the file size limit #ifdef RLIMIT_FSIZE diff --git a/src/coreclr/pal/src/map/virtual.cpp b/src/coreclr/pal/src/map/virtual.cpp index caf5388b1b37c9..ef03fc61d27d69 100644 --- a/src/coreclr/pal/src/map/virtual.cpp +++ b/src/coreclr/pal/src/map/virtual.cpp @@ -31,7 +31,6 @@ SET_DEFAULT_DEBUG_CHANNEL(VIRTUAL); // some headers have code with asserts, so d #include "common.h" #include -#include #include #include #include @@ -41,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 @@ -1616,20 +1616,15 @@ void ExecutableMemoryAllocator::TryReserveInitialMemory() int32_t sizeOfAllocation = MaxExecutableMemorySizeNearCoreClr; int32_t initialReserveLimit = -1; #if defined(TARGET_WASI) - // WASI has neither RLIMIT_AS nor RLIMIT_DATA nor RLIM_INFINITY; wasm32 has a - // hard 4 GiB limit and no separate VM accounting. Skip the rlimit-based - // sizing and let sizeOfAllocation stay at MaxExecutableMemorySizeNearCoreClr. + // WASI has a hard 4 GiB limit and no separate VM accounting. + // Skip the virtual address space limit check and let sizeOfAllocation stay + // at MaxExecutableMemorySizeNearCoreClr. #else -#ifdef RLIMIT_AS - int addressSpace = RLIMIT_AS; -#else - int addressSpace = RLIMIT_DATA; -#endif - rlimit addressSpaceLimit; - if ((getrlimit(addressSpace, &addressSpaceLimit) == 0) && (addressSpaceLimit.rlim_cur != RLIM_INFINITY)) + size_t virtualAddressSpaceLimit = minipal_get_virtual_address_space_limit(); + if (virtualAddressSpaceLimit != SIZE_MAX) { // By default reserve max 20% of the available virtual address space - rlim_t initialExecMemoryPerc = 20; + size_t initialExecMemoryPerc = 20; CLRConfigNoCache defInitialExecMemoryPerc = CLRConfigNoCache::Get("InitialExecMemoryPercent", /*noprefix*/ false, &getenv); if (defInitialExecMemoryPerc.IsSet()) { @@ -1640,7 +1635,12 @@ void ExecutableMemoryAllocator::TryReserveInitialMemory() } } - initialReserveLimit = addressSpaceLimit.rlim_cur * initialExecMemoryPerc / 100; + size_t computedLimit = virtualAddressSpaceLimit * initialExecMemoryPerc / 100; + if (computedLimit > INT32_MAX) + { + computedLimit = INT32_MAX; + } + initialReserveLimit = (int32_t)computedLimit; if (initialReserveLimit < sizeOfAllocation) { sizeOfAllocation = initialReserveLimit; diff --git a/src/native/minipal/CMakeLists.txt b/src/native/minipal/CMakeLists.txt index 2aab34901c228c..55ca44af357db5 100644 --- a/src/native/minipal/CMakeLists.txt +++ b/src/native/minipal/CMakeLists.txt @@ -29,7 +29,7 @@ if(NOT WIN32 AND NOT HOST_WASM AND NOT (CLR_CMAKE_TARGET_ARCH_WASM AND NOT CLR_C endif() if(CLR_CMAKE_HOST_UNIX) - list(APPEND SOURCES cpucount.c) + list(APPEND SOURCES cpucount.c vmlimit.c) endif() # Provide an object library for scenarios where we ship static libraries diff --git a/src/native/minipal/vmlimit.c b/src/native/minipal/vmlimit.c new file mode 100644 index 00000000000000..fab718bf1bb164 --- /dev/null +++ b/src/native/minipal/vmlimit.c @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "vmlimit.h" + +#include +#include +#include + +#include + +#if !defined(__APPLE__) && !defined(__HAIKU__) +static size_t get_vmalloc_total(void) +{ + FILE* memInfoFile = fopen("/proc/meminfo", "r"); + if (memInfoFile == NULL) + { + return SIZE_MAX; + } + + size_t result = SIZE_MAX; + char* line = NULL; + size_t lineLen = 0; + + while (getline(&line, &lineLen, memInfoFile) != -1) + { + uint64_t value; + char units = '\0'; + if (sscanf(line, "VmallocTotal: %" SCNu64 " %cB", &value, &units) >= 1) + { + uint64_t multiplier = 1; + switch (units) + { + case 'g': + case 'G': multiplier = 1024 * 1024 * 1024; break; + case 'm': + case 'M': multiplier = 1024 * 1024; break; + case 'k': + case 'K': multiplier = 1024; break; + } + + uint64_t total = value * multiplier; + result = (total < (uint64_t)SIZE_MAX) ? (size_t)total : SIZE_MAX; + break; + } + } + + free(line); + fclose(memInfoFile); + + return result; +} +#endif // !defined(__APPLE__) && !defined(__HAIKU__) + +size_t minipal_get_virtual_address_space_limit(void) +{ + // Cache the result since the values don't change during process lifetime. + // Use 0 as the "not yet computed" sentinel since a zero limit is not a valid result. + static volatile size_t cached_limit = 0; + + if (cached_limit != 0) + { + return cached_limit; + } + + size_t limit = SIZE_MAX; + +#ifdef RLIMIT_AS + struct rlimit addressSpaceLimit; + if ((getrlimit(RLIMIT_AS, &addressSpaceLimit) == 0) && (addressSpaceLimit.rlim_cur != RLIM_INFINITY)) + { + limit = addressSpaceLimit.rlim_cur; + } +#endif + +#if !defined(__APPLE__) && !defined(__HAIKU__) + size_t vmallocTotal = get_vmalloc_total(); + if (vmallocTotal < limit) + { + limit = vmallocTotal; + } +#endif + + cached_limit = limit; + + return limit; +} diff --git a/src/native/minipal/vmlimit.h b/src/native/minipal/vmlimit.h new file mode 100644 index 00000000000000..71a9a47e4d44f7 --- /dev/null +++ b/src/native/minipal/vmlimit.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 HAVE_MINIPAL_VMLIMIT_H +#define HAVE_MINIPAL_VMLIMIT_H + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif // __cplusplus + +// Returns the effective virtual address space limit for the current process +// by taking the minimum of RLIMIT_AS (if set and finite) and the VmallocTotal +// value from /proc/meminfo (on Linux). +// Returns SIZE_MAX if neither limit is available or applicable. +size_t minipal_get_virtual_address_space_limit(void); + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // HAVE_MINIPAL_VMLIMIT_H From 6f9ac8258f2cae4c6abbb571f75617631af41cb5 Mon Sep 17 00:00:00 2001 From: Jan Vorlicek Date: Mon, 20 Jul 2026 17:04:28 +0200 Subject: [PATCH 2/4] PR feedback - change ifdef for using the /proc/meminfo --- src/coreclr/gc/unix/gcenv.unix.cpp | 4 ++-- src/native/minipal/vmlimit.c | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/coreclr/gc/unix/gcenv.unix.cpp b/src/coreclr/gc/unix/gcenv.unix.cpp index 31594da6d1757d..b3f695f80023a9 100644 --- a/src/coreclr/gc/unix/gcenv.unix.cpp +++ b/src/coreclr/gc/unix/gcenv.unix.cpp @@ -823,7 +823,7 @@ static uint64_t GetMemorySizeMultiplier(char units) return 1; } -#if !defined(__APPLE__) && !defined(__HAIKU__) +#ifdef TARGET_LINUX // 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) @@ -856,7 +856,7 @@ static bool ReadMemAvailable(uint64_t* memAvailable) return foundMemAvailable; } -#endif // !defined(__APPLE__) && !defined(__HAIKU__) +#endif // TARGET_LINUX // Get size of the largest cache on the processor die // Parameters: diff --git a/src/native/minipal/vmlimit.c b/src/native/minipal/vmlimit.c index fab718bf1bb164..cde29966259b6e 100644 --- a/src/native/minipal/vmlimit.c +++ b/src/native/minipal/vmlimit.c @@ -9,7 +9,7 @@ #include -#if !defined(__APPLE__) && !defined(__HAIKU__) +#ifdef TARGET_LINUX static size_t get_vmalloc_total(void) { FILE* memInfoFile = fopen("/proc/meminfo", "r"); @@ -50,7 +50,7 @@ static size_t get_vmalloc_total(void) return result; } -#endif // !defined(__APPLE__) && !defined(__HAIKU__) +#endif // TARGET_LINUX size_t minipal_get_virtual_address_space_limit(void) { @@ -73,13 +73,13 @@ size_t minipal_get_virtual_address_space_limit(void) } #endif -#if !defined(__APPLE__) && !defined(__HAIKU__) +#ifndef TARGET_LINUX size_t vmallocTotal = get_vmalloc_total(); if (vmallocTotal < limit) { limit = vmallocTotal; } -#endif +#endif // TARGET_LINUX cached_limit = limit; From 7344c7906e8a0c3eaca8fb82040d94f7deb735a5 Mon Sep 17 00:00:00 2001 From: Jan Vorlicek Date: Mon, 20 Jul 2026 18:26:57 +0200 Subject: [PATCH 3/4] Fix ifdefs --- src/native/minipal/vmlimit.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/native/minipal/vmlimit.c b/src/native/minipal/vmlimit.c index cde29966259b6e..562a3bfe86383a 100644 --- a/src/native/minipal/vmlimit.c +++ b/src/native/minipal/vmlimit.c @@ -65,15 +65,21 @@ size_t minipal_get_virtual_address_space_limit(void) size_t limit = SIZE_MAX; +#if defined(RLIMIT_AS) || defined(RLIMIT_DATA) #ifdef RLIMIT_AS + int addressSpace = RLIMIT_AS; +#else + int addressSpace = RLIMIT_DATA; +#endif + struct rlimit addressSpaceLimit; - if ((getrlimit(RLIMIT_AS, &addressSpaceLimit) == 0) && (addressSpaceLimit.rlim_cur != RLIM_INFINITY)) + if ((getrlimit(addressSpace, &addressSpaceLimit) == 0) && (addressSpaceLimit.rlim_cur != RLIM_INFINITY)) { limit = addressSpaceLimit.rlim_cur; } -#endif +#endif // RLIMIT_AS || RLIMIT_DATA -#ifndef TARGET_LINUX +#ifdef TARGET_LINUX size_t vmallocTotal = get_vmalloc_total(); if (vmallocTotal < limit) { From ebb6e022ffadd8412056aca691f0e324dea5eab7 Mon Sep 17 00:00:00 2001 From: Jan Vorlicek Date: Mon, 20 Jul 2026 19:10:17 +0200 Subject: [PATCH 4/4] PR feedback - fix comment --- src/native/minipal/vmlimit.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/native/minipal/vmlimit.h b/src/native/minipal/vmlimit.h index 71a9a47e4d44f7..7c7af1b6cbc8c5 100644 --- a/src/native/minipal/vmlimit.h +++ b/src/native/minipal/vmlimit.h @@ -13,8 +13,8 @@ extern "C" #endif // __cplusplus // Returns the effective virtual address space limit for the current process -// by taking the minimum of RLIMIT_AS (if set and finite) and the VmallocTotal -// value from /proc/meminfo (on Linux). +// by taking the minimum of RLIMIT_AS / RLIMIT_DATA (if set and finite) and +// the VmallocTotal value from /proc/meminfo (on Linux). // Returns SIZE_MAX if neither limit is available or applicable. size_t minipal_get_virtual_address_space_limit(void);