[compiler-rt] [llvm] [msan] Add interceptors for fortified memory functions (__memcpy_chk, etc.) (PR #206702)

Austin Schuh via llvm-commits llvm-commits at lists.llvm.org
Sat Aug 8 00:44:33 PDT 2026


https://github.com/AustinSchuh updated https://github.com/llvm/llvm-project/pull/206702

>From c9c1615265b586d2b19adb34fe442d02b929312a Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Tue, 30 Jun 2026 03:23:09 -0700
Subject: [PATCH 01/16] [msan] Add interceptors for fortified memory functions
 (__memcpy_chk, etc.)

When compiled with glibc's source fortification enabled (e.g., -D_FORTIFY_SOURCE=2
along with compiler optimization), calls to standard memory functions like
memcpy, memmove, memset, and mempcpy are replaced with their fortified
variants: __memcpy_chk, __memmove_chk, __memset_chk, and __mempcpy_chk.

Clang's compile-time MemorySanitizer (MSan) pass does not lower these fortified
symbol calls to llvm.memcpy/memset intrinsics. Since compiler-rt lacked runtime
interceptors for these functions, shadow memory and origin state were not
propagated across fortified copy or fill operations. This led to false-positive
"use-of-uninitialized-value" warnings when memory copied via fortified functions
was subsequently accessed.

This patch adds MSan interceptors for:
- __memcpy_chk
- __memmove_chk
- __memset_chk
- __mempcpy_chk

Each interceptor updates the destination shadow and origin memory, and forwards
the execution to the underlying glibc implementation via REAL(...) to preserve
the original runtime buffer overflow checks. These interceptors are guarded
by `#if SANITIZER_GLIBC` as fortified _chk functions are a glibc extension.

We also add a new unit test suite (compiler-rt/test/msan/Linux/fortify.cpp)
to verify correct shadow propagation through these interceptors under both -O0
and -O2 compiler optimization levels.

Signed-off-by: Austin Schuh <austin.linux at gmail.com>
---
 compiler-rt/lib/msan/msan_interceptors.cpp | 88 ++++++++++++++++++++++
 compiler-rt/test/msan/Linux/fortify.cpp    | 68 +++++++++++++++++
 2 files changed, 156 insertions(+)
 create mode 100644 compiler-rt/test/msan/Linux/fortify.cpp

diff --git a/compiler-rt/lib/msan/msan_interceptors.cpp b/compiler-rt/lib/msan/msan_interceptors.cpp
index ce2eae02d4179..862ecd60adcf0 100644
--- a/compiler-rt/lib/msan/msan_interceptors.cpp
+++ b/compiler-rt/lib/msan/msan_interceptors.cpp
@@ -63,6 +63,17 @@ DECLARE_REAL(SIZE_T, strnlen, const char *s, SIZE_T maxlen)
 DECLARE_REAL(void *, memcpy, void *dest, const void *src, SIZE_T n)
 DECLARE_REAL(void *, memset, void *dest, int c, SIZE_T n)
 
+#if SANITIZER_GLIBC
+DECLARE_REAL(void *, __memcpy_chk, void *dest, const void *src, SIZE_T n,
+             SIZE_T dest_size)
+DECLARE_REAL(void *, __memmove_chk, void *dest, const void *src, SIZE_T n,
+             SIZE_T dest_size)
+DECLARE_REAL(void *, __memset_chk, void *dest, int c, SIZE_T n,
+             SIZE_T dest_size)
+DECLARE_REAL(void *, __mempcpy_chk, void *dest, const void *src, SIZE_T n,
+             SIZE_T dest_size)
+#endif
+
 // True if this is a nested interceptor.
 static THREADLOCAL int in_interceptor_scope;
 
@@ -158,6 +169,79 @@ INTERCEPTOR(void *, bcopy, const void *src, void *dest, SIZE_T n) {
   return __msan_memmove(dest, src, n);
 }
 
