[compiler-rt] [ASan][Windows] Fix false positive for zero sized rtl allocations (PR #181015)
Zack Johnson via llvm-commits
llvm-commits at lists.llvm.org
Thu Feb 12 07:02:34 PST 2026
https://github.com/zacklj89 updated https://github.com/llvm/llvm-project/pull/181015
>From 79c281a4b8c9a772fae5e2b76366a6c2f1ab24cb Mon Sep 17 00:00:00 2001
From: Zack Johnson <zajohnson at microsoft.com>
Date: Wed, 11 Feb 2026 15:59:20 -0500
Subject: [PATCH 1/2] [compiler-rt][ASan][Windows] Fix false positive for zero
sized rtl allocations
---
compiler-rt/lib/asan/asan_allocator.cpp | 44 +++++++
.../TestCases/Windows/rtlsizeheap_zero.cpp | 107 ++++++++++++++++++
2 files changed, 151 insertions(+)
create mode 100644 compiler-rt/test/asan/TestCases/Windows/rtlsizeheap_zero.cpp
diff --git a/compiler-rt/lib/asan/asan_allocator.cpp b/compiler-rt/lib/asan/asan_allocator.cpp
index 05ae3a430cabd..1b314f6879529 100644
--- a/compiler-rt/lib/asan/asan_allocator.cpp
+++ b/compiler-rt/lib/asan/asan_allocator.cpp
@@ -93,6 +93,11 @@ class ChunkHeader {
atomic_uint8_t chunk_state;
u8 alloc_type : 2;
u8 lsan_tag : 2;
+#if SANITIZER_WINDOWS
+ // True if this was a zero-size allocation upgraded to size 1.
+ // Used to report the original size (0) to the user via HeapSize/RtlSizeHeap.
+ u8 from_zero_alloc : 1;
+#endif
// align < 8 -> 0
// else -> log2(min(align, 512)) - 2
@@ -610,6 +615,9 @@ struct Allocator {
uptr chunk_beg = user_beg - kChunkHeaderSize;
AsanChunk *m = reinterpret_cast<AsanChunk *>(chunk_beg);
m->alloc_type = alloc_type;
+#if SANITIZER_WINDOWS
+ m->from_zero_alloc = upgraded_from_zero;
+#endif
CHECK(size);
m->SetUsedSize(size);
m->user_requested_alignment_log = user_requested_alignment_log;
@@ -863,8 +871,22 @@ struct Allocator {
return m->UsedSize();
}
+#if SANITIZER_WINDOWS
+ // Returns true if the allocation at p was a zero-size request that was
+ // internally upgraded to size 1.
+ bool FromZeroAllocation(uptr p) {
+ return reinterpret_cast<AsanChunk *>(p - kChunkHeaderSize)->from_zero_alloc;
+ }
+#endif
+
uptr AllocationSizeFast(uptr p) {
+#if SANITIZER_WINDOWS
+ AsanChunk *c = reinterpret_cast<AsanChunk *>(p - kChunkHeaderSize);
+ if (c->from_zero_alloc) return 0;
+ return c->UsedSize();
+#else
return reinterpret_cast<AsanChunk *>(p - kChunkHeaderSize)->UsedSize();
+#endif
}
AsanChunkView FindHeapChunkByAddress(uptr addr) {
@@ -1125,6 +1147,14 @@ uptr asan_malloc_usable_size(const void *ptr, uptr pc, uptr bp) {
GET_STACK_TRACE_FATAL(pc, bp);
ReportMallocUsableSizeNotOwned((uptr)ptr, &stack);
}
+#if SANITIZER_WINDOWS
+ // Zero-size allocations are internally upgraded to size 1, but we should
+ // report the originally requested size (0) to the user via
+ // HeapSize/RtlSizeHeap.
+ if (usable_size > 0 &&
+ instance.FromZeroAllocation(reinterpret_cast<uptr>(ptr)))
+ return 0;
+#endif
return usable_size;
}
@@ -1222,7 +1252,14 @@ void asan_delete_array_sized_aligned(void *ptr, uptr size, uptr alignment,
}
uptr asan_mz_size(const void *ptr) {
+#if SANITIZER_WINDOWS
+ uptr size = instance.AllocationSize(reinterpret_cast<uptr>(ptr));
+ if (size > 0 && instance.FromZeroAllocation(reinterpret_cast<uptr>(ptr)))
+ return 0;
+ return size;
+#else
return instance.AllocationSize(reinterpret_cast<uptr>(ptr));
+#endif
}
void asan_mz_force_lock() SANITIZER_NO_THREAD_SAFETY_ANALYSIS {
@@ -1366,6 +1403,13 @@ uptr __sanitizer_get_allocated_size(const void *p) {
GET_STACK_TRACE_FATAL_HERE;
ReportSanitizerGetAllocatedSizeNotOwned(ptr, &stack);
}
+#if SANITIZER_WINDOWS
+ // Zero-size allocations are internally upgraded to size 1, but report
+ // the originally requested size (0) to the user via
+ // HeapSize/RtlSizeHeap.
+ if (instance.FromZeroAllocation(ptr))
+ return 0;
+#endif
return allocated_size;
}
diff --git a/compiler-rt/test/asan/TestCases/Windows/rtlsizeheap_zero.cpp b/compiler-rt/test/asan/TestCases/Windows/rtlsizeheap_zero.cpp
new file mode 100644
index 0000000000000..741275c6cac02
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/Windows/rtlsizeheap_zero.cpp
@@ -0,0 +1,107 @@
+// RUN: %clang_cl_asan %s %Fe%t
+// RUN: %run %t 2>&1 | FileCheck %s
+// RUN: %clang_cl_asan %s %Fe%t /DFAIL_CHECK
+// RUN: not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-FAIL
+//
+// Verify that zero-size heap allocations report size 0 through Windows heap
+// size APIs, preventing false positives when the result is used with memset.
+
+#include <malloc.h>
+#include <stdio.h>
+#include <windows.h>
+
+using AllocateFunctionPtr = PVOID(__stdcall *)(PVOID, ULONG, SIZE_T);
+using FreeFunctionPtr = BOOL(__stdcall *)(PVOID, ULONG, PVOID);
+using SizeFunctionPtr = SIZE_T(__stdcall *)(PVOID, ULONG, PVOID);
+
+int main() {
+ HMODULE NtDllHandle = GetModuleHandle("ntdll.dll");
+ if (!NtDllHandle) {
+ puts("Couldn't load ntdll");
+ return -1;
+ }
+
+ auto RtlAllocateHeap_ptr =
+ (AllocateFunctionPtr)GetProcAddress(NtDllHandle, "RtlAllocateHeap");
+ auto RtlFreeHeap_ptr =
+ (FreeFunctionPtr)GetProcAddress(NtDllHandle, "RtlFreeHeap");
+ auto RtlSizeHeap_ptr =
+ (SizeFunctionPtr)GetProcAddress(NtDllHandle, "RtlSizeHeap");
+
+ if (!RtlAllocateHeap_ptr || !RtlFreeHeap_ptr || !RtlSizeHeap_ptr) {
+ puts("Couldn't find Rtl heap functions");
+ return -1;
+ }
+
+ // Test RtlAllocateHeap with zero size
+ {
+ char *buffer =
+ (char *)RtlAllocateHeap_ptr(GetProcessHeap(), HEAP_ZERO_MEMORY, 0);
+ if (buffer) {
+ auto size = RtlSizeHeap_ptr(GetProcessHeap(), 0, buffer);
+ memset(buffer, 0, size);
+#ifdef FAIL_CHECK
+ // heap-buffer-overflow since actual size is 0
+ memset(buffer, 0, 1);
+#endif
+ RtlFreeHeap_ptr(GetProcessHeap(), 0, buffer);
+ }
+ }
+
+ // Test malloc with zero size
+ {
+ char *buffer = (char *)malloc(0);
+ if (buffer) {
+ auto size = _msize(buffer);
+ auto rtl_size = RtlSizeHeap_ptr(GetProcessHeap(), 0, buffer);
+ memset(buffer, 0, size);
+ memset(buffer, 0, rtl_size);
+ free(buffer);
+ }
+ }
+
+ // Test operator new with zero size
+ {
+ char *buffer = new char[0];
+ auto size = _msize(buffer);
+ auto rtl_size = RtlSizeHeap_ptr(GetProcessHeap(), 0, buffer);
+ memset(buffer, 0, size);
+ memset(buffer, 0, rtl_size);
+ delete[] buffer;
+ }
+
+ // Test GlobalAlloc with zero size.
+ // GlobalAlloc calls RtlAllocateHeap internally.
+ {
+ HGLOBAL hMem = GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, 0);
+ if (hMem) {
+ char *buffer = (char *)hMem;
+ auto size = GlobalSize(hMem);
+ auto rtl_size = RtlSizeHeap_ptr(GetProcessHeap(), 0, buffer);
+ memset(buffer, 0, size);
+ memset(buffer, 0, rtl_size);
+ GlobalFree(hMem);
+ }
+ }
+
+ // Test LocalAlloc with zero size.
+ // LocalAlloc calls RtlAllocateHeap internally.
+ {
+ HLOCAL hMem = LocalAlloc(LMEM_FIXED | LMEM_ZEROINIT, 0);
+ if (hMem) {
+ char *buffer = (char *)hMem;
+ auto size = LocalSize(hMem);
+ auto rtl_size = RtlSizeHeap_ptr(GetProcessHeap(), 0, buffer);
+ memset(buffer, 0, size);
+ memset(buffer, 0, rtl_size);
+ LocalFree(hMem);
+ }
+ }
+
+ puts("Success");
+ return 0;
+}
+
+// CHECK: Success
+// CHECK-NOT: AddressSanitizer: heap-buffer-overflow
+// CHECK-FAIL: AddressSanitizer: heap-buffer-overflow
>From 293fb73adad27eafa53b623223023f03a5589681 Mon Sep 17 00:00:00 2001
From: Zack Johnson <zajohnson at microsoft.com>
Date: Thu, 12 Feb 2026 10:00:29 -0500
Subject: [PATCH 2/2] PR Feedback + fixing tests
---
compiler-rt/lib/asan/asan_allocator.cpp | 38 +++++++++++++------
compiler-rt/lib/asan/asan_malloc_win.cpp | 14 ++++++-
.../Windows/heaprealloc_alloc_zero.cpp | 29 ++++----------
3 files changed, 48 insertions(+), 33 deletions(-)
diff --git a/compiler-rt/lib/asan/asan_allocator.cpp b/compiler-rt/lib/asan/asan_allocator.cpp
index 1b314f6879529..e849c089085a8 100644
--- a/compiler-rt/lib/asan/asan_allocator.cpp
+++ b/compiler-rt/lib/asan/asan_allocator.cpp
@@ -875,18 +875,28 @@ struct Allocator {
// Returns true if the allocation at p was a zero-size request that was
// internally upgraded to size 1.
bool FromZeroAllocation(uptr p) {
- return reinterpret_cast<AsanChunk *>(p - kChunkHeaderSize)->from_zero_alloc;
+ return reinterpret_cast<AsanChunk*>(p - kChunkHeaderSize)->from_zero_alloc;
+ }
+
+ // Marks an existing size 1 allocation as having originally been zero-size.
+ // Used by SharedReAlloc which augments size 0 to 1 before calling
+ // asan_realloc, bypassing Allocate's own zero-size tracking.
+ void MarkAsZeroAllocation(uptr p) {
+ AsanChunk* m = reinterpret_cast<AsanChunk*>(p - kChunkHeaderSize);
+ m->from_zero_alloc = 1;
+ PoisonShadow(p, ASAN_SHADOW_GRANULARITY, kAsanHeapLeftRedzoneMagic);
}
#endif
uptr AllocationSizeFast(uptr p) {
+ AsanChunk* c = reinterpret_cast<AsanChunk*>(p - kChunkHeaderSize);
+
#if SANITIZER_WINDOWS
- AsanChunk *c = reinterpret_cast<AsanChunk *>(p - kChunkHeaderSize);
- if (c->from_zero_alloc) return 0;
- return c->UsedSize();
-#else
- return reinterpret_cast<AsanChunk *>(p - kChunkHeaderSize)->UsedSize();
+ if (c->from_zero_alloc)
+ return 0;
#endif
+
+ return c->UsedSize();
}
AsanChunkView FindHeapChunkByAddress(uptr addr) {
@@ -1251,17 +1261,23 @@ void asan_delete_array_sized_aligned(void *ptr, uptr size, uptr alignment,
asan_delete_sized_aligned(ptr, size, alignment, stack, /*array=*/true);
}
-uptr asan_mz_size(const void *ptr) {
-#if SANITIZER_WINDOWS
+uptr asan_mz_size(const void* ptr) {
uptr size = instance.AllocationSize(reinterpret_cast<uptr>(ptr));
+
+#if SANITIZER_WINDOWS
if (size > 0 && instance.FromZeroAllocation(reinterpret_cast<uptr>(ptr)))
return 0;
- return size;
-#else
- return instance.AllocationSize(reinterpret_cast<uptr>(ptr));
#endif
+
+ return size;
}
+#if SANITIZER_WINDOWS
+void asan_mark_zero_allocation(void* ptr) {
+ instance.MarkAsZeroAllocation(reinterpret_cast<uptr>(ptr));
+}
+#endif
+
void asan_mz_force_lock() SANITIZER_NO_THREAD_SAFETY_ANALYSIS {
instance.ForceLock();
}
diff --git a/compiler-rt/lib/asan/asan_malloc_win.cpp b/compiler-rt/lib/asan/asan_malloc_win.cpp
index ea6f7dfaa08cf..0a556c90220ec 100644
--- a/compiler-rt/lib/asan/asan_malloc_win.cpp
+++ b/compiler-rt/lib/asan/asan_malloc_win.cpp
@@ -53,6 +53,14 @@ BOOL WINAPI HeapValidate(HANDLE hHeap, DWORD dwFlags, LPCVOID lpMem);
using namespace __asan;
+# if SANITIZER_WINDOWS
+// Marks an allocation as originally zero-size. Must be called on allocations
+// that were changed from size 0 to 1 outside of Allocate() (e.g. SharedReAlloc).
+namespace __asan {
+void asan_mark_zero_allocation(void *ptr);
+}
+# endif
+
// MT: Simply defining functions with the same signature in *.obj
// files overrides the standard functions in the CRT.
// MD: Memory allocation functions are defined in the CRT .dll,
@@ -371,7 +379,8 @@ void *SharedReAlloc(ReAllocFunction reallocFunc, SizeFunction heapSizeFunc,
// passing a 0 size into asan_realloc will free the allocation.
// To avoid this and keep behavior consistent, fudge the size if 0.
// (asan_malloc already does this)
- if (dwBytes == 0)
+ bool was_zero_size = (dwBytes == 0);
+ if (was_zero_size)
dwBytes = 1;
size_t old_size;
@@ -382,6 +391,9 @@ void *SharedReAlloc(ReAllocFunction reallocFunc, SizeFunction heapSizeFunc,
if (ptr == nullptr)
return nullptr;
+ if (was_zero_size)
+ asan_mark_zero_allocation(ptr);
+
if (dwFlags & HEAP_ZERO_MEMORY) {
size_t new_size = asan_malloc_usable_size(ptr, pc, bp);
if (old_size < new_size)
diff --git a/compiler-rt/test/asan/TestCases/Windows/heaprealloc_alloc_zero.cpp b/compiler-rt/test/asan/TestCases/Windows/heaprealloc_alloc_zero.cpp
index 6a5f8a1e7ea09..fbf63541386f0 100644
--- a/compiler-rt/test/asan/TestCases/Windows/heaprealloc_alloc_zero.cpp
+++ b/compiler-rt/test/asan/TestCases/Windows/heaprealloc_alloc_zero.cpp
@@ -10,29 +10,16 @@ int main() {
if (ptr)
std::cerr << "allocated!\n";
- // Check the 'allocate 1 instead of 0' hack hasn't changed
- // Note that as of b3452d90b043a398639e62b0ab01aa339cc649de, dereferencing
- // the pointer will be detected as a heap-buffer-overflow.
- if (__sanitizer_get_allocated_size(ptr) != 1)
+ // Zero-size allocations are internally upgraded to size 1, but
+ // user-facing size APIs now correctly report the original size (0).
+ // Dereferencing the pointer will be detected as a heap-buffer-overflow.
+ if (__sanitizer_get_allocated_size(ptr) != 0)
return 1;
free(ptr);
- /*
- HeapAlloc hack for our asan interceptor is to change 0
- sized allocations to size 1 to avoid weird inconsistencies
- between how realloc and heaprealloc handle 0 size allocations.
-
- Note this test relies on these instructions being intercepted.
- Without ASAN HeapRealloc on line 27 would return a ptr whose
- HeapSize would be 0. This test makes sure that the underlying behavior
- of our hack hasn't changed underneath us.
-
- We can get rid of the test (or change it to test for the correct
- behavior) once we fix the interceptor or write a different allocator
- to handle 0 sized allocations properly by default.
-
- */
+ // Zero-size HeapAlloc/HeapReAlloc should report size 0 via HeapSize,
+ // matching the originally requested size.
ptr = HeapAlloc(GetProcessHeap(), 0, 0);
if (!ptr)
return 1;
@@ -40,8 +27,8 @@ int main() {
if (!ptr2)
return 1;
size_t heapsize = HeapSize(GetProcessHeap(), 0, ptr2);
- if (heapsize != 1) { // will be 0 without ASAN turned on
- std::cerr << "HeapAlloc size failure! " << heapsize << " != 1\n";
+ if (heapsize != 0) {
+ std::cerr << "HeapAlloc size failure! " << heapsize << " != 0\n";
return 1;
}
void *ptr3 = HeapReAlloc(GetProcessHeap(), 0, ptr2, 3);
More information about the llvm-commits
mailing list