On 2026-07-26 05:37, Bruno Haible via Gnulib discussion list wrote:

Let's hear Paul's opinion.

I also don't see downsides to Lasse's patch. It looks like a good patch and necessary for some perhaps-theoretical optimizations. So I installed it. Thanks, Lasse.

The idea about adding __attribute__((no_sanitize_address)) to rawmemchr and strchrnul is iffier, as it could cause these functions to not report internal errors that they should report. Instead, these functions can simply avoid the optimized-but-in-theory-invalid behavior if address sanitization is in effect. Although that slows things down, sanitization is already slower so it's not that big a deal. I installed the attached to do that.
From f6b3efe5018211f371305e693c8d786d9489a769 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email protected]>
Date: Sun, 26 Jul 2026 13:46:46 -0700
Subject: [PATCH] Pacify -fsanitize=address in rawmemchr, strchrnul

Problem reported by Lasse Collin in:
https://lists.gnu.org/r/bug-gnulib/2026-07/msg00174.html
* lib/memchr.c (__memchr), lib/memchr2.c (memchr2):
* lib/rawmemchr.c (rawmemchr), lib/strchrnul.c (strchrnul):
Omit the longword optimization if __SANITIZE_ADDRESS__.
---
 ChangeLog       |  7 +++++++
 lib/memchr.c    | 18 +++++++++++++++---
 lib/memchr2.c   | 26 +++++++++++++++++++-------
 lib/rawmemchr.c | 24 ++++++++++++++++--------
 lib/strchrnul.c | 27 +++++++++++++++++++--------
 5 files changed, 76 insertions(+), 26 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index e74699ea91..109821b783 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,12 @@
 2026-07-26  Paul Eggert  <[email protected]>
 
+	Pacify -fsanitize=address in rawmemchr, strchrnul
+	Problem reported by Lasse Collin in:
+	https://lists.gnu.org/r/bug-gnulib/2026-07/msg00174.html
+	* lib/memchr.c (__memchr), lib/memchr2.c (memchr2):
+	* lib/rawmemchr.c (rawmemchr), lib/strchrnul.c (strchrnul):
+	Omit the longword optimization if __SANITIZE_ADDRESS__.
+
 	Fix strict aliasing violations in memchr etc
 	Problem and fix reported by Lasse Collin in:
 	https://lists.gnu.org/r/bug-gnulib/2026-07/msg00172.html
diff --git a/lib/memchr.c b/lib/memchr.c
index 0e3c2c8a35..b023f478bf 100644
--- a/lib/memchr.c
+++ b/lib/memchr.c
@@ -58,6 +58,16 @@
 void *
 __memchr (void const *s, int c_in, size_t n)
 {
+  unsigned reg_char c = c_in;
+
+  /* Examine the memory an aligned longword at a time, when possible.
+     Doing so can go past the end of a lesser-aligned object,
+     which is harmless on most platforms but violates the rules of C,
+     so suppress this optimization on platforms where it is known to be
+     dangerous, namely, those using address sanitization.  */
+
+#if ! (defined __SANITIZE_ADDRESS__ || defined __CHERI_PURE_CAPABILITY__)
+
   /* On 32-bit hardware, choosing longword to be a 32-bit unsigned
      long instead of a 64-bit uintmax_t tends to give better
      performance.  On 64-bit hardware, unsigned long is generally 64
@@ -65,8 +75,6 @@ __memchr (void const *s, int c_in, size_t n)
      performance.  */
   typedef unsigned long int longword _GL_ATTRIBUTE_MAY_ALIAS;
 
-  unsigned reg_char c = (unsigned char) c_in;
-
   const longword *longword_ptr;
 
   /* Handle the first few bytes by reading one byte at a time.
@@ -144,10 +152,14 @@ __memchr (void const *s, int c_in, size_t n)
         longword_ptr++;
         n -= sizeof (longword);
       }
+
+    s = longword_ptr;
   }
 
+#endif
+
   {
-    const unsigned char *char_ptr = (const unsigned char *) longword_ptr;
+    const unsigned char *char_ptr = s;
 
     /* At this point, we know that either n < sizeof (longword), or one of the
        sizeof (longword) bytes starting at char_ptr is == c.  On little-endian
diff --git a/lib/memchr2.c b/lib/memchr2.c
index 610f9165a0..ff2bd9c0ad 100644
--- a/lib/memchr2.c
+++ b/lib/memchr2.c
@@ -35,6 +35,17 @@
 void *
 memchr2 (void const *s, int c1_in, int c2_in, size_t n)
 {
+  unsigned char c1 = c1_in;
+  unsigned char c2 = c2_in;
+
+  /* Examine the memory an aligned longword at a time, when possible.
+     Doing so can go past the end of a lesser-aligned object,
+     which is harmless on most platforms but violates the rules of C,
+     so suppress this optimization on platforms where it is known to be
+     dangerous, namely, those using address sanitization.  */
+
+#if ! (defined __SANITIZE_ADDRESS__ || defined __CHERI_PURE_CAPABILITY__)
+
   /* On 32-bit hardware, choosing longword to be a 32-bit unsigned
      long instead of a 64-bit uintmax_t tends to give better
      performance.  On 64-bit hardware, unsigned long is generally 64
@@ -42,9 +53,6 @@ memchr2 (void const *s, int c1_in, int c2_in, size_t n)
      performance.  */
   typedef unsigned long int longword _GL_ATTRIBUTE_MAY_ALIAS;
 
-  unsigned char c1 = (unsigned char) c1_in;
-  unsigned char c2 = (unsigned char) c2_in;
-
   if (c1 == c2)
     return (void *) memchr (s, c1, n);
 
@@ -137,10 +145,6 @@ memchr2 (void const *s, int c1_in, int c2_in, size_t n)
         longword_ptr++;
         n -= sizeof (longword);
       }