+#if SANITIZER_GLIBC
+INTERCEPTOR(void *, __memcpy_chk, void *dest, const void *src, SIZE_T n,
+            SIZE_T dest_size) {
+  if (!msan_inited)
+    return internal_memcpy(dest, src, n);
+  if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
+    return REAL(__memcpy_chk)(dest, src, n, dest_size);
+  ENSURE_MSAN_INITED();
+  GET_STORE_STACK_TRACE;
+  void *res = REAL(__memcpy_chk)(dest, src, n, dest_size);
+  CopyShadowAndOrigin(dest, src, n, &stack);
+  return res;
+}
+#define MSAN_MAYBE_INTERCEPT___MEMCPY_CHK INTERCEPT_FUNCTION(__memcpy_chk)
+#else
+#define MSAN_MAYBE_INTERCEPT___MEMCPY_CHK
+#endif
+
+#if SANITIZER_GLIBC
+INTERCEPTOR(void *, __memmove_chk, void *dest, const void *src, SIZE_T n,
+            SIZE_T dest_size) {
+  if (!msan_inited)
+    return internal_memmove(dest, src, n);
+  if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
+    return REAL(__memmove_chk)(dest, src, n, dest_size);
+  ENSURE_MSAN_INITED();
+  GET_STORE_STACK_TRACE;
+  void *res = REAL(__memmove_chk)(dest, src, n, dest_size);
+  MoveShadowAndOrigin(dest, src, n, &stack);
+  return res;
+}
+#define MSAN_MAYBE_INTERCEPT___MEMMOVE_CHK INTERCEPT_FUNCTION(__memmove_chk)
+#else
+#define MSAN_MAYBE_INTERCEPT___MEMMOVE_CHK
+#endif
+
+#if SANITIZER_GLIBC
+INTERCEPTOR(void *, __memset_chk, void *dest, int c, SIZE_T n,
+            SIZE_T dest_size) {
+  if (!msan_inited)
+    return internal_memset(dest, c, n);
+  if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
+    return REAL(__memset_chk)(dest, c, n, dest_size);
+  ENSURE_MSAN_INITED();
+  void *res = REAL(__memset_chk)(dest, c, n, dest_size);
+  __msan_unpoison(dest, n);
+  return res;
+}
+#define MSAN_MAYBE_INTERCEPT___MEMSET_CHK INTERCEPT_FUNCTION(__memset_chk)
+#else
+#define MSAN_MAYBE_INTERCEPT___MEMSET_CHK
+#endif
+
+#if SANITIZER_GLIBC
+INTERCEPTOR(void *, __mempcpy_chk, void *dest, const void *src, SIZE_T n,
+            SIZE_T dest_size) {
+  if (!msan_inited) {
+    internal_memcpy(dest, src, n);
+    return (char *)dest + n;
+  }
+  if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
+    return REAL(__mempcpy_chk)(dest, src, n, dest_size);
+  ENSURE_MSAN_INITED();
+  GET_STORE_STACK_TRACE;
+  void *res = REAL(__mempcpy_chk)(dest, src, n, dest_size);
+  CopyShadowAndOrigin(dest, src, n, &stack);
+  return res;
+}
+#define MSAN_MAYBE_INTERCEPT___MEMPCPY_CHK INTERCEPT_FUNCTION(__mempcpy_chk)
+#else
+#define MSAN_MAYBE_INTERCEPT___MEMPCPY_CHK
+#endif
+
 INTERCEPTOR(int, posix_memalign, void **memptr, SIZE_T alignment, SIZE_T size) {
   GET_MALLOC_STACK_TRACE;
   CHECK_NE(memptr, 0);
@@ -1822,6 +1906,10 @@ void InitializeInterceptors() {
   MSAN_MAYBE_INTERCEPT_FREAD_UNLOCKED;
   INTERCEPT_FUNCTION(memccpy);
   MSAN_MAYBE_INTERCEPT_MEMPCPY;
+  MSAN_MAYBE_INTERCEPT___MEMCPY_CHK;
+  MSAN_MAYBE_INTERCEPT___MEMMOVE_CHK;
+  MSAN_MAYBE_INTERCEPT___MEMSET_CHK;
+  MSAN_MAYBE_INTERCEPT___MEMPCPY_CHK;
   INTERCEPT_FUNCTION(bcopy);
   INTERCEPT_FUNCTION(wmemset);
   INTERCEPT_FUNCTION(wmemcpy);
diff --git a/compiler-rt/test/msan/Linux/fortify.cpp b/compiler-rt/test/msan/Linux/fortify.cpp
new file mode 100644
index 0000000000000..4568aa1827a94
--- /dev/null
+++ b/compiler-rt/test/msan/Linux/fortify.cpp
@@ -0,0 +1,68 @@
+// RUN: %clangxx_msan -O0 %s -o %t && %run %t
+// RUN: %clangxx_msan -O2 %s -o %t && %run %t
+
+// REQUIRES: glibc
+
+#include <assert.h>
+#include <string.h>
+#include <sanitizer/msan_interface.h>
+
+extern "C" void *__memcpy_chk(void *dest, const void *src, size_t len, size_t destlen);
+extern "C" void *__memmove_chk(void *dest, const void *src, size_t len, size_t destlen);
+extern "C" void *__memset_chk(void *dest, int c, size_t len, size_t destlen);
+extern "C" void *__mempcpy_chk(void *dest, const void *src, size_t len, size_t destlen);
+
+int main(int argc, char *argv[]) {
+  // Test __memcpy_chk shadow propagation
+  {
+    char src[10];
+    char dest[10];
+    __msan_unpoison(dest, sizeof(dest));
+    __msan_poison(src, sizeof(src));
+    src[1] = 1;
+    src[2] = 2;
+    __memcpy_chk(dest, src, 5, sizeof(dest));
+    assert(__msan_test_shadow(dest, 5) == 0); // dest[0] is uninitialized
+    assert(__msan_test_shadow(dest + 1, 4) == 2); // dest[1], dest[2] are initialized, dest[3..4] are uninitialized
+    assert(__msan_test_shadow(dest + 5, 5) == -1); // dest[5..9] remain initialized
+  }
+
+  // Test __memmove_chk shadow propagation
+  {
+    char src[10];
+    char dest[10];
+    __msan_unpoison(dest, sizeof(dest));
+    __msan_poison(src, sizeof(src));
+    src[1] = 1;
+    src[2] = 2;
+    __memmove_chk(dest, src, 5, sizeof(dest));
+    assert(__msan_test_shadow(dest, 5) == 0);
+    assert(__msan_test_shadow(dest + 1, 4) == 2);
+    assert(__msan_test_shadow(dest + 5, 5) == -1);
+  }
+
+  // Test __memset_chk shadow propagation
+  {
+    char dest[10];
+    __msan_poison(dest, sizeof(dest));
+    __memset_chk(dest, 42, 5, sizeof(dest));
+    assert(__msan_test_shadow(dest, 10) == 5); // first 5 bytes are initialized, remaining 5 are uninitialized
+  }
+
+  // Test __mempcpy_chk shadow propagation
+  {
+    char src[10];
+    char dest[10];
+    __msan_unpoison(dest, sizeof(dest));
+    __msan_poison(src, sizeof(src));
+    src[1] = 1;
+    src[2] = 2;
+    char *res = (char *)__mempcpy_chk(dest, src, 5, sizeof(dest));
+    assert(res == dest + 5);
+    assert(__msan_test_shadow(dest, 5) == 0);
+    assert(__msan_test_shadow(dest + 1, 4) == 2);
+    assert(__msan_test_shadow(dest + 5, 5) == -1);
+  }
+
+  return 0;
+}

>From 87907e7caa5efd8ec4988d6d70bd60f7f66607e8 Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Tue, 30 Jun 2026 12:33:18 -0700
Subject: [PATCH 02/16] Clang format

---
 compiler-rt/lib/msan/msan_interceptors.cpp | 43 +++++++++++-----------
 compiler-rt/test/msan/Linux/fortify.cpp    | 23 ++++++++----
 2 files changed, 36 insertions(+), 30 deletions(-)

diff --git a/compiler-rt/lib/msan/msan_interceptors.cpp b/compiler-rt/lib/msan/msan_interceptors.cpp
index 862ecd60adcf0..6a5565564bca7 100644
--- a/compiler-rt/lib/msan/msan_interceptors.cpp
+++ b/compiler-rt/lib/msan/msan_interceptors.cpp
@@ -64,13 +64,12 @@ DECLARE_REAL(void *, memcpy, void *dest, const void *src, SIZE_T n)
 DECLARE_REAL(void *, memset, void *dest, int c, SIZE_T n)
 
 #if SANITIZER_GLIBC
-DECLARE_REAL(void *, __memcpy_chk, void *dest, const void *src, SIZE_T n,
+DECLARE_REAL(void*, __memcpy_chk, void* dest, const void* src, SIZE_T n,
              SIZE_T dest_size)
-DECLARE_REAL(void *, __memmove_chk, void *dest, const void *src, SIZE_T n,
+DECLARE_REAL(void*, __memmove_chk, void* dest, const void* src, SIZE_T n,
              SIZE_T dest_size)
-DECLARE_REAL(void *, __memset_chk, void *dest, int c, SIZE_T n,
-             SIZE_T dest_size)
-DECLARE_REAL(void *, __mempcpy_chk, void *dest, const void *src, SIZE_T n,
+DECLARE_REAL(void*, __memset_chk, void* dest, int c, SIZE_T n, SIZE_T dest_size)
+DECLARE_REAL(void*, __mempcpy_chk, void* dest, const void* src, SIZE_T n,
              SIZE_T dest_size)
 #endif
 
@@ -170,7 +169,7 @@ INTERCEPTOR(void *, bcopy, const void *src, void *dest, SIZE_T n) {
 }
 
 #if SANITIZER_GLIBC
-INTERCEPTOR(void *, __memcpy_chk, void *dest, const void *src, SIZE_T n,
+INTERCEPTOR(void*, __memcpy_chk, void* dest, const void* src, SIZE_T n,
             SIZE_T dest_size) {
   if (!msan_inited)
     return internal_memcpy(dest, src, n);
@@ -178,17 +177,17 @@ INTERCEPTOR(void *, __memcpy_chk, void *dest, const void *src, SIZE_T n,
     return REAL(__memcpy_chk)(dest, src, n, dest_size);
   ENSURE_MSAN_INITED();
   GET_STORE_STACK_TRACE;
-  void *res = REAL(__memcpy_chk)(dest, src, n, dest_size);
+  void* res = REAL(__memcpy_chk)(dest, src, n, dest_size);
   CopyShadowAndOrigin(dest, src, n, &stack);
   return res;
 }
-#define MSAN_MAYBE_INTERCEPT___MEMCPY_CHK INTERCEPT_FUNCTION(__memcpy_chk)
+#  define MSAN_MAYBE_INTERCEPT___MEMCPY_CHK INTERCEPT_FUNCTION(__memcpy_chk)
 #else
-#define MSAN_MAYBE_INTERCEPT___MEMCPY_CHK
+#  define MSAN_MAYBE_INTERCEPT___MEMCPY_CHK
 #endif
 
 #if SANITIZER_GLIBC
-INTERCEPTOR(void *, __memmove_chk, void *dest, const void *src, SIZE_T n,
+INTERCEPTOR(void*, __memmove_chk, void* dest, const void* src, SIZE_T n,
             SIZE_T dest_size) {
   if (!msan_inited)
     return internal_memmove(dest, src, n);
@@ -196,50 +195,50 @@ INTERCEPTOR(void *, __memmove_chk, void *dest, const void *src, SIZE_T n,
     return REAL(__memmove_chk)(dest, src, n, dest_size);
   ENSURE_MSAN_INITED();
   GET_STORE_STACK_TRACE;
-  void *res = REAL(__memmove_chk)(dest, src, n, dest_size);
+  void* res = REAL(__memmove_chk)(dest, src, n, dest_size);
   MoveShadowAndOrigin(dest, src, n, &stack);
   return res;
 }
-#define MSAN_MAYBE_INTERCEPT___MEMMOVE_CHK INTERCEPT_FUNCTION(__memmove_chk)
+#  define MSAN_MAYBE_INTERCEPT___MEMMOVE_CHK INTERCEPT_FUNCTION(__memmove_chk)
 #else
-#define MSAN_MAYBE_INTERCEPT___MEMMOVE_CHK
+#  define MSAN_MAYBE_INTERCEPT___MEMMOVE_CHK
 #endif
 
 #if SANITIZER_GLIBC
-INTERCEPTOR(void *, __memset_chk, void *dest, int c, SIZE_T n,
+INTERCEPTOR(void*, __memset_chk, void* dest, int c, SIZE_T n,
             SIZE_T dest_size) {
   if (!msan_inited)
     return internal_memset(dest, c, n);
   if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
     return REAL(__memset_chk)(dest, c, n, dest_size);
   ENSURE_MSAN_INITED();
-  void *res = REAL(__memset_chk)(dest, c, n, dest_size);
+  void* res = REAL(__memset_chk)(dest, c, n, dest_size);
   __msan_unpoison(dest, n);
   return res;
 }
-#define MSAN_MAYBE_INTERCEPT___MEMSET_CHK INTERCEPT_FUNCTION(__memset_chk)
+#  define MSAN_MAYBE_INTERCEPT___MEMSET_CHK INTERCEPT_FUNCTION(__memset_chk)
 #else
-#define MSAN_MAYBE_INTERCEPT___MEMSET_CHK
+#  define MSAN_MAYBE_INTERCEPT___MEMSET_CHK
 #endif
 
 #if SANITIZER_GLIBC
-INTERCEPTOR(void *, __mempcpy_chk, void *dest, const void *src, SIZE_T n,
+INTERCEPTOR(void*, __mempcpy_chk, void* dest, const void* src, SIZE_T n,
             SIZE_T dest_size) {
   if (!msan_inited) {
     internal_memcpy(dest, src, n);
-    return (char *)dest + n;
+    return (char*)dest + n;
   }
   if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
     return REAL(__mempcpy_chk)(dest, src, n, dest_size);
   ENSURE_MSAN_INITED();
   GET_STORE_STACK_TRACE;
-  void *res = REAL(__mempcpy_chk)(dest, src, n, dest_size);
+  void* res = REAL(__mempcpy_chk)(dest, src, n, dest_size);
   CopyShadowAndOrigin(dest, src, n, &stack);
   return res;
 }
-#define MSAN_MAYBE_INTERCEPT___MEMPCPY_CHK INTERCEPT_FUNCTION(__mempcpy_chk)
+#  define MSAN_MAYBE_INTERCEPT___MEMPCPY_CHK INTERCEPT_FUNCTION(__mempcpy_chk)
 #else
-#define MSAN_MAYBE_INTERCEPT___MEMPCPY_CHK
+#  define MSAN_MAYBE_INTERCEPT___MEMPCPY_CHK
 #endif
 
 INTERCEPTOR(int, posix_memalign, void **memptr, SIZE_T alignment, SIZE_T size) {
diff --git a/compiler-rt/test/msan/Linux/fortify.cpp b/compiler-rt/test/msan/Linux/fortify.cpp
index 4568aa1827a94..041428655d847 100644
--- a/compiler-rt/test/msan/Linux/fortify.cpp
+++ b/compiler-rt/test/msan/Linux/fortify.cpp
@@ -4,13 +4,16 @@
 // REQUIRES: glibc
 
 #include <assert.h>
-#include <string.h>
 #include <sanitizer/msan_interface.h>
+#include <string.h>
 
-extern "C" void *__memcpy_chk(void *dest, const void *src, size_t len, size_t destlen);
-extern "C" void *__memmove_chk(void *dest, const void *src, size_t len, size_t destlen);
+extern "C" void *__memcpy_chk(void *dest, const void *src, size_t len,
+                              size_t destlen);
+extern "C" void *__memmove_chk(void *dest, const void *src, size_t len,
+                               size_t destlen);
 extern "C" void *__memset_chk(void *dest, int c, size_t len, size_t destlen);
-extern "C" void *__mempcpy_chk(void *dest, const void *src, size_t len, size_t destlen);
+extern "C" void *__mempcpy_chk(void *dest, const void *src, size_t len,
+                               size_t destlen);
 
 int main(int argc, char *argv[]) {
   // Test __memcpy_chk shadow propagation
@@ -22,9 +25,12 @@ int main(int argc, char *argv[]) {
     src[1] = 1;
     src[2] = 2;
     __memcpy_chk(dest, src, 5, sizeof(dest));
-    assert(__msan_test_shadow(dest, 5) == 0); // dest[0] is uninitialized
-    assert(__msan_test_shadow(dest + 1, 4) == 2); // dest[1], dest[2] are initialized, dest[3..4] are uninitialized
-    assert(__msan_test_shadow(dest + 5, 5) == -1); // dest[5..9] remain initialized
+    // dest[0] is uninitialized
+    assert(__msan_test_shadow(dest, 5) == 0);
+    // dest[1], dest[2] are initialized, dest[3..4] are uninitialized
+    assert(__msan_test_shadow(dest + 1, 4) == 2);
+    // dest[5..9] remain initialized
+    assert(__msan_test_shadow(dest + 5, 5) == -1);
   }
 
   // Test __memmove_chk shadow propagation
@@ -46,7 +52,8 @@ int main(int argc, char *argv[]) {
     char dest[10];
     __msan_poison(dest, sizeof(dest));
     __memset_chk(dest, 42, 5, sizeof(dest));
-    assert(__msan_test_shadow(dest, 10) == 5); // first 5 bytes are initialized, remaining 5 are uninitialized
+    // first 5 bytes are initialized, remaining 5 are uninitialized
+    assert(__msan_test_shadow(dest, 10) == 5);
   }
 
   // Test __mempcpy_chk shadow propagation

>From 23a328703506f3b81d724989478d48e2bc83e5b2 Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Wed, 1 Jul 2026 15:46:55 -0700
Subject: [PATCH 03/16] Addressed comments

---
 .../TestCases}/Linux/fortify.cpp              | 44 ++++++++++++++++---
 llvm/runtimes/CMakeLists.txt                  |  5 ++-
 2 files changed, 41 insertions(+), 8 deletions(-)
 rename compiler-rt/test/{msan => sanitizer_common/TestCases}/Linux/fortify.cpp (67%)

diff --git a/compiler-rt/test/msan/Linux/fortify.cpp b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify.cpp
similarity index 67%
rename from compiler-rt/test/msan/Linux/fortify.cpp
rename to compiler-rt/test/sanitizer_common/TestCases/Linux/fortify.cpp
index 041428655d847..381697f74cf98 100644
--- a/compiler-rt/test/msan/Linux/fortify.cpp
+++ b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify.cpp
@@ -1,12 +1,13 @@
-// RUN: %clangxx_msan -O0 %s -o %t && %run %t
-// RUN: %clangxx_msan -O2 %s -o %t && %run %t
+// RUN: %clangxx -O0 %s -o %t && %run %t
+// RUN: %clangxx -O2 %s -o %t && %run %t
 
 // REQUIRES: glibc
 
 #include <assert.h>
-#include <sanitizer/msan_interface.h>
 #include <string.h>
 
+#include "sanitizer_common/sanitizer_specific.h"
+
 extern "C" void *__memcpy_chk(void *dest, const void *src, size_t len,
                               size_t destlen);
 extern "C" void *__memmove_chk(void *dest, const void *src, size_t len,
@@ -16,59 +17,88 @@ extern "C" void *__mempcpy_chk(void *dest, const void *src, size_t len,
                                size_t destlen);
 
 int main(int argc, char *argv[]) {
-  // Test __memcpy_chk shadow propagation
+  // Test __memcpy_chk basic behavior & shadow propagation
   {
     char src[10];
     char dest[10];
+#if __has_feature(memory_sanitizer)
     __msan_unpoison(dest, sizeof(dest));
     __msan_poison(src, sizeof(src));
+#endif
     src[1] = 1;
     src[2] = 2;
     __memcpy_chk(dest, src, 5, sizeof(dest));
+#if __has_feature(memory_sanitizer)
     // dest[0] is uninitialized
     assert(__msan_test_shadow(dest, 5) == 0);
     // dest[1], dest[2] are initialized, dest[3..4] are uninitialized
     assert(__msan_test_shadow(dest + 1, 4) == 2);
     // dest[5..9] remain initialized
     assert(__msan_test_shadow(dest + 5, 5) == -1);
+#else
+    assert(dest[1] == 1);
+    assert(dest[2] == 2);
+#endif
   }
 
-  // Test __memmove_chk shadow propagation
+  // Test __memmove_chk basic behavior & shadow propagation
   {
     char src[10];
     char dest[10];
+#if __has_feature(memory_sanitizer)
     __msan_unpoison(dest, sizeof(dest));
     __msan_poison(src, sizeof(src));
+#endif
     src[1] = 1;
     src[2] = 2;
     __memmove_chk(dest, src, 5, sizeof(dest));
+#if __has_feature(memory_sanitizer)
     assert(__msan_test_shadow(dest, 5) == 0);
     assert(__msan_test_shadow(dest + 1, 4) == 2);
     assert(__msan_test_shadow(dest + 5, 5) == -1);
+#else
+    assert(dest[1] == 1);
+    assert(dest[2] == 2);
+#endif
   }
 
-  // Test __memset_chk shadow propagation
+  // Test __memset_chk basic behavior & shadow propagation
   {
     char dest[10];
+#if __has_feature(memory_sanitizer)
     __msan_poison(dest, sizeof(dest));
+#endif
     __memset_chk(dest, 42, 5, sizeof(dest));
+#if __has_feature(memory_sanitizer)
     // first 5 bytes are initialized, remaining 5 are uninitialized
     assert(__msan_test_shadow(dest, 10) == 5);
+#else
+    for (int i = 0; i < 5; ++i) {
+      assert(dest[i] == 42);
+    }
+#endif
   }
 
-  // Test __mempcpy_chk shadow propagation
+  // Test __mempcpy_chk basic behavior & shadow propagation
   {
     char src[10];
     char dest[10];
+#if __has_feature(memory_sanitizer)
     __msan_unpoison(dest, sizeof(dest));
     __msan_poison(src, sizeof(src));
+#endif
     src[1] = 1;
     src[2] = 2;
     char *res = (char *)__mempcpy_chk(dest, src, 5, sizeof(dest));
     assert(res == dest + 5);
+#if __has_feature(memory_sanitizer)
     assert(__msan_test_shadow(dest, 5) == 0);
     assert(__msan_test_shadow(dest + 1, 4) == 2);
     assert(__msan_test_shadow(dest + 5, 5) == -1);
+#else
+    assert(dest[1] == 1);
+    assert(dest[2] == 2);
+#endif
   }
 
   return 0;
diff --git a/llvm/runtimes/CMakeLists.txt b/llvm/runtimes/CMakeLists.txt
index 501ea55a327c7..529253542785a 100644
--- a/llvm/runtimes/CMakeLists.txt
+++ b/llvm/runtimes/CMakeLists.txt
@@ -327,7 +327,10 @@ function(runtime_default_target)
 
   if(LLVM_INCLUDE_TESTS)
     set_property(GLOBAL APPEND PROPERTY LLVM_ALL_LIT_TESTSUITES "@${LLVM_BINARY_DIR}/runtimes/runtimes-bins/lit.tests")
-    list(APPEND test_targets runtimes-test-depends check-runtimes check-builtins)
+    list(APPEND test_targets runtimes-test-depends check-runtimes)
+    if(NOT "compiler-rt" IN_LIST LLVM_ENABLE_PROJECTS)
+      list(APPEND test_targets check-builtins)
+    endif()
     list(APPEND ARG_CMAKE_ARGS "-DCOMPILER_RT_TEST_EXTERNAL_BUILTINS=ON")
   endif()
 

>From a1e0d6f30a73a4d3ec265436becdd02ae5b56185 Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Tue, 14 Jul 2026 18:38:59 -0700
Subject: [PATCH 04/16] Revert CMakeLists change

---
 llvm/runtimes/CMakeLists.txt | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/llvm/runtimes/CMakeLists.txt b/llvm/runtimes/CMakeLists.txt
index 529253542785a..501ea55a327c7 100644
--- a/llvm/runtimes/CMakeLists.txt
+++ b/llvm/runtimes/CMakeLists.txt
@@ -327,10 +327,7 @@ function(runtime_default_target)
 
   if(LLVM_INCLUDE_TESTS)
     set_property(GLOBAL APPEND PROPERTY LLVM_ALL_LIT_TESTSUITES "@${LLVM_BINARY_DIR}/runtimes/runtimes-bins/lit.tests")
-    list(APPEND test_targets runtimes-test-depends check-runtimes)
-    if(NOT "compiler-rt" IN_LIST LLVM_ENABLE_PROJECTS)
-      list(APPEND test_targets check-builtins)
-    endif()
+    list(APPEND test_targets runtimes-test-depends check-runtimes check-builtins)
     list(APPEND ARG_CMAKE_ARGS "-DCOMPILER_RT_TEST_EXTERNAL_BUILTINS=ON")
   endif()
 

>From 9039c25bca4d79f1cbe72cbfee979cc2aac94097 Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Fri, 17 Jul 2026 03:01:34 -0700
Subject: [PATCH 05/16] Switch to COMMON_INTERCEPTOR_MEM*

---
 compiler-rt/lib/msan/msan_interceptors.cpp    | 145 +++++++-----------
 ...izer_common_interceptors_memintrinsics.inc |  60 ++++++++
 .../sanitizer_platform_interceptors.h         |   4 +
 3 files changed, 122 insertions(+), 87 deletions(-)

diff --git a/compiler-rt/lib/msan/msan_interceptors.cpp b/compiler-rt/lib/msan/msan_interceptors.cpp
index 6a5565564bca7..09a8af62d7a76 100644
--- a/compiler-rt/lib/msan/msan_interceptors.cpp
+++ b/compiler-rt/lib/msan/msan_interceptors.cpp
@@ -63,16 +63,6 @@ DECLARE_REAL(SIZE_T, strnlen, const char *s, SIZE_T maxlen)
 DECLARE_REAL(void *, memcpy, void *dest, const void *src, SIZE_T n)
 DECLARE_REAL(void *, memset, void *dest, int c, SIZE_T n)
 
-#if SANITIZER_GLIBC
-DECLARE_REAL(void*, __memcpy_chk, void* dest, const void* src, SIZE_T n,
-             SIZE_T dest_size)
-DECLARE_REAL(void*, __memmove_chk, void* dest, const void* src, SIZE_T n,
-             SIZE_T dest_size)
-DECLARE_REAL(void*, __memset_chk, void* dest, int c, SIZE_T n, SIZE_T dest_size)
-DECLARE_REAL(void*, __mempcpy_chk, void* dest, const void* src, SIZE_T n,
-             SIZE_T dest_size)
-#endif
-
 // True if this is a nested interceptor.
 static THREADLOCAL int in_interceptor_scope;
 
@@ -168,79 +158,6 @@ INTERCEPTOR(void *, bcopy, const void *src, void *dest, SIZE_T n) {
   return __msan_memmove(dest, src, n);
 }
 
-#if SANITIZER_GLIBC
-INTERCEPTOR(void*, __memcpy_chk, void* dest, const void* src, SIZE_T n,
-            SIZE_T dest_size) {
-  if (!msan_inited)
-    return internal_memcpy(dest, src, n);
-  if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
-    return REAL(__memcpy_chk)(dest, src, n, dest_size);
-  ENSURE_MSAN_INITED();
-  GET_STORE_STACK_TRACE;
-  void* res = REAL(__memcpy_chk)(dest, src, n, dest_size);
-  CopyShadowAndOrigin(dest, src, n, &stack);
-  return res;
-}
-#  define MSAN_MAYBE_INTERCEPT___MEMCPY_CHK INTERCEPT_FUNCTION(__memcpy_chk)
-#else
-#  define MSAN_MAYBE_INTERCEPT___MEMCPY_CHK
-#endif
-
-#if SANITIZER_GLIBC
-INTERCEPTOR(void*, __memmove_chk, void* dest, const void* src, SIZE_T n,
-            SIZE_T dest_size) {
-  if (!msan_inited)
-    return internal_memmove(dest, src, n);
-  if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
-    return REAL(__memmove_chk)(dest, src, n, dest_size);
-  ENSURE_MSAN_INITED();
-  GET_STORE_STACK_TRACE;
-  void* res = REAL(__memmove_chk)(dest, src, n, dest_size);
-  MoveShadowAndOrigin(dest, src, n, &stack);
-  return res;
-}
-#  define MSAN_MAYBE_INTERCEPT___MEMMOVE_CHK INTERCEPT_FUNCTION(__memmove_chk)
-#else
-#  define MSAN_MAYBE_INTERCEPT___MEMMOVE_CHK
-#endif
-
-#if SANITIZER_GLIBC
-INTERCEPTOR(void*, __memset_chk, void* dest, int c, SIZE_T n,
-            SIZE_T dest_size) {
-  if (!msan_inited)
-    return internal_memset(dest, c, n);
-  if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
-    return REAL(__memset_chk)(dest, c, n, dest_size);
-  ENSURE_MSAN_INITED();
-  void* res = REAL(__memset_chk)(dest, c, n, dest_size);
-  __msan_unpoison(dest, n);
-  return res;
-}
-#  define MSAN_MAYBE_INTERCEPT___MEMSET_CHK INTERCEPT_FUNCTION(__memset_chk)
-#else
-#  define MSAN_MAYBE_INTERCEPT___MEMSET_CHK
-#endif
-
-#if SANITIZER_GLIBC
-INTERCEPTOR(void*, __mempcpy_chk, void* dest, const void* src, SIZE_T n,
-            SIZE_T dest_size) {
-  if (!msan_inited) {
-    internal_memcpy(dest, src, n);
-    return (char*)dest + n;
-  }
-  if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
-    return REAL(__mempcpy_chk)(dest, src, n, dest_size);
-  ENSURE_MSAN_INITED();
-  GET_STORE_STACK_TRACE;
-  void* res = REAL(__mempcpy_chk)(dest, src, n, dest_size);
-  CopyShadowAndOrigin(dest, src, n, &stack);
-  return res;
-}
-#  define MSAN_MAYBE_INTERCEPT___MEMPCPY_CHK INTERCEPT_FUNCTION(__mempcpy_chk)
-#else
-#  define MSAN_MAYBE_INTERCEPT___MEMPCPY_CHK
-#endif
-
 INTERCEPTOR(int, posix_memalign, void **memptr, SIZE_T alignment, SIZE_T size) {
   GET_MALLOC_STACK_TRACE;
   CHECK_NE(memptr, 0);
@@ -1586,6 +1503,64 @@ int OnExit() {
     return __msan_memcpy(to, from, size);                   \
   }
 
+// The "_chk" variants delegate to the corresponding REAL(__*_chk) function so
+// that glibc's _FORTIFY_SOURCE overflow checks are preserved, while still
+// propagating shadow like the plain memory intrinsics above. These are only
+// expanded by the glibc-gated interceptors in the memintrinsics .inc file.
+#define COMMON_INTERCEPTOR_MEMSET_CHK_IMPL(ctx, dest, c, n, dest_size) \
+  {                                                                    \
+    (void)ctx;                                                         \
+    if (!msan_inited)                                                  \
+      return internal_memset(dest, c, n);                              \
+    if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())     \
+      return REAL(__memset_chk)(dest, c, n, dest_size);                \
+    ENSURE_MSAN_INITED();                                              \
+    void *res = REAL(__memset_chk)(dest, c, n, dest_size);             \
+    __msan_unpoison(dest, n);                                          \
+    return res;                                                        \
+  }
+#define COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL(ctx, dest, src, n, dest_size) \
+  {                                                                       \
+    (void)ctx;                                                            \
+    if (!msan_inited)                                                     \
+      return internal_memmove(dest, src, n);                              \
+    if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())        \
+      return REAL(__memmove_chk)(dest, src, n, dest_size);                \
+    ENSURE_MSAN_INITED();                                                 \
+    GET_STORE_STACK_TRACE;                                                \
+    void *res = REAL(__memmove_chk)(dest, src, n, dest_size);             \
+    MoveShadowAndOrigin(dest, src, n, &stack);                            \
+    return res;                                                           \
+  }
+#define COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL(ctx, dest, src, n, dest_size) \
+  {                                                                      \
+    (void)ctx;                                                           \
+    if (!msan_inited)                                                    \
+      return internal_memcpy(dest, src, n);                              \
+    if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())       \
+      return REAL(__memcpy_chk)(dest, src, n, dest_size);                \
+    ENSURE_MSAN_INITED();                                                \
+    GET_STORE_STACK_TRACE;                                               \
+    void *res = REAL(__memcpy_chk)(dest, src, n, dest_size);             \
+    CopyShadowAndOrigin(dest, src, n, &stack);                           \
+    return res;                                                          \
+  }
+#define COMMON_INTERCEPTOR_MEMPCPY_CHK_IMPL(ctx, dest, src, n, dest_size) \
+  {                                                                       \
+    (void)ctx;                                                            \
+    if (!msan_inited) {                                                   \
+      internal_memcpy(dest, src, n);                                      \
+      return (char *)dest + n;                                            \
+    }                                                                     \
+    if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())        \
+      return REAL(__mempcpy_chk)(dest, src, n, dest_size);                \
+    ENSURE_MSAN_INITED();                                                 \
+    GET_STORE_STACK_TRACE;                                                \
+    void *res = REAL(__mempcpy_chk)(dest, src, n, dest_size);             \
+    CopyShadowAndOrigin(dest, src, n, &stack);                            \
+    return res;                                                           \
+  }
+
 #define COMMON_INTERCEPTOR_COPY_STRING(ctx, to, from, size) \
   do {                                                      \
     GET_STORE_STACK_TRACE;                                  \
@@ -1905,10 +1880,6 @@ void InitializeInterceptors() {
   MSAN_MAYBE_INTERCEPT_FREAD_UNLOCKED;
   INTERCEPT_FUNCTION(memccpy);
   MSAN_MAYBE_INTERCEPT_MEMPCPY;
-  MSAN_MAYBE_INTERCEPT___MEMCPY_CHK;
-  MSAN_MAYBE_INTERCEPT___MEMMOVE_CHK;
-  MSAN_MAYBE_INTERCEPT___MEMSET_CHK;
-  MSAN_MAYBE_INTERCEPT___MEMPCPY_CHK;
   INTERCEPT_FUNCTION(bcopy);
   INTERCEPT_FUNCTION(wmemset);
   INTERCEPT_FUNCTION(wmemcpy);
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..de31dea974613 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
@@ -22,6 +22,14 @@
 //   COMMON_INTERCEPTOR_MEMMOVE_IMPL
 //   COMMON_INTERCEPTOR_MEMCPY_IMPL
 //   COMMON_INTERCEPTOR_NOTHING_IS_INITIALIZED
+//
+// A tool may additionally define any of the following macros to intercept the
+// fortified _chk variants; the corresponding interceptor is only emitted when
+// the matching macro is defined:
+//   COMMON_INTERCEPTOR_MEMSET_CHK_IMPL
+//   COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL
+//   COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL
+//   COMMON_INTERCEPTOR_MEMPCPY_CHK_IMPL
 //===----------------------------------------------------------------------===//
 
 #ifdef SANITIZER_REDEFINE_BUILTINS_H
@@ -233,6 +241,54 @@ INTERCEPTOR(void *, bzero, void *block, usize size) {
 #define INIT_BZERO
 #endif  // SANITIZER_INTERCEPT_BZERO
 
+#if SANITIZER_INTERCEPT___MEMSET_CHK && \
+    defined(COMMON_INTERCEPTOR_MEMSET_CHK_IMPL)
+INTERCEPTOR(void *, __memset_chk, void *dst, int v, usize size,
+            usize dst_size) {
+  void *ctx;
+  COMMON_INTERCEPTOR_MEMSET_CHK_IMPL(ctx, dst, v, size, dst_size);
+}
+#define INIT___MEMSET_CHK COMMON_INTERCEPT_FUNCTION(__memset_chk);
+#else
+#define INIT___MEMSET_CHK
+#endif  // SANITIZER_INTERCEPT___MEMSET_CHK
+
+#if SANITIZER_INTERCEPT___MEMMOVE_CHK && \
+    defined(COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL)
+INTERCEPTOR(void *, __memmove_chk, void *dst, const void *src, usize size,
+            usize dst_size) {
+  void *ctx;
+  COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL(ctx, dst, src, size, dst_size);
+}
+#define INIT___MEMMOVE_CHK COMMON_INTERCEPT_FUNCTION(__memmove_chk);
+#else
+#define INIT___MEMMOVE_CHK
+#endif  // SANITIZER_INTERCEPT___MEMMOVE_CHK
+
+#if SANITIZER_INTERCEPT___MEMCPY_CHK && \
+    defined(COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL)
+INTERCEPTOR(void *, __memcpy_chk, void *dst, const void *src, usize size,
+            usize dst_size) {
+  void *ctx;
+  COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL(ctx, dst, src, size, dst_size);
+}
+#define INIT___MEMCPY_CHK COMMON_INTERCEPT_FUNCTION(__memcpy_chk);
+#else
+#define INIT___MEMCPY_CHK
+#endif  // SANITIZER_INTERCEPT___MEMCPY_CHK
+
+#if SANITIZER_INTERCEPT___MEMPCPY_CHK && \
+    defined(COMMON_INTERCEPTOR_MEMPCPY_CHK_IMPL)
+INTERCEPTOR(void *, __mempcpy_chk, void *dst, const void *src, usize size,
+            usize dst_size) {
+  void *ctx;
+  COMMON_INTERCEPTOR_MEMPCPY_CHK_IMPL(ctx, dst, src, size, dst_size);
+}
+#define INIT___MEMPCPY_CHK COMMON_INTERCEPT_FUNCTION(__mempcpy_chk);
+#else
+#define INIT___MEMPCPY_CHK
+#endif  // SANITIZER_INTERCEPT___MEMPCPY_CHK
+
 namespace __sanitizer {
 // This does not need to be called if InitializeCommonInterceptors() is called.
 void InitializeMemintrinsicInterceptors() {
@@ -242,5 +298,9 @@ void InitializeMemintrinsicInterceptors() {
   INIT_AEABI_MEM;
   INIT___BZERO;
   INIT_BZERO;
+  INIT___MEMSET_CHK;
+  INIT___MEMMOVE_CHK;
+  INIT___MEMCPY_CHK;
+  INIT___MEMPCPY_CHK;
 }
 }  // namespace __sanitizer
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h b/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h
index 9f6fc4e9b9bfa..c72a7e4990be9 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h
@@ -181,6 +181,10 @@ SANITIZER_WEAK_IMPORT void *aligned_alloc(__sanitizer::usize __alignment,
 #define SANITIZER_INTERCEPT_MEMSET 1
 #define SANITIZER_INTERCEPT_MEMMOVE 1
 #define SANITIZER_INTERCEPT_MEMCPY 1
+#define SANITIZER_INTERCEPT___MEMCPY_CHK SI_GLIBC
+#define SANITIZER_INTERCEPT___MEMMOVE_CHK SI_GLIBC
+#define SANITIZER_INTERCEPT___MEMSET_CHK SI_GLIBC
+#define SANITIZER_INTERCEPT___MEMPCPY_CHK SI_GLIBC
 #define SANITIZER_INTERCEPT_MEMCMP SI_NOT_FUCHSIA
 #define SANITIZER_INTERCEPT_BCMP \
   SANITIZER_INTERCEPT_MEMCMP &&  \

>From 272cf8ba2e6f01644db0da1fa5302734e22026ea Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Fri, 17 Jul 2026 03:17:35 -0700
Subject: [PATCH 06/16] Clang format

---
 compiler-rt/lib/msan/msan_interceptors.cpp    | 10 +++---
 ...izer_common_interceptors_memintrinsics.inc | 33 +++++++++----------
 2 files changed, 21 insertions(+), 22 deletions(-)

diff --git a/compiler-rt/lib/msan/msan_interceptors.cpp b/compiler-rt/lib/msan/msan_interceptors.cpp
index 09a8af62d7a76..0e9be2d303edf 100644
--- a/compiler-rt/lib/msan/msan_interceptors.cpp
+++ b/compiler-rt/lib/msan/msan_interceptors.cpp
@@ -1515,7 +1515,7 @@ int OnExit() {
     if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())     \
       return REAL(__memset_chk)(dest, c, n, dest_size);                \
     ENSURE_MSAN_INITED();                                              \
-    void *res = REAL(__memset_chk)(dest, c, n, dest_size);             \
+    void* res = REAL(__memset_chk)(dest, c, n, dest_size);             \
     __msan_unpoison(dest, n);                                          \
     return res;                                                        \
   }
@@ -1528,7 +1528,7 @@ int OnExit() {
       return REAL(__memmove_chk)(dest, src, n, dest_size);                \
     ENSURE_MSAN_INITED();                                                 \
     GET_STORE_STACK_TRACE;                                                \
-    void *res = REAL(__memmove_chk)(dest, src, n, dest_size);             \
+    void* res = REAL(__memmove_chk)(dest, src, n, dest_size);             \
     MoveShadowAndOrigin(dest, src, n, &stack);                            \
     return res;                                                           \
   }
@@ -1541,7 +1541,7 @@ int OnExit() {
       return REAL(__memcpy_chk)(dest, src, n, dest_size);                \
     ENSURE_MSAN_INITED();                                                \
     GET_STORE_STACK_TRACE;                                               \
-    void *res = REAL(__memcpy_chk)(dest, src, n, dest_size);             \
+    void* res = REAL(__memcpy_chk)(dest, src, n, dest_size);             \
     CopyShadowAndOrigin(dest, src, n, &stack);                           \
     return res;                                                          \
   }
@@ -1550,13 +1550,13 @@ int OnExit() {
     (void)ctx;                                                            \
     if (!msan_inited) {                                                   \
       internal_memcpy(dest, src, n);                                      \
-      return (char *)dest + n;                                            \
+      return (char*)dest + n;                                             \
     }                                                                     \
     if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())        \
       return REAL(__mempcpy_chk)(dest, src, n, dest_size);                \
     ENSURE_MSAN_INITED();                                                 \
     GET_STORE_STACK_TRACE;                                                \
-    void *res = REAL(__mempcpy_chk)(dest, src, n, dest_size);             \
+    void* res = REAL(__mempcpy_chk)(dest, src, n, dest_size);             \
     CopyShadowAndOrigin(dest, src, n, &stack);                            \
     return res;                                                           \
   }
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 de31dea974613..caa48ded1934f 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
@@ -243,50 +243,49 @@ INTERCEPTOR(void *, bzero, void *block, usize size) {
 
 #if SANITIZER_INTERCEPT___MEMSET_CHK && \
     defined(COMMON_INTERCEPTOR_MEMSET_CHK_IMPL)
-INTERCEPTOR(void *, __memset_chk, void *dst, int v, usize size,
-            usize dst_size) {
-  void *ctx;
+INTERCEPTOR(void*, __memset_chk, void* dst, int v, usize size, usize dst_size) {
+  void* ctx;
   COMMON_INTERCEPTOR_MEMSET_CHK_IMPL(ctx, dst, v, size, dst_size);
 }
-#define INIT___MEMSET_CHK COMMON_INTERCEPT_FUNCTION(__memset_chk);
+#  define INIT___MEMSET_CHK COMMON_INTERCEPT_FUNCTION(__memset_chk);
 #else
-#define INIT___MEMSET_CHK
+#  define INIT___MEMSET_CHK
 #endif  // SANITIZER_INTERCEPT___MEMSET_CHK
 
 #if SANITIZER_INTERCEPT___MEMMOVE_CHK && \
     defined(COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL)
-INTERCEPTOR(void *, __memmove_chk, void *dst, const void *src, usize size,
+INTERCEPTOR(void*, __memmove_chk, void* dst, const void* src, usize size,
             usize dst_size) {
-  void *ctx;
+  void* ctx;
   COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL(ctx, dst, src, size, dst_size);
 }
-#define INIT___MEMMOVE_CHK COMMON_INTERCEPT_FUNCTION(__memmove_chk);
+#  define INIT___MEMMOVE_CHK COMMON_INTERCEPT_FUNCTION(__memmove_chk);
 #else
-#define INIT___MEMMOVE_CHK
+#  define INIT___MEMMOVE_CHK
 #endif  // SANITIZER_INTERCEPT___MEMMOVE_CHK
 
 #if SANITIZER_INTERCEPT___MEMCPY_CHK && \
     defined(COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL)
-INTERCEPTOR(void *, __memcpy_chk, void *dst, const void *src, usize size,
+INTERCEPTOR(void*, __memcpy_chk, void* dst, const void* src, usize size,
             usize dst_size) {
-  void *ctx;
+  void* ctx;
   COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL(ctx, dst, src, size, dst_size);
 }
-#define INIT___MEMCPY_CHK COMMON_INTERCEPT_FUNCTION(__memcpy_chk);
+#  define INIT___MEMCPY_CHK COMMON_INTERCEPT_FUNCTION(__memcpy_chk);
 #else
-#define INIT___MEMCPY_CHK
+#  define INIT___MEMCPY_CHK
 #endif  // SANITIZER_INTERCEPT___MEMCPY_CHK
 
 #if SANITIZER_INTERCEPT___MEMPCPY_CHK && \
     defined(COMMON_INTERCEPTOR_MEMPCPY_CHK_IMPL)
-INTERCEPTOR(void *, __mempcpy_chk, void *dst, const void *src, usize size,
+INTERCEPTOR(void*, __mempcpy_chk, void* dst, const void* src, usize size,
             usize dst_size) {
-  void *ctx;
+  void* ctx;
   COMMON_INTERCEPTOR_MEMPCPY_CHK_IMPL(ctx, dst, src, size, dst_size);
 }
-#define INIT___MEMPCPY_CHK COMMON_INTERCEPT_FUNCTION(__mempcpy_chk);
+#  define INIT___MEMPCPY_CHK COMMON_INTERCEPT_FUNCTION(__mempcpy_chk);
 #else
-#define INIT___MEMPCPY_CHK
+#  define INIT___MEMPCPY_CHK
 #endif  // SANITIZER_INTERCEPT___MEMPCPY_CHK
 
 namespace __sanitizer {

>From 6ac364fb5ace99df2549b720b40cbb8e13c0ce04 Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Fri, 17 Jul 2026 13:33:18 -0700
Subject: [PATCH 07/16] Split out __msan_memset_chk

---
 compiler-rt/lib/msan/msan_interceptors.cpp | 104 +++++++++++++--------
 1 file changed, 63 insertions(+), 41 deletions(-)

diff --git a/compiler-rt/lib/msan/msan_interceptors.cpp b/compiler-rt/lib/msan/msan_interceptors.cpp
index 0e9be2d303edf..45db510933880 100644
--- a/compiler-rt/lib/msan/msan_interceptors.cpp
+++ b/compiler-rt/lib/msan/msan_interceptors.cpp
@@ -1503,62 +1503,29 @@ int OnExit() {
     return __msan_memcpy(to, from, size);                   \
   }
 
-// The "_chk" variants delegate to the corresponding REAL(__*_chk) function so
-// that glibc's _FORTIFY_SOURCE overflow checks are preserved, while still
-// propagating shadow like the plain memory intrinsics above. These are only
-// expanded by the glibc-gated interceptors in the memintrinsics .inc file.
+// The "_chk" variants forward to dedicated __msan_*_chk helpers (defined below)
+// that mirror the plain __msan_mem* handlers while calling the fortified
+// REAL(__*_chk). These are only expanded by the glibc-gated interceptors in the
+// memintrinsics .inc file.
 #define COMMON_INTERCEPTOR_MEMSET_CHK_IMPL(ctx, dest, c, n, dest_size) \
   {                                                                    \
     (void)ctx;                                                         \
-    if (!msan_inited)                                                  \
-      return internal_memset(dest, c, n);                              \
-    if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())     \
-      return REAL(__memset_chk)(dest, c, n, dest_size);                \
-    ENSURE_MSAN_INITED();                                              \
-    void* res = REAL(__memset_chk)(dest, c, n, dest_size);             \
-    __msan_unpoison(dest, n);                                          \
-    return res;                                                        \
+    return __msan_memset_chk(dest, c, n, dest_size);                   \
   }
 #define COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL(ctx, dest, src, n, dest_size) \
   {                                                                       \
     (void)ctx;                                                            \
-    if (!msan_inited)                                                     \
-      return internal_memmove(dest, src, n);                              \
-    if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())        \
-      return REAL(__memmove_chk)(dest, src, n, dest_size);                \
-    ENSURE_MSAN_INITED();                                                 \
-    GET_STORE_STACK_TRACE;                                                \
-    void* res = REAL(__memmove_chk)(dest, src, n, dest_size);             \
-    MoveShadowAndOrigin(dest, src, n, &stack);                            \
-    return res;                                                           \
+    return __msan_memmove_chk(dest, src, n, dest_size);                   \
   }
 #define COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL(ctx, dest, src, n, dest_size) \
   {                                                                      \
     (void)ctx;                                                           \
-    if (!msan_inited)                                                    \
-      return internal_memcpy(dest, src, n);                              \
-    if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())       \
-      return REAL(__memcpy_chk)(dest, src, n, dest_size);                \
-    ENSURE_MSAN_INITED();                                                \
-    GET_STORE_STACK_TRACE;                                               \
-    void* res = REAL(__memcpy_chk)(dest, src, n, dest_size);             \
-    CopyShadowAndOrigin(dest, src, n, &stack);                           \
-    return res;                                                          \
+    return __msan_memcpy_chk(dest, src, n, dest_size);                   \
   }
 #define COMMON_INTERCEPTOR_MEMPCPY_CHK_IMPL(ctx, dest, src, n, dest_size) \
   {                                                                       \
     (void)ctx;                                                            \
-    if (!msan_inited) {                                                   \
-      internal_memcpy(dest, src, n);                                      \
-      return (char*)dest + n;                                             \
-    }                                                                     \
-    if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())        \
-      return REAL(__mempcpy_chk)(dest, src, n, dest_size);                \
-    ENSURE_MSAN_INITED();                                                 \
-    GET_STORE_STACK_TRACE;                                                \
-    void* res = REAL(__mempcpy_chk)(dest, src, n, dest_size);             \
-    CopyShadowAndOrigin(dest, src, n, &stack);                            \
-    return res;                                                           \
+    return (char*)__msan_memcpy_chk(dest, src, n, dest_size) + n;         \
   }
 
 #define COMMON_INTERCEPTOR_COPY_STRING(ctx, to, from, size) \
@@ -1574,6 +1541,18 @@ int OnExit() {
     return mmap_interceptor(REAL(mmap), addr, sz, prot, flags, fd, off);       \
   } while (false)
 
+#if SANITIZER_GLIBC
+// Forward declarations for the fortified "_chk" helpers used by the
+// memintrinsic interceptors below. They are defined after the interceptors that
+// provide the REAL(__*_chk) pointers, and are intentionally not part of the
+// public MSan interface (hence not declared in the interface header).
+static void* __msan_memset_chk(void* dest, int c, SIZE_T n, SIZE_T dest_size);
+static void* __msan_memmove_chk(void* dest, const void* src, SIZE_T n,
+                                SIZE_T dest_size);
+static void* __msan_memcpy_chk(void* dest, const void* src, SIZE_T n,
+                               SIZE_T dest_size);
+#endif  // SANITIZER_GLIBC
+
 #include "sanitizer_common/sanitizer_platform_interceptors.h"
 #include "sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc"
 #include "sanitizer_common/sanitizer_common_interceptors.inc"
@@ -1840,6 +1819,49 @@ void *__msan_memmove(void *dest, const void *src, SIZE_T n) {
   return res;
 }
 
+#if SANITIZER_GLIBC
+// Fortified "_chk" helpers. These mirror the plain __msan_mem* handlers above
+// but call the corresponding REAL(__*_chk) so that glibc's _FORTIFY_SOURCE
+// overflow checks are preserved. They have internal linkage and are not part of
+// the MSan interface.
+static void* __msan_memset_chk(void* dest, int c, SIZE_T n, SIZE_T dest_size) {
+  if (!msan_inited)
+    return internal_memset(dest, c, n);
+  if (msan_init_is_running)
+    return REAL(__memset_chk)(dest, c, n, dest_size);
+  ENSURE_MSAN_INITED();
+  void* res = REAL(__memset_chk)(dest, c, n, dest_size);
+  __msan_unpoison(dest, n);
+  return res;
+}
+
+static void* __msan_memmove_chk(void* dest, const void* src, SIZE_T n,
+                                SIZE_T dest_size) {
+  if (!msan_inited)
+    return internal_memmove(dest, src, n);
+  if (msan_init_is_running)
+    return REAL(__memmove_chk)(dest, src, n, dest_size);
+  ENSURE_MSAN_INITED();
+  GET_STORE_STACK_TRACE;
+  void* res = REAL(__memmove_chk)(dest, src, n, dest_size);
+  MoveShadowAndOrigin(dest, src, n, &stack);
+  return res;
+}
+
+static void* __msan_memcpy_chk(void* dest, const void* src, SIZE_T n,
+                               SIZE_T dest_size) {
+  if (!msan_inited)
+    return internal_memcpy(dest, src, n);
+  if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
+    return REAL(__memcpy_chk)(dest, src, n, dest_size);
+  ENSURE_MSAN_INITED();
+  GET_STORE_STACK_TRACE;
+  void* res = REAL(__memcpy_chk)(dest, src, n, dest_size);
+  CopyShadowAndOrigin(dest, src, n, &stack);
+  return res;
+}
+#endif  // SANITIZER_GLIBC
+
 void __msan_unpoison_string(const char* s) {
   if (!MEM_IS_APP(s)) return;
   __msan_unpoison(s, internal_strlen(s) + 1);

>From baa330414b78fbf4392258592ad8c8fa661efae1 Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Fri, 17 Jul 2026 22:06:17 -0700
Subject: [PATCH 08/16] Add fallback definitions

---
 compiler-rt/lib/msan/msan_interceptors.cpp    |  6 +--
 ...izer_common_interceptors_memintrinsics.inc | 44 +++++++++++++++----
 2 files changed, 38 insertions(+), 12 deletions(-)

diff --git a/compiler-rt/lib/msan/msan_interceptors.cpp b/compiler-rt/lib/msan/msan_interceptors.cpp
index 45db510933880..8d4db30cd06d6 100644
--- a/compiler-rt/lib/msan/msan_interceptors.cpp
+++ b/compiler-rt/lib/msan/msan_interceptors.cpp
@@ -1826,7 +1826,7 @@ void *__msan_memmove(void *dest, const void *src, SIZE_T n) {
 // the MSan interface.
 static void* __msan_memset_chk(void* dest, int c, SIZE_T n, SIZE_T dest_size) {
   if (!msan_inited)
-    return internal_memset(dest, c, n);
+    return internal_memset(dest, c, Min(n, dest_size));
   if (msan_init_is_running)
     return REAL(__memset_chk)(dest, c, n, dest_size);
   ENSURE_MSAN_INITED();
@@ -1838,7 +1838,7 @@ static void* __msan_memset_chk(void* dest, int c, SIZE_T n, SIZE_T dest_size) {
 static void* __msan_memmove_chk(void* dest, const void* src, SIZE_T n,
                                 SIZE_T dest_size) {
   if (!msan_inited)
-    return internal_memmove(dest, src, n);
+    return internal_memmove(dest, src, Min(n, dest_size));
   if (msan_init_is_running)
     return REAL(__memmove_chk)(dest, src, n, dest_size);
   ENSURE_MSAN_INITED();
@@ -1851,7 +1851,7 @@ static void* __msan_memmove_chk(void* dest, const void* src, SIZE_T n,
 static void* __msan_memcpy_chk(void* dest, const void* src, SIZE_T n,
                                SIZE_T dest_size) {
   if (!msan_inited)
-    return internal_memcpy(dest, src, n);
+    return internal_memcpy(dest, src, Min(n, dest_size));
   if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
     return REAL(__memcpy_chk)(dest, src, n, dest_size);
   ENSURE_MSAN_INITED();
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 caa48ded1934f..45cbab3c8ce57 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
@@ -23,12 +23,13 @@
 //   COMMON_INTERCEPTOR_MEMCPY_IMPL
 //   COMMON_INTERCEPTOR_NOTHING_IS_INITIALIZED
 //
-// A tool may additionally define any of the following macros to intercept the
-// fortified _chk variants; the corresponding interceptor is only emitted when
-// the matching macro is defined:
+// The fortified _chk variants below reuse the macros above by default (a tool
+// may override the following to customize their behavior):
 //   COMMON_INTERCEPTOR_MEMSET_CHK_IMPL
 //   COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL
 //   COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL
+// There is no common plain "mempcpy" interceptor to fall back on, so the
+// __mempcpy_chk interceptor is only emitted when a tool defines:
 //   COMMON_INTERCEPTOR_MEMPCPY_CHK_IMPL
 //===----------------------------------------------------------------------===//
 
@@ -91,6 +92,34 @@
   }
 #endif
 
+// By default the fortified _chk variants behave like their plain counterparts;
+// the fortification bound (dst_size) is unused because the tool's own bounds
+// checking already covers the access. A tool may override these to preserve the
+// libc _FORTIFY_SOURCE semantics (e.g. by calling REAL(__memset_chk)).
+#ifndef COMMON_INTERCEPTOR_MEMSET_CHK_IMPL
+#define COMMON_INTERCEPTOR_MEMSET_CHK_IMPL(ctx, dst, v, size, dst_size) \
+  {                                                                     \
+    (void)dst_size;                                                     \
+    COMMON_INTERCEPTOR_MEMSET_IMPL(ctx, dst, v, size);                  \
+  }
+#endif
+
+#ifndef COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL
+#define COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL(ctx, dst, src, size, dst_size) \
+  {                                                                        \
+    (void)dst_size;                                                        \
+    COMMON_INTERCEPTOR_MEMMOVE_IMPL(ctx, dst, src, size);                  \
+  }
+#endif
+
+#ifndef COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL
+#define COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL(ctx, dst, src, size, dst_size) \
+  {                                                                       \
+    (void)dst_size;                                                       \
+    COMMON_INTERCEPTOR_MEMCPY_IMPL(ctx, dst, src, size);                  \
+  }
+#endif
+
 #if SANITIZER_INTERCEPT_MEMSET
 INTERCEPTOR(void *, memset, void *dst, int v, usize size) {
   void *ctx;
@@ -241,8 +270,7 @@ INTERCEPTOR(void *, bzero, void *block, usize size) {
 #define INIT_BZERO
 #endif  // SANITIZER_INTERCEPT_BZERO
 
-#if SANITIZER_INTERCEPT___MEMSET_CHK && \
-    defined(COMMON_INTERCEPTOR_MEMSET_CHK_IMPL)
+#if SANITIZER_INTERCEPT___MEMSET_CHK
 INTERCEPTOR(void*, __memset_chk, void* dst, int v, usize size, usize dst_size) {
   void* ctx;
   COMMON_INTERCEPTOR_MEMSET_CHK_IMPL(ctx, dst, v, size, dst_size);
@@ -252,8 +280,7 @@ INTERCEPTOR(void*, __memset_chk, void* dst, int v, usize size, usize dst_size) {
 #  define INIT___MEMSET_CHK
 #endif  // SANITIZER_INTERCEPT___MEMSET_CHK
 
-#if SANITIZER_INTERCEPT___MEMMOVE_CHK && \
-    defined(COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL)
+#if SANITIZER_INTERCEPT___MEMMOVE_CHK
 INTERCEPTOR(void*, __memmove_chk, void* dst, const void* src, usize size,
             usize dst_size) {
   void* ctx;
@@ -264,8 +291,7 @@ INTERCEPTOR(void*, __memmove_chk, void* dst, const void* src, usize size,
 #  define INIT___MEMMOVE_CHK
 #endif  // SANITIZER_INTERCEPT___MEMMOVE_CHK
 
-#if SANITIZER_INTERCEPT___MEMCPY_CHK && \
-    defined(COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL)
+#if SANITIZER_INTERCEPT___MEMCPY_CHK
 INTERCEPTOR(void*, __memcpy_chk, void* dst, const void* src, usize size,
             usize dst_size) {
   void* ctx;

>From c774e19624f4394b165d296e8a44024f1feb5c9c Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Fri, 17 Jul 2026 22:06:34 -0700
Subject: [PATCH 09/16] Extend _chk variants to more platforms

---
 .../sanitizer_platform_interceptors.h            | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h b/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h
index c72a7e4990be9..f5229899d3f28 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h
@@ -181,10 +181,18 @@ SANITIZER_WEAK_IMPORT void *aligned_alloc(__sanitizer::usize __alignment,
 #define SANITIZER_INTERCEPT_MEMSET 1
 #define SANITIZER_INTERCEPT_MEMMOVE 1
 #define SANITIZER_INTERCEPT_MEMCPY 1
-#define SANITIZER_INTERCEPT___MEMCPY_CHK SI_GLIBC
-#define SANITIZER_INTERCEPT___MEMMOVE_CHK SI_GLIBC
-#define SANITIZER_INTERCEPT___MEMSET_CHK SI_GLIBC
-#define SANITIZER_INTERCEPT___MEMPCPY_CHK SI_GLIBC
+// The fortified _chk variants are provided by:
+//   - glibc
+//   - Bionic (Android)
+//   - Apple libc (macOS/iOS)
+//   - FreeBSD libc
+//   - MinGW/Windows (via GCC's libssp, linked with -lssp)
+#define SANITIZER_FORTIFY_CHK \
+  (SI_GLIBC || SI_ANDROID || SI_MAC || SI_FREEBSD || SI_WINDOWS)
+#define SANITIZER_INTERCEPT___MEMCPY_CHK SANITIZER_FORTIFY_CHK
+#define SANITIZER_INTERCEPT___MEMMOVE_CHK SANITIZER_FORTIFY_CHK
+#define SANITIZER_INTERCEPT___MEMSET_CHK SANITIZER_FORTIFY_CHK
+#define SANITIZER_INTERCEPT___MEMPCPY_CHK SANITIZER_FORTIFY_CHK
 #define SANITIZER_INTERCEPT_MEMCMP SI_NOT_FUCHSIA
 #define SANITIZER_INTERCEPT_BCMP \
   SANITIZER_INTERCEPT_MEMCMP &&  \

>From 5c710c6beabdfbbf81b869a57897e9c227736722 Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Fri, 17 Jul 2026 22:13:03 -0700
Subject: [PATCH 10/16] clang-format

---
 ...izer_common_interceptors_memintrinsics.inc | 30 +++++++++----------
 1 file changed, 15 insertions(+), 15 deletions(-)

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 45cbab3c8ce57..79c640cb2782f 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
@@ -97,27 +97,27 @@
 // checking already covers the access. A tool may override these to preserve the
 // libc _FORTIFY_SOURCE semantics (e.g. by calling REAL(__memset_chk)).
 #ifndef COMMON_INTERCEPTOR_MEMSET_CHK_IMPL
-#define COMMON_INTERCEPTOR_MEMSET_CHK_IMPL(ctx, dst, v, size, dst_size) \
-  {                                                                     \
-    (void)dst_size;                                                     \
-    COMMON_INTERCEPTOR_MEMSET_IMPL(ctx, dst, v, size);                  \
-  }
+#  define COMMON_INTERCEPTOR_MEMSET_CHK_IMPL(ctx, dst, v, size, dst_size) \
+    {                                                                     \
+      (void)dst_size;                                                     \
+      COMMON_INTERCEPTOR_MEMSET_IMPL(ctx, dst, v, size);                  \
+    }
 #endif
 
 #ifndef COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL
-#define COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL(ctx, dst, src, size, dst_size) \
-  {                                                                        \
-    (void)dst_size;                                                        \
-    COMMON_INTERCEPTOR_MEMMOVE_IMPL(ctx, dst, src, size);                  \
-  }
+#  define COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL(ctx, dst, src, size, dst_size) \
+    {                                                                        \
+      (void)dst_size;                                                        \
+      COMMON_INTERCEPTOR_MEMMOVE_IMPL(ctx, dst, src, size);                  \
+    }
 #endif
 
 #ifndef COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL
-#define COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL(ctx, dst, src, size, dst_size) \
-  {                                                                       \
-    (void)dst_size;                                                       \
-    COMMON_INTERCEPTOR_MEMCPY_IMPL(ctx, dst, src, size);                  \
-  }
+#  define COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL(ctx, dst, src, size, dst_size) \
+    {                                                                       \
+      (void)dst_size;                                                       \
+      COMMON_INTERCEPTOR_MEMCPY_IMPL(ctx, dst, src, size);                  \
+    }
 #endif
 
 #if SANITIZER_INTERCEPT_MEMSET

>From c2797d758f7f3bdfcfe60717ad490161e88bdaff Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Sat, 18 Jul 2026 00:29:27 -0700
Subject: [PATCH 11/16] Exclude Windows from _chk interceptors (no __*_chk in
 MSVC/UCRT)

---
 .../lib/sanitizer_common/sanitizer_platform_interceptors.h | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h b/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h
index f5229899d3f28..3427ccde5659a 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_platform_interceptors.h
@@ -186,9 +186,10 @@ SANITIZER_WEAK_IMPORT void *aligned_alloc(__sanitizer::usize __alignment,
 //   - Bionic (Android)
 //   - Apple libc (macOS/iOS)
 //   - FreeBSD libc
-//   - MinGW/Windows (via GCC's libssp, linked with -lssp)
-#define SANITIZER_FORTIFY_CHK \
-  (SI_GLIBC || SI_ANDROID || SI_MAC || SI_FREEBSD || SI_WINDOWS)
+// Windows is intentionally excluded: the MSVC/UCRT runtime does not export
+// __*_chk symbols (they only exist on MinGW when linking GCC's libssp), so
+// intercepting them would produce unresolved externals at link time.
+#define SANITIZER_FORTIFY_CHK (SI_GLIBC || SI_ANDROID || SI_MAC || SI_FREEBSD)
 #define SANITIZER_INTERCEPT___MEMCPY_CHK SANITIZER_FORTIFY_CHK
 #define SANITIZER_INTERCEPT___MEMMOVE_CHK SANITIZER_FORTIFY_CHK
 #define SANITIZER_INTERCEPT___MEMSET_CHK SANITIZER_FORTIFY_CHK

>From e536c0142b522aea15f30aa5c2bb6915d56d6cdc Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Fri, 24 Jul 2026 20:23:18 -0700
Subject: [PATCH 12/16] Honor dst_size in the default _chk implementations

Instead of ignoring the fortification bound, forward the overflow case to
the real __*_chk function so that the _FORTIFY_SOURCE abort is preserved,
and handle every other case with the tool's normal implementation, which is
semantically identical for size <= dst_size.

Add a regression test covering a dst_size smaller than the allocation.
---
 ...izer_common_interceptors_memintrinsics.inc | 19 ++++++++-----
 .../TestCases/Linux/fortify-overflow.cpp      | 27 +++++++++++++++++++
 2 files changed, 39 insertions(+), 7 deletions(-)
 create mode 100644 compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-overflow.cpp

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 79c640cb2782f..e6b28d711e334 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
@@ -92,14 +92,17 @@
   }
 #endif
 
-// By default the fortified _chk variants behave like their plain counterparts;
-// the fortification bound (dst_size) is unused because the tool's own bounds
-// checking already covers the access. A tool may override these to preserve the
-// libc _FORTIFY_SOURCE semantics (e.g. by calling REAL(__memset_chk)).
+// __{memset,memmove,memcpy}_chk(dst, ..., size, dst_size) is defined as:
+// abort via __chk_fail() when size > dst_size, and otherwise behave exactly
+// like the corresponding plain function. So the overflow case is forwarded to
+// the real fortified function, which keeps the _FORTIFY_SOURCE check intact,
+// and every other case is handled by the tool's normal implementation, which
+// is semantically identical and applies the usual instrumentation.
 #ifndef COMMON_INTERCEPTOR_MEMSET_CHK_IMPL
 #  define COMMON_INTERCEPTOR_MEMSET_CHK_IMPL(ctx, dst, v, size, dst_size) \
     {                                                                     \
-      (void)dst_size;                                                     \
+      if (UNLIKELY(size > dst_size))                                      \
+        return REAL(__memset_chk)(dst, v, size, dst_size);                \
       COMMON_INTERCEPTOR_MEMSET_IMPL(ctx, dst, v, size);                  \
     }
 #endif
@@ -107,7 +110,8 @@
 #ifndef COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL
 #  define COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL(ctx, dst, src, size, dst_size) \
     {                                                                        \
-      (void)dst_size;                                                        \
+      if (UNLIKELY(size > dst_size))                                         \
+        return REAL(__memmove_chk)(dst, src, size, dst_size);                \
       COMMON_INTERCEPTOR_MEMMOVE_IMPL(ctx, dst, src, size);                  \
     }
 #endif
@@ -115,7 +119,8 @@
 #ifndef COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL
 #  define COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL(ctx, dst, src, size, dst_size) \
     {                                                                       \
-      (void)dst_size;                                                       \
+      if (UNLIKELY(size > dst_size))                                        \
+        return REAL(__memcpy_chk)(dst, src, size, dst_size);                \
       COMMON_INTERCEPTOR_MEMCPY_IMPL(ctx, dst, src, size);                  \
     }
 #endif
diff --git a/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-overflow.cpp b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-overflow.cpp
new file mode 100644
index 0000000000000..b5396f4a57dcd
--- /dev/null
+++ b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-overflow.cpp
@@ -0,0 +1,27 @@
+// Check that the fortified _chk interceptors keep honoring the destination size
+// argument. The write below is in bounds for the allocation, so nothing but the
+// fortification bound can detect it.
+
+// RUN: %clangxx -O0 %s -o %t && not --crash %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx -O2 %s -o %t && not --crash %run %t 2>&1 | FileCheck %s
+
+// REQUIRES: glibc
+
+#include <stdio.h>
+#include <string.h>
+
+extern "C" void *__memset_chk(void *dest, int c, size_t len, size_t destlen);
+
+int main(int argc, char *argv[]) {
+  char dest[10];
+  // Keep the sizes opaque so that the check is not folded at compile time.
+  volatile size_t len = sizeof(dest);
+  volatile size_t destlen = sizeof(dest) / 2;
+
+  fprintf(stderr, "before\n");
+  // CHECK: before
+  __memset_chk(dest, 42, len, destlen);
+  fprintf(stderr, "unreachable\n");
+  // CHECK-NOT: unreachable
+  return 0;
+}

>From f3589fb9458af0a13f115cead77e2b964d259aa5 Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Sun, 26 Jul 2026 22:47:49 -0700
Subject: [PATCH 13/16] Document why the overflow case delegates to the real
 _chk function

Explain that Die() and the libc failure hooks were considered and rejected:
the failure entry points are not portable across glibc/Bionic/Apple/FreeBSD,
delegating reproduces each libc's native diagnostic, and it keeps termination
consistent with uninstrumented builds and with tools that do not intercept
these functions at all.
---
 ...izer_common_interceptors_memintrinsics.inc | 22 ++++++++++++++-----
 .../TestCases/Linux/fortify-overflow.cpp      |  3 +++
 2 files changed, 20 insertions(+), 5 deletions(-)

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 e6b28d711e334..072756f52ee62 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
@@ -92,12 +92,24 @@
   }
 #endif
 
-// __{memset,memmove,memcpy}_chk(dst, ..., size, dst_size) is defined as:
-// abort via __chk_fail() when size > dst_size, and otherwise behave exactly
+// __{memset,memmove,memcpy}_chk(dst, ..., size, dst_size) abort when the
+// operation would write more than dst_size bytes, and otherwise behave exactly
 // like the corresponding plain function. So the overflow case is forwarded to
-// the real fortified function, which keeps the _FORTIFY_SOURCE check intact,
-// and every other case is handled by the tool's normal implementation, which
-// is semantically identical and applies the usual instrumentation.
+// the real fortified function, and every other case is handled by the tool's
+// normal implementation, which is semantically identical and applies the usual
+// instrumentation.
+//
+// The overflow case deliberately calls the real function rather than Die() or
+// the libc's failure hook directly:
+//   - Portability. glibc exports __chk_fail(), but Bionic (__fortify_fatal),
+//     Apple (__chk_fail_overflow) and FreeBSD all use different, mostly
+//     unexported entry points, and glibc's __fortify_fail is GLIBC_PRIVATE.
+//   - Diagnostics. Each libc prints its own message, and Bionic even reports
+//     the offending sizes. Delegating reproduces that for free.
+//   - Consistency. Termination stays identical to an uninstrumented build, and
+//     to the tools which do not intercept these at all (e.g. lsan and ubsan).
+//     Die() would instead exit silently, and only under some of the tools.
+// A stack trace is still available for the resulting abort via handle_abort=1.
 #ifndef COMMON_INTERCEPTOR_MEMSET_CHK_IMPL
 #  define COMMON_INTERCEPTOR_MEMSET_CHK_IMPL(ctx, dst, v, size, dst_size) \
     {                                                                     \
diff --git a/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-overflow.cpp b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-overflow.cpp
index b5396f4a57dcd..eb30be8eb880e 100644
--- a/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-overflow.cpp
+++ b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-overflow.cpp
@@ -1,6 +1,9 @@
 // Check that the fortified _chk interceptors keep honoring the destination size
 // argument. The write below is in bounds for the allocation, so nothing but the
 // fortification bound can detect it.
+//
+// The interceptors hand the overflow to the real fortified function, so the
+// libc aborts exactly like it would without a sanitizer, hence "not --crash".
 
 // RUN: %clangxx -O0 %s -o %t && not --crash %run %t 2>&1 | FileCheck %s
 // RUN: %clangxx -O2 %s -o %t && not --crash %run %t 2>&1 | FileCheck %s

>From 72700d63fef9d10b25d620e320925730f3b37d0d Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Mon, 3 Aug 2026 00:48:05 -0700
Subject: [PATCH 14/16] Enforce the fortification bound before initialization
 completes

REAL() is not resolved while COMMON_INTERCEPTOR_NOTHING_IS_INITIALIZED is
true, so check the bound with CHECK_LE there and let the plain implementation's
early startup path perform the access.

asan and memprof override the plain *_IMPL macros and therefore did not define
COMMON_INTERCEPTOR_NOTHING_IS_INITIALIZED in their memintrinsic translation
units, even though the header documents it as required. Define it in both,
reusing the predicate each already uses internally.

Signed-off-by: Austin Schuh <austin.linux at gmail.com>
---
 .../asan/asan_interceptors_memintrinsics.cpp  |  6 +++++-
 .../memprof_interceptors_memintrinsics.cpp    |  4 ++++
 ...izer_common_interceptors_memintrinsics.inc | 19 ++++++++++++++++---
 3 files changed, 25 insertions(+), 4 deletions(-)

diff --git a/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp b/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp
index f52ae9ae8d17c..b22b9732c6b38 100644
--- a/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp
+++ b/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp
@@ -103,6 +103,10 @@ extern "C" decltype(__asan_memset) memset[[gnu::alias("__asan_memset")]];
     ASAN_MEMSET_IMPL(ctx, block, c, size);                  \
   } while (false)
 
-#include "sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc"
+// Needed by the default fortified _chk implementations, which cannot use REAL()
+// until initialization has completed.
+#  define COMMON_INTERCEPTOR_NOTHING_IS_INITIALIZED (!AsanInited())
+
+#  include "sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc"
 
 #endif  // SANITIZER_FUCHSIA
diff --git a/compiler-rt/lib/memprof/memprof_interceptors_memintrinsics.cpp b/compiler-rt/lib/memprof/memprof_interceptors_memintrinsics.cpp
index 56bd11614d6ac..d12cd0a6f3049 100644
--- a/compiler-rt/lib/memprof/memprof_interceptors_memintrinsics.cpp
+++ b/compiler-rt/lib/memprof/memprof_interceptors_memintrinsics.cpp
@@ -77,6 +77,10 @@ using namespace __memprof;
     MEMPROF_MEMSET_IMPL(block, c, size);                                       \
   } while (false)
 
+// Needed by the default fortified _chk implementations, which cannot use REAL()
+// until initialization has completed.
+#define COMMON_INTERCEPTOR_NOTHING_IS_INITIALIZED (!memprof_inited)
+
 #include "sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc"
 
 void *__memprof_memcpy(void *to, const void *from, uptr size) {
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 072756f52ee62..84423ec49e940 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc
@@ -110,11 +110,18 @@
 //     to the tools which do not intercept these at all (e.g. lsan and ubsan).
 //     Die() would instead exit silently, and only under some of the tools.
 // A stack trace is still available for the resulting abort via handle_abort=1.
+//
+// REAL() is not resolved yet while nothing is initialized, so the bound is
+// enforced with CHECK_LE there instead, and the plain implementation's own
+// early startup path performs the access.
 #ifndef COMMON_INTERCEPTOR_MEMSET_CHK_IMPL
 #  define COMMON_INTERCEPTOR_MEMSET_CHK_IMPL(ctx, dst, v, size, dst_size) \
     {                                                                     \
-      if (UNLIKELY(size > dst_size))                                      \
+      if (COMMON_INTERCEPTOR_NOTHING_IS_INITIALIZED) {                    \
+        CHECK_LE(size, dst_size);                                         \
+      } else if (UNLIKELY(size > dst_size)) {                             \
         return REAL(__memset_chk)(dst, v, size, dst_size);                \
+      }                                                                   \
       COMMON_INTERCEPTOR_MEMSET_IMPL(ctx, dst, v, size);                  \
     }
 #endif
@@ -122,8 +129,11 @@
 #ifndef COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL
 #  define COMMON_INTERCEPTOR_MEMMOVE_CHK_IMPL(ctx, dst, src, size, dst_size) \
     {                                                                        \
-      if (UNLIKELY(size > dst_size))                                         \
+      if (COMMON_INTERCEPTOR_NOTHING_IS_INITIALIZED) {                       \
+        CHECK_LE(size, dst_size);                                            \
+      } else if (UNLIKELY(size > dst_size)) {                                \
         return REAL(__memmove_chk)(dst, src, size, dst_size);                \
+      }                                                                      \
       COMMON_INTERCEPTOR_MEMMOVE_IMPL(ctx, dst, src, size);                  \
     }
 #endif
@@ -131,8 +141,11 @@
 #ifndef COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL
 #  define COMMON_INTERCEPTOR_MEMCPY_CHK_IMPL(ctx, dst, src, size, dst_size) \
     {                                                                       \
-      if (UNLIKELY(size > dst_size))                                        \
+      if (COMMON_INTERCEPTOR_NOTHING_IS_INITIALIZED) {                      \
+        CHECK_LE(size, dst_size);                                           \
+      } else if (UNLIKELY(size > dst_size)) {                               \
         return REAL(__memcpy_chk)(dst, src, size, dst_size);                \
+      }                                                                     \
       COMMON_INTERCEPTOR_MEMCPY_IMPL(ctx, dst, src, size);                  \
     }
 #endif

>From 184d56a1673201eb22f86ddd2ad507518c8b3161 Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Sat, 8 Aug 2026 00:11:36 -0700
Subject: [PATCH 15/16] Test the fortified _chk interceptors during early
 startup

__<tool>_default_options() is called from the runtime's flag initialization,
before the interceptors are installed, which is exactly the state the
COMMON_INTERCEPTOR_NOTHING_IS_INITIALIZED branch handles.

fortify-early-init.cpp covers a call within the bound on every tool, and
fortify-early-init-overflow.cpp covers an overflowing one reaching CHECK_LE.
Without that branch the latter dereferences a null REAL() and segfaults.
---
 .../Linux/fortify-early-init-overflow.cpp     | 50 +++++++++++++++
 .../TestCases/Linux/fortify-early-init.cpp    | 64 +++++++++++++++++++
 2 files changed, 114 insertions(+)
 create mode 100644 compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init-overflow.cpp
 create mode 100644 compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init.cpp

diff --git a/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init-overflow.cpp b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init-overflow.cpp
new file mode 100644
index 0000000000000..74f7788d9c0b9
--- /dev/null
+++ b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init-overflow.cpp
@@ -0,0 +1,50 @@
+// The same early startup path as fortify-early-init.cpp, but with a size which
+// exceeds the destination. REAL() cannot be used to report this yet, so the
+// fortification bound is enforced with CHECK_LE instead.
+
+// RUN: %clangxx -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx -O2 %s -o %t && not %run %t 2>&1 | FileCheck %s
+
+// Only the tools using the common default _chk implementation reach CHECK_LE:
+// msan supplies its own, and lsan and ubsan do not intercept these at all.
+// tsan reaches it too, but faults while unwinding this early in startup, so
+// its exit status is not stable enough to match on.
+// REQUIRES: glibc && (asan || hwasan)
+
+#include <stddef.h>
+
+extern "C" void *__memset_chk(void *dest, int c, size_t len, size_t destlen);
+
+static char buf[16];
+
+static const char *test() __attribute__((disable_sanitizer_instrumentation)) {
+  // Keep the sizes opaque so that the bound check is not folded away.
+  volatile size_t len = sizeof(buf);
+  volatile size_t destlen = sizeof(buf) / 2;
+  __memset_chk(buf, 42, len, destlen);
+  return "";
+}
+
+extern "C" {
+const char *__asan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+const char *__hwasan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+const char *__tsan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+}
+
+// The reported values confirm the destination size was the bound applied.
+// CHECK: CHECK failed: {{.*}}memintrinsics.inc{{.*}}size{{.*}}dst_size{{.*}}(0x10, 0x8)
+
+int main(int argc, char *argv[]) {
+  // CHECK-NOT: unreachable
+  __builtin_printf("unreachable\n");
+  return 0;
+}
diff --git a/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init.cpp b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init.cpp
new file mode 100644
index 0000000000000..a10fe8aa121fa
--- /dev/null
+++ b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init.cpp
@@ -0,0 +1,64 @@
+// Check the fortified _chk interceptors before initialization has finished.
+// __<tool>_default_options() is called from the runtime's flag initialization,
+// which runs before the interceptors are installed, so REAL() is still null
+// there and the interceptor has to take its early startup path instead.
+
+// RUN: %clangxx -O0 %s -o %t && %run %t
+// RUN: %clangxx -O2 %s -o %t && %run %t
+
+// REQUIRES: glibc
+
+#include <assert.h>
+#include <stddef.h>
+
+extern "C" void *__memset_chk(void *dest, int c, size_t len, size_t destlen);
+
+static char buf[16];
+
+static const char *test() __attribute__((disable_sanitizer_instrumentation)) {
+  // Keep the sizes opaque so that the bound check is not folded away.
+  volatile size_t len = sizeof(buf) / 2;
+  volatile size_t destlen = sizeof(buf);
+  __memset_chk(buf, 42, len, destlen);
+  return "";
+}
+
+extern "C" {
+const char *__asan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+const char *__hwasan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+const char *__lsan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+const char *__msan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+const char *__rtsan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+const char *__tsan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+const char *__ubsan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+}
+
+int main(int argc, char *argv[]) {
+  // The write happened during initialization and stayed within the bound.
+  for (size_t i = 0; i < sizeof(buf) / 2; ++i)
+    assert(buf[i] == 42);
+  for (size_t i = sizeof(buf) / 2; i < sizeof(buf); ++i)
+    assert(buf[i] == 0);
+  return 0;
+}

>From 36b8724c44440323fcf2c41c49adfa18a310309e Mon Sep 17 00:00:00 2001
From: Austin Schuh <austin.linux at gmail.com>
Date: Sat, 8 Aug 2026 00:40:35 -0700
Subject: [PATCH 16/16] Report the overflow from msan's early startup path too

The _chk helpers truncated to dest_size before initialization completed, which
silently discarded the overflow that these functions exist to report: every
other configuration, including an uninstrumented build, terminates instead.
Enforce the bound with CHECK_LE the same way the default implementations do.

This lets fortify-early-init-overflow.cpp cover msan as well.
---
 compiler-rt/lib/msan/msan_interceptors.cpp    | 23 ++++++++++----
 .../Linux/fortify-early-init-overflow.cpp     | 31 ++++++++++++++-----
 2 files changed, 41 insertions(+), 13 deletions(-)

diff --git a/compiler-rt/lib/msan/msan_interceptors.cpp b/compiler-rt/lib/msan/msan_interceptors.cpp
index 8d4db30cd06d6..5dde9b3d2a77e 100644
--- a/compiler-rt/lib/msan/msan_interceptors.cpp
+++ b/compiler-rt/lib/msan/msan_interceptors.cpp
@@ -1824,9 +1824,16 @@ void *__msan_memmove(void *dest, const void *src, SIZE_T n) {
 // but call the corresponding REAL(__*_chk) so that glibc's _FORTIFY_SOURCE
 // overflow checks are preserved. They have internal linkage and are not part of
 // the MSan interface.
+//
+// REAL() is not resolved before initialization completes, so the bound is
+// enforced with CHECK_LE there instead, matching the default implementations in
+// sanitizer_common_interceptors_memintrinsics.inc. Truncating to dest_size
+// would silently hide the overflow that these functions exist to report.
 static void* __msan_memset_chk(void* dest, int c, SIZE_T n, SIZE_T dest_size) {
-  if (!msan_inited)
-    return internal_memset(dest, c, Min(n, dest_size));
+  if (!msan_inited) {
+    CHECK_LE(n, dest_size);
+    return internal_memset(dest, c, n);
+  }
   if (msan_init_is_running)
     return REAL(__memset_chk)(dest, c, n, dest_size);
   ENSURE_MSAN_INITED();
@@ -1837,8 +1844,10 @@ static void* __msan_memset_chk(void* dest, int c, SIZE_T n, SIZE_T dest_size) {
 
 static void* __msan_memmove_chk(void* dest, const void* src, SIZE_T n,
                                 SIZE_T dest_size) {
-  if (!msan_inited)
-    return internal_memmove(dest, src, Min(n, dest_size));
+  if (!msan_inited) {
+    CHECK_LE(n, dest_size);
+    return internal_memmove(dest, src, n);
+  }
   if (msan_init_is_running)
     return REAL(__memmove_chk)(dest, src, n, dest_size);
   ENSURE_MSAN_INITED();
@@ -1850,8 +1859,10 @@ static void* __msan_memmove_chk(void* dest, const void* src, SIZE_T n,
 
 static void* __msan_memcpy_chk(void* dest, const void* src, SIZE_T n,
                                SIZE_T dest_size) {
-  if (!msan_inited)
-    return internal_memcpy(dest, src, Min(n, dest_size));
+  if (!msan_inited) {
+    CHECK_LE(n, dest_size);
+    return internal_memcpy(dest, src, n);
+  }
   if (msan_init_is_running || __msan::IsInSymbolizerOrUnwider())
     return REAL(__memcpy_chk)(dest, src, n, dest_size);
   ENSURE_MSAN_INITED();
diff --git a/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init-overflow.cpp b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init-overflow.cpp
index 74f7788d9c0b9..b9700511f97bf 100644
--- a/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init-overflow.cpp
+++ b/compiler-rt/test/sanitizer_common/TestCases/Linux/fortify-early-init-overflow.cpp
@@ -5,11 +5,11 @@
 // RUN: %clangxx -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s
 // RUN: %clangxx -O2 %s -o %t && not %run %t 2>&1 | FileCheck %s
 
-// Only the tools using the common default _chk implementation reach CHECK_LE:
-// msan supplies its own, and lsan and ubsan do not intercept these at all.
-// tsan reaches it too, but faults while unwinding this early in startup, so
-// its exit status is not stable enough to match on.
-// REQUIRES: glibc && (asan || hwasan)
+// lsan and ubsan do not intercept these, so they just get glibc's own abort,
+// which fortify-overflow.cpp already covers. tsan does reach the CHECK, but
+// then faults while unwinding this early in startup, so its exit status is not
+// stable enough to match on.
+// REQUIRES: glibc && (asan || hwasan || msan)
 
 #include <stddef.h>
 
@@ -34,14 +34,31 @@ const char *__hwasan_default_options()
     __attribute__((disable_sanitizer_instrumentation)) {
   return test();
 }
+const char *__lsan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+const char *__msan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
+const char *__rtsan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
 const char *__tsan_default_options()
     __attribute__((disable_sanitizer_instrumentation)) {
   return test();
 }
+const char *__ubsan_default_options()
+    __attribute__((disable_sanitizer_instrumentation)) {
+  return test();
+}
 }
 
-// The reported values confirm the destination size was the bound applied.
-// CHECK: CHECK failed: {{.*}}memintrinsics.inc{{.*}}size{{.*}}dst_size{{.*}}(0x10, 0x8)
+// The reported values confirm the destination size was the bound applied. Which
+// file reports it differs, as msan supplies its own _chk implementations.
+// CHECK: CHECK failed: {{.*}} (0x10, 0x8)
 
 int main(int argc, char *argv[]) {
   // CHECK-NOT: unreachable



More information about the llvm-commits mailing list