ソースを参照

gcc: add support for libsanitizer with uClibc-ng from Ramin

Waldemar Brodkorb 3 週間 前
コミット
f1449815f0

+ 8 - 0
target/config/Config.in.toolchain

@@ -59,6 +59,14 @@ endmenu
 
 menu "Advanced Toolchain options"
 
+config ADK_TOOLCHAIN_WITH_SANITIZER
+	bool "Enable libsanitizer in GCC"
+	select ADK_TOOLCHAIN_WITH_CXX
+	depends on ADK_TOOLCHAIN_GCC_15
+	help
+	  You have to add TARGET_CFLAGS/TARGET_LDFLAGS/TARGET_CXXFLAGS on
+	  your own to the packages you want to check.
+
 config ADK_TOOLCHAIN_WITH_SSP
 	bool
 

+ 6 - 1
toolchain/gcc/Makefile

@@ -52,7 +52,6 @@ GCC_CONFOPTS:=		--prefix=$(TOOLCHAIN_DIR)/usr \
 			--with-system-zlib \
 			--with-gnu-ld \
 			--with-gnu-as \
-			--disable-libsanitizer \
 			--disable-install-libiberty \
 			--disable-libitm \
 			--disable-libmudflap \
@@ -120,6 +119,12 @@ else
 GCC_CONFOPTS+=		--disable-lto
 endif
 
+ifeq ($(ADK_TOOLCHAIN_WITH_SANITIZER),y)
+GCC_CONFOPTS+=		--enable-libsanitizer
+else
+GCC_CONFOPTS+=		--disable-libsanitizer
+endif
+
 #
 # architecture specific
 #

+ 50 - 0
toolchain/gcc/patches/15.2.0/0001-libsanitizer-do-not-treat-uClibc-ng-as-glibc.patch

@@ -0,0 +1,50 @@
+libsanitizer: do not treat uClibc-ng as glibc; introduce SANITIZER_UCLIBC
+
+uClibc-ng defines __GLIBC__ to 2 in features.h for source-level
+compatibility, but it is not glibc.  Until now SANITIZER_GLIBC
+would evaluate to 1 on uClibc-ng, causing the glibc-only path of
+libsanitizer to pull in headers like fstab.h, obstack.h,
+netrom/netrom.h, rpc/xdr.h, scsi/scsi.h, and to reference
+glibc-internal symbols (struct __res_state layout, __libc_stack_end,
+NPTL internals, dlinfo flags, etc.) that uClibc-ng does not provide.
+
+uClibc-ng advertises itself honestly via __UCLIBC__ (features.h:200).
+Add a !defined(__UCLIBC__) guard so libsanitizer falls through to
+the non-glibc Linux path, which uses kernel headers directly.
+
+Also introduce SANITIZER_UCLIBC (1 on uClibc-ng, 0 elsewhere) so
+subsequent patches can use a consistent SANITIZER_* macro instead of
+sprinkling defined(__UCLIBC__) checks across the codebase.
+
+This is the same approach used by other distributions to ship
+libsanitizer on uClibc / musl based toolchains.
+
+Signed-off-by: Ramin Moussavi <ramin.moussavi@yacoub.de>
+---
+--- a/libsanitizer/sanitizer_common/sanitizer_platform.h
++++ b/libsanitizer/sanitizer_common/sanitizer_platform.h
+@@ -31,12 +31,23 @@
+ #  define SANITIZER_LINUX 0
+ #endif
+
+-#if defined(__GLIBC__)
++/* uClibc-ng defines __GLIBC__ for glibc compatibility but is not glibc;
++   it advertises itself honestly with __UCLIBC__.  Without this guard
++   libsanitizer pulls glibc-only headers (fstab.h, obstack.h, netrom/...)
++   and tries to read glibc-internal symbols, which uClibc-ng does not
++   provide.  */
++#if defined(__GLIBC__) && !defined(__UCLIBC__)
+ #  define SANITIZER_GLIBC 1
+ #else
+ #  define SANITIZER_GLIBC 0
+ #endif
+
++#if defined(__UCLIBC__)
++#  define SANITIZER_UCLIBC 1
++#else
++#  define SANITIZER_UCLIBC 0
++#endif
++
+ #if defined(__FreeBSD__)
+ #  define SANITIZER_FREEBSD 1
+ #else

+ 43 - 0
toolchain/gcc/patches/15.2.0/0002-libsanitizer-uclibc-sigaction-layout.patch