-  }
-
-  {
-    const unsigned char *char_ptr = (const unsigned char *) longword_ptr;
 
     /* At this point, we know that either n < sizeof (longword), or one of the
        sizeof (longword) bytes starting at char_ptr is == c1 or == c2.  On
@@ -149,6 +153,14 @@ memchr2 (void const *s, int c1_in, int c2_in, size_t n)
        from the last loop iteration.  But this does not work on big-endian
        machines.  Choose code that works in both cases.  */
 
+    s = longword_ptr;
+  }
+
+#endif
+
+  {
+    unsigned char const *char_ptr = s;
+
     for (; n > 0; --n, ++char_ptr)
       {
         if (*char_ptr == c1 || *char_ptr == c2)
diff --git a/lib/rawmemchr.c b/lib/rawmemchr.c
index 4eaada4774..e3cb63bd21 100644
--- a/lib/rawmemchr.c
+++ b/lib/rawmemchr.c
@@ -41,13 +41,21 @@ rawmemchr (const void *s, int c_in)
                            - __builtin_cheri_offset_get (s)));
 # else
 
+  unsigned char c = c_in;
+
+  /* Examine the memory an aligned longword at a time, when possible.
+     Doing so can go past the end of a lesser-aligned object,
+     which is harmless on most platforms but violates the rules of C,
+     so suppress this optimization on platforms where it is known to be
+     dangerous, namely, those using address sanitization.  */
+
+#  ifndef __SANITIZE_ADDRESS__
+
   /* You can change this typedef to experiment with performance.  */
   typedef uintptr_t longword _GL_ATTRIBUTE_MAY_ALIAS;
   /* Verify that the longword type lacks padding bits.  */
   static_assert (UINTPTR_WIDTH == UCHAR_WIDTH * sizeof (uintptr_t));
 
-  unsigned char c = c_in;
-
   {
     const unsigned char *char_ptr;
 
@@ -123,12 +131,6 @@ rawmemchr (const void *s, int c_in)
         longword_ptr++;
       }
 
-    s = longword_ptr;
-  }
-
-  {
-    const unsigned char *char_ptr = s;
-
     /* At this point, we know that one of the sizeof (longword) bytes
        starting at char_ptr is == c.  If we knew endianness, we
        could determine the first such byte without any further memory
@@ -136,6 +138,12 @@ rawmemchr (const void *s, int c_in)
        iteration.  However, the following simple and portable code does
        not attempt this potential optimization.  */
 
+    s = longword_ptr;
+  }
+#  endif
+
+  {
+    const unsigned char *char_ptr = s;
     while (*char_ptr != c)
       char_ptr++;
     return (void *) char_ptr;
diff --git a/lib/strchrnul.c b/lib/strchrnul.c
index 2e813bf580..74dd7e2aaf 100644
--- a/lib/strchrnul.c
+++ b/lib/strchrnul.c
@@ -23,6 +23,18 @@
 char *
 strchrnul (const char *s, int c_in)
 {
+  unsigned char c = c_in;
+  if (!c)
+    return rawmemchr (s, 0);
+
+  /* Examine the memory an aligned longword at a time, when possible.
+     Doing so can go past the end of a lesser-aligned object,
+     which is harmless on most platforms but violates the rules of C,
+     so suppress this optimization on platforms where it is known to be
+     dangerous, namely, those using address sanitization.  */
+
+#if ! (defined __SANITIZE_ADDRESS__ || defined __CHERI_PURE_CAPABILITY__)
+
   /* On 32-bit hardware, choosing longword to be a 32-bit unsigned
      long instead of a 64-bit uintmax_t tends to give better
      performance.  On 64-bit hardware, unsigned long is generally 64
@@ -30,10 +42,6 @@ strchrnul (const char *s, int c_in)
      performance.  */
   typedef unsigned long int longword _GL_ATTRIBUTE_MAY_ALIAS;
 
-  unsigned char c = (unsigned char) c_in;
-  if (!c)
-    return rawmemchr (s, 0);
-
   const longword *longword_ptr;
 
   {
@@ -121,10 +129,6 @@ strchrnul (const char *s, int c_in)
           break;
         longword_ptr++;
       }
-  }
-
-  {
-    const unsigned char *char_ptr = (const unsigned char *) longword_ptr;
 
     /* At this point, we know that one of the sizeof (longword) bytes
        starting at char_ptr is == 0 or == c.  On little-endian machines,
@@ -133,6 +137,13 @@ strchrnul (const char *s, int c_in)
        iteration.  But this does not work on big-endian machines.
        Choose code that works in both cases.  */
 
+    s = (char const *) longword_ptr;
+  }
+
+#endif
+
+  {
+    unsigned char const *char_ptr = (unsigned char const *) s;
     while (*char_ptr && (*char_ptr != c))
       char_ptr++;
     return (char *) char_ptr;
-- 
2.53.0

Reply via email to