[compiler-rt] [ASan][Windows] Fix memmove/memcpy interception on x64 (PR #192060)

Alexandre Ganea via llvm-commits llvm-commits at lists.llvm.org
Thu Apr 16 05:43:54 PDT 2026


https://github.com/aganea updated https://github.com/llvm/llvm-project/pull/192060

>From 73ee2978d42c9c7af2a3b6b13ab8e281379c1384 Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <alex_toresh at yahoo.fr>
Date: Tue, 14 Apr 2026 09:48:47 -0400
Subject: [PATCH 1/4] [ASan][Windows] Fix memmove/memcpy interception on x64

On Windows x64, memcpy and memmove used to share an implementation in
vcruntime140.dll, but newer versions of the CRT may provide them as
separate functions. Detect this at runtime and intercept accordingly,
rather than unconditionally assuming they alias each other.

Should fix https://github.com/llvm/llvm-project/issues/126077
---
 .../lib/asan/asan_win_static_runtime_thunk.cpp |  3 ---
 ...tizer_common_interceptors_memintrinsics.inc | 18 ++++++++++++++++++
 2 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/compiler-rt/lib/asan/asan_win_static_runtime_thunk.cpp b/compiler-rt/lib/asan/asan_win_static_runtime_thunk.cpp
index 46e0e90738f24..601b0d2dea11f 100644
--- a/compiler-rt/lib/asan/asan_win_static_runtime_thunk.cpp
+++ b/compiler-rt/lib/asan/asan_win_static_runtime_thunk.cpp
@@ -42,10 +42,7 @@ INTERCEPT_LIBRARY_FUNCTION_ASAN(memchr);
 #  endif
 INTERCEPT_LIBRARY_FUNCTION_ASAN(memcmp);
 INTERCEPT_LIBRARY_FUNCTION_ASAN(memcpy);
-#  ifndef _WIN64
-// memmove and memcpy share an implementation on amd64
 INTERCEPT_LIBRARY_FUNCTION_ASAN(memmove);
-#  endif
 INTERCEPT_LIBRARY_FUNCTION_ASAN(memset);
 INTERCEPT_LIBRARY_FUNCTION_ASAN(strcat);
 INTERCEPT_LIBRARY_FUNCTION_ASAN(strchr);
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc b/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
index 0b6731c89950b..8b50392a45f8d 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
@@ -237,8 +237,26 @@ namespace __sanitizer {
 // This does not need to be called if InitializeCommonInterceptors() is called.
 void InitializeMemintrinsicInterceptors() {
   INIT_MEMSET;
+#if SANITIZER_WINDOWS64
+  // On Windows x64, memcpy and memmove used to be the same function in the
+  // CRT (vcruntime140.dll), but newer versions of the runtime may provide
+  // them as separate functions.  Detect at runtime before INIT_MEMMOVE
+  // patches the entry point.
+  // Use volatile to prevent the compiler from assuming distinct declarations
+  // imply distinct addresses and folding the comparison to true.
+  volatile uptr memcpy_addr = (uptr)memcpy;
+  volatile uptr memmove_addr = (uptr)memmove;
+  bool memcpy_is_separate = (memcpy_addr != memmove_addr);
+  INIT_MEMMOVE;
+  if (memcpy_is_separate)
+    COMMON_INTERCEPT_FUNCTION(memcpy);
+  else
+    ASSIGN_REAL(memcpy, memmove);
+  CHECK(REAL(memcpy));
+#else
   INIT_MEMMOVE;
   INIT_MEMCPY;
+#endif
   INIT_AEABI_MEM;
   INIT___BZERO;
   INIT_BZERO;

>From 1f013c5e8a7ebbdfe94afe9557abf1d4f0dcccb2 Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <aganea at havenstudios.com>
Date: Tue, 14 Apr 2026 11:03:35 -0400
Subject: [PATCH 2/4] Use internal_memcpy for realloc copy to avoid CRT
 interceptor re-entry

---
 compiler-rt/lib/asan/asan_allocator.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/compiler-rt/lib/asan/asan_allocator.cpp b/compiler-rt/lib/asan/asan_allocator.cpp
index 46ba7e16da9b2..d42282ad23749 100644
--- a/compiler-rt/lib/asan/asan_allocator.cpp
+++ b/compiler-rt/lib/asan/asan_allocator.cpp
@@ -803,11 +803,11 @@ struct Allocator {
       u8 chunk_state = atomic_load(&m->chunk_state, memory_order_acquire);
       if (chunk_state != CHUNK_ALLOCATED)
         ReportInvalidFree(old_ptr, chunk_state, stack);
-      CHECK_NE(REAL(memcpy), nullptr);
       uptr memcpy_size = Min(new_size, m->UsedSize());
       // If realloc() races with free(), we may start copying freed memory.
       // However, we will report racy double-free later anyway.
-      REAL(memcpy)(new_ptr, old_ptr, memcpy_size);
+      // Avoid intercepted CRT memcpy/memmove (see sanitizer_allocator_combined.h).
+      internal_memcpy(new_ptr, old_ptr, memcpy_size);
       Deallocate(old_ptr, 0, 0, stack, FROM_MALLOC);
     }
     return new_ptr;

>From 36560a02710701236da9a84e770c8f923100ff92 Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <aganea at havenstudios.com>
Date: Tue, 14 Apr 2026 11:58:39 -0400
Subject: [PATCH 3/4] clang-format

---
 compiler-rt/lib/asan/asan_allocator.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/compiler-rt/lib/asan/asan_allocator.cpp b/compiler-rt/lib/asan/asan_allocator.cpp
index d42282ad23749..9aad62a84a723 100644
--- a/compiler-rt/lib/asan/asan_allocator.cpp
+++ b/compiler-rt/lib/asan/asan_allocator.cpp
@@ -806,7 +806,8 @@ struct Allocator {
       uptr memcpy_size = Min(new_size, m->UsedSize());
       // If realloc() races with free(), we may start copying freed memory.
       // However, we will report racy double-free later anyway.
-      // Avoid intercepted CRT memcpy/memmove (see sanitizer_allocator_combined.h).
+      // Avoid intercepted CRT memcpy/memmove (see
+      // sanitizer_allocator_combined.h)
       internal_memcpy(new_ptr, old_ptr, memcpy_size);
       Deallocate(old_ptr, 0, 0, stack, FROM_MALLOC);
     }

>From 6111b3fbd14f56aadf748ffe5929705fa4fc3f14 Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <alex_toresh at yahoo.fr>
Date: Thu, 16 Apr 2026 08:43:14 -0400
Subject: [PATCH 4/4] Ensure the new codepath is only for Windows, and only for
 the specific CRT

---
 compiler-rt/lib/asan/asan_allocator.cpp       | 22 ++++++++++++++++---
 ...izer_common_interceptors_memintrinsics.inc | 16 +++++++++-----
 2 files changed, 29 insertions(+), 9 deletions(-)

diff --git a/compiler-rt/lib/asan/asan_allocator.cpp b/compiler-rt/lib/asan/asan_allocator.cpp
index 9aad62a84a723..f3520f8ae72c3 100644
--- a/compiler-rt/lib/asan/asan_allocator.cpp
+++ b/compiler-rt/lib/asan/asan_allocator.cpp
@@ -34,6 +34,13 @@
 #include "sanitizer_common/sanitizer_quarantine.h"
 #include "sanitizer_common/sanitizer_stackdepot.h"
 
+#if SANITIZER_WINDOWS64
+namespace __sanitizer {
+extern bool win64_memcpy_memmove_are_disjoint;
+}
+using __sanitizer::win64_memcpy_memmove_are_disjoint;
+#endif
+
 namespace __asan {
 
 // Valid redzone sizes are 16, 32, 64, ... 2048, so we encode them in 3 bits.
@@ -803,12 +810,21 @@ struct Allocator {
       u8 chunk_state = atomic_load(&m->chunk_state, memory_order_acquire);
       if (chunk_state != CHUNK_ALLOCATED)
         ReportInvalidFree(old_ptr, chunk_state, stack);
+      CHECK_NE(REAL(memcpy), nullptr);
       uptr memcpy_size = Min(new_size, m->UsedSize());
       // If realloc() races with free(), we may start copying freed memory.
       // However, we will report racy double-free later anyway.
-      // Avoid intercepted CRT memcpy/memmove (see
-      // sanitizer_allocator_combined.h)
-      internal_memcpy(new_ptr, old_ptr, memcpy_size);
+#if SANITIZER_WINDOWS64
+      // When memcpy/memmove are aliased in the CRT, REAL(memcpy) may
+      // re-enter the interceptor after thunk + runtime patching.
+      // Use internal_memcpy to avoid false positives in that case.
+      if (win64_memcpy_memmove_are_disjoint)
+        REAL(memcpy)(new_ptr, old_ptr, memcpy_size);
+      else
+        internal_memcpy(new_ptr, old_ptr, memcpy_size);
+#else
+      REAL(memcpy)(new_ptr, old_ptr, memcpy_size);
+#endif
       Deallocate(old_ptr, 0, 0, stack, FROM_MALLOC);
     }
     return new_ptr;
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc b/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
index 8b50392a45f8d..9e0496a553a60 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
@@ -234,21 +234,25 @@ INTERCEPTOR(void *, bzero, void *block, usize size) {
 #endif  // SANITIZER_INTERCEPT_BZERO
 
 namespace __sanitizer {
+
+#if SANITIZER_WINDOWS64
+// On Windows x64, memcpy and memmove used to be the same function in the
+// CRT (vcruntime140.dll), but newer versions of the runtime may provide
+// them as separate functions.  Set at runtime during interceptor init.
+bool win64_memcpy_memmove_are_disjoint;
+#endif
+
 // This does not need to be called if InitializeCommonInterceptors() is called.
 void InitializeMemintrinsicInterceptors() {
   INIT_MEMSET;
 #if SANITIZER_WINDOWS64
-  // On Windows x64, memcpy and memmove used to be the same function in the
-  // CRT (vcruntime140.dll), but newer versions of the runtime may provide
-  // them as separate functions.  Detect at runtime before INIT_MEMMOVE
-  // patches the entry point.
   // Use volatile to prevent the compiler from assuming distinct declarations
   // imply distinct addresses and folding the comparison to true.
   volatile uptr memcpy_addr = (uptr)memcpy;
   volatile uptr memmove_addr = (uptr)memmove;
-  bool memcpy_is_separate = (memcpy_addr != memmove_addr);
+  win64_memcpy_memmove_are_disjoint = (memcpy_addr != memmove_addr);
   INIT_MEMMOVE;
-  if (memcpy_is_separate)
+  if (win64_memcpy_memmove_are_disjoint)
     COMMON_INTERCEPT_FUNCTION(memcpy);
   else
     ASSIGN_REAL(memcpy, memmove);



More information about the llvm-commits mailing list