@@ -0,0 +1,43 @@
+libsanitizer: add SANITIZER_UCLIBC branch for struct sigaction layout
+
+uClibc-ng's struct sigaction has fields in the kernel order
+(sa_handler, sa_flags, sa_restorer, sa_mask) so that the sigaction()
+syscall wrapper can pass the struct to rt_sigaction without
+translation -- see uClibc-ng's bits/sigaction.h.
+
+libsanitizer's __sanitizer_sigaction assumed glibc's userspace order
+(sa_handler, sa_mask, sa_flags, sa_restorer) which does not match,
+causing CHECK_STRUCT_SIZE_AND_OFFSET(sigaction, sa_mask/sa_flags/
+sa_restorer) to fire with comparisons reducing to '(4 == 12)',
+'(132 == 4)' and '(136 == 8)'.
+
+Add a dedicated SANITIZER_UCLIBC branch that mirrors the kernel layout.
+This is consistent with the existing per-libc/per-arch variants
+(Android 32-bit, MIPS, FreeBSD, s390x, sparc) already in the same
+struct.  No effect on glibc or musl builds.
+
+Signed-off-by: Ramin Moussavi <ramin.moussavi@yacoub.de>
+---
+--- a/libsanitizer/sanitizer_common/sanitizer_platform_limits_posix.h
++++ b/libsanitizer/sanitizer_common/sanitizer_platform_limits_posix.h
+@@ -642,6 +642,20 @@
+   uptr sa_flags;
+   void (*sa_restorer)();
+ };
++#elif SANITIZER_UCLIBC
++// uClibc-ng's struct sigaction mirrors the Linux kernel's layout
++// (sa_flags / sa_restorer before sa_mask) so that the sigaction()
++// syscall wrapper passes the struct to the kernel without translation.
++// See uClibc-ng's libc/sysdeps/linux/common/bits/sigaction.h.
++struct __sanitizer_sigaction {
++  union {
++    __sanitizer_sigactionhandler_ptr sigaction;
++    __sanitizer_sighandler_ptr handler;
++  };
++  unsigned long sa_flags;
++  void (*sa_restorer)(void);
++  __sanitizer_sigset_t sa_mask;
++};
+ #else // !SANITIZER_ANDROID
+ struct __sanitizer_sigaction {
+ #if defined(__mips__) && !SANITIZER_FREEBSD

+ 45 - 0
toolchain/gcc/patches/15.2.0/0003-libsanitizer-uclibc-sigset-size.patch

@@ -0,0 +1,45 @@
+libsanitizer: __sanitizer_sigset_t for uClibc-ng matches kernel size
+
+libsanitizer assumes Linux sigset_t is glibc-sized (128 bytes / 1024
+bits).  uClibc-ng deliberately keeps sigset_t the same size as the
+kernel sigset (8 bytes / 64 bits, except 16 bytes / 128 bits on MIPS)
+so its syscall wrappers can pass userspace sigset directly without
+translation.  See libc/sysdeps/linux/common/bits/sigset.h.
+
+Without an override here, CHECK_TYPE_SIZE(sigset_t) and
+CHECK_STRUCT_SIZE_AND_OFFSET(sigaction, sa_mask) static asserts
+would fail.  An earlier attempt bumped uClibc-ng's _SIGSET_NWORDS to
+the glibc value, which made the asserts pass at compile time but
+caused all sigaction()/sigprocmask() syscalls to return EINVAL at
+runtime, because the kernel rejects an over-sized sigsetsize argument
+("runsvdir: Failed to ignore SIGCHLD: Invalid argument").
+
+Right thing: uClibc-ng's design is correct, libsanitizer just has to
+learn its actual layout.  Add a SANITIZER_UCLIBC branch above the
+SANITIZER_LINUX fallback that produces the smaller sigset_t (16 bytes
+on MIPS, 8 bytes elsewhere).
+
+Signed-off-by: Ramin Moussavi <ramin.moussavi@yacoub.de>
+---
+--- a/libsanitizer/sanitizer_common/sanitizer_platform_limits_posix.h
++++ b/libsanitizer/sanitizer_common/sanitizer_platform_limits_posix.h
+@@ -570,6 +570,19 @@
+ # endif
+ #elif SANITIZER_APPLE
+ typedef unsigned __sanitizer_sigset_t;
++#elif SANITIZER_UCLIBC
++// uClibc-ng deliberately keeps sigset_t the same size as the kernel's
++// sigset_t to avoid translation in syscall wrappers (see uClibc-ng
++// libc/sysdeps/linux/common/bits/sigset.h).  Mirror that size here so
++// CHECK_TYPE_SIZE(sigset_t) and the sigaction sa_mask offset checks
++// agree with the actual uClibc-ng layout.
++struct __sanitizer_sigset_t {
++# if defined(__mips__)
++  unsigned long val[128 / (sizeof(unsigned long) * 8)];
++# else
++  unsigned long val[64 / (sizeof(unsigned long) * 8)];
++# endif
++};
+ #elif SANITIZER_LINUX
+ struct __sanitizer_sigset_t {
+   // The size is determined by looking at sizeof of real sigset_t on linux.

+ 39 - 0
toolchain/gcc/patches/15.2.0/0004-libsanitizer-uclibc-sock-fprog-sz.patch

@@ -0,0 +1,39 @@
+libsanitizer: define struct_sock_fprog_sz for SANITIZER_UCLIBC
+
+struct_sock_fprog_sz is declared unconditionally for SANITIZER_LINUX in
+sanitizer_platform_limits_posix.h, but its definition in the .cpp is
+guarded by #if SANITIZER_GLIBC.  With uClibc-ng (SANITIZER_GLIBC=0,
+SANITIZER_UCLIBC=1) the symbol is left undefined and linking any
+-fsanitize=address binary fails:
+
+  libasan.so: undefined reference to `__sanitizer::struct_sock_fprog_sz'
+
+Fix:
+  1. Include <linux/filter.h> in the non-glibc Linux branch so that
+     struct sock_fprog is available regardless of SANITIZER_GLIBC.
+  2. Provide the definition of struct_sock_fprog_sz guarded by
+     SANITIZER_UCLIBC to match the unconditional header declaration.
+
+Signed-off-by: Ramin Moussavi <ramin.moussavi@yacoub.de>
+---
+--- a/libsanitizer/sanitizer_common/sanitizer_platform_limits_posix.cpp
++++ b/libsanitizer/sanitizer_common/sanitizer_platform_limits_posix.cpp
+@@ -129,6 +129,7 @@
+ #      endif
+ #      include <scsi/scsi.h>
+ #else
++#include <linux/filter.h>
+ #include <linux/if_ppp.h>
+ #include <linux/kd.h>
+ #include <linux/ppp_defs.h>
+@@ -537,6 +538,10 @@
+   unsigned struct_sock_fprog_sz = sizeof(struct sock_fprog);
+ #  endif  // SANITIZER_GLIBC
+
++#if SANITIZER_UCLIBC
++  unsigned struct_sock_fprog_sz = sizeof(struct sock_fprog);
++#endif
++
+ #  if !SANITIZER_ANDROID && !SANITIZER_APPLE
+   unsigned struct_sioc_sg_req_sz = sizeof(struct sioc_sg_req);
+   unsigned struct_sioc_vif_req_sz = sizeof(struct sioc_vif_req);

+ 41 - 0
toolchain/gcc/patches/15.2.0/0005-libsanitizer-use-uclibc-getauxval.patch

@@ -0,0 +1,41 @@
+libsanitizer: enable getauxval() on uClibc-ng
+
+uClibc-ng has provided getauxval() since 1.0.41 (commit d869bb1, 2022-12)
+but cannot satisfy __GLIBC_PREREQ(2, 16) because it deliberately reports
+__GLIBC__=2 / __GLIBC_MINOR__=2 for source compatibility.  As a result
+SANITIZER_USE_GETAUXVAL evaluates to 0 on uClibc-ng and GetPageSize()
+falls back to sysconf(_SC_PAGESIZE).
+
+That sysconf() call is not safe to make from libsanitizer's .init_array
+constructor (runs before libc is fully initialized) and can return 0,
+which makes ReadFileToBuffer compute kMinFileLen = 0 and call
+MmapOrDie(0) -- the kernel rejects mmap(NULL, 0, ...) with EINVAL:
+
+  ==NN==ERROR: AddressSanitizer failed to allocate 0x0 (0) bytes of
+               ReadFileToBuffer (error code: 22)
+  ERROR: Failed to mmap
+
+Add SANITIZER_UCLIBC to the SANITIZER_USE_GETAUXVAL activation predicate
+so libsanitizer uses uClibc-ng's getauxval() directly.  This also gives
+correct AT_EXECFN (symbolizer) and AT_BASE (LSan) values via the
+auxiliary vector instead of the unsafe sysconf()/proc-based fallbacks.
+
+SANITIZER_UCLIBC is provided by the earlier
+0001-libsanitizer-do-not-treat-uClibc-ng-as-glibc.patch and
+sanitizer_getauxval.h already includes sanitizer_platform.h, so the
+macro is in scope here.
+
+Signed-off-by: Ramin Moussavi <ramin.moussavi@yacoub.de>
+---
+--- a/libsanitizer/sanitizer_common/sanitizer_getauxval.h
++++ b/libsanitizer/sanitizer_common/sanitizer_getauxval.h
+@@ -21,7 +21,8 @@
+ #if SANITIZER_LINUX || SANITIZER_FUCHSIA
+ 
+ # if (__GLIBC_PREREQ(2, 16) || (SANITIZER_ANDROID && __ANDROID_API__ >= 21) || \
+-      SANITIZER_FUCHSIA) &&                                                    \
++      SANITIZER_FUCHSIA ||                                                     \
++      SANITIZER_UCLIBC) &&                                                     \
+      !SANITIZER_GO
+ #  define SANITIZER_USE_GETAUXVAL 1
+ # else

+ 52 - 0
toolchain/gcc/patches/15.2.0/0006-libsanitizer-enable-swapcontext-interceptor-on-uClibc.patch

@@ -0,0 +1,52 @@
+libsanitizer: enable swapcontext/makecontext interceptor on uClibc-ng
+
+The swapcontext/makecontext ASan interceptor is gated on
+ASAN_INTERCEPT_SWAPCONTEXT, which until now was only enabled for
+SANITIZER_GLIBC || SANITIZER_SOLARIS.  Since 0001-libsanitizer-do-not-
+treat-uClibc-ng-as-glibc.patch forces SANITIZER_GLIBC=0 on uClibc-ng,
+the interceptor is silently dropped on our toolchain.  Two consequences:
+
+  1. asan_interceptors.cpp:399 (INTERCEPTOR(swapcontext, ...)) is not
+     compiled, so the runtime never emits the documented
+       "WARNING: ASan doesn't fully support makecontext/swapcontext
+        functions and may produce false positives in some cases!"
+     and the GCC testsuite case c-c++-common/asan/swapcontext-test-1.c
+     hits "output pattern test FAIL" for the first dg-output regex.
+
+  2. ClearShadowMemoryForContextStack() is never called around a
+     ucontext switch, so stack bytes that were poisoned by ASan in
+     one context stay poisoned after we resume on another stack.
+     This is a real runtime bug, not just a missing print:
+     coroutine code that touches a previously-poisoned region will
+     trip false-positive use-after-scope / stack-use-after-return
+     reports.
+
+The gated code in asan_interceptors.cpp:339-428 and asan_linux.cpp:
+201-226 only uses the POSIX surface (stack_t::ss_sp, ss_size, ss_flags,
+swapcontext(), makecontext()) plus the libsanitizer-internal INTERCEPTOR
+machinery, none of which is glibc-specific.  uClibc-ng provides all of
+these (ss_flags has existed in uClibc since the initial ucontext port,
+swapcontext()/makecontext() are now correct after the 0004-arm-ucontext-
+fix-vfp-save-restore-area.patch in patches/uClibc-ng/), so it is safe
+to flip the gate on for SANITIZER_UCLIBC as well.
+
+This is the natural follow-up to 0001 once swapcontext on uClibc-ng
+actually works.
+
+Signed-off-by: Ramin Moussavi <ramin.moussavi@yacoub.de>
+---
+--- a/libsanitizer/asan/asan_interceptors.h
++++ b/libsanitizer/asan/asan_interceptors.h
+@@ -50,7 +50,11 @@
+ # define ASAN_USE_ALIAS_ATTRIBUTE_FOR_INDEX 0
+ #endif
+ 
+-#if SANITIZER_GLIBC || SANITIZER_SOLARIS
++/* uClibc-ng provides swapcontext()/makecontext() and the standard
++   stack_t layout, and our 0004-arm-ucontext-fix-vfp-save-restore-area
++   patch in patches/uClibc-ng/ makes context switching actually work,
++   so enable the swapcontext interceptor on uClibc as well.  */
++#if SANITIZER_GLIBC || SANITIZER_SOLARIS || SANITIZER_UCLIBC
+ # define ASAN_INTERCEPT_SWAPCONTEXT 1
+ #else
+ # define ASAN_INTERCEPT_SWAPCONTEXT 0

+ 91 - 0
toolchain/gcc/patches/15.2.0/0007-testsuite-asan-skip-valloc-pvalloc-on-uClibc.patch

@@ -0,0 +1,91 @@
+testsuite asan: skip Valloc/PvallocTest on uClibc-ng
+
+g++.dg/asan/asan_test.C -O2 fails on uClibc-based targets with:
+
+    error: 'valloc' was not declared in this scope
+    error: 'pvalloc' was not declared in this scope
+
+uClibc-ng intentionally does not export valloc()/pvalloc(): neither
+the symbols nor the <malloc.h>/<stdlib.h> declarations.  Both functions
+are legacy:
+
+  - valloc():   POSIX.1-2001 marked OBSOLESCENT, POSIX.1-2008 removed
+                it entirely.  Linux manpage says "deprecated".
+  - pvalloc():  never POSIX, glibc-only extension.
+
+musl follows the same policy, which is why upstream LLVM compiler-rt
+already addressed PVALLOC in commit 0b56e3c (Dec 2020,
+"[sanitizer] Defined SANITIZER_TEST_HAS_PVALLOC only on glibc").  That
+patch was never picked up by GCC's libsanitizer mirror, so the GCC
+testsuite copy of sanitizer_test_utils.h still uses the old
+"!APPLE && !FreeBSD && !_WIN32" exclusion list.  We follow the LLVM
+patch's spirit but with two adjustments:
+
+  1. uClibc-ng defines __GLIBC__ for source-level compatibility (see
+     also our 0001-libsanitizer-do-not-treat-uClibc-ng-as-glibc.patch),
+     so a plain "#if defined(__GLIBC__)" is not enough; we use
+     "defined(__GLIBC__) && !defined(__UCLIBC__)", matching the
+     discriminator already in sanitizer_platform.h after patch 0001.
+
+  2. Upstream LLVM still leaves the VallocTest gated only on
+     "!defined(_WIN32)", which also breaks on musl/uClibc.  We
+     introduce SANITIZER_TEST_HAS_VALLOC analogous to the existing
+     SANITIZER_TEST_HAS_PVALLOC and switch the VallocTest body to it.
+
+libsanitizer itself is consistent with this gating:
+ASAN_INTERCEPT_PVALLOC is defined as (SI_GLIBC || SI_ANDROID) in
+sanitizer_platform_interceptors.h, both 0 on our build, so there is no
+interceptor to test on uClibc anyway.
+
+Memalign / posix_memalign / malloc_usable_size remain enabled --
+uClibc-ng exports those (verified in libuClibc-1.0.54.so).
+
+Signed-off-by: Ramin Moussavi <ramin.moussavi@yacoub.de>
+Cf. https://github.com/llvm/llvm-project/commit/0b56e3cdda501b19bbfee6ee3899f72d9bce121c
+---
+--- a/gcc/testsuite/g++.dg/asan/sanitizer_test_utils.h	2025-08-08 06:51:41.558367288 +0000
++++ b/gcc/testsuite/g++.dg/asan/sanitizer_test_utils.h	2026-05-11 18:28:20.144978738 +0000
+@@ -101,14 +101,30 @@
+ 
+ #if !defined(__APPLE__) && !defined(__FreeBSD__) && !defined(_WIN32)
+ # define SANITIZER_TEST_HAS_MEMALIGN 1
+-# define SANITIZER_TEST_HAS_PVALLOC 1
+ # define SANITIZER_TEST_HAS_MALLOC_USABLE_SIZE 1
+ #else
+ # define SANITIZER_TEST_HAS_MEMALIGN 0
+-# define SANITIZER_TEST_HAS_PVALLOC 0
+ # define SANITIZER_TEST_HAS_MALLOC_USABLE_SIZE 0
+ #endif
+ 
++/*
++ * valloc()/pvalloc() are legacy glibc-isms not provided by musl or
++ * uClibc-ng.  Follow upstream LLVM compiler-rt commit 0b56e3c (2020)
++ * which gates PVALLOC on __GLIBC__, but use the
++ * "__GLIBC__ && !__UCLIBC__" discriminator from our libsanitizer
++ * patch 0001 because uClibc-ng falsely advertises __GLIBC__ for
++ * source-level compatibility.  Also introduce SANITIZER_TEST_HAS_VALLOC
++ * (not present upstream) so the VallocTest body in asan_test.cc can be
++ * compiled out cleanly.
++ */
++#if defined(__GLIBC__) && !defined(__UCLIBC__)
++# define SANITIZER_TEST_HAS_PVALLOC 1
++# define SANITIZER_TEST_HAS_VALLOC 1
++#else
++# define SANITIZER_TEST_HAS_PVALLOC 0
++# define SANITIZER_TEST_HAS_VALLOC 0
++#endif
++
+ #if !defined(__APPLE__)
+ # define SANITIZER_TEST_HAS_STRNLEN 1
+ #else
+--- a/gcc/testsuite/g++.dg/asan/asan_test.cc	2025-08-08 06:51:41.558367288 +0000
++++ b/gcc/testsuite/g++.dg/asan/asan_test.cc	2026-05-11 18:28:25.382228300 +0000
+@@ -114,7 +114,7 @@
+   }
+ }
+ 
+-#if !defined(_WIN32)  // No valloc on Windows.
++#if SANITIZER_TEST_HAS_VALLOC
+ TEST(AddressSanitizer, VallocTest) {
+   void *a = valloc(100);
+   EXPECT_EQ(0U, (uintptr_t)a % kPageSize);

+ 102 - 0
toolchain/gcc/patches/15.2.0/0008-libsanitizer-uclibc-off_t-abi-mismatch.patch

@@ -0,0 +1,102 @@
+libsanitizer: fix OFF_T ABI mismatch on uClibc-ng
+
+On 32-bit ARM with uClibc-ng, AddressSanitizer's mmap interceptor was
+silently corrupting the offset argument, making every internal mmap()
+call from libuClibc.so fail with EINVAL.  The most visible casualty was
+pthread_create(): allocate_stack() in libpthread/nptl/allocatestack.c
+mmap()s the new thread stack, the syscall returned EINVAL, and the
+caller saw "Invalid argument" before the thread ever started.
+
+Two GCC testsuite tests were observed to fail because of this:
+
+  - g++.dg/asan/deep-thread-stack-1.C  (any subsequent thread create)
+  - c-c++-common/asan/pr98920.c
+
+but in practice _every_ ASan-instrumented program that calls
+pthread_create (including indirectly via std::thread, OpenMP, libc
+routines, ...) was broken on uClibc-ng/ARM.
+
+Root cause
+==========
+
+libsanitizer/sanitizer_common/sanitizer_internal_defs.h selects the
+type used for off_t in interceptor signatures:
+
+    #if SANITIZER_FREEBSD || SANITIZER_NETBSD || SANITIZER_APPLE ||
+        (SANITIZER_SOLARIS && (defined(_LP64) || _FILE_OFFSET_BITS == 64)) ||
+        (SANITIZER_LINUX && !SANITIZER_GLIBC && !SANITIZER_ANDROID) ||  <-- HERE
+        (SANITIZER_LINUX && (defined(__x86_64__) || defined(__hexagon__)))
+    typedef u64 OFF_T;
+    #else
+    typedef uptr OFF_T;
+    #endif
+
+The "Linux && !glibc && !Android" branch is meant for musl, which
+indeed uses 64-bit off_t on all architectures.  But after our patch
+0001-libsanitizer-do-not-treat-uClibc-ng-as-glibc.patch SANITIZER_GLIBC
+is also 0 on uClibc-ng builds, so uClibc-ng falls through this branch
+too, and OFF_T is set to u64.
+
+uClibc-ng on 32-bit ARM, however, keeps __off_t = long = 32 bit.
+libuClibc.so calls its own mmap() wrapper with a 32-bit offset.
+Once ASan inserts itself between the caller and the wrapper, its
+interceptor reads "OFF_T offset" off the stack/registers as 64 bit,
+picks up garbage in the upper word, and forwards that to the mmap2
+syscall.  The kernel's mmap2 expects (offset >> PAGE_SHIFT) to fit in
+unsigned long, so any non-zero upper word -> EINVAL.
+
+This was also confirmed empirically: a syscall(192, ...) issued from
+the same call site succeeded under ASan, while the libc wrapper
+failed.  Bypassing the interceptor entirely fixed the symptom.
+
+Verification
+============
+
+Direct trigger:
+
+  asan: pthread_create(stack=16M) -> r=22 errno=22 (Invalid argument)
+              ^^^ before patch (silent thread-create failure)
+
+  asan: pthread_create(stack=16M) -> r=0 errno=0 (Success)
+              ^^^ after patch
+
+deep-thread-stack-1.C now produces the expected
+"AddressSanitizer: heap-use-after-free" report; pr98920.c exits 0.
+
+The fix
+=======
+
+Exclude SANITIZER_UCLIBC from the "Linux && !glibc && !Android"
+fallthrough so OFF_T defaults to uptr on 32-bit uClibc-ng targets,
+matching the host ABI.  No effect on glibc/Android/musl/BSD/macOS.
+
+A static "sizeof(::OFF_T) == sizeof(off_t)" check exists in
+interception/interception_type_test.cpp and would have caught this if
+SANITIZER_UCLIBC had been distinguished from SANITIZER_GLIBC earlier;
+patch 0001 introduced that discriminator and this patch puts it to
+work where it actually matters at runtime.
+
+Other OFF_T users were audited
+(sanitizer_common_interceptors.inc: pread/pwrite/preadv/pwritev/
+ftello/fseeko/funopen/SHA1FileChunk/RMD160FileChunk/mmap and
+sanitizer_linux.cpp / sanitizer_posix.cpp: internal_lseek,
+MapWritableFileToMemory, internal_syscall(SYS_mmap, SYS_ftruncate)).
+All of them benefit from the same one-line change because they all
+share the same OFF_T typedef, and uClibc-ng uses a 32-bit __off_t for
+all of them on 32-bit ARM (the *64 variants take OFF64_T which is
+unaffected).
+
+Signed-off-by: Ramin Moussavi <ramin.moussavi@yacoub.de>
+---
+--- a/libsanitizer/sanitizer_common/sanitizer_internal_defs.h	2025-08-08 06:51:41.000000000 +0000
++++ b/libsanitizer/sanitizer_common/sanitizer_internal_defs.h	2026-05-16 00:30:00.000000000 +0000
+@@ -189,7 +189,8 @@
+ 
+ #if SANITIZER_FREEBSD || SANITIZER_NETBSD || SANITIZER_APPLE ||             \
+     (SANITIZER_SOLARIS && (defined(_LP64) || _FILE_OFFSET_BITS == 64)) || \
+-    (SANITIZER_LINUX && !SANITIZER_GLIBC && !SANITIZER_ANDROID) ||        \
++    (SANITIZER_LINUX && !SANITIZER_GLIBC && !SANITIZER_ANDROID &&         \
++     !SANITIZER_UCLIBC) ||                                                \
+     (SANITIZER_LINUX && (defined(__x86_64__) || defined(__hexagon__)))
+ typedef u64 OFF_T;
+ #else

+ 116 - 0
toolchain/gcc/patches/15.2.0/0009-builtins-iseqsig-fix-fe-invalid.patch

@@ -0,0 +1,116 @@
+From: Ramin Moussavi <ramin.moussavi@yacoub.de>
+Date: Sat May 16 16:00:00 2026 +0000
+Subject: [PATCH] builtins: fix __builtin_iseqsig dropping FE_INVALID under -O2
+
+`__builtin_iseqsig` (added in r14-... by commit
+34cf27a64e7af949538e65bc266963c24f8da458) silently fails to raise
+FE_INVALID on a quiet-NaN operand under -O2 on ARM and other targets
+where signaling and quiet IEEE 754 comparisons map to different machine
+instructions (ARM VCMPE vs VCMP, x86 UCOMISS vs COMISS).
+
+The tests added by that commit -- gcc.dg/torture/builtin-iseqsig-{1,2,3}.c --
+have been observed FAILing on:
+
+  * arm-linux-gnueabihf, GCC 14.2 native (Debian), -Os
+    https://lists.debian.org/debian-gcc/2024/08/msg00143.html
+  * arm-bbs-linux-uclibcgnueabihf, GCC 15.2 cross,
+    -O2 and the LTO torture iterations
+
+Two interacting issues:
+
+1. BUILT_IN_ISEQSIG was registered with ATTR_CONST_NOTHROW_TYPEGENERIC_LEAF.
+   That matches the other IS* predicates (isgreater, isless, ...), which
+   are *quiet* per IEEE 754.  But iseqsig is the *signaling* equality:
+   per IEEE 754 it must raise FE_INVALID on any NaN operand, both quiet
+   and signaling.  Marking it CONST tells the middle-end it has no side
+   effect.
+
+2. fold_builtin_iseqsig folds the call to
+   "(a >= b) && (a <= b)" using TRUTH_AND_EXPR (non-short-circuit).
+   That is gimplified to BIT_AND_EXPR, producing a linear "compute both
+   comparison flags then AND" sequence.  The RTL scheduler is then free
+   to reorder the signaling VCMPE compares around fenv reads -- e.g.
+   fetestexcept(FE_INVALID) -- silently dropping the FE_INVALID side
+   effect that the comparisons raise on NaN.
+
+   The same source-level expression "(a >= b) && (a <= b)" written by
+   the user works correctly: && is parsed as TRUTH_ANDIF_EXPR, which
+   gimplifies to a branched if-then-else where the first comparison
+   stays anchored as a branch condition and cannot move across other
+   side-effecting calls.
+
+Fix:
+
+  - Drop ATTR_CONST_NOTHROW_TYPEGENERIC_LEAF, use
+    ATTR_NOTHROW_TYPEGENERIC_LEAF (already defined in builtin-attrs.def,
+    previously unused).
+
+  - Fold using TRUTH_ANDIF_EXPR rather than TRUTH_AND_EXPR.
+
+After this patch, gcc.dg/torture/builtin-iseqsig-{1,2,3}.c PASS on
+arm-bbs-linux-uclibcgnueabihf (cortex-a5, neon-vfpv4, hard float,
+uClibc-ng) at every torture option:
+
+  -O0, -O1, -O2, -O3, -Os, -O2 -flto -fno-fat-lto-objects,
+  -O2 -flto -fuse-linker-plugin -fno-fat-lto-objects
+
+The same harness reproduces the FAIL on stock 15.2.0 sources and PASSes
+with this patch applied.
+
+Related: PR middle-end/34678, PR middle-end/38960.
+
+gcc/
+	* builtins.def (BUILT_IN_ISEQSIG): Drop ATTR_CONST; iseqsig has
+	an observable fenv side effect (FE_INVALID), unlike the other IS*
+	predicates which are quiet per IEEE 754.
+	* builtins.cc (fold_builtin_iseqsig): Fold using TRUTH_ANDIF_EXPR
+	rather than TRUTH_AND_EXPR.  The former gimplifies to a branched
+	if-then-else where the leading signaling comparison is anchored
+	as a branch condition; the latter gimplifies to BIT_AND_EXPR,
+	which the RTL scheduler may reorder around fenv reads, dropping
+	the FE_INVALID side effect.
+
+Signed-off-by: Ramin Moussavi <ramin.moussavi@yacoub.de>
+---
+ gcc/builtins.cc  | 12 +++++++++++-
+ gcc/builtins.def |  6 +++++-
+ 2 files changed, 16 insertions(+), 2 deletions(-)
+
+diff --git a/gcc/builtins.cc b/gcc/builtins.cc
+--- a/gcc/builtins.cc
++++ b/gcc/builtins.cc
+@@ -10098,7 +10098,17 @@ fold_builtin_iseqsig (location_t loc, tree arg0, tree arg1)
+   cmp1 = fold_build2_loc (loc, GE_EXPR, integer_type_node, arg0, arg1);
+   cmp2 = fold_build2_loc (loc, LE_EXPR, integer_type_node, arg0, arg1);
+
+-  return fold_build2_loc (loc, TRUTH_AND_EXPR, integer_type_node, cmp1, cmp2);
++  /* Use short-circuit && (TRUTH_ANDIF_EXPR) rather than & (TRUTH_AND_EXPR).
++     TRUTH_AND_EXPR is gimplified to BIT_AND_EXPR, producing a linear
++     "compute both flags then AND" sequence that the RTL scheduler may
++     reorder around fenv reads (fetestexcept) - silently dropping the
++     FE_INVALID side effect that the signaling GE/LE comparisons must
++     raise on NaN operands.  TRUTH_ANDIF_EXPR gimplifies to a branched
++     if-then-else where the first comparison stays anchored as a
++     branch condition, so the signaling VCMPE (ARM) / UCOMISS (x86)
++     cannot move across other side-effecting calls.  */
++  return fold_build2_loc (loc, TRUTH_ANDIF_EXPR, integer_type_node,
++			  cmp1, cmp2);
+ }
+
+ /* Fold __builtin_{,s,u}{add,sub,mul}{,l,ll}_overflow, either into normal
+diff --git a/gcc/builtins.def b/gcc/builtins.def
+--- a/gcc/builtins.def
++++ b/gcc/builtins.def
+@@ -1072,7 +1072,11 @@ DEF_GCC_BUILTIN        (BUILT_IN_ISLESS, "isless", BT_FN_INT_VAR, ATTR_CONST_NOT
+ DEF_GCC_BUILTIN        (BUILT_IN_ISLESSEQUAL, "islessequal", BT_FN_INT_VAR, ATTR_CONST_NOTHROW_TYPEGENERIC_LEAF)
+ DEF_GCC_BUILTIN        (BUILT_IN_ISLESSGREATER, "islessgreater", BT_FN_INT_VAR, ATTR_CONST_NOTHROW_TYPEGENERIC_LEAF)
+ DEF_GCC_BUILTIN        (BUILT_IN_ISUNORDERED, "isunordered", BT_FN_INT_VAR, ATTR_CONST_NOTHROW_TYPEGENERIC_LEAF)
+-DEF_GCC_BUILTIN        (BUILT_IN_ISEQSIG, "iseqsig", BT_FN_INT_VAR, ATTR_CONST_NOTHROW_TYPEGENERIC_LEAF)
++/* iseqsig is the SIGNALING variant of equality: per IEEE 754 it must
++   raise FE_INVALID on any NaN operand (both quiet and signaling NaNs).
++   Unlike the other IS* predicates (which are quiet) it has an observable
++   fenv side effect, so it must NOT carry the const attribute.  */
++DEF_GCC_BUILTIN        (BUILT_IN_ISEQSIG, "iseqsig", BT_FN_INT_VAR, ATTR_NOTHROW_TYPEGENERIC_LEAF)
+ DEF_GCC_BUILTIN        (BUILT_IN_ISSIGNALING, "issignaling", BT_FN_INT_VAR, ATTR_CONST_NOTHROW_TYPEGENERIC_LEAF)
+ DEF_LIB_BUILTIN        (BUILT_IN_LABS, "labs", BT_FN_LONG_LONG, ATTR_CONST_NOTHROW_LEAF_LIST)
+ DEF_C99_BUILTIN        (BUILT_IN_LLABS, "llabs", BT_FN_LONGLONG_LONGLONG, ATTR_CONST_NOTHROW_LEAF_LIST)