From 40d1a36a1678d661303f53a5d0a37c4ed38c97af Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Sun, 23 Aug 2026 02:22:36 +0200 Subject: [PATCH 1/7] Remove the uniprocessor (non-TLS) allocation helpers Removes the Windows x86/x64 uniprocessor allocation helpers (RhpNewFast_UP and friends), which allocate from a single global ee_alloc_context under a hand-rolled spin lock instead of the thread-local one, purely to skip the TLS access. * Barely reachable. It needs GetSystemInfo() to report exactly one processor, plus workstation GC and no CPU groups. That check never consults GetCurrentProcessCpuCount(), so container CPU limits (--cpus=1, job-object rate control) and process affinity don't trigger it - only a genuinely 1-vCPU machine does. * Known broken. GCStress is unsafe on this path and can corrupt the heap (#7670, filed in 2017, never fixed), papered over with assert(UseThreadAllocationContexts()) in four places. RuntimeTypeHandle::InternalAllocNoChecks_FastPath also just bails out when it's active. * Untested. GCUseGlobalAllocationContext is #if DEBUG only and set nowhere in the repo, and no Windows CI queue is single-core. * Not free. ~370 lines of hand-written x86/x64 asm, a lock whose asm half uses a non-atomic inc, and special cases in alloc context enumeration, GCStress and suspension. * Little to gain. The TLS access it avoids is 5 inline instructions on x64 and 4 on x86 (INLINE_GET_ALLOC_CONTEXT_BASE) - and it replaces them with a read-modify-write on a shared global plus an unlock store. Everything now goes through the thread allocation context. GCUseGlobalAllocationContext is removed as well. g_global_alloc_context itself stays, permanently zeroed, so that the GlobalAllocContext global required by version c1 of the GC data contract keeps resolving. Diagnostics are otherwise untouched: DAC, cDAC and SOS now consistently report an empty global allocation context, which is what the runtime always has. Follow-up to #115102. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/inc/clrconfigvalues.h | 7 - src/coreclr/vm/amd64/AllocSlow.asm | 174 --------------------- src/coreclr/vm/gccover.cpp | 6 - src/coreclr/vm/gcenv.ee.cpp | 35 ++--- src/coreclr/vm/gcheaputilities.cpp | 16 +- src/coreclr/vm/gcheaputilities.h | 19 +-- src/coreclr/vm/gchelpers.cpp | 120 +------------- src/coreclr/vm/gcstress.h | 3 - src/coreclr/vm/i386/AllocSlow.asm | 198 ------------------------ src/coreclr/vm/i386/jitinterfacex86.cpp | 2 - src/coreclr/vm/jitinterface.h | 7 - src/coreclr/vm/jitinterfacegen.cpp | 34 +--- src/coreclr/vm/runtimehandles.cpp | 5 - src/coreclr/vm/threadsuspend.cpp | 3 - 14 files changed, 28 insertions(+), 601 deletions(-) diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index d4303963fc69db..d6badce75704c0 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -262,13 +262,6 @@ RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_HeapVerify, W("HeapVerify"), 0, "When set v RETAIL_CONFIG_DWORD_INFO(EXTERNAL_GCCpuGroup, W("GCCpuGroup"), 0, "Specifies if to enable GC to support CPU groups") RETAIL_CONFIG_STRING_INFO(EXTERNAL_GCName, W("GCName"), "") RETAIL_CONFIG_STRING_INFO(EXTERNAL_GCPath, W("GCPath"), "") -/** - * This flag allows us to force the runtime to use global allocation context on Windows x86/amd64 instead of thread allocation context just for testing purpose. - * The flag is unsafe for a subtle reason. Although the access to the g_global_alloc_context is protected under a lock. The implementation of - * that lock in the JIT helpers are not multi-core safe (in particular, it used and inc instruction without using the LOCK prefix). This is - * only useful for ad-hoc testing. - */ -CONFIG_DWORD_INFO(INTERNAL_GCUseGlobalAllocationContext, W("GCUseGlobalAllocationContext"), 0, "Force using the global allocation context for testing only") /// /// JIT diff --git a/src/coreclr/vm/amd64/AllocSlow.asm b/src/coreclr/vm/amd64/AllocSlow.asm index fbe8876ee13d56..af89bb58666596 100644 --- a/src/coreclr/vm/amd64/AllocSlow.asm +++ b/src/coreclr/vm/amd64/AllocSlow.asm @@ -9,9 +9,6 @@ EXTERN RhpNewVariableSizeObject : PROC EXTERN RhpGcAllocMaybeFrozen : PROC EXTERN RhExceptionHandling_FailedAllocation_Helper : PROC -EXTERN g_global_alloc_lock : DWORD -EXTERN g_global_alloc_context : QWORD - ; ; Object* RhpNew(MethodTable *pMT) ; @@ -71,175 +68,4 @@ NESTED_ENTRY RhExceptionHandling_FailedAllocation, _TEXT NESTED_END RhExceptionHandling_FailedAllocation, _TEXT -; -; void RhpNewFast_UP(MethodTable *pMT) -; -; Allocate non-array object, uniprocessor version -; -LEAF_ENTRY RhpNewFast_UP, _TEXT - - inc [g_global_alloc_lock] - jnz RhpNewFast_UP_RarePath - - ;; - ;; rcx contains MethodTable pointer - ;; - mov r8d, [rcx + OFFSETOF__MethodTable__m_uBaseSize] - - ;; - ;; eax: base size - ;; rcx: MethodTable pointer - ;; rdx: ee_alloc_context pointer - ;; - - mov rax, [g_global_alloc_context + OFFSETOF__ee_alloc_context__alloc_ptr] - add r8, rax - cmp r8, [g_global_alloc_context + OFFSETOF__ee_alloc_context__combined_limit] - ja RhpNewFast_UP_RarePath_Unlock - - ;; set the new alloc pointer - mov [g_global_alloc_context + OFFSETOF__ee_alloc_context__alloc_ptr], r8 - - ;; set the new object's MethodTable pointer - mov [rax], rcx - mov [g_global_alloc_lock], -1 - ret - -RhpNewFast_UP_RarePath_Unlock: - mov [g_global_alloc_lock], -1 - -RhpNewFast_UP_RarePath: - xor edx, edx - jmp RhpNewObject - -LEAF_END RhpNewFast_UP, _TEXT - -; -; Shared code for RhNewString_UP, RhpNewArrayFast_UP and RhpNewPtrArrayFast_UP -; RAX == string/array size -; RCX == MethodTable -; RDX == character/element count -; -NEW_ARRAY_FAST_UP MACRO - - inc [g_global_alloc_lock] - jnz RhpNewVariableSizeObject - - mov r8, rax - add rax, [g_global_alloc_context + OFFSETOF__ee_alloc_context__alloc_ptr] - jc NewArrayFast_RarePath - - ; rax == new alloc ptr - ; rcx == MethodTable - ; rdx == element count - ; r8 == array size - cmp rax, [g_global_alloc_context + OFFSETOF__ee_alloc_context__combined_limit] - ja NewArrayFast_RarePath - - mov [g_global_alloc_context + OFFSETOF__ee_alloc_context__alloc_ptr], rax - - ; calc the new object pointer - sub rax, r8 - - mov [rax + OFFSETOF__Object__m_pEEType], rcx - mov [rax + OFFSETOF__Array__m_Length], edx - mov [g_global_alloc_lock], -1 - ret - -NewArrayFast_RarePath: - mov [g_global_alloc_lock], -1 - jmp RhpNewVariableSizeObject - -ENDM - -; -; Object* RhNewString_UP(MethodTable *pMT, DWORD stringLength) -; -; Allocate a string, uniprocessor version -; -LEAF_ENTRY RhNewString_UP, _TEXT - - ; we want to limit the element count to the non-negative 32-bit int range - cmp rdx, MAX_STRING_LENGTH - ja StringSizeOverflow - - ; Compute overall allocation size (align(base size + (element size * elements), 8)). - lea rax, [(rdx * STRING_COMPONENT_SIZE) + (STRING_BASE_SIZE + 7)] - and rax, -8 - - NEW_ARRAY_FAST_UP - -StringSizeOverflow: - ; We get here if the size of the final string object can't be represented as an unsigned - ; 32-bit value. We're going to tail-call to a managed helper that will throw - ; an OOM exception that the caller of this allocator understands. - - ; rcx holds MethodTable pointer already - xor edx, edx ; Indicate that we should throw OOM. - jmp RhExceptionHandling_FailedAllocation - -LEAF_END RhNewString_UP, _TEXT - -; -; Object* RhpNewArrayFast_UP(MethodTable *pMT, INT_PTR elementCount) -; Object* RhpNewArrayFast_UP_OBJ(MethodTable *pMT, INT_PTR elementCount) -; -; Allocate one dimensional, zero based array (SZARRAY), uniprocessor version -; -LEAF_ENTRY RhpNewArrayFast_UP, _TEXT - - ; we want to limit the element count to the non-negative 32-bit int range - cmp rdx, 07fffffffh - ja ArraySizeOverflow - - ; save element count - mov r8, rdx - - ; Compute overall allocation size (align(base size + (element size * elements), 8)). - movzx eax, word ptr [rcx + OFFSETOF__MethodTable__m_usComponentSize] - imul rax, rdx - lea rax, [rax + SZARRAY_BASE_SIZE + 7] - and rax, -8 - - mov rdx, r8 - - NEW_ARRAY_FAST_UP - -ArraySizeOverflow: - ; We get here if the size of the final array object can't be represented as an unsigned - ; 32-bit value. We're going to tail-call to a managed helper that will throw - ; an overflow exception that the caller of this allocator understands. - - ; rcx holds MethodTable pointer already - mov edx, 1 ; Indicate that we should throw OverflowException - jmp RhExceptionHandling_FailedAllocation - -LEAF_END RhpNewArrayFast_UP, _TEXT - -; -; Object* RhpNewPtrArrayFast_UP(MethodTable *pMT, INT_PTR elementCount) -; -; Allocate one dimensional, zero based array (SZARRAY) of pointer sized elements, -; uniprocessor version -; -LEAF_ENTRY RhpNewPtrArrayFast_UP, _TEXT - - ; Delegate overflow handling to the generic helper conservatively - - cmp rdx, (40000000h / 8) ; sizeof(void*) - jae RhpNewVariableSizeObject - - ; In this case we know the element size is sizeof(void *), or 8 for x64 - ; This helps us in two ways - we can shift instead of multiplying, and - ; there's no need to align the size either - - lea eax, [edx * 8 + SZARRAY_BASE_SIZE] - - ; No need for rounding in this case - element size is 8, and m_BaseSize is guaranteed - ; to be a multiple of 8. - - NEW_ARRAY_FAST_UP - -LEAF_END RhpNewPtrArrayFast_UP, _TEXT - end diff --git a/src/coreclr/vm/gccover.cpp b/src/coreclr/vm/gccover.cpp index 62bcffa98ad674..63f3440f5efbe8 100644 --- a/src/coreclr/vm/gccover.cpp +++ b/src/coreclr/vm/gccover.cpp @@ -887,9 +887,6 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) // Do the actual stress work // - // BUG(github #10318) - when not using allocation contexts, the alloc lock - // must be acquired here. Until fixed, this assert prevents random heap corruption. - assert(GCHeapUtilities::UseThreadAllocationContexts()); GCHeapUtilities::GetGCHeap()->StressHeap(&t_runtime_thread_locals.alloc_context.m_GCAllocContext); // StressHeap can exit early w/o forcing a SuspendEE to trigger the instruction update @@ -1195,9 +1192,6 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) // Do the actual stress work // - // BUG(github #10318)- when not using allocation contexts, the alloc lock - // must be acquired here. Until fixed, this assert prevents random heap corruption. - assert(GCHeapUtilities::UseThreadAllocationContexts()); GCHeapUtilities::GetGCHeap()->StressHeap(&t_runtime_thread_locals.alloc_context.m_GCAllocContext); // StressHeap can exit early w/o forcing a SuspendEE to trigger the instruction update diff --git a/src/coreclr/vm/gcenv.ee.cpp b/src/coreclr/vm/gcenv.ee.cpp index 14f7572a09c4f3..541b478acb1173 100644 --- a/src/coreclr/vm/gcenv.ee.cpp +++ b/src/coreclr/vm/gcenv.ee.cpp @@ -495,35 +495,24 @@ void GCToEEInterface::GcEnumAllocContexts(enum_alloc_context_func* fn, void* par } CONTRACTL_END; - if (GCHeapUtilities::UseThreadAllocationContexts()) + Thread * pThread = NULL; + while ((pThread = ThreadStore::GetThreadList(pThread)) != NULL) { - Thread * pThread = NULL; - while ((pThread = ThreadStore::GetThreadList(pThread)) != NULL) + ee_alloc_context* palloc_context = pThread->GetEEAllocContext(); + if (palloc_context != nullptr) { - ee_alloc_context* palloc_context = pThread->GetEEAllocContext(); - if (palloc_context != nullptr) + gc_alloc_context* ac = &palloc_context->m_GCAllocContext; + fn(ac, param); + // The GC may zero the alloc_ptr and alloc_limit fields of AC during enumeration and we need to keep + // m_CombinedLimit up-to-date. Note that the GC has multiple threads running this enumeration concurrently + // with no synchronization. If you need to change this code think carefully about how that concurrency + // may affect the results. + if (ac->alloc_limit == 0 && palloc_context->m_CombinedLimit != 0) { - gc_alloc_context* ac = &palloc_context->m_GCAllocContext; - fn(ac, param); - // The GC may zero the alloc_ptr and alloc_limit fields of AC during enumeration and we need to keep - // m_CombinedLimit up-to-date. Note that the GC has multiple threads running this enumeration concurrently - // with no synchronization. If you need to change this code think carefully about how that concurrency - // may affect the results. - if (ac->alloc_limit == 0 && palloc_context->m_CombinedLimit != 0) - { - palloc_context->m_CombinedLimit = 0; - } + palloc_context->m_CombinedLimit = 0; } } } - else - { - fn(&g_global_alloc_context.m_GCAllocContext, param); - if (g_global_alloc_context.m_GCAllocContext.alloc_limit == 0 && g_global_alloc_context.m_CombinedLimit != 0) - { - g_global_alloc_context.m_CombinedLimit = 0; - } - } } diff --git a/src/coreclr/vm/gcheaputilities.cpp b/src/coreclr/vm/gcheaputilities.cpp index e86508ab207fab..c831f51c1fe296 100644 --- a/src/coreclr/vm/gcheaputilities.cpp +++ b/src/coreclr/vm/gcheaputilities.cpp @@ -41,6 +41,7 @@ bool g_sw_ww_enabled_for_gc_heap = false; #endif // FEATURE_USE_SOFTWARE_WRITE_WATCH_FOR_GC_HEAP +// Kept permanently zeroed for GC data contract c1 compatibility. See gcheaputilities.h. GVAL_IMPL_INIT(ee_alloc_context, g_global_alloc_context, {}); thread_local ee_alloc_context::PerThreadRandom ee_alloc_context::t_random = PerThreadRandom(); @@ -66,8 +67,6 @@ VersionInfo g_gc_version_info; // The module that contains the GC. PTR_VOID g_gc_module_base; -bool GCHeapUtilities::s_useThreadAllocationContexts; - // GC entrypoints for the linked-in GC. These symbols are invoked // directly if we are not using a standalone GC. extern "C" void LOCALGC_CALLCONV GC_VersionInfo(/* Out */ VersionInfo* info); @@ -371,19 +370,6 @@ HRESULT GCHeapUtilities::LoadAndInitialize() { LIMITED_METHOD_CONTRACT; - // When running on a single-proc Intel system, it's more efficient to use a single global - // allocation context for SOH allocations than to use one for every thread. -#if (defined(TARGET_X86) || defined(TARGET_AMD64)) && !defined(TARGET_UNIX) -#if DEBUG - bool useGlobalAllocationContext = (CLRConfig::GetConfigValue(CLRConfig::INTERNAL_GCUseGlobalAllocationContext) != 0); -#else - bool useGlobalAllocationContext = false; -#endif - s_useThreadAllocationContexts = !useGlobalAllocationContext && (IsServerHeap() || ::g_SystemInfo.dwNumberOfProcessors != 1 || CPUGroupInfo::CanEnableGCCPUGroups()); -#else - s_useThreadAllocationContexts = true; -#endif - // we should only call this once on startup. Attempting to load a GC // twice is an error. assert(g_pGCHeap == nullptr); diff --git a/src/coreclr/vm/gcheaputilities.h b/src/coreclr/vm/gcheaputilities.h index 7215d0ce189c3b..11ad3f092ebbce 100644 --- a/src/coreclr/vm/gcheaputilities.h +++ b/src/coreclr/vm/gcheaputilities.h @@ -147,10 +147,10 @@ GPTR_DECL(uint8_t,g_highest_address); GPTR_DECL(uint32_t,g_card_table); GVAL_DECL(GCHeapType, g_heap_type); -// For single-proc machines, the EE will use a single, shared alloc context -// for all allocations. In order to avoid extra indirections in assembly -// allocation helpers, the EE owns the global allocation context and the -// GC will update it when it needs to. +// The runtime no longer allocates out of a global allocation context - every allocation goes +// through the thread allocation context. This global is kept permanently zeroed so that +// diagnostic tools implementing version c1 of the GC data contract, which requires the +// GlobalAllocContext global, keep working against this runtime. GVAL_DECL(ee_alloc_context, g_global_alloc_context); #ifndef DACCESS_COMPILE @@ -254,15 +254,6 @@ class GCHeapUtilities { #endif // FEATURE_SVR_GC } - static bool UseThreadAllocationContexts() - { -#if (defined(TARGET_X86) || defined(TARGET_AMD64)) && !defined(TARGET_UNIX) - return s_useThreadAllocationContexts; -#else - return true; -#endif - } - #ifdef FEATURE_USE_SOFTWARE_WRITE_WATCH_FOR_GC_HEAP // Returns True if software write watch is currently enabled for the GC Heap, @@ -344,8 +335,6 @@ class GCHeapUtilities { private: // This class should never be instantiated. GCHeapUtilities() = delete; - - static bool s_useThreadAllocationContexts; }; #endif // _GCHEAPUTILITIES_H_ diff --git a/src/coreclr/vm/gchelpers.cpp b/src/coreclr/vm/gchelpers.cpp index 1751ce3273b62f..24147a2ec1d7c9 100644 --- a/src/coreclr/vm/gchelpers.cpp +++ b/src/coreclr/vm/gchelpers.cpp @@ -45,8 +45,6 @@ EXTERN_C ee_alloc_context* GetThreadEEAllocContext() { WRAPPER_NO_CONTRACT; - assert(GCHeapUtilities::UseThreadAllocationContexts()); - return &t_runtime_thread_locals.alloc_context; } @@ -201,105 +199,6 @@ EXTERN_C void RhExceptionHandling_FailedAllocation_Helper(MethodTable* pMT, bool pFrame->Pop(CURRENT_THREAD); } -// When not using per-thread allocation contexts, we (the EE) need to take care that -// no two threads are concurrently modifying the global allocation context. This lock -// must be acquired before any sort of operations involving the global allocation context -// can occur. -// -// This lock is acquired by all allocations when not using per-thread allocation contexts. -// It is acquired in two kinds of places: -// 1) JIT_TrialAllocFastSP (and related assembly alloc helpers), which attempt to -// acquire it but move into an alloc slow path if acquiring fails -// (but does not decrement the lock variable when doing so) -// 2) Alloc in gchelpers.cpp, which acquire the lock using -// the Acquire and Release methods below. -class GlobalAllocLock { - friend struct AsmOffsets; -private: - // The lock variable. This field must always be first. - LONG m_lock; - -public: - // Creates a new GlobalAllocLock in the unlocked state. - GlobalAllocLock() : m_lock(-1) {} - - // Copy and copy-assignment operators should never be invoked - // for this type - GlobalAllocLock(const GlobalAllocLock&) = delete; - GlobalAllocLock& operator=(const GlobalAllocLock&) = delete; - - // Acquires the lock, spinning if necessary to do so. When this method - // returns, m_lock will be zero and the lock will be acquired. - void Acquire() - { - CONTRACTL { - NOTHROW; - GC_TRIGGERS; // switch to preemptive mode - MODE_COOPERATIVE; - } CONTRACTL_END; - - DWORD spinCount = 0; - while(InterlockedExchange(&m_lock, 0) != -1) - { - GCX_PREEMP(); - __SwitchToThread(0, spinCount++); - } - - assert(m_lock == 0); - } - - // Releases the lock. - void Release() - { - LIMITED_METHOD_CONTRACT; - - // the lock may not be exactly 0. This is because the - // assembly alloc routines increment the lock variable and - // jump if not zero to the slow alloc path, which eventually - // will try to acquire the lock again. At that point, it will - // spin in Acquire (since m_lock is some number that's not zero). - // When the thread that /does/ hold the lock releases it, the spinning - // thread will continue. - MemoryBarrier(); - assert(m_lock >= 0); - m_lock = -1; - } - - // Static helper to acquire a lock, for use with the Holder template. - static void AcquireLock(GlobalAllocLock *lock) - { - WRAPPER_NO_CONTRACT; - lock->Acquire(); - } - - // Static helper to release a lock, for use with the Holder template - static void ReleaseLock(GlobalAllocLock *lock) - { - WRAPPER_NO_CONTRACT; - lock->Release(); - } - - typedef class Holder Holder; -}; - -typedef GlobalAllocLock::Holder GlobalAllocLockHolder; - -struct AsmOffsets { - static_assert(offsetof(GlobalAllocLock, m_lock) == 0, "ASM code relies on this property"); -}; - -// For single-proc machines, the global allocation context is protected -// from concurrent modification by this lock. -// -// When not using per-thread allocation contexts, certain methods on IGCHeap -// require that this lock be held before calling. These methods are documented -// on the IGCHeap interface. -extern "C" -{ - GlobalAllocLock g_global_alloc_lock; -} - - // Checks to see if the given allocation size exceeds the // largest object size allowed - if it does, it throws // an OutOfMemoryException with a message indicating that @@ -475,21 +374,10 @@ inline Object* Alloc(size_t size, GC_ALLOC_FLAGS flags) Object *retVal = NULL; CheckObjectSize(size); - if (GCHeapUtilities::UseThreadAllocationContexts()) - { - ee_alloc_context *threadContext = GetThreadEEAllocContext(); - CdacStress::MaybeVerify(); - GCStress::MaybeTrigger(&threadContext->m_GCAllocContext); - retVal = Alloc(threadContext, size, flags); - } - else - { - GlobalAllocLockHolder holder(&g_global_alloc_lock); - ee_alloc_context *globalContext = &g_global_alloc_context; - CdacStress::MaybeVerify(); - GCStress::MaybeTrigger(&globalContext->m_GCAllocContext); - retVal = Alloc(globalContext, size, flags); - } + ee_alloc_context *threadContext = GetThreadEEAllocContext(); + CdacStress::MaybeVerify(); + GCStress::MaybeTrigger(&threadContext->m_GCAllocContext); + retVal = Alloc(threadContext, size, flags); if (!retVal) diff --git a/src/coreclr/vm/gcstress.h b/src/coreclr/vm/gcstress.h index b1eccb02c56097..55199d67f0c736 100644 --- a/src/coreclr/vm/gcstress.h +++ b/src/coreclr/vm/gcstress.h @@ -295,9 +295,6 @@ namespace _GCStress FORCEINLINE static void Trigger() { - // BUG(github #10318) - when not using allocation contexts, the alloc lock - // must be acquired here. Until fixed, this assert prevents random heap corruption. - _ASSERTE(GCHeapUtilities::UseThreadAllocationContexts()); GCHeapUtilities::GetGCHeap()->StressHeap(&t_runtime_thread_locals.alloc_context.m_GCAllocContext); } diff --git a/src/coreclr/vm/i386/AllocSlow.asm b/src/coreclr/vm/i386/AllocSlow.asm index a8f37b88506829..1a846526493d8f 100644 --- a/src/coreclr/vm/i386/AllocSlow.asm +++ b/src/coreclr/vm/i386/AllocSlow.asm @@ -14,12 +14,6 @@ EXTERN _RhExceptionHandling_FailedAllocation_Helper@12 : PROC EXTERN @RhpNewObject@8 : PROC EXTERN @RhpNewVariableSizeObject@8 : PROC -g_global_alloc_lock EQU _g_global_alloc_lock -g_global_alloc_context EQU _g_global_alloc_context - -EXTERN g_global_alloc_lock : DWORD -EXTERN g_global_alloc_context : DWORD - ; ; Object* RhpNew(MethodTable *pMT) ; @@ -79,196 +73,4 @@ RhExceptionHandling_FailedAllocation PROC PUBLIC ret RhExceptionHandling_FailedAllocation ENDP -; -; void RhpNewFast_UP(MethodTable *pMT) -; -; Allocate non-array object, uniprocessor version -; -FASTCALL_FUNC RhpNewFast_UP, 4 - inc [g_global_alloc_lock] - jnz AllocFailed - - mov eax, [ecx + OFFSETOF__MethodTable__m_uBaseSize] - add eax, [g_global_alloc_context + OFFSETOF__ee_alloc_context__alloc_ptr] - jc AllocFailed_Unlock - cmp eax, [g_global_alloc_context + OFFSETOF__ee_alloc_context__combined_limit] - ja AllocFailed_Unlock - mov [g_global_alloc_context + OFFSETOF__ee_alloc_context__alloc_ptr], eax - - ; calc the new object pointer and initialize it - sub eax, [ecx + OFFSETOF__MethodTable__m_uBaseSize] - mov [eax + OFFSETOF__Object__m_pEEType], ecx - - mov [g_global_alloc_lock], -1 - ret - -AllocFailed_Unlock: - mov [g_global_alloc_lock], -1 - -AllocFailed: - xor edx, edx - jmp @RhpNewObject@8 -FASTCALL_ENDFUNC - -; -; Shared code for RhNewString_UP, RhpNewArrayFast_UP and RhpNewPtrArrayFast_UP -; EAX == string/array size -; ECX == MethodTable -; EDX == character/element count -; -NEW_ARRAY_FAST_PROLOG_UP MACRO - inc [g_global_alloc_lock] - jnz @RhpNewVariableSizeObject@8 - - push ecx - push edx -ENDM - -NEW_ARRAY_FAST_UP MACRO - LOCAL AllocContextOverflow - - ; ECX == MethodTable - ; EAX == allocation size - ; EDX == string length - - mov ecx, eax - add eax, [g_global_alloc_context + OFFSETOF__ee_alloc_context__alloc_ptr] - jc AllocContextOverflow - cmp eax, [g_global_alloc_context + OFFSETOF__ee_alloc_context__combined_limit] - ja AllocContextOverflow - - ; ECX == allocation size - ; EAX == new alloc ptr - - ; set the new alloc pointer - mov [g_global_alloc_context + OFFSETOF__ee_alloc_context__alloc_ptr], eax - - ; calc the new object pointer - sub eax, ecx - - ; Restore the element count and put it in edx - pop edx - ; Restore the MethodTable and put it in ecx - pop ecx - - ; set the new object's MethodTable pointer and element count - mov [eax + OFFSETOF__Object__m_pEEType], ecx - mov [eax + OFFSETOF__Array__m_Length], edx - mov [g_global_alloc_lock], -1 - ret - -AllocContextOverflow: - ; Restore the element count and put it in edx - pop edx - ; Restore the MethodTable and put it in ecx - pop ecx - - mov [g_global_alloc_lock], -1 - jmp @RhpNewVariableSizeObject@8 -ENDM - -; -; Object* RhNewString_UP(MethodTable *pMT, DWORD stringLength) -; -; Allocate a string, uniprocessor version -; -FASTCALL_FUNC RhNewString_UP, 8 - ;; Make sure computing the aligned overall allocation size won't overflow - cmp edx, MAX_STRING_LENGTH - ja StringSizeOverflow - - ; Compute overall allocation size (align(base size + (element size * elements), 4)). - lea eax, [(edx * STRING_COMPONENT_SIZE) + (STRING_BASE_SIZE + 3)] - and eax, -4 - - NEW_ARRAY_FAST_PROLOG_UP - NEW_ARRAY_FAST_UP - -StringSizeOverflow: - ;; We get here if the size of the final string object can't be represented as an unsigned - ;; 32-bit value. We're going to tail-call to a managed helper that will throw - ;; an OOM exception that the caller of this allocator understands. - - ;; ecx holds MethodTable pointer already - xor edx, edx ; Indicate that we should throw OOM. - jmp RhExceptionHandling_FailedAllocation -FASTCALL_ENDFUNC - -; -; Object* RhpNewArrayFast_UP(MethodTable *pMT, INT_PTR elementCount) -; -; Allocate one dimensional, zero based array (SZARRAY), uniprocessor version -; -FASTCALL_FUNC RhpNewArrayFast_UP, 8 - NEW_ARRAY_FAST_PROLOG_UP - - ; Compute overall allocation size (align(base size + (element size * elements), 4)). - ; if the element count is <= 0x10000, no overflow is possible because the component size is - ; <= 0xffff, and thus the product is <= 0xffff0000, and the base size for the worst case - ; (32 dimensional MdArray) is less than 0xffff. - movzx eax, word ptr [ecx + OFFSETOF__MethodTable__m_usComponentSize] - cmp edx,010000h - ja ArraySizeBig - mul edx - lea eax, [eax + SZARRAY_BASE_SIZE + 3] -ArrayAlignSize: - and eax, -4 - - NEW_ARRAY_FAST_UP - -ArraySizeBig: - ; Compute overall allocation size (align(base size + (element size * elements), 4)). - ; if the element count is negative, it's an overflow, otherwise it's out of memory - cmp edx, 0 - jl ArraySizeOverflow - mul edx - jc ArrayOutOfMemoryNoFrame - add eax, [ecx + OFFSETOF__MethodTable__m_uBaseSize] - jc ArrayOutOfMemoryNoFrame - add eax, 3 - jc ArrayOutOfMemoryNoFrame - jmp ArrayAlignSize - -ArrayOutOfMemoryNoFrame: - add esp, 8 - - ; ecx holds MethodTable pointer already - xor edx, edx ; Indicate that we should throw OOM. - jmp RhExceptionHandling_FailedAllocation - -ArraySizeOverflow: - add esp, 8 - - ; We get here if the size of the final array object can't be represented as an unsigned - ; 32-bit value. We're going to tail-call to a managed helper that will throw - ; an overflow exception that the caller of this allocator understands. - - ; ecx holds MethodTable pointer already - mov edx, 1 ; Indicate that we should throw OverflowException - jmp RhExceptionHandling_FailedAllocation -FASTCALL_ENDFUNC - -; -; Object* RhpNewPtrArrayFast_UP(MethodTable *pMT, INT_PTR elementCount) -; -; Allocate one dimensional, zero based array (SZARRAY) of pointer sized elements, -; uniprocessor version -; -FASTCALL_FUNC RhpNewPtrArrayFast_UP, 8 - ; Delegate overflow handling to the generic helper conservatively - - cmp edx, (40000000h / 4) ; sizeof(void*) - jae @RhpNewVariableSizeObject@8 - - ; In this case we know the element size is sizeof(void *), or 4 for x86 - ; This helps us in two ways - we can shift instead of multiplying, and - ; there's no need to align the size either - - lea eax, [edx * 4 + SZARRAY_BASE_SIZE] - - NEW_ARRAY_FAST_PROLOG_UP - NEW_ARRAY_FAST_UP -FASTCALL_ENDFUNC - - end diff --git a/src/coreclr/vm/i386/jitinterfacex86.cpp b/src/coreclr/vm/i386/jitinterfacex86.cpp index 8c6c4eabf988d2..75093192538673 100644 --- a/src/coreclr/vm/i386/jitinterfacex86.cpp +++ b/src/coreclr/vm/i386/jitinterfacex86.cpp @@ -28,8 +28,6 @@ #define WRITE_BARRIER_CHECK 1 #endif -extern "C" LONG g_global_alloc_lock; - extern "C" void STDCALL JIT_WriteBarrierReg_PreGrow();// JIThelp.asm/JIThelp.s extern "C" void STDCALL JIT_WriteBarrierReg_PostGrow();// JIThelp.asm/JIThelp.s diff --git a/src/coreclr/vm/jitinterface.h b/src/coreclr/vm/jitinterface.h index 7727e7d8114832..54a58f15b0fe2b 100644 --- a/src/coreclr/vm/jitinterface.h +++ b/src/coreclr/vm/jitinterface.h @@ -160,13 +160,6 @@ EXTERN_C FCDECL1(Object*, RhpNewFastMisalign, MethodTable* pMT); EXTERN_C FCDECL2(Object*, RhpNewArrayFastAlign8, MethodTable* pMT, INT_PTR size); #endif -#if defined(TARGET_WINDOWS) && (defined(TARGET_AMD64) || defined(TARGET_X86)) -EXTERN_C FCDECL1(Object*, RhpNewFast_UP, MethodTable* pMT); -EXTERN_C FCDECL2(Object*, RhpNewArrayFast_UP, MethodTable* pMT, INT_PTR size); -EXTERN_C FCDECL2(Object*, RhpNewPtrArrayFast_UP, MethodTable* pMT, INT_PTR size); -EXTERN_C FCDECL2(Object*, RhNewString_UP, MethodTable* pMT, INT_PTR stringLength); -#endif - EXTERN_C FCDECL1(Object*, RhpNew, MethodTable* pMT); EXTERN_C FCDECL2(Object*, RhpNewVariableSizeObject, MethodTable* pMT, INT_PTR size); EXTERN_C FCDECL1(Object*, RhpNewMaybeFrozen, MethodTable* pMT); diff --git a/src/coreclr/vm/jitinterfacegen.cpp b/src/coreclr/vm/jitinterfacegen.cpp index 015a2e8dc7c75b..7d1fbf19615036 100644 --- a/src/coreclr/vm/jitinterfacegen.cpp +++ b/src/coreclr/vm/jitinterfacegen.cpp @@ -38,37 +38,17 @@ void InitJITAllocationHelpers() // Allocation helpers, faster but non-logging if (!(TrackAllocationsEnabled() || LoggingOn(LF_GCALLOC, LL_INFO10))) { - // if (multi-proc || server GC || non-Windows) - if (GCHeapUtilities::UseThreadAllocationContexts()) - { - SetJitHelperFunction(CORINFO_HELP_NEWSFAST, RhpNewFast); - SetJitHelperFunction(CORINFO_HELP_NEWARR_1_VC, RhpNewArrayFast); - SetJitHelperFunction(CORINFO_HELP_NEWARR_1_PTR, RhpNewPtrArrayFast); + SetJitHelperFunction(CORINFO_HELP_NEWSFAST, RhpNewFast); + SetJitHelperFunction(CORINFO_HELP_NEWARR_1_VC, RhpNewArrayFast); + SetJitHelperFunction(CORINFO_HELP_NEWARR_1_PTR, RhpNewPtrArrayFast); #if defined(FEATURE_64BIT_ALIGNMENT) - SetJitHelperFunction(CORINFO_HELP_NEWSFAST_ALIGN8, RhpNewFastAlign8); - SetJitHelperFunction(CORINFO_HELP_NEWSFAST_ALIGN8_VC, RhpNewFastMisalign); - SetJitHelperFunction(CORINFO_HELP_NEWARR_1_ALIGN8, RhpNewArrayFastAlign8); + SetJitHelperFunction(CORINFO_HELP_NEWSFAST_ALIGN8, RhpNewFastAlign8); + SetJitHelperFunction(CORINFO_HELP_NEWSFAST_ALIGN8_VC, RhpNewFastMisalign); + SetJitHelperFunction(CORINFO_HELP_NEWARR_1_ALIGN8, RhpNewArrayFastAlign8); #endif - ECall::DynamicallyAssignFCallImpl(GetEEFuncEntryPoint(RhNewString), ECall::FastAllocateString); - } - else - { -#if defined(TARGET_WINDOWS) && (defined(TARGET_AMD64) || defined(TARGET_X86)) - // Replace the 1p slow allocation helpers with faster version - // - // When we're running Workstation GC on a single proc box we don't have - // InlineGetThread versions because there is no need to call GetThread - SetJitHelperFunction(CORINFO_HELP_NEWSFAST, RhpNewFast_UP); - SetJitHelperFunction(CORINFO_HELP_NEWARR_1_VC, RhpNewArrayFast_UP); - SetJitHelperFunction(CORINFO_HELP_NEWARR_1_PTR, RhpNewPtrArrayFast_UP); - - ECall::DynamicallyAssignFCallImpl(GetEEFuncEntryPoint(RhNewString_UP), ECall::FastAllocateString); -#else - _ASSERTE(!"Expected to use ThreadAllocationContexts"); -#endif - } + ECall::DynamicallyAssignFCallImpl(GetEEFuncEntryPoint(RhNewString), ECall::FastAllocateString); } // Debugger depends on new helper names starting with CORINFO_HELP_NEW diff --git a/src/coreclr/vm/runtimehandles.cpp b/src/coreclr/vm/runtimehandles.cpp index b65134fd611157..8a8e7f73a9b828 100644 --- a/src/coreclr/vm/runtimehandles.cpp +++ b/src/coreclr/vm/runtimehandles.cpp @@ -1162,11 +1162,6 @@ FCIMPL1(Object*, RuntimeTypeHandle::InternalAllocNoChecks_FastPath, MethodTable* _ASSERTE(pMT != nullptr); - if (!GCHeapUtilities::UseThreadAllocationContexts()) - { - return NULL; - } - if (pMT->HasFinalizer()) { return NULL; diff --git a/src/coreclr/vm/threadsuspend.cpp b/src/coreclr/vm/threadsuspend.cpp index ffa7a535611f9e..0a02c64b97be43 100644 --- a/src/coreclr/vm/threadsuspend.cpp +++ b/src/coreclr/vm/threadsuspend.cpp @@ -2326,9 +2326,6 @@ void Thread::PerformPreemptiveGC() GCX_COOP(); m_bGCStressing = TRUE; - // BUG(github #10318) - when not using allocation contexts, the alloc lock - // must be acquired here. Until fixed, this assert prevents random heap corruption. - _ASSERTE(GCHeapUtilities::UseThreadAllocationContexts()); GCHeapUtilities::GetGCHeap()->StressHeap(&t_runtime_thread_locals.alloc_context.m_GCAllocContext); m_bGCStressing = FALSE; } From 11797ce634694e52baf0e882be2b3ec8c1d409ae Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Sun, 23 Aug 2026 14:54:53 +0200 Subject: [PATCH 2/7] (test commit) remove g_global_alloc_context Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa75de3f-c6b8-4734-b051-8d32f405a6b7 --- docs/design/datacontracts/GC.md | 24 +++++++++++++------ .../data-descriptor-meanings.json | 2 +- src/coreclr/debug/daccess/dacdbiimpl.cpp | 9 +------ src/coreclr/debug/daccess/request.cpp | 7 +++--- src/coreclr/inc/dacvars.h | 1 - .../vm/datadescriptor/datadescriptor.inc | 1 - src/coreclr/vm/gcheaputilities.cpp | 3 --- src/coreclr/vm/gcheaputilities.h | 6 ----- .../Contracts/IGC.cs | 2 ++ .../Contracts/GC/GC_1.cs | 11 +++++++-- .../tests/DumpTests/WorkstationGCDumpTests.cs | 13 +++++----- 11 files changed, 40 insertions(+), 39 deletions(-) diff --git a/docs/design/datacontracts/GC.md b/docs/design/datacontracts/GC.md index 02cf2917aa54e6..4c8473c3421010 100644 --- a/docs/design/datacontracts/GC.md +++ b/docs/design/datacontracts/GC.md @@ -116,7 +116,8 @@ public readonly struct GCOomData HandleType[] GetHandleTypes(uint[] types); // Gets the extra info (user data) associated with a dependent handle TargetNUInt GetHandleExtraInfo(TargetPointer handle); - // Gets the global allocation context pointer and limit + // Gets the global allocation context pointer and limit. Both are null when the target + // runtime does not allocate out of a global allocation context. void GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit); // Gets handle table memory regions (segments) @@ -293,7 +294,7 @@ public readonly record struct GCHeapSegmentInfo( | `GCHighestAddress` | `pointer` | Highest GC address as recorded by the VM/GC interface | | `GCIdentifiers` | `string` | CSV string containing identifiers of the GC. Current values are "server", "workstation", "regions", and "segments" | | `GCLowestAddress` | `pointer` | Lowest GC address as recorded by the VM/GC interface | -| `GlobalAllocContext` | `pointer` | Pointer to the global EEAllocContext | +| `GlobalAllocContext` | `pointer` | Optional. Pointer to the global EEAllocContext. Absent in runtimes which only allocate out of thread allocation contexts | | `GlobalFreeHugeRegions` | `pointer` | Pointer to the global free huge region list | | `GlobalMechanismsLength` | `uint32` | Number of counters in the global GC mechanisms array | | `GlobalRegionsToDecommit` | `pointer` | Pointer to the global regions-to-decommit array | @@ -881,7 +882,15 @@ GetGlobalAllocationContext ```csharp void IGC.GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit) { - TargetPointer globalAllocContextAddress = target.ReadGlobalPointer("GlobalAllocContext"); + // "GlobalAllocContext" is optional: runtimes which only allocate out of thread allocation + // contexts do not define it. Report an empty context in that case. + if (!target.TryReadGlobalPointer("GlobalAllocContext", out TargetPointer? globalAllocContextAddress)) + { + allocPtr = TargetPointer.Null; + allocLimit = TargetPointer.Null; + return; + } + allocPtr = target.ReadPointer(globalAllocContextAddress + /* EEAllocContext::GCAllocationContext offset */ + /* GCAllocContext::Pointer offset */); allocLimit = target.ReadPointer(globalAllocContextAddress + /* EEAllocContext::GCAllocationContext offset */ + /* GCAllocContext::Limit offset */); } @@ -1149,10 +1158,11 @@ IEnumerable<(HeapSegment Segment, TargetPointer Address)> WalkSegmentList(Target GetPotentialNextObjectAddress Computes the next candidate object address when walking a Gen0/Ephemeral segment. -Active allocation contexts (per-thread, the global non-thread-local context, and -the per-heap Gen0 context) carve out reserved-but-not-yet-allocated ranges inside -such segments; when the naive `current + size` lands on one of those ranges the -walk must skip past it. The contexts are collected via `IThread.GetThreadStoreData` +Active allocation contexts (per-thread, the global non-thread-local context when the +target runtime has one, and the per-heap Gen0 context) carve out +reserved-but-not-yet-allocated ranges inside such segments; when the naive +`current + size` lands on one of those ranges the walk must skip past it. The +contexts are collected via `IThread.GetThreadStoreData` and `IThread.GetThreadData` (per-thread contexts), `IGC.GetGlobalAllocationContext` (global context), and `IGC.GetGCIdentifiers` + `IGC.GetGCHeaps` + `IGC.GetHeapData` (per-heap Gen0 contexts). diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index 2188d62962bdf8..28feafef7b534e 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -781,7 +781,7 @@ "GCLowestAddress": "Lowest GC address as recorded by the VM/GC interface", "GcNotificationFlags": "Global flag for storing GC notification data", "GCThread": "Pointer to the GC thread", - "GlobalAllocContext": "Pointer to the global EEAllocContext", + "GlobalAllocContext": "Optional. Pointer to the global EEAllocContext. Absent in runtimes which only allocate out of thread allocation contexts", "GlobalFreeHugeRegions": "Pointer to the global free huge region list", "GlobalMechanismsLength": "Number of counters in the global GC mechanisms array", "GlobalRegionsToDecommit": "Pointer to the global regions-to-decommit array", diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index 1de344cd271f3c..30f214e136e43b 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -6641,7 +6641,7 @@ HRESULT DacHeapWalker::Init(CORDB_ADDRESS start, CORDB_ADDRESS end) if (threadStore != NULL) { int count = (int)threadStore->ThreadCountInEE(); - mAllocInfo = new (nothrow) AllocInfo[count + 1]; + mAllocInfo = new (nothrow) AllocInfo[count]; if (mAllocInfo == NULL) return E_OUTOFMEMORY; @@ -6668,13 +6668,6 @@ HRESULT DacHeapWalker::Init(CORDB_ADDRESS start, CORDB_ADDRESS end) j++; } } - gc_alloc_context globalCtx = ((ee_alloc_context)g_global_alloc_context).m_GCAllocContext; - if (globalCtx.alloc_ptr != nullptr) - { - mAllocInfo[j].Ptr = (CORDB_ADDRESS)globalCtx.alloc_ptr; - mAllocInfo[j].Limit = (CORDB_ADDRESS)globalCtx.alloc_limit; - j++; - } mAllocContextCount = j; } diff --git a/src/coreclr/debug/daccess/request.cpp b/src/coreclr/debug/daccess/request.cpp index a206ea3db232ea..8df3746c6a8c9f 100644 --- a/src/coreclr/debug/daccess/request.cpp +++ b/src/coreclr/debug/daccess/request.cpp @@ -5498,9 +5498,10 @@ HRESULT ClrDataAccess::GetGlobalAllocationContext( } SOSDacEnter(); - gc_alloc_context global_alloc_context = ((ee_alloc_context)g_global_alloc_context).m_GCAllocContext; - *allocPtr = (CLRDATA_ADDRESS)global_alloc_context.alloc_ptr; - *allocLimit = (CLRDATA_ADDRESS)global_alloc_context.alloc_limit; + // The runtime does not allocate out of a global allocation context - every allocation goes + // through a thread allocation context. Report an empty context. + *allocPtr = (CLRDATA_ADDRESS)0; + *allocLimit = (CLRDATA_ADDRESS)0; SOSDacLeave(); return hr; } diff --git a/src/coreclr/inc/dacvars.h b/src/coreclr/inc/dacvars.h index 600b0e9c553c30..1dd3ecbc4c5103 100644 --- a/src/coreclr/inc/dacvars.h +++ b/src/coreclr/inc/dacvars.h @@ -150,7 +150,6 @@ DEFINE_DACVAR(ProfControlBlock, dac__g_profControlBlock, ::g_profControlBlock) DEFINE_DACVAR(PTR_DWORD, dac__g_card_table, ::g_card_table) DEFINE_DACVAR(PTR_BYTE, dac__g_lowest_address, ::g_lowest_address) DEFINE_DACVAR(PTR_BYTE, dac__g_highest_address, ::g_highest_address) -DEFINE_DACVAR(ee_alloc_context, dac__g_global_alloc_context, ::g_global_alloc_context) DEFINE_DACVAR(IGCHeap, dac__g_pGCHeap, ::g_pGCHeap) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index d9e25e04380be1..f7a09d9745158d 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1797,7 +1797,6 @@ CDAC_GLOBAL_POINTER(ComWrappersVtablePtrs, InteropLib::ABI::g_knownQueryInterfac CDAC_GLOBAL_POINTER(GcNotificationFlags, &::g_gcNotificationFlags) CDAC_GLOBAL_POINTER(JITNotificationTable, &::g_pNotificationTable) CDAC_GLOBAL(JITNotificationTableSize, T_UINT32, JIT_NOTIFICATION_TABLE_SIZE) -CDAC_GLOBAL_POINTER(GlobalAllocContext, &::g_global_alloc_context) CDAC_GLOBAL_POINTER(CoreLib, &::g_CoreLib) #ifdef TARGET_WINDOWS CDAC_GLOBAL_POINTER(TlsIndexBase, &::_tls_index) diff --git a/src/coreclr/vm/gcheaputilities.cpp b/src/coreclr/vm/gcheaputilities.cpp index c831f51c1fe296..aa1a5c201966eb 100644 --- a/src/coreclr/vm/gcheaputilities.cpp +++ b/src/coreclr/vm/gcheaputilities.cpp @@ -41,9 +41,6 @@ bool g_sw_ww_enabled_for_gc_heap = false; #endif // FEATURE_USE_SOFTWARE_WRITE_WATCH_FOR_GC_HEAP -// Kept permanently zeroed for GC data contract c1 compatibility. See gcheaputilities.h. -GVAL_IMPL_INIT(ee_alloc_context, g_global_alloc_context, {}); - thread_local ee_alloc_context::PerThreadRandom ee_alloc_context::t_random = PerThreadRandom(); enum GC_LOAD_STATUS { diff --git a/src/coreclr/vm/gcheaputilities.h b/src/coreclr/vm/gcheaputilities.h index 11ad3f092ebbce..8636354d5e13e5 100644 --- a/src/coreclr/vm/gcheaputilities.h +++ b/src/coreclr/vm/gcheaputilities.h @@ -147,12 +147,6 @@ GPTR_DECL(uint8_t,g_highest_address); GPTR_DECL(uint32_t,g_card_table); GVAL_DECL(GCHeapType, g_heap_type); -// The runtime no longer allocates out of a global allocation context - every allocation goes -// through the thread allocation context. This global is kept permanently zeroed so that -// diagnostic tools implementing version c1 of the GC data contract, which requires the -// GlobalAllocContext global, keep working against this runtime. -GVAL_DECL(ee_alloc_context, g_global_alloc_context); - #ifndef DACCESS_COMPILE } #endif // !DACCESS_COMPILE diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs index 92f215570618fa..22e7e5aceb65e5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs @@ -161,6 +161,8 @@ public interface IGC : IContract HandleType[] GetHandleTypes(uint[] types) => throw new NotImplementedException(); TargetNUInt GetHandleExtraInfo(TargetPointer handle) => throw new NotImplementedException(); + // Gets the global allocation context pointer and limit. Both are null when the target + // runtime does not allocate out of a global allocation context. void GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit) => throw new NotImplementedException(); IReadOnlyList GetHandleTableMemoryRegions() => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs index 6b9370ace38d82..6071162bc91fd8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs @@ -292,8 +292,15 @@ private static GCOomData GetGCOomData(Data.OomHistory oomHistory) void IGC.GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit) { - TargetPointer globalAllocContextAddress = _target.ReadGlobalPointer(Constants.Globals.GlobalAllocContext); - Data.EEAllocContext eeAllocContext = _target.ProcessedData.GetOrAdd(globalAllocContextAddress); + // Runtimes which never allocate out of a global allocation context do not export the global. + if (!_target.TryReadGlobalPointer(Constants.Globals.GlobalAllocContext, out TargetPointer? globalAllocContextAddress)) + { + allocPtr = TargetPointer.Null; + allocLimit = TargetPointer.Null; + return; + } + + Data.EEAllocContext eeAllocContext = _target.ProcessedData.GetOrAdd(globalAllocContextAddress.Value); allocPtr = eeAllocContext.GCAllocationContext.Pointer; allocLimit = eeAllocContext.GCAllocationContext.Limit; } diff --git a/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs index 733263f7001b21..d4f7c3ecc8146e 100644 --- a/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs @@ -130,18 +130,17 @@ public void WorkstationGC_CanEnumerateExpectedHandles(TestConfiguration config) [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] - public void WorkstationGC_GlobalAllocationContextIsReadable(TestConfiguration config) + public void WorkstationGC_GlobalAllocationContextIsEmpty(TestConfiguration config) { InitializeDumpTest(config); IGC gcContract = Target.Contracts.GC; + + // The runtime allocates exclusively out of thread allocation contexts and does not + // export the optional GlobalAllocContext global, so the contract reports an empty context. gcContract.GetGlobalAllocationContext(out TargetPointer pointer, out TargetPointer limit); - if (pointer != TargetPointer.Null) - { - Assert.NotEqual(TargetPointer.Null, limit); - Assert.True(pointer <= limit, - $"Expected allocPtr (0x{pointer:X}) <= allocLimit (0x{limit:X})"); - } + Assert.Equal(TargetPointer.Null, pointer); + Assert.Equal(TargetPointer.Null, limit); } [ConditionalTheory] From f7a0f96e4ddd9b2eef1add9944a71cf0a3afa0c8 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Mon, 24 Aug 2026 13:41:39 +0200 Subject: [PATCH 3/7] Address review feedback - Match the wording convention used by the other conditionally emitted GC globals (CurrentGCState, DynamicAdaptationMode) when documenting GlobalAllocContext as no longer emitted. - Drop the now-vestigial dwNumberOfProcessors assert in InitJITAllocationHelpers - its only consumer was the removed uniprocessor helper selection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/GC.md | 2 +- docs/design/datacontracts/data-descriptor-meanings.json | 2 +- src/coreclr/vm/jitinterfacegen.cpp | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/design/datacontracts/GC.md b/docs/design/datacontracts/GC.md index 4c8473c3421010..6bca1c321c8901 100644 --- a/docs/design/datacontracts/GC.md +++ b/docs/design/datacontracts/GC.md @@ -294,7 +294,7 @@ public readonly record struct GCHeapSegmentInfo( | `GCHighestAddress` | `pointer` | Highest GC address as recorded by the VM/GC interface | | `GCIdentifiers` | `string` | CSV string containing identifiers of the GC. Current values are "server", "workstation", "regions", and "segments" | | `GCLowestAddress` | `pointer` | Lowest GC address as recorded by the VM/GC interface | -| `GlobalAllocContext` | `pointer` | Optional. Pointer to the global EEAllocContext. Absent in runtimes which only allocate out of thread allocation contexts | +| `GlobalAllocContext` | `pointer` | Pointer to the global EEAllocContext. Only available in runtimes that allocate out of a global allocation context instead of thread allocation contexts. | | `GlobalFreeHugeRegions` | `pointer` | Pointer to the global free huge region list | | `GlobalMechanismsLength` | `uint32` | Number of counters in the global GC mechanisms array | | `GlobalRegionsToDecommit` | `pointer` | Pointer to the global regions-to-decommit array | diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index 28feafef7b534e..850b0196acbd48 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -781,7 +781,7 @@ "GCLowestAddress": "Lowest GC address as recorded by the VM/GC interface", "GcNotificationFlags": "Global flag for storing GC notification data", "GCThread": "Pointer to the GC thread", - "GlobalAllocContext": "Optional. Pointer to the global EEAllocContext. Absent in runtimes which only allocate out of thread allocation contexts", + "GlobalAllocContext": "Pointer to the global EEAllocContext. Only available in runtimes that allocate out of a global allocation context instead of thread allocation contexts.", "GlobalFreeHugeRegions": "Pointer to the global free huge region list", "GlobalMechanismsLength": "Number of counters in the global GC mechanisms array", "GlobalRegionsToDecommit": "Pointer to the global regions-to-decommit array", diff --git a/src/coreclr/vm/jitinterfacegen.cpp b/src/coreclr/vm/jitinterfacegen.cpp index 7d1fbf19615036..7d50747a56d688 100644 --- a/src/coreclr/vm/jitinterfacegen.cpp +++ b/src/coreclr/vm/jitinterfacegen.cpp @@ -33,8 +33,6 @@ void InitJITAllocationHelpers() { STANDARD_VM_CONTRACT; - _ASSERTE(g_SystemInfo.dwNumberOfProcessors != 0); - // Allocation helpers, faster but non-logging if (!(TrackAllocationsEnabled() || LoggingOn(LF_GCALLOC, LL_INFO10))) { From a288b9829c9ef25f44e1bdb843601bdd51ccc6d3 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Tue, 25 Aug 2026 19:33:52 +0200 Subject: [PATCH 4/7] Keep GlobalAllocContext to avoid a breaking GC contract change Reverts the removal of g_global_alloc_context and of the GlobalAllocContext data-descriptor global. Version c1 of the GC data contract requires that global, and its documented GetGlobalAllocationContext algorithm reads it unconditionally, so dropping it is a breaking contract change - not an amendment - even though no shipped tool depends on it yet. The global stays permanently zeroed, which reads back as an empty allocation context: exactly what the runtime now always has. DAC, cDAC, SOS, the GC.md contract description and the dump tests are therefore all unchanged from main, while every runtime use of the global (allocation, alloc context enumeration, GC stress, suspension) is still gone with the uniprocessor helpers. It can be deleted the next time the GC contract takes a breaking change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/GC.md | 24 ++++++------------- .../data-descriptor-meanings.json | 2 +- src/coreclr/debug/daccess/dacdbiimpl.cpp | 9 ++++++- src/coreclr/debug/daccess/request.cpp | 7 +++--- src/coreclr/inc/dacvars.h | 1 + .../vm/datadescriptor/datadescriptor.inc | 1 + src/coreclr/vm/gcheaputilities.cpp | 3 +++ src/coreclr/vm/gcheaputilities.h | 8 +++++++ .../Contracts/IGC.cs | 2 -- .../Contracts/GC/GC_1.cs | 11 ++------- .../tests/DumpTests/WorkstationGCDumpTests.cs | 13 +++++----- 11 files changed, 41 insertions(+), 40 deletions(-) diff --git a/docs/design/datacontracts/GC.md b/docs/design/datacontracts/GC.md index 6bca1c321c8901..02cf2917aa54e6 100644 --- a/docs/design/datacontracts/GC.md +++ b/docs/design/datacontracts/GC.md @@ -116,8 +116,7 @@ public readonly struct GCOomData HandleType[] GetHandleTypes(uint[] types); // Gets the extra info (user data) associated with a dependent handle TargetNUInt GetHandleExtraInfo(TargetPointer handle); - // Gets the global allocation context pointer and limit. Both are null when the target - // runtime does not allocate out of a global allocation context. + // Gets the global allocation context pointer and limit void GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit); // Gets handle table memory regions (segments) @@ -294,7 +293,7 @@ public readonly record struct GCHeapSegmentInfo( | `GCHighestAddress` | `pointer` | Highest GC address as recorded by the VM/GC interface | | `GCIdentifiers` | `string` | CSV string containing identifiers of the GC. Current values are "server", "workstation", "regions", and "segments" | | `GCLowestAddress` | `pointer` | Lowest GC address as recorded by the VM/GC interface | -| `GlobalAllocContext` | `pointer` | Pointer to the global EEAllocContext. Only available in runtimes that allocate out of a global allocation context instead of thread allocation contexts. | +| `GlobalAllocContext` | `pointer` | Pointer to the global EEAllocContext | | `GlobalFreeHugeRegions` | `pointer` | Pointer to the global free huge region list | | `GlobalMechanismsLength` | `uint32` | Number of counters in the global GC mechanisms array | | `GlobalRegionsToDecommit` | `pointer` | Pointer to the global regions-to-decommit array | @@ -882,15 +881,7 @@ GetGlobalAllocationContext ```csharp void IGC.GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit) { - // "GlobalAllocContext" is optional: runtimes which only allocate out of thread allocation - // contexts do not define it. Report an empty context in that case. - if (!target.TryReadGlobalPointer("GlobalAllocContext", out TargetPointer? globalAllocContextAddress)) - { - allocPtr = TargetPointer.Null; - allocLimit = TargetPointer.Null; - return; - } - + TargetPointer globalAllocContextAddress = target.ReadGlobalPointer("GlobalAllocContext"); allocPtr = target.ReadPointer(globalAllocContextAddress + /* EEAllocContext::GCAllocationContext offset */ + /* GCAllocContext::Pointer offset */); allocLimit = target.ReadPointer(globalAllocContextAddress + /* EEAllocContext::GCAllocationContext offset */ + /* GCAllocContext::Limit offset */); } @@ -1158,11 +1149,10 @@ IEnumerable<(HeapSegment Segment, TargetPointer Address)> WalkSegmentList(Target GetPotentialNextObjectAddress Computes the next candidate object address when walking a Gen0/Ephemeral segment. -Active allocation contexts (per-thread, the global non-thread-local context when the -target runtime has one, and the per-heap Gen0 context) carve out -reserved-but-not-yet-allocated ranges inside such segments; when the naive -`current + size` lands on one of those ranges the walk must skip past it. The -contexts are collected via `IThread.GetThreadStoreData` +Active allocation contexts (per-thread, the global non-thread-local context, and +the per-heap Gen0 context) carve out reserved-but-not-yet-allocated ranges inside +such segments; when the naive `current + size` lands on one of those ranges the +walk must skip past it. The contexts are collected via `IThread.GetThreadStoreData` and `IThread.GetThreadData` (per-thread contexts), `IGC.GetGlobalAllocationContext` (global context), and `IGC.GetGCIdentifiers` + `IGC.GetGCHeaps` + `IGC.GetHeapData` (per-heap Gen0 contexts). diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index 850b0196acbd48..2188d62962bdf8 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -781,7 +781,7 @@ "GCLowestAddress": "Lowest GC address as recorded by the VM/GC interface", "GcNotificationFlags": "Global flag for storing GC notification data", "GCThread": "Pointer to the GC thread", - "GlobalAllocContext": "Pointer to the global EEAllocContext. Only available in runtimes that allocate out of a global allocation context instead of thread allocation contexts.", + "GlobalAllocContext": "Pointer to the global EEAllocContext", "GlobalFreeHugeRegions": "Pointer to the global free huge region list", "GlobalMechanismsLength": "Number of counters in the global GC mechanisms array", "GlobalRegionsToDecommit": "Pointer to the global regions-to-decommit array", diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index 30f214e136e43b..1de344cd271f3c 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -6641,7 +6641,7 @@ HRESULT DacHeapWalker::Init(CORDB_ADDRESS start, CORDB_ADDRESS end) if (threadStore != NULL) { int count = (int)threadStore->ThreadCountInEE(); - mAllocInfo = new (nothrow) AllocInfo[count]; + mAllocInfo = new (nothrow) AllocInfo[count + 1]; if (mAllocInfo == NULL) return E_OUTOFMEMORY; @@ -6668,6 +6668,13 @@ HRESULT DacHeapWalker::Init(CORDB_ADDRESS start, CORDB_ADDRESS end) j++; } } + gc_alloc_context globalCtx = ((ee_alloc_context)g_global_alloc_context).m_GCAllocContext; + if (globalCtx.alloc_ptr != nullptr) + { + mAllocInfo[j].Ptr = (CORDB_ADDRESS)globalCtx.alloc_ptr; + mAllocInfo[j].Limit = (CORDB_ADDRESS)globalCtx.alloc_limit; + j++; + } mAllocContextCount = j; } diff --git a/src/coreclr/debug/daccess/request.cpp b/src/coreclr/debug/daccess/request.cpp index 8df3746c6a8c9f..a206ea3db232ea 100644 --- a/src/coreclr/debug/daccess/request.cpp +++ b/src/coreclr/debug/daccess/request.cpp @@ -5498,10 +5498,9 @@ HRESULT ClrDataAccess::GetGlobalAllocationContext( } SOSDacEnter(); - // The runtime does not allocate out of a global allocation context - every allocation goes - // through a thread allocation context. Report an empty context. - *allocPtr = (CLRDATA_ADDRESS)0; - *allocLimit = (CLRDATA_ADDRESS)0; + gc_alloc_context global_alloc_context = ((ee_alloc_context)g_global_alloc_context).m_GCAllocContext; + *allocPtr = (CLRDATA_ADDRESS)global_alloc_context.alloc_ptr; + *allocLimit = (CLRDATA_ADDRESS)global_alloc_context.alloc_limit; SOSDacLeave(); return hr; } diff --git a/src/coreclr/inc/dacvars.h b/src/coreclr/inc/dacvars.h index 1dd3ecbc4c5103..600b0e9c553c30 100644 --- a/src/coreclr/inc/dacvars.h +++ b/src/coreclr/inc/dacvars.h @@ -150,6 +150,7 @@ DEFINE_DACVAR(ProfControlBlock, dac__g_profControlBlock, ::g_profControlBlock) DEFINE_DACVAR(PTR_DWORD, dac__g_card_table, ::g_card_table) DEFINE_DACVAR(PTR_BYTE, dac__g_lowest_address, ::g_lowest_address) DEFINE_DACVAR(PTR_BYTE, dac__g_highest_address, ::g_highest_address) +DEFINE_DACVAR(ee_alloc_context, dac__g_global_alloc_context, ::g_global_alloc_context) DEFINE_DACVAR(IGCHeap, dac__g_pGCHeap, ::g_pGCHeap) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index bfc416e3c2b280..5257688a418f3d 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1792,6 +1792,7 @@ CDAC_GLOBAL_POINTER(ComWrappersVtablePtrs, InteropLib::ABI::g_knownQueryInterfac CDAC_GLOBAL_POINTER(GcNotificationFlags, &::g_gcNotificationFlags) CDAC_GLOBAL_POINTER(JITNotificationTable, &::g_pNotificationTable) CDAC_GLOBAL(JITNotificationTableSize, T_UINT32, JIT_NOTIFICATION_TABLE_SIZE) +CDAC_GLOBAL_POINTER(GlobalAllocContext, &::g_global_alloc_context) CDAC_GLOBAL_POINTER(CoreLib, &::g_CoreLib) #ifdef TARGET_WINDOWS CDAC_GLOBAL_POINTER(TlsIndexBase, &::_tls_index) diff --git a/src/coreclr/vm/gcheaputilities.cpp b/src/coreclr/vm/gcheaputilities.cpp index aa1a5c201966eb..211c3671f1d371 100644 --- a/src/coreclr/vm/gcheaputilities.cpp +++ b/src/coreclr/vm/gcheaputilities.cpp @@ -41,6 +41,9 @@ bool g_sw_ww_enabled_for_gc_heap = false; #endif // FEATURE_USE_SOFTWARE_WRITE_WATCH_FOR_GC_HEAP +// Unused - kept permanently zeroed for GC data contract c1 compatibility. See gcheaputilities.h. +GVAL_IMPL_INIT(ee_alloc_context, g_global_alloc_context, {}); + thread_local ee_alloc_context::PerThreadRandom ee_alloc_context::t_random = PerThreadRandom(); enum GC_LOAD_STATUS { diff --git a/src/coreclr/vm/gcheaputilities.h b/src/coreclr/vm/gcheaputilities.h index 8636354d5e13e5..3eb950c595c80b 100644 --- a/src/coreclr/vm/gcheaputilities.h +++ b/src/coreclr/vm/gcheaputilities.h @@ -147,6 +147,14 @@ GPTR_DECL(uint8_t,g_highest_address); GPTR_DECL(uint32_t,g_card_table); GVAL_DECL(GCHeapType, g_heap_type); +// Unused by the runtime - every allocation goes through a thread allocation context. This global +// is kept, permanently zeroed, as the backing storage for the GlobalAllocContext global that +// version c1 of the GC data contract requires; dropping it would break diagnostic tools written +// against that contract. Zero reads back as an empty allocation context, which is what this +// runtime always has, so the DAC, the cDAC and SOS all keep reporting a consistent (empty) +// result. It can go away the next time the GC contract takes a breaking change. +GVAL_DECL(ee_alloc_context, g_global_alloc_context); + #ifndef DACCESS_COMPILE } #endif // !DACCESS_COMPILE diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs index 22e7e5aceb65e5..92f215570618fa 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs @@ -161,8 +161,6 @@ public interface IGC : IContract HandleType[] GetHandleTypes(uint[] types) => throw new NotImplementedException(); TargetNUInt GetHandleExtraInfo(TargetPointer handle) => throw new NotImplementedException(); - // Gets the global allocation context pointer and limit. Both are null when the target - // runtime does not allocate out of a global allocation context. void GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit) => throw new NotImplementedException(); IReadOnlyList GetHandleTableMemoryRegions() => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs index 6071162bc91fd8..6b9370ace38d82 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs @@ -292,15 +292,8 @@ private static GCOomData GetGCOomData(Data.OomHistory oomHistory) void IGC.GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit) { - // Runtimes which never allocate out of a global allocation context do not export the global. - if (!_target.TryReadGlobalPointer(Constants.Globals.GlobalAllocContext, out TargetPointer? globalAllocContextAddress)) - { - allocPtr = TargetPointer.Null; - allocLimit = TargetPointer.Null; - return; - } - - Data.EEAllocContext eeAllocContext = _target.ProcessedData.GetOrAdd(globalAllocContextAddress.Value); + TargetPointer globalAllocContextAddress = _target.ReadGlobalPointer(Constants.Globals.GlobalAllocContext); + Data.EEAllocContext eeAllocContext = _target.ProcessedData.GetOrAdd(globalAllocContextAddress); allocPtr = eeAllocContext.GCAllocationContext.Pointer; allocLimit = eeAllocContext.GCAllocationContext.Limit; } diff --git a/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs index d4f7c3ecc8146e..733263f7001b21 100644 --- a/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs @@ -130,17 +130,18 @@ public void WorkstationGC_CanEnumerateExpectedHandles(TestConfiguration config) [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] - public void WorkstationGC_GlobalAllocationContextIsEmpty(TestConfiguration config) + public void WorkstationGC_GlobalAllocationContextIsReadable(TestConfiguration config) { InitializeDumpTest(config); IGC gcContract = Target.Contracts.GC; - - // The runtime allocates exclusively out of thread allocation contexts and does not - // export the optional GlobalAllocContext global, so the contract reports an empty context. gcContract.GetGlobalAllocationContext(out TargetPointer pointer, out TargetPointer limit); - Assert.Equal(TargetPointer.Null, pointer); - Assert.Equal(TargetPointer.Null, limit); + if (pointer != TargetPointer.Null) + { + Assert.NotEqual(TargetPointer.Null, limit); + Assert.True(pointer <= limit, + $"Expected allocPtr (0x{pointer:X}) <= allocLimit (0x{limit:X})"); + } } [ConditionalTheory] From 2fe2cc2cd1e82d47f4a79a6418245eb0e5d7de0a Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 27 Aug 2026 15:39:08 +0200 Subject: [PATCH 5/7] Prepare GlobalAllocContext for removal in GC contract c2 Addresses review feedback: keep the unused global for c1 compatibility, but make the future cleanup trivial and discoverable. - g_global_alloc_context keeps a one-line comment pointing at the cleanup list. - GC.md and data-descriptor-meanings.json document GlobalAllocContext as required in c1 and optional in c2 and later, and GetGlobalAllocationContext reports an empty context when the global is absent. - GC_1 reads the global with TryReadGlobalPointer, so the same implementation works against both versions. - datadescriptor.inc gets a "Pending cleanup" list tracking entries that can only be deleted with a contract version bump. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/GC.md | 25 +++++++++++++------ .../data-descriptor-meanings.json | 2 +- .../vm/datadescriptor/datadescriptor.inc | 13 ++++++++++ src/coreclr/vm/gcheaputilities.cpp | 2 +- src/coreclr/vm/gcheaputilities.h | 7 +----- .../Contracts/IGC.cs | 2 ++ .../Contracts/GC/GC_1.cs | 12 +++++++-- 7 files changed, 46 insertions(+), 17 deletions(-) diff --git a/docs/design/datacontracts/GC.md b/docs/design/datacontracts/GC.md index 02cf2917aa54e6..5a34674798a7e2 100644 --- a/docs/design/datacontracts/GC.md +++ b/docs/design/datacontracts/GC.md @@ -116,7 +116,8 @@ public readonly struct GCOomData HandleType[] GetHandleTypes(uint[] types); // Gets the extra info (user data) associated with a dependent handle TargetNUInt GetHandleExtraInfo(TargetPointer handle); - // Gets the global allocation context pointer and limit + // Gets the global allocation context pointer and limit. Both are null when the target + // runtime does not allocate out of a global allocation context. void GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit); // Gets handle table memory regions (segments) @@ -293,7 +294,7 @@ public readonly record struct GCHeapSegmentInfo( | `GCHighestAddress` | `pointer` | Highest GC address as recorded by the VM/GC interface | | `GCIdentifiers` | `string` | CSV string containing identifiers of the GC. Current values are "server", "workstation", "regions", and "segments" | | `GCLowestAddress` | `pointer` | Lowest GC address as recorded by the VM/GC interface | -| `GlobalAllocContext` | `pointer` | Pointer to the global EEAllocContext | +| `GlobalAllocContext` | `pointer` | Pointer to the global EEAllocContext. Required in GC contract version `c1`; optional in `c2` and later, where it is absent in runtimes which allocate only out of thread allocation contexts | | `GlobalFreeHugeRegions` | `pointer` | Pointer to the global free huge region list | | `GlobalMechanismsLength` | `uint32` | Number of counters in the global GC mechanisms array | | `GlobalRegionsToDecommit` | `pointer` | Pointer to the global regions-to-decommit array | @@ -881,7 +882,16 @@ GetGlobalAllocationContext ```csharp void IGC.GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit) { - TargetPointer globalAllocContextAddress = target.ReadGlobalPointer("GlobalAllocContext"); + // "GlobalAllocContext" is required in c1 and optional in c2 and later: runtimes which + // allocate only out of thread allocation contexts do not define it. Report an empty + // context in that case, so that the same implementation serves both versions. + if (!target.TryReadGlobalPointer("GlobalAllocContext", out TargetPointer? globalAllocContextAddress)) + { + allocPtr = TargetPointer.Null; + allocLimit = TargetPointer.Null; + return; + } + allocPtr = target.ReadPointer(globalAllocContextAddress + /* EEAllocContext::GCAllocationContext offset */ + /* GCAllocContext::Pointer offset */); allocLimit = target.ReadPointer(globalAllocContextAddress + /* EEAllocContext::GCAllocationContext offset */ + /* GCAllocContext::Limit offset */); } @@ -1149,10 +1159,11 @@ IEnumerable<(HeapSegment Segment, TargetPointer Address)> WalkSegmentList(Target GetPotentialNextObjectAddress Computes the next candidate object address when walking a Gen0/Ephemeral segment. -Active allocation contexts (per-thread, the global non-thread-local context, and -the per-heap Gen0 context) carve out reserved-but-not-yet-allocated ranges inside -such segments; when the naive `current + size` lands on one of those ranges the -walk must skip past it. The contexts are collected via `IThread.GetThreadStoreData` +Active allocation contexts (per-thread, the global non-thread-local context when the +target runtime has one, and the per-heap Gen0 context) carve out +reserved-but-not-yet-allocated ranges inside such segments; when the naive +`current + size` lands on one of those ranges the walk must skip past it. The +contexts are collected via `IThread.GetThreadStoreData` and `IThread.GetThreadData` (per-thread contexts), `IGC.GetGlobalAllocationContext` (global context), and `IGC.GetGCIdentifiers` + `IGC.GetGCHeaps` + `IGC.GetHeapData` (per-heap Gen0 contexts). diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index 2188d62962bdf8..e2a6ce1ac919df 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -781,7 +781,7 @@ "GCLowestAddress": "Lowest GC address as recorded by the VM/GC interface", "GcNotificationFlags": "Global flag for storing GC notification data", "GCThread": "Pointer to the GC thread", - "GlobalAllocContext": "Pointer to the global EEAllocContext", + "GlobalAllocContext": "Pointer to the global EEAllocContext. Required in GC contract version `c1`; optional in `c2` and later, where it is absent in runtimes which allocate only out of thread allocation contexts", "GlobalFreeHugeRegions": "Pointer to the global free huge region list", "GlobalMechanismsLength": "Number of counters in the global GC mechanisms array", "GlobalRegionsToDecommit": "Pointer to the global regions-to-decommit array", diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 5257688a418f3d..a544ade3efffc2 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1792,6 +1792,7 @@ CDAC_GLOBAL_POINTER(ComWrappersVtablePtrs, InteropLib::ABI::g_knownQueryInterfac CDAC_GLOBAL_POINTER(GcNotificationFlags, &::g_gcNotificationFlags) CDAC_GLOBAL_POINTER(JITNotificationTable, &::g_pNotificationTable) CDAC_GLOBAL(JITNotificationTableSize, T_UINT32, JIT_NOTIFICATION_TABLE_SIZE) +// Unused by the runtime - see "Pending cleanup" at the end of this file. CDAC_GLOBAL_POINTER(GlobalAllocContext, &::g_global_alloc_context) CDAC_GLOBAL_POINTER(CoreLib, &::g_CoreLib) #ifdef TARGET_WINDOWS @@ -1895,3 +1896,15 @@ CDAC_GLOBAL_CONTRACT(Thread, c1) CDAC_GLOBAL_CONTRACT(WindowsErrorReporting, c1) CDAC_GLOBALS_END() + +// Pending cleanup +// +// Data descriptor entries that the runtime itself no longer uses, but that cannot be deleted +// without breaking a frozen contract version. Each one can only go away together with a bump of +// the contract version that requires it. Keep this list up to date so that the next version bump +// does not have to rediscover them. +// +// - Delete GlobalAllocContext (and ::g_global_alloc_context) once the GC contract version is +// bumped to c2. The runtime allocates exclusively out of thread allocation contexts, so the +// global is permanently zeroed; c1 requires it, docs/design/datacontracts/GC.md already +// documents it as optional in c2, and the cDAC's GC_1 reads it with TryReadGlobalPointer. diff --git a/src/coreclr/vm/gcheaputilities.cpp b/src/coreclr/vm/gcheaputilities.cpp index 211c3671f1d371..17c03eb49b66e7 100644 --- a/src/coreclr/vm/gcheaputilities.cpp +++ b/src/coreclr/vm/gcheaputilities.cpp @@ -41,7 +41,7 @@ bool g_sw_ww_enabled_for_gc_heap = false; #endif // FEATURE_USE_SOFTWARE_WRITE_WATCH_FOR_GC_HEAP -// Unused - kept permanently zeroed for GC data contract c1 compatibility. See gcheaputilities.h. +// Unused - kept for GC data contract c1 compatibility, see datadescriptor/datadescriptor.inc. GVAL_IMPL_INIT(ee_alloc_context, g_global_alloc_context, {}); thread_local ee_alloc_context::PerThreadRandom ee_alloc_context::t_random = PerThreadRandom(); diff --git a/src/coreclr/vm/gcheaputilities.h b/src/coreclr/vm/gcheaputilities.h index 3eb950c595c80b..f6fde55fc0f502 100644 --- a/src/coreclr/vm/gcheaputilities.h +++ b/src/coreclr/vm/gcheaputilities.h @@ -147,12 +147,7 @@ GPTR_DECL(uint8_t,g_highest_address); GPTR_DECL(uint32_t,g_card_table); GVAL_DECL(GCHeapType, g_heap_type); -// Unused by the runtime - every allocation goes through a thread allocation context. This global -// is kept, permanently zeroed, as the backing storage for the GlobalAllocContext global that -// version c1 of the GC data contract requires; dropping it would break diagnostic tools written -// against that contract. Zero reads back as an empty allocation context, which is what this -// runtime always has, so the DAC, the cDAC and SOS all keep reporting a consistent (empty) -// result. It can go away the next time the GC contract takes a breaking change. +// Unused - kept for GC data contract c1 compatibility, see datadescriptor/datadescriptor.inc. GVAL_DECL(ee_alloc_context, g_global_alloc_context); #ifndef DACCESS_COMPILE diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs index 92f215570618fa..22e7e5aceb65e5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs @@ -161,6 +161,8 @@ public interface IGC : IContract HandleType[] GetHandleTypes(uint[] types) => throw new NotImplementedException(); TargetNUInt GetHandleExtraInfo(TargetPointer handle) => throw new NotImplementedException(); + // Gets the global allocation context pointer and limit. Both are null when the target + // runtime does not allocate out of a global allocation context. void GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit) => throw new NotImplementedException(); IReadOnlyList GetHandleTableMemoryRegions() => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs index 6b9370ace38d82..f320abe6a05ed3 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs @@ -292,8 +292,16 @@ private static GCOomData GetGCOomData(Data.OomHistory oomHistory) void IGC.GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit) { - TargetPointer globalAllocContextAddress = _target.ReadGlobalPointer(Constants.Globals.GlobalAllocContext); - Data.EEAllocContext eeAllocContext = _target.ProcessedData.GetOrAdd(globalAllocContextAddress); + // Runtimes which allocate only out of thread allocation contexts do not export the global. + // It is required in c1 and optional in c2 and later. + if (!_target.TryReadGlobalPointer(Constants.Globals.GlobalAllocContext, out TargetPointer? globalAllocContextAddress)) + { + allocPtr = TargetPointer.Null; + allocLimit = TargetPointer.Null; + return; + } + + Data.EEAllocContext eeAllocContext = _target.ProcessedData.GetOrAdd(globalAllocContextAddress.Value); allocPtr = eeAllocContext.GCAllocationContext.Pointer; allocLimit = eeAllocContext.GCAllocationContext.Limit; } From c50edc040d44f36b111af56ef6d48427453a82c5 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 27 Aug 2026 18:35:57 +0200 Subject: [PATCH 6/7] Update src/coreclr/vm/datadescriptor/datadescriptor.inc Co-authored-by: Jan Kotas --- src/coreclr/vm/datadescriptor/datadescriptor.inc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index a544ade3efffc2..f654eb6fb2c12a 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1905,6 +1905,4 @@ CDAC_GLOBALS_END() // does not have to rediscover them. // // - Delete GlobalAllocContext (and ::g_global_alloc_context) once the GC contract version is -// bumped to c2. The runtime allocates exclusively out of thread allocation contexts, so the -// global is permanently zeroed; c1 requires it, docs/design/datacontracts/GC.md already -// documents it as optional in c2, and the cDAC's GC_1 reads it with TryReadGlobalPointer. +// bumped to c2. From bedb85cbe4e094ef5d4b6689669c887908ccd1b8 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 27 Aug 2026 19:05:17 +0200 Subject: [PATCH 7/7] Stop reading g_global_alloc_context from the fragile DAC The runtime no longer allocates out of the global allocation context, so the DAC does not need to read it any more: - DacHeapWalker no longer reserves an extra AllocInfo slot for it and no longer carves it out of the heap walk. - ClrDataAccess::GetGlobalAllocationContext reports an empty context directly. The global itself, its data descriptor entry and its dacvars.h entry all stay. The dacvars.h entry is what reports the global into mini and triage dumps via enummem.cpp, and the cDAC still reads it there for the GC contract c1 GlobalAllocContext global, so removing it would make GetGlobalAllocationContext and cDAC heap walking fail on those dump types. Both are noted in the pending cleanup list so they can go away together when the GC contract is bumped to c2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c7a32a7b-9ced-4b78-9f0b-6520af0a00f2 --- src/coreclr/debug/daccess/dacdbiimpl.cpp | 10 +--------- src/coreclr/debug/daccess/request.cpp | 6 +++--- src/coreclr/inc/dacvars.h | 2 ++ src/coreclr/vm/datadescriptor/datadescriptor.inc | 4 ++-- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index 1de344cd271f3c..3c3dda4efec56e 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -6641,7 +6641,7 @@ HRESULT DacHeapWalker::Init(CORDB_ADDRESS start, CORDB_ADDRESS end) if (threadStore != NULL) { int count = (int)threadStore->ThreadCountInEE(); - mAllocInfo = new (nothrow) AllocInfo[count + 1]; + mAllocInfo = new (nothrow) AllocInfo[count]; if (mAllocInfo == NULL) return E_OUTOFMEMORY; @@ -6668,14 +6668,6 @@ HRESULT DacHeapWalker::Init(CORDB_ADDRESS start, CORDB_ADDRESS end) j++; } } - gc_alloc_context globalCtx = ((ee_alloc_context)g_global_alloc_context).m_GCAllocContext; - if (globalCtx.alloc_ptr != nullptr) - { - mAllocInfo[j].Ptr = (CORDB_ADDRESS)globalCtx.alloc_ptr; - mAllocInfo[j].Limit = (CORDB_ADDRESS)globalCtx.alloc_limit; - j++; - } - mAllocContextCount = j; } diff --git a/src/coreclr/debug/daccess/request.cpp b/src/coreclr/debug/daccess/request.cpp index a206ea3db232ea..d10e488da6ab33 100644 --- a/src/coreclr/debug/daccess/request.cpp +++ b/src/coreclr/debug/daccess/request.cpp @@ -5498,9 +5498,9 @@ HRESULT ClrDataAccess::GetGlobalAllocationContext( } SOSDacEnter(); - gc_alloc_context global_alloc_context = ((ee_alloc_context)g_global_alloc_context).m_GCAllocContext; - *allocPtr = (CLRDATA_ADDRESS)global_alloc_context.alloc_ptr; - *allocLimit = (CLRDATA_ADDRESS)global_alloc_context.alloc_limit; + // The runtime does not allocate out of a global allocation context. + *allocPtr = (CLRDATA_ADDRESS)0; + *allocLimit = (CLRDATA_ADDRESS)0; SOSDacLeave(); return hr; } diff --git a/src/coreclr/inc/dacvars.h b/src/coreclr/inc/dacvars.h index 600b0e9c553c30..26f32397914682 100644 --- a/src/coreclr/inc/dacvars.h +++ b/src/coreclr/inc/dacvars.h @@ -150,6 +150,8 @@ DEFINE_DACVAR(ProfControlBlock, dac__g_profControlBlock, ::g_profControlBlock) DEFINE_DACVAR(PTR_DWORD, dac__g_card_table, ::g_card_table) DEFINE_DACVAR(PTR_BYTE, dac__g_lowest_address, ::g_lowest_address) DEFINE_DACVAR(PTR_BYTE, dac__g_highest_address, ::g_highest_address) +// Not read by the DAC. Kept so that the (permanently zeroed) global is still captured in +// mini/triage dumps, where the cDAC reads it for the GC contract c1 GlobalAllocContext global. DEFINE_DACVAR(ee_alloc_context, dac__g_global_alloc_context, ::g_global_alloc_context) DEFINE_DACVAR(IGCHeap, dac__g_pGCHeap, ::g_pGCHeap) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index f654eb6fb2c12a..57483724cfb60a 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1904,5 +1904,5 @@ CDAC_GLOBALS_END() // the contract version that requires it. Keep this list up to date so that the next version bump // does not have to rediscover them. // -// - Delete GlobalAllocContext (and ::g_global_alloc_context) once the GC contract version is -// bumped to c2. +// - Delete GlobalAllocContext (and ::g_global_alloc_context, together with its dacvars.h entry +// that keeps it captured in mini/triage dumps) once the GC contract version is bumped to c2.