From 780b688470bf13320a4ea2e8161ae81470d4b714 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Tue, 22 Jul 2025 12:51:30 -0700 Subject: [PATCH 1/7] Revert "Remove custom allocator." This reverts commit c62bc5b2721673b14e702348a8ccf5f976a2b7a3. --- .../CMakeLists.txt | 6 + .../System.IO.Compression.Native/pal_zlib.c | 4 + .../zlib_allocator.h | 8 + .../zlib_allocator_unix.c | 151 +++++++++++++++ .../zlib_allocator_win.c | 180 ++++++++++++++++++ 5 files changed, 349 insertions(+) create mode 100644 src/native/libs/System.IO.Compression.Native/zlib_allocator.h create mode 100644 src/native/libs/System.IO.Compression.Native/zlib_allocator_unix.c create mode 100644 src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c diff --git a/src/native/libs/System.IO.Compression.Native/CMakeLists.txt b/src/native/libs/System.IO.Compression.Native/CMakeLists.txt index cd5f4200d009c5..74a2fe9a2544f2 100644 --- a/src/native/libs/System.IO.Compression.Native/CMakeLists.txt +++ b/src/native/libs/System.IO.Compression.Native/CMakeLists.txt @@ -36,6 +36,12 @@ set(NATIVECOMPRESSION_SOURCES pal_zlib.c ) +if (HOST_WIN32 OR CLR_CMAKE_TARGET_WIN32) + list(APPEND NATIVECOMPRESSION_SOURCES "zlib_allocator_win.c") +else() + list(APPEND NATIVECOMPRESSION_SOURCES "zlib_allocator_unix.c") +endif() + if (NOT CLR_CMAKE_TARGET_BROWSER AND NOT CLR_CMAKE_TARGET_WASI) set (NATIVECOMPRESSION_SOURCES ${NATIVECOMPRESSION_SOURCES} diff --git a/src/native/libs/System.IO.Compression.Native/pal_zlib.c b/src/native/libs/System.IO.Compression.Native/pal_zlib.c index 5ef4c8c7c1a4b4..1612040460b7d1 100644 --- a/src/native/libs/System.IO.Compression.Native/pal_zlib.c +++ b/src/native/libs/System.IO.Compression.Native/pal_zlib.c @@ -11,6 +11,7 @@ #else #include "pal_utilities.h" #endif +#include #include c_static_assert(PAL_Z_NOFLUSH == Z_NO_FLUSH); @@ -39,6 +40,9 @@ static int32_t Init(PAL_ZStream* stream) { z_stream* zStream = (z_stream*)calloc(1, sizeof(z_stream)); + zStream->zalloc = z_custom_calloc; + zStream->zfree = z_custom_cfree; + stream->internalState = zStream; if (zStream != NULL) diff --git a/src/native/libs/System.IO.Compression.Native/zlib_allocator.h b/src/native/libs/System.IO.Compression.Native/zlib_allocator.h new file mode 100644 index 00000000000000..cadd00bb5879c5 --- /dev/null +++ b/src/native/libs/System.IO.Compression.Native/zlib_allocator.h @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include // voidpf + +voidpf z_custom_calloc(voidpf opaque, unsigned items, unsigned size); + +void z_custom_cfree(voidpf opaque, voidpf ptr); diff --git a/src/native/libs/System.IO.Compression.Native/zlib_allocator_unix.c b/src/native/libs/System.IO.Compression.Native/zlib_allocator_unix.c new file mode 100644 index 00000000000000..38638d61585264 --- /dev/null +++ b/src/native/libs/System.IO.Compression.Native/zlib_allocator_unix.c @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include +#include +#include +#include +#include +#include + +/* A custom allocator for zlib that provides some defense-in-depth over standard malloc / free. + * (non-Windows version) + * + * 1. When zlib allocates fixed-length data structures for containing stream metadata, we zero + * the memory before using it, preventing use of uninitialized memory within these structures. + * Ideally we would do this for dynamically-sized buffers as well, but there is a measurable + * perf impact to doing this. Zeroing fixed structures seems like a good trade-off here, since + * these data structures contain most of the metadata used for managing the variable-length + * dynamically allocated buffers. + * + * 2. We put a cookie both before and after any allocated memory, which allows us to detect local + * buffer overruns on the call to free(). The cookie values are tied to the addresses where + * the data is located in memory. + * + * 3. We trash the aforementioned cookie on free(), which allows us to detect double-free. + * + * If any of these checks fails, the application raises SIGABRT. + */ + +#ifndef MEMORY_ALLOCATION_ALIGNMENT +// malloc() returns an address suitably aligned for any built-in data type. +// Historically, this has been twice the arch's natural word size. +#ifdef HOST_64BIT +#define MEMORY_ALLOCATION_ALIGNMENT 16 +#else +#define MEMORY_ALLOCATION_ALIGNMENT 8 +#endif +#endif + +typedef struct _DOTNET_ALLOC_COOKIE +{ + void* Address; + size_t Size; +} DOTNET_ALLOC_COOKIE; + +static bool SafeAdd(size_t a, size_t b, size_t* sum) +{ + if (SIZE_MAX - a >= b) { *sum = a + b; return true; } + else { *sum = 0; return false; } +} + +static bool SafeMult(size_t a, size_t b, size_t* product) +{ + if (SIZE_MAX / a >= b) { *product = a * b; return true; } + else { *product = 0; return false; } +} + +static DOTNET_ALLOC_COOKIE ReadAllocCookieUnaligned(const void* pSrc) +{ + DOTNET_ALLOC_COOKIE vCookie; + memcpy(&vCookie, pSrc, sizeof(DOTNET_ALLOC_COOKIE)); + return vCookie; +} + +static void WriteAllocCookieUnaligned(void* pDest, DOTNET_ALLOC_COOKIE vCookie) +{ + memcpy(pDest, &vCookie, sizeof(DOTNET_ALLOC_COOKIE)); +} + +// Historically, the memory allocator always returns addresses aligned to some +// particular boundary. We'll make that same guarantee here just in case somebody +// depends on it. +const size_t DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING = (sizeof(DOTNET_ALLOC_COOKIE) + MEMORY_ALLOCATION_ALIGNMENT - 1) & ~((size_t)MEMORY_ALLOCATION_ALIGNMENT - 1); +const size_t DOTNET_ALLOC_TRAILER_COOKIE_SIZE = sizeof(DOTNET_ALLOC_COOKIE); + +voidpf z_custom_calloc(voidpf opaque, unsigned items, unsigned size) +{ + (void)opaque; // unreferenced formal parameter + + // If initializing a fixed-size structure, zero the memory. + bool fZeroMemory = (items == 1); + + size_t cbRequested; + if (sizeof(items) + sizeof(size) <= sizeof(cbRequested)) + { + // multiplication can't overflow; no need for safeint + cbRequested = (size_t)items * (size_t)size; + } + else + { + // multiplication can overflow; go through safeint + if (!SafeMult((size_t)items, (size_t)size, &cbRequested)) { return NULL; } + } + + // Make sure the actual allocation has enough room for our frontside & backside cookies. + size_t cbActualAllocationSize; + if (!SafeAdd(cbRequested, DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING + DOTNET_ALLOC_TRAILER_COOKIE_SIZE, &cbActualAllocationSize)) { return NULL; } + + void* pAlloced = (fZeroMemory) ? calloc(1, cbActualAllocationSize) : malloc(cbActualAllocationSize); + if (pAlloced == NULL) { return NULL; } // OOM + + DOTNET_ALLOC_COOKIE* pHeaderCookie = (DOTNET_ALLOC_COOKIE*)pAlloced; + uint8_t* pReturnToCaller = (uint8_t*)pAlloced + DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING; + uint8_t* pTrailerCookie = pReturnToCaller + cbRequested; + + // Write out the same cookie for the header & the trailer, then we're done. + + DOTNET_ALLOC_COOKIE vCookie = { 0 }; + vCookie.Address = pReturnToCaller; + vCookie.Size = cbRequested; + *pHeaderCookie = vCookie; // aligned + WriteAllocCookieUnaligned(pTrailerCookie, vCookie); + + return pReturnToCaller; +} + +static void zcfree_trash_cookie(void* pCookie) +{ + memset(pCookie, 0, sizeof(DOTNET_ALLOC_COOKIE)); +} + +void z_custom_cfree(voidpf opaque, voidpf ptr) +{ + (void)opaque; // unreferenced formal parameter + + if (ptr == NULL) { return; } // ok to free nullptr + + // Check cookie at beginning + + DOTNET_ALLOC_COOKIE* pHeaderCookie = (DOTNET_ALLOC_COOKIE*)((uint8_t*)ptr - DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING); + if (pHeaderCookie->Address != ptr) { goto Fail; } + size_t cbRequested = pHeaderCookie->Size; + + // Check cookie at end + + uint8_t* pTrailerCookie = (uint8_t*)ptr + cbRequested; + DOTNET_ALLOC_COOKIE vTrailerCookie = ReadAllocCookieUnaligned(pTrailerCookie); + if (vTrailerCookie.Address != ptr) { goto Fail; } + if (vTrailerCookie.Size != cbRequested) { goto Fail; } + + // Checks passed - now trash the cookies and free memory + + zcfree_trash_cookie(pHeaderCookie); + zcfree_trash_cookie(pTrailerCookie); + + free(pHeaderCookie); + return; + +Fail: + abort(); // cookie check failed +} diff --git a/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c b/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c new file mode 100644 index 00000000000000..fefba550c16ed5 --- /dev/null +++ b/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c @@ -0,0 +1,180 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include +#include +#include +#include +#include /* _ASSERTE */ + +#include +#include +#include +#include + +/* A custom allocator for zlib that provides some defense-in-depth over standard malloc / free. + * (Windows-specific version) + * + * 1. In 64-bit processes, we use a custom heap rather than relying on the standard process heap. + * This should cause zlib's buffers to go into a separate address range from the rest of app + * data, making it more difficult for buffer overruns to affect non-zlib-related data structures. + * + * 2. When zlib allocates fixed-length data structures for containing stream metadata, we zero + * the memory before using it, preventing use of uninitialized memory within these structures. + * Ideally we would do this for dynamically-sized buffers as well, but there is a measurable + * perf impact to doing this. Zeroing fixed structures seems like a good trade-off here, since + * these data structures contain most of the metadata used for managing the variable-length + * dynamically allocated buffers. + * + * 3. We put a cookie both before and after any allocated memory, which allows us to detect local + * buffer overruns on the call to free(). The cookie values are enciphered to make it more + * difficult for somebody to guess a correct value. + * + * 4. We trash the aforementioned cookie on free(), which allows us to detect double-free. + * + * If any of these checks fails, the application terminates immediately, optionally triggering a + * crash dump. We use a special code that's easy to search for in Watson. + */ + +// Gets the special heap we'll allocate from. +HANDLE GetZlibHeap() +{ +#ifdef _WIN64 + static HANDLE s_hPublishedHeap = NULL; + + // If already initialized, return immediately. + // We don't need a volatile read here since the publish is performed with release semantics. + if (s_hPublishedHeap != NULL) { return s_hPublishedHeap; } + + // Attempt to create a new heap. The heap will be dynamically sized. + HANDLE hNewHeap = HeapCreate(0, 0, 0); + + if (hNewHeap != NULL) + { + // We created a new heap. Attempt to publish it. + if (InterlockedCompareExchangePointer(&s_hPublishedHeap, hNewHeap, NULL) != NULL) + { + HeapDestroy(hNewHeap); // Somebody published before us. Destroy our heap. + hNewHeap = NULL; // Guard against accidental use later in the method. + } + } + else + { + // If we can't create a new heap, fall back to the process default heap. + InterlockedCompareExchangePointer(&s_hPublishedHeap, GetProcessHeap(), NULL); + } + + // Some thread - perhaps us, perhaps somebody else - published the heap. Return it. + // We don't need a volatile read here since the publish is performed with release semantics. + _ASSERTE(s_hPublishedHeap != NULL); + return s_hPublishedHeap; +#else + // We don't want to create a new heap in a 32-bit process because it could end up + // reserving too much of the address space. Instead, fall back to the normal process heap. + return GetProcessHeap(); +#endif +} + +typedef struct _DOTNET_ALLOC_COOKIE +{ + PVOID CookieValue; + union _Size + { + SIZE_T RawValue; + LPVOID EncodedValue; + } Size; +} DOTNET_ALLOC_COOKIE; + +// Historically, the Windows memory allocator always returns addresses aligned to some +// particular boundary. We'll make that same guarantee here just in case somebody +// depends on it. +const SIZE_T DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING = (sizeof(DOTNET_ALLOC_COOKIE) + MEMORY_ALLOCATION_ALIGNMENT - 1) & ~((SIZE_T)MEMORY_ALLOCATION_ALIGNMENT - 1); +const SIZE_T DOTNET_ALLOC_TRAILER_COOKIE_SIZE = sizeof(DOTNET_ALLOC_COOKIE); + +voidpf z_custom_calloc(opaque, items, size) + voidpf opaque; + unsigned items; + unsigned size; +{ + (void)opaque; // suppress C4100 - unreferenced formal parameter + + // If initializing a fixed-size structure, zero the memory. + DWORD dwFlags = (items == 1) ? HEAP_ZERO_MEMORY : 0; + + SIZE_T cbRequested; + if (sizeof(items) + sizeof(size) <= sizeof(cbRequested)) + { + // multiplication can't overflow; no need for safeint + cbRequested = (SIZE_T)items * (SIZE_T)size; + } + else + { + // multiplication can overflow; go through safeint + if (FAILED(SIZETMult(items, size, &cbRequested))) { return NULL; } + } + + // Make sure the actual allocation has enough room for our frontside & backside cookies. + SIZE_T cbActualAllocationSize; + if (FAILED(SIZETAdd(cbRequested, DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING + DOTNET_ALLOC_TRAILER_COOKIE_SIZE, &cbActualAllocationSize))) { return NULL; } + + LPVOID pAlloced = HeapAlloc(GetZlibHeap(), dwFlags, cbActualAllocationSize); + if (pAlloced == NULL) { return NULL; } // OOM + + // Now set the header & trailer cookies + DOTNET_ALLOC_COOKIE* pHeaderCookie = (DOTNET_ALLOC_COOKIE*)pAlloced; + pHeaderCookie->CookieValue = EncodePointer(&pHeaderCookie->CookieValue); + pHeaderCookie->Size.RawValue = cbRequested; + + LPBYTE pReturnToCaller = (LPBYTE)pHeaderCookie + DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING; + + UNALIGNED DOTNET_ALLOC_COOKIE* pTrailerCookie = (UNALIGNED DOTNET_ALLOC_COOKIE*)(pReturnToCaller + cbRequested); + pTrailerCookie->CookieValue = EncodePointer(&pTrailerCookie->CookieValue); + pTrailerCookie->Size.EncodedValue = EncodePointer((PVOID)cbRequested); + + return pReturnToCaller; +} + +FORCEINLINE +void zcfree_trash_cookie(UNALIGNED DOTNET_ALLOC_COOKIE* pCookie) +{ + memset(pCookie, 0, sizeof(*pCookie)); + pCookie->CookieValue = (PVOID)(SIZE_T)0xDEADBEEF; +} + +// Marked noinline to keep it on the call stack during crash reports. +DECLSPEC_NOINLINE +DECLSPEC_NORETURN +void zcfree_cookie_check_failed() +{ + __fastfail(FAST_FAIL_HEAP_METADATA_CORRUPTION); +} + +void z_custom_cfree(opaque, ptr) + voidpf opaque; + voidpf ptr; +{ + (void)opaque; // suppress C4100 - unreferenced formal parameter + + if (ptr == NULL) { return; } // ok to free nullptr + + // Check cookie at beginning and end + + DOTNET_ALLOC_COOKIE* pHeaderCookie = (DOTNET_ALLOC_COOKIE*)((LPBYTE)ptr - DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING); + if (DecodePointer(pHeaderCookie->CookieValue) != &pHeaderCookie->CookieValue) { goto Fail; } + SIZE_T cbRequested = pHeaderCookie->Size.RawValue; + + UNALIGNED DOTNET_ALLOC_COOKIE* pTrailerCookie = (UNALIGNED DOTNET_ALLOC_COOKIE*)((LPBYTE)ptr + cbRequested); + if (DecodePointer(pTrailerCookie->CookieValue) != &pTrailerCookie->CookieValue) { goto Fail; } + if (DecodePointer(pTrailerCookie->Size.EncodedValue) != (LPVOID)cbRequested) { goto Fail; } + + // Checks passed - now trash the cookies and free memory + + zcfree_trash_cookie(pHeaderCookie); + zcfree_trash_cookie(pTrailerCookie); + + if (!HeapFree(GetZlibHeap(), 0, pHeaderCookie)) { goto Fail; } + return; + +Fail: + zcfree_cookie_check_failed(); +} From 1bc2d06347f7f32fcb9ac63bff8b8d34c1338f67 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Tue, 22 Jul 2025 19:41:36 -0700 Subject: [PATCH 2/7] Remove zlib custom allocator for linux. --- .../CMakeLists.txt | 2 - .../System.IO.Compression.Native/pal_zlib.c | 4 +- .../zlib_allocator_unix.c | 151 ------------------ 3 files changed, 3 insertions(+), 154 deletions(-) delete mode 100644 src/native/libs/System.IO.Compression.Native/zlib_allocator_unix.c diff --git a/src/native/libs/System.IO.Compression.Native/CMakeLists.txt b/src/native/libs/System.IO.Compression.Native/CMakeLists.txt index 74a2fe9a2544f2..e906e5d80dd7cc 100644 --- a/src/native/libs/System.IO.Compression.Native/CMakeLists.txt +++ b/src/native/libs/System.IO.Compression.Native/CMakeLists.txt @@ -38,8 +38,6 @@ set(NATIVECOMPRESSION_SOURCES if (HOST_WIN32 OR CLR_CMAKE_TARGET_WIN32) list(APPEND NATIVECOMPRESSION_SOURCES "zlib_allocator_win.c") -else() - list(APPEND NATIVECOMPRESSION_SOURCES "zlib_allocator_unix.c") endif() if (NOT CLR_CMAKE_TARGET_BROWSER AND NOT CLR_CMAKE_TARGET_WASI) diff --git a/src/native/libs/System.IO.Compression.Native/pal_zlib.c b/src/native/libs/System.IO.Compression.Native/pal_zlib.c index 1612040460b7d1..493e915b35e772 100644 --- a/src/native/libs/System.IO.Compression.Native/pal_zlib.c +++ b/src/native/libs/System.IO.Compression.Native/pal_zlib.c @@ -8,10 +8,10 @@ #ifdef _WIN32 #define c_static_assert(e) static_assert((e),"") #include "../Common/pal_utilities.h" + #include #else #include "pal_utilities.h" #endif -#include #include c_static_assert(PAL_Z_NOFLUSH == Z_NO_FLUSH); @@ -40,8 +40,10 @@ static int32_t Init(PAL_ZStream* stream) { z_stream* zStream = (z_stream*)calloc(1, sizeof(z_stream)); +#ifdef _WIN32 zStream->zalloc = z_custom_calloc; zStream->zfree = z_custom_cfree; +#endif stream->internalState = zStream; diff --git a/src/native/libs/System.IO.Compression.Native/zlib_allocator_unix.c b/src/native/libs/System.IO.Compression.Native/zlib_allocator_unix.c deleted file mode 100644 index 38638d61585264..00000000000000 --- a/src/native/libs/System.IO.Compression.Native/zlib_allocator_unix.c +++ /dev/null @@ -1,151 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#include -#include -#include -#include -#include -#include - -/* A custom allocator for zlib that provides some defense-in-depth over standard malloc / free. - * (non-Windows version) - * - * 1. When zlib allocates fixed-length data structures for containing stream metadata, we zero - * the memory before using it, preventing use of uninitialized memory within these structures. - * Ideally we would do this for dynamically-sized buffers as well, but there is a measurable - * perf impact to doing this. Zeroing fixed structures seems like a good trade-off here, since - * these data structures contain most of the metadata used for managing the variable-length - * dynamically allocated buffers. - * - * 2. We put a cookie both before and after any allocated memory, which allows us to detect local - * buffer overruns on the call to free(). The cookie values are tied to the addresses where - * the data is located in memory. - * - * 3. We trash the aforementioned cookie on free(), which allows us to detect double-free. - * - * If any of these checks fails, the application raises SIGABRT. - */ - -#ifndef MEMORY_ALLOCATION_ALIGNMENT -// malloc() returns an address suitably aligned for any built-in data type. -// Historically, this has been twice the arch's natural word size. -#ifdef HOST_64BIT -#define MEMORY_ALLOCATION_ALIGNMENT 16 -#else -#define MEMORY_ALLOCATION_ALIGNMENT 8 -#endif -#endif - -typedef struct _DOTNET_ALLOC_COOKIE -{ - void* Address; - size_t Size; -} DOTNET_ALLOC_COOKIE; - -static bool SafeAdd(size_t a, size_t b, size_t* sum) -{ - if (SIZE_MAX - a >= b) { *sum = a + b; return true; } - else { *sum = 0; return false; } -} - -static bool SafeMult(size_t a, size_t b, size_t* product) -{ - if (SIZE_MAX / a >= b) { *product = a * b; return true; } - else { *product = 0; return false; } -} - -static DOTNET_ALLOC_COOKIE ReadAllocCookieUnaligned(const void* pSrc) -{ - DOTNET_ALLOC_COOKIE vCookie; - memcpy(&vCookie, pSrc, sizeof(DOTNET_ALLOC_COOKIE)); - return vCookie; -} - -static void WriteAllocCookieUnaligned(void* pDest, DOTNET_ALLOC_COOKIE vCookie) -{ - memcpy(pDest, &vCookie, sizeof(DOTNET_ALLOC_COOKIE)); -} - -// Historically, the memory allocator always returns addresses aligned to some -// particular boundary. We'll make that same guarantee here just in case somebody -// depends on it. -const size_t DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING = (sizeof(DOTNET_ALLOC_COOKIE) + MEMORY_ALLOCATION_ALIGNMENT - 1) & ~((size_t)MEMORY_ALLOCATION_ALIGNMENT - 1); -const size_t DOTNET_ALLOC_TRAILER_COOKIE_SIZE = sizeof(DOTNET_ALLOC_COOKIE); - -voidpf z_custom_calloc(voidpf opaque, unsigned items, unsigned size) -{ - (void)opaque; // unreferenced formal parameter - - // If initializing a fixed-size structure, zero the memory. - bool fZeroMemory = (items == 1); - - size_t cbRequested; - if (sizeof(items) + sizeof(size) <= sizeof(cbRequested)) - { - // multiplication can't overflow; no need for safeint - cbRequested = (size_t)items * (size_t)size; - } - else - { - // multiplication can overflow; go through safeint - if (!SafeMult((size_t)items, (size_t)size, &cbRequested)) { return NULL; } - } - - // Make sure the actual allocation has enough room for our frontside & backside cookies. - size_t cbActualAllocationSize; - if (!SafeAdd(cbRequested, DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING + DOTNET_ALLOC_TRAILER_COOKIE_SIZE, &cbActualAllocationSize)) { return NULL; } - - void* pAlloced = (fZeroMemory) ? calloc(1, cbActualAllocationSize) : malloc(cbActualAllocationSize); - if (pAlloced == NULL) { return NULL; } // OOM - - DOTNET_ALLOC_COOKIE* pHeaderCookie = (DOTNET_ALLOC_COOKIE*)pAlloced; - uint8_t* pReturnToCaller = (uint8_t*)pAlloced + DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING; - uint8_t* pTrailerCookie = pReturnToCaller + cbRequested; - - // Write out the same cookie for the header & the trailer, then we're done. - - DOTNET_ALLOC_COOKIE vCookie = { 0 }; - vCookie.Address = pReturnToCaller; - vCookie.Size = cbRequested; - *pHeaderCookie = vCookie; // aligned - WriteAllocCookieUnaligned(pTrailerCookie, vCookie); - - return pReturnToCaller; -} - -static void zcfree_trash_cookie(void* pCookie) -{ - memset(pCookie, 0, sizeof(DOTNET_ALLOC_COOKIE)); -} - -void z_custom_cfree(voidpf opaque, voidpf ptr) -{ - (void)opaque; // unreferenced formal parameter - - if (ptr == NULL) { return; } // ok to free nullptr - - // Check cookie at beginning - - DOTNET_ALLOC_COOKIE* pHeaderCookie = (DOTNET_ALLOC_COOKIE*)((uint8_t*)ptr - DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING); - if (pHeaderCookie->Address != ptr) { goto Fail; } - size_t cbRequested = pHeaderCookie->Size; - - // Check cookie at end - - uint8_t* pTrailerCookie = (uint8_t*)ptr + cbRequested; - DOTNET_ALLOC_COOKIE vTrailerCookie = ReadAllocCookieUnaligned(pTrailerCookie); - if (vTrailerCookie.Address != ptr) { goto Fail; } - if (vTrailerCookie.Size != cbRequested) { goto Fail; } - - // Checks passed - now trash the cookies and free memory - - zcfree_trash_cookie(pHeaderCookie); - zcfree_trash_cookie(pTrailerCookie); - - free(pHeaderCookie); - return; - -Fail: - abort(); // cookie check failed -} From 2e56890a31a9ea94729bd199f6af99eec3a42e88 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Tue, 22 Jul 2025 19:41:55 -0700 Subject: [PATCH 3/7] Remove cookie check from windows custom zlib allocator --- .../zlib_allocator_win.c | 99 ++----------------- 1 file changed, 6 insertions(+), 93 deletions(-) diff --git a/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c b/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c index fefba550c16ed5..cc69a178309e05 100644 --- a/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c +++ b/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c @@ -4,36 +4,13 @@ #include #include #include -#include #include /* _ASSERTE */ -#include -#include -#include #include -/* A custom allocator for zlib that provides some defense-in-depth over standard malloc / free. - * (Windows-specific version) - * - * 1. In 64-bit processes, we use a custom heap rather than relying on the standard process heap. - * This should cause zlib's buffers to go into a separate address range from the rest of app - * data, making it more difficult for buffer overruns to affect non-zlib-related data structures. - * - * 2. When zlib allocates fixed-length data structures for containing stream metadata, we zero - * the memory before using it, preventing use of uninitialized memory within these structures. - * Ideally we would do this for dynamically-sized buffers as well, but there is a measurable - * perf impact to doing this. Zeroing fixed structures seems like a good trade-off here, since - * these data structures contain most of the metadata used for managing the variable-length - * dynamically allocated buffers. - * - * 3. We put a cookie both before and after any allocated memory, which allows us to detect local - * buffer overruns on the call to free(). The cookie values are enciphered to make it more - * difficult for somebody to guess a correct value. - * - * 4. We trash the aforementioned cookie on free(), which allows us to detect double-free. - * - * If any of these checks fails, the application terminates immediately, optionally triggering a - * crash dump. We use a special code that's easy to search for in Watson. +/* A custom allocator for zlib that uses a dedicated heap. + This provides better performance and avoids fragmentation + that can occur on Windows when using the default process heap. */ // Gets the special heap we'll allocate from. @@ -75,22 +52,6 @@ HANDLE GetZlibHeap() #endif } -typedef struct _DOTNET_ALLOC_COOKIE -{ - PVOID CookieValue; - union _Size - { - SIZE_T RawValue; - LPVOID EncodedValue; - } Size; -} DOTNET_ALLOC_COOKIE; - -// Historically, the Windows memory allocator always returns addresses aligned to some -// particular boundary. We'll make that same guarantee here just in case somebody -// depends on it. -const SIZE_T DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING = (sizeof(DOTNET_ALLOC_COOKIE) + MEMORY_ALLOCATION_ALIGNMENT - 1) & ~((SIZE_T)MEMORY_ALLOCATION_ALIGNMENT - 1); -const SIZE_T DOTNET_ALLOC_TRAILER_COOKIE_SIZE = sizeof(DOTNET_ALLOC_COOKIE); - voidpf z_custom_calloc(opaque, items, size) voidpf opaque; unsigned items; @@ -113,40 +74,7 @@ voidpf z_custom_calloc(opaque, items, size) if (FAILED(SIZETMult(items, size, &cbRequested))) { return NULL; } } - // Make sure the actual allocation has enough room for our frontside & backside cookies. - SIZE_T cbActualAllocationSize; - if (FAILED(SIZETAdd(cbRequested, DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING + DOTNET_ALLOC_TRAILER_COOKIE_SIZE, &cbActualAllocationSize))) { return NULL; } - - LPVOID pAlloced = HeapAlloc(GetZlibHeap(), dwFlags, cbActualAllocationSize); - if (pAlloced == NULL) { return NULL; } // OOM - - // Now set the header & trailer cookies - DOTNET_ALLOC_COOKIE* pHeaderCookie = (DOTNET_ALLOC_COOKIE*)pAlloced; - pHeaderCookie->CookieValue = EncodePointer(&pHeaderCookie->CookieValue); - pHeaderCookie->Size.RawValue = cbRequested; - - LPBYTE pReturnToCaller = (LPBYTE)pHeaderCookie + DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING; - - UNALIGNED DOTNET_ALLOC_COOKIE* pTrailerCookie = (UNALIGNED DOTNET_ALLOC_COOKIE*)(pReturnToCaller + cbRequested); - pTrailerCookie->CookieValue = EncodePointer(&pTrailerCookie->CookieValue); - pTrailerCookie->Size.EncodedValue = EncodePointer((PVOID)cbRequested); - - return pReturnToCaller; -} - -FORCEINLINE -void zcfree_trash_cookie(UNALIGNED DOTNET_ALLOC_COOKIE* pCookie) -{ - memset(pCookie, 0, sizeof(*pCookie)); - pCookie->CookieValue = (PVOID)(SIZE_T)0xDEADBEEF; -} - -// Marked noinline to keep it on the call stack during crash reports. -DECLSPEC_NOINLINE -DECLSPEC_NORETURN -void zcfree_cookie_check_failed() -{ - __fastfail(FAST_FAIL_HEAP_METADATA_CORRUPTION); + return HeapAlloc(GetZlibHeap(), dwFlags, cbRequested); } void z_custom_cfree(opaque, ptr) @@ -157,24 +85,9 @@ void z_custom_cfree(opaque, ptr) if (ptr == NULL) { return; } // ok to free nullptr - // Check cookie at beginning and end - - DOTNET_ALLOC_COOKIE* pHeaderCookie = (DOTNET_ALLOC_COOKIE*)((LPBYTE)ptr - DOTNET_ALLOC_HEADER_COOKIE_SIZE_WITH_PADDING); - if (DecodePointer(pHeaderCookie->CookieValue) != &pHeaderCookie->CookieValue) { goto Fail; } - SIZE_T cbRequested = pHeaderCookie->Size.RawValue; - - UNALIGNED DOTNET_ALLOC_COOKIE* pTrailerCookie = (UNALIGNED DOTNET_ALLOC_COOKIE*)((LPBYTE)ptr + cbRequested); - if (DecodePointer(pTrailerCookie->CookieValue) != &pTrailerCookie->CookieValue) { goto Fail; } - if (DecodePointer(pTrailerCookie->Size.EncodedValue) != (LPVOID)cbRequested) { goto Fail; } - - // Checks passed - now trash the cookies and free memory - - zcfree_trash_cookie(pHeaderCookie); - zcfree_trash_cookie(pTrailerCookie); - - if (!HeapFree(GetZlibHeap(), 0, pHeaderCookie)) { goto Fail; } + if (!HeapFree(GetZlibHeap(), 0, ptr)) { goto Fail; } return; Fail: - zcfree_cookie_check_failed(); + __fastfail(FAST_FAIL_HEAP_METADATA_CORRUPTION); } From 069e349ecc8d2a1329fefbe5d79f8de2e27bcc42 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Wed, 23 Jul 2025 08:14:09 -0700 Subject: [PATCH 4/7] Remove zeroing of memory in zlib-allocator --- .../System.IO.Compression.Native/zlib_allocator_win.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c b/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c index cc69a178309e05..3dddad3017118e 100644 --- a/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c +++ b/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c @@ -57,10 +57,6 @@ voidpf z_custom_calloc(opaque, items, size) unsigned items; unsigned size; { - (void)opaque; // suppress C4100 - unreferenced formal parameter - - // If initializing a fixed-size structure, zero the memory. - DWORD dwFlags = (items == 1) ? HEAP_ZERO_MEMORY : 0; SIZE_T cbRequested; if (sizeof(items) + sizeof(size) <= sizeof(cbRequested)) @@ -74,15 +70,13 @@ voidpf z_custom_calloc(opaque, items, size) if (FAILED(SIZETMult(items, size, &cbRequested))) { return NULL; } } - return HeapAlloc(GetZlibHeap(), dwFlags, cbRequested); + return HeapAlloc(GetZlibHeap(), 0, cbRequested); } void z_custom_cfree(opaque, ptr) voidpf opaque; voidpf ptr; { - (void)opaque; // suppress C4100 - unreferenced formal parameter - if (ptr == NULL) { return; } // ok to free nullptr if (!HeapFree(GetZlibHeap(), 0, ptr)) { goto Fail; } From 75e67adea881f7d6a1e8ffc036a47906c249270d Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Wed, 23 Jul 2025 12:51:29 -0700 Subject: [PATCH 5/7] Don't use custom allocator on x86 --- src/native/libs/System.IO.Compression.Native/CMakeLists.txt | 4 +++- src/native/libs/System.IO.Compression.Native/pal_zlib.c | 6 ++++-- .../libs/System.IO.Compression.Native/zlib_allocator_win.c | 6 ------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/native/libs/System.IO.Compression.Native/CMakeLists.txt b/src/native/libs/System.IO.Compression.Native/CMakeLists.txt index e906e5d80dd7cc..4741ca8cd7a008 100644 --- a/src/native/libs/System.IO.Compression.Native/CMakeLists.txt +++ b/src/native/libs/System.IO.Compression.Native/CMakeLists.txt @@ -37,7 +37,9 @@ set(NATIVECOMPRESSION_SOURCES ) if (HOST_WIN32 OR CLR_CMAKE_TARGET_WIN32) - list(APPEND NATIVECOMPRESSION_SOURCES "zlib_allocator_win.c") + if (CLR_CMAKE_TARGET_ARCH_AMD64) + list(APPEND NATIVECOMPRESSION_SOURCES "zlib_allocator_win.c") + endif() endif() if (NOT CLR_CMAKE_TARGET_BROWSER AND NOT CLR_CMAKE_TARGET_WASI) diff --git a/src/native/libs/System.IO.Compression.Native/pal_zlib.c b/src/native/libs/System.IO.Compression.Native/pal_zlib.c index 493e915b35e772..d932eb522e30ee 100644 --- a/src/native/libs/System.IO.Compression.Native/pal_zlib.c +++ b/src/native/libs/System.IO.Compression.Native/pal_zlib.c @@ -8,7 +8,9 @@ #ifdef _WIN32 #define c_static_assert(e) static_assert((e),"") #include "../Common/pal_utilities.h" - #include + #if _WIN64 + #include + #endif #else #include "pal_utilities.h" #endif @@ -40,7 +42,7 @@ static int32_t Init(PAL_ZStream* stream) { z_stream* zStream = (z_stream*)calloc(1, sizeof(z_stream)); -#ifdef _WIN32 +#ifdef _WIN64 zStream->zalloc = z_custom_calloc; zStream->zfree = z_custom_cfree; #endif diff --git a/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c b/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c index 3dddad3017118e..239a46e0bfb64b 100644 --- a/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c +++ b/src/native/libs/System.IO.Compression.Native/zlib_allocator_win.c @@ -16,7 +16,6 @@ // Gets the special heap we'll allocate from. HANDLE GetZlibHeap() { -#ifdef _WIN64 static HANDLE s_hPublishedHeap = NULL; // If already initialized, return immediately. @@ -45,11 +44,6 @@ HANDLE GetZlibHeap() // We don't need a volatile read here since the publish is performed with release semantics. _ASSERTE(s_hPublishedHeap != NULL); return s_hPublishedHeap; -#else - // We don't want to create a new heap in a 32-bit process because it could end up - // reserving too much of the address space. Instead, fall back to the normal process heap. - return GetProcessHeap(); -#endif } voidpf z_custom_calloc(opaque, items, size) From bb96fdcf3edc52f3b0467f2ed0c7ef10d4d486d8 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Wed, 23 Jul 2025 13:22:52 -0700 Subject: [PATCH 6/7] Refine allocator condition --- src/native/libs/System.IO.Compression.Native/CMakeLists.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/native/libs/System.IO.Compression.Native/CMakeLists.txt b/src/native/libs/System.IO.Compression.Native/CMakeLists.txt index 4741ca8cd7a008..0451636840459b 100644 --- a/src/native/libs/System.IO.Compression.Native/CMakeLists.txt +++ b/src/native/libs/System.IO.Compression.Native/CMakeLists.txt @@ -36,10 +36,8 @@ set(NATIVECOMPRESSION_SOURCES pal_zlib.c ) -if (HOST_WIN32 OR CLR_CMAKE_TARGET_WIN32) - if (CLR_CMAKE_TARGET_ARCH_AMD64) - list(APPEND NATIVECOMPRESSION_SOURCES "zlib_allocator_win.c") - endif() +if (CLR_CMAKE_TARGET_WIN32 AND NOT CLR_CMAKE_TARGET_ARCH_i386) + list(APPEND NATIVECOMPRESSION_SOURCES "zlib_allocator_win.c") endif() if (NOT CLR_CMAKE_TARGET_BROWSER AND NOT CLR_CMAKE_TARGET_WASI) From 7fcc661626674522c9d0dcd4fe2e8dc5fe45e2f3 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Wed, 23 Jul 2025 14:41:51 -0700 Subject: [PATCH 7/7] Update src/native/libs/System.IO.Compression.Native/CMakeLists.txt --- src/native/libs/System.IO.Compression.Native/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/libs/System.IO.Compression.Native/CMakeLists.txt b/src/native/libs/System.IO.Compression.Native/CMakeLists.txt index 0451636840459b..0adecdad88b0ac 100644 --- a/src/native/libs/System.IO.Compression.Native/CMakeLists.txt +++ b/src/native/libs/System.IO.Compression.Native/CMakeLists.txt @@ -36,7 +36,7 @@ set(NATIVECOMPRESSION_SOURCES pal_zlib.c ) -if (CLR_CMAKE_TARGET_WIN32 AND NOT CLR_CMAKE_TARGET_ARCH_i386) +if (CLR_CMAKE_TARGET_WIN32 AND NOT CLR_CMAKE_TARGET_ARCH_I386) list(APPEND NATIVECOMPRESSION_SOURCES "zlib_allocator_win.c") endif()