From 376305901fad2479ed32034d16d0623037aed764 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Mon, 31 Aug 2026 13:44:38 -0400 Subject: [PATCH] Align CoreCLR GS cookie protection with NativeAOT (#132925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #99977 Hosting CoreCLR inside Apple's stock `/usr/bin/lldb` on macOS arm64 fails: `coreclr_initialize` returns `HRESULT 0x8007000C`. `InitGSCookie()` temporarily calls `ClrVirtualProtect(PAGE_READWRITE)` on `s_gsCookie`, which lives in Apple's `__DATA_CONST` segment (via `const`/`READONLY_ATTR`). Apple marks that segment immutable once a Mach exception port owns the process — which happens when CoreCLR is hosted inside LLDB with `PAL_MachExceptionMode` set to avoid Apple's guarded Mach exception-port operations — so the underlying `mprotect` call fails. NativeAOT hit and fixed the identical Apple problem in #99173. Its read-only GS cookie feature is also disabled on WebAssembly, where changing page protections is unnecessary overhead, and on OpenBSD, where `ld.so` marks read-only segments immutable at load time. This PR aligns CoreCLR with that existing NativeAOT platform policy: - Define `FEATURE_READONLY_GS_COOKIE` on all targets except Apple, WebAssembly, and OpenBSD. - `vars.hpp`/`vars.cpp`: `s_gsCookie` keeps its read-only `const`/`READONLY_ATTR` declaration when the feature is enabled; otherwise it is plain writable data. - `ceemain.cpp`: `InitGSCookie()` skips both `ClrVirtualProtect` calls when the feature is disabled. Cookie generation and the write itself are unchanged on every platform. On Apple and OpenBSD, this trades a narrow defense-in-depth mitigation for compatibility with the platform's immutable read-only segments. On WebAssembly, it avoids page-protection overhead that provides no benefit. Other platforms retain the existing read-only cookie behavior. > [!NOTE] > This description was drafted with the assistance of an AI coding agent (GitHub Copilot). --- src/coreclr/vm/CMakeLists.txt | 4 ++++ src/coreclr/vm/ceemain.cpp | 4 ++++ src/coreclr/vm/vars.cpp | 4 ++++ src/coreclr/vm/vars.hpp | 7 ++++++- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index 05fcb33903f3e7..64332c0c566990 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -17,6 +17,10 @@ include_directories(${CLR_SRC_NATIVE_DIR}/libs/Common) add_definitions(-DUNICODE) add_definitions(-D_UNICODE) +if(NOT CLR_CMAKE_TARGET_APPLE AND NOT CLR_CMAKE_TARGET_ARCH_WASM AND NOT CLR_CMAKE_TARGET_OPENBSD) + add_definitions(-DFEATURE_READONLY_GS_COOKIE) +endif() + if(CLR_CMAKE_TARGET_ANDROID OR CLR_CMAKE_TARGET_OPENBSD) # OpenBSD's ld.so cannot resolve native TLS relocations in shared objects and has no # __tls_get_addr, so the runtime must use emulated TLS (like Android). diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index e59c29fed207f3..b295dba46e9273 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -470,6 +470,7 @@ void InitGSCookie() volatile GSCookie * pGSCookiePtr = GetProcessGSCookiePtr(); +#ifdef FEATURE_READONLY_GS_COOKIE // The GS cookie is stored in a read only data segment DWORD oldProtection; if(!ClrVirtualProtect((LPVOID)pGSCookiePtr, sizeof(GSCookie), PAGE_READWRITE, &oldProtection)) @@ -481,6 +482,7 @@ void InitGSCookie() // PAL layer is unable to extract old protection for regions that were not allocated using VirtualAlloc oldProtection = PAGE_READONLY; #endif // TARGET_UNIX +#endif // FEATURE_READONLY_GS_COOKIE #ifndef TARGET_UNIX // The GSCookie cannot be in a writeable page @@ -507,10 +509,12 @@ void InitGSCookie() val ++; *pGSCookiePtr = val; +#ifdef FEATURE_READONLY_GS_COOKIE if(!ClrVirtualProtect((LPVOID)pGSCookiePtr, sizeof(GSCookie), oldProtection, &oldProtection)) { ThrowLastError(); } +#endif // FEATURE_READONLY_GS_COOKIE } Volatile g_bIsGarbageCollectorFullyInitialized = FALSE; diff --git a/src/coreclr/vm/vars.cpp b/src/coreclr/vm/vars.cpp index 57046ff71c5a8b..ae813d80f56c72 100644 --- a/src/coreclr/vm/vars.cpp +++ b/src/coreclr/vm/vars.cpp @@ -252,7 +252,11 @@ void OBJECTREF_EnumMemoryRegions(OBJECTREF ref) // // We need the following to be the compiler's notion of volatile. // +#ifdef FEATURE_READONLY_GS_COOKIE extern "C" RAW_KEYWORD(volatile) const GSCookie s_gsCookie = 0; +#else +extern "C" RAW_KEYWORD(volatile) GSCookie s_gsCookie = 0; +#endif #else __GlobalVal< GSCookie > s_gsCookie(&DacGlobals::dac__s_gsCookie); diff --git a/src/coreclr/vm/vars.hpp b/src/coreclr/vm/vars.hpp index 6960585b9e7482..b250a23ef27e5c 100644 --- a/src/coreclr/vm/vars.hpp +++ b/src/coreclr/vm/vars.hpp @@ -594,11 +594,16 @@ typedef DPTR(GSCookie) PTR_GSCookie; #endif #ifndef DACCESS_COMPILE -// const is so that it gets placed in the .text section (which is read-only) +#ifdef FEATURE_READONLY_GS_COOKIE + +// const places the cookie in a read-only data section. // volatile is so that accesses to it do not get optimized away because of the const // extern "C" RAW_KEYWORD(volatile) READONLY_ATTR const GSCookie s_gsCookie; +#else +extern "C" RAW_KEYWORD(volatile) GSCookie s_gsCookie; +#endif // FEATURE_READONLY_GS_COOKIE inline GSCookie * GetProcessGSCookiePtr() { return const_cast(&s_gsCookie); }