https://github.com/kenballus created 
https://github.com/llvm/llvm-project/pull/172086

# SignalSanitizer (`sigsan`)

## Summary

This PR adds a new sanitizer to detect async-signal-unsafe function calls made 
from signal handlers, to catch bugs like 
[RegreSSHion](https://www.qualys.com/2024/07/01/cve-2024-6387/regresshion.txt).

## Background

>From signal-safety(7):
> An async-signal-safe function is one that can be safely called from within a 
> signal handler.  Many functions are not async-signal-safe.  In particular, 
> nonreentrant functions are generally unsafe to call from a signal handler.
> ...
> To avoid problems with unsafe functions, there are two possible choices:
> (a)  Ensure that (1) the signal handler calls only async-signal-safe 
> functions, and (2) the signal handler itself is reentrant with respect to 
> global variables in the main program.
> (b)  Block signal delivery in the main program when calling functions that 
> are unsafe or operating on global data that is also accessed by the signal 
> handler.
> Generally, the second choice is difficult in programs of any complexity, so 
> the first choice is taken.

Consider the following C program:
```C
#include <signal.h> // for signal, SIGINT
#include <stdlib.h> // for malloc, free

void handler(int) { free(malloc(128)); }

#define ARRAY_LEN(a) (sizeof(a) / sizeof(*a))

int main(void) {
  signal(SIGINT, handler);
  while (1) {
    void *p[128];
    for (size_t i = 0; i < ARRAY_LEN(p); i++) {
      p[i] = malloc(128);
    }
    for (size_t i = 0; i < ARRAY_LEN(p); i++) {
      free(p[i]);
    }
  }
}
```
This program just `malloc`s and `free`s in a loop. When it receives `SIGINT`, 
it `malloc`s once and `free`s it immediately.

Because `malloc` and `free` are not async-signal-safe, `handler` can corrupt 
the heap. For a demo, try building the above program at `-O0` and send `SIGINT` 
in a loop; the program should crash with errors like this:
```
Fatal glibc error: malloc.c:2610 (sysmalloc): assertion failed: (old_top == 
initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && 
prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)
```
or this:
```
malloc(): mismatching next->prev_size (unsorted)
```
or this:
```
double free or corruption (!prev)
```
or this:
```
free(): double free detected in tcache 2
```

On my amd64 GNU/Linux system, it takes ~30 `SIGINT`s before one of them causes 
detectable heap corruption.

## Mechanism

To catch these bugs at runtime, a sanitizer might employ the following 
high-level strategy:
- Whenever someone tries to install a signal handler, instead install a wrapper 
that increments a thread-local counter, calls the provided handler, then 
decrements the counter.
- For each async-signal-unsafe function that tends to cause problems, intercept 
it with a wrapper that fails with a backtrace if the counter is nonzero, and 
calls the function otherwise.

This draft PR implements these ideas, and intercepts a few async-signal-unsafe 
libc functions as a PoC. It also ensures that signal handlers do not clobber 
`errno`, because doing so opens the door to `errno`-checking race conditions.

## Demo

To see this catch a real bug (RegreSSHion, which enabled RCE in i386 OpenSSH), 
do the following (on a computer you don't really care about):
```bash
# Download the OpenSSH 9.7p1 source code
wget 
https://github.com/openssh/openssh-portable/archive/refs/tags/V_9_7_P1.tar.gz
tar xf V_9_7_P1.tar.gz
cd openssh-portable-V_9_7_P1

# Build it
export CC=/the/path/to/this/build/of/clang CFLAGS='-O0 -g -fsanitize=signal' 
LDFLAGS='-fsanitize=signal'
autoreconf -fi
./configure --without-openssl
make -j$(nproc)

# Install it
sudo make install

# Run it on port 9999 with a 1 second LoginGracePeriod
sudo /usr/local/sbin/sshd -g 1 -eD -p 9999

# From another shell, talk to it
nc localhost 9999
```

A second after getting the OpenSSH banner, you should be kicked off and see a 
backtrace on the `sshd`'s `stderr`:
```
==2338676==ERROR: SignalSanitizer: async-signal-unsafe function vsnprintf 
called from a signal handler.
    #0 0x5d96f00828d5  (/usr/local/sbin/sshd+0x3e8d5)
    #1 0x5d96f00829a5  (/usr/local/sbin/sshd+0x3e9a5)
    #2 0x5d96f0083238  (/usr/local/sbin/sshd+0x3f238)
    #3 0x5d96f00f5eef  (/usr/local/sbin/sshd+0xb1eef)
    #4 0x5d96f00f5a33  (/usr/local/sbin/sshd+0xb1a33)
    #5 0x5d96f00f5c23  (/usr/local/sbin/sshd+0xb1c23)
    #6 0x5d96f00896b8  (/usr/local/sbin/sshd+0x456b8)
    #7 0x5d96f0082a5f  (/usr/local/sbin/sshd+0x3ea5f)
    #8 0x7b8f8fc3e4cf  (/usr/lib/libc.so.6+0x3e4cf) (BuildId: 
2f722da304c0a508c891285e6840199c35019c8d)
    #9 0x7b8f8fc9318d  (/usr/lib/libc.so.6+0x9318d) (BuildId: 
2f722da304c0a508c891285e6840199c35019c8d)
    #10 0x7b8f8fc931b3  (/usr/lib/libc.so.6+0x931b3) (BuildId: 
2f722da304c0a508c891285e6840199c35019c8d)
    #11 0x7b8f8fd0da2d  (/usr/lib/libc.so.6+0x10da2d) (BuildId: 
2f722da304c0a508c891285e6840199c35019c8d)
    #12 0x5d96f01070fc  (/usr/local/sbin/sshd+0xc30fc)
    #13 0x5d96f0107272  (/usr/local/sbin/sshd+0xc3272)
    #14 0x5d96f01248dd  (/usr/local/sbin/sshd+0xe08dd)
    #15 0x5d96f00871dc  (/usr/local/sbin/sshd+0x431dc)
    #16 0x7b8f8fc27634  (/usr/lib/libc.so.6+0x27634) (BuildId: 
2f722da304c0a508c891285e6840199c35019c8d)
    #17 0x7b8f8fc276e8  (/usr/lib/libc.so.6+0x276e8) (BuildId: 
2f722da304c0a508c891285e6840199c35019c8d)
    #18 0x5d96f005d114  (/usr/local/sbin/sshd+0x19114)
```

## Future Directions

I'd be happy to make large changes to this PR to help it better fit the LLVM 
style and structure. This whole thing might belong in UBSan instead of being 
its own separate part of compiler-rt.

It would be interesting to try to statically classify functions in IR as 
async-signal-safe or unsafe and insert the counter checks in a pass, but for 
now I went with a simple `INTERCEPTOR`-based scheme because it's easy and 
doesn't require recompiling the world :)

>From 92afc2af6c11b89425c038a2e722cbd496fb80d6 Mon Sep 17 00:00:00 2001
From: Ben Kallus <[email protected]>
Date: Thu, 11 Dec 2025 23:17:53 -0500
Subject: [PATCH 1/4] Add SigSan

---
 clang/include/clang/Basic/Sanitizers.def   |   2 +
 clang/include/clang/Driver/SanitizerArgs.h |   1 +
 clang/lib/Driver/ToolChains/CommonArgs.cpp |   7 +
 clang/lib/Driver/ToolChains/Linux.cpp      |   1 +
 compiler-rt/lib/CMakeLists.txt             |   1 +
 compiler-rt/lib/sigsan/CMakeLists.txt      |  18 ++
 compiler-rt/lib/sigsan/sigsan.cpp          | 200 +++++++++++++++++++++
 7 files changed, 230 insertions(+)
 create mode 100644 compiler-rt/lib/sigsan/CMakeLists.txt
 create mode 100644 compiler-rt/lib/sigsan/sigsan.cpp

diff --git a/clang/include/clang/Basic/Sanitizers.def 
b/clang/include/clang/Basic/Sanitizers.def
index da85431625026..9594e0166c780 100644
--- a/clang/include/clang/Basic/Sanitizers.def
+++ b/clang/include/clang/Basic/Sanitizers.def
@@ -88,6 +88,8 @@ SANITIZER("realtime", Realtime)
 // LeakSanitizer
 SANITIZER("leak", Leak)
 
+SANITIZER("signal", Signal)
+
 // UndefinedBehaviorSanitizer
 SANITIZER("alignment", Alignment)
 SANITIZER("array-bounds", ArrayBounds)
diff --git a/clang/include/clang/Driver/SanitizerArgs.h 
b/clang/include/clang/Driver/SanitizerArgs.h
index eea7897e96afd..7ae1f202eb41d 100644
--- a/clang/include/clang/Driver/SanitizerArgs.h
+++ b/clang/include/clang/Driver/SanitizerArgs.h
@@ -83,6 +83,7 @@ class SanitizerArgs {
   SanitizerArgs(const ToolChain &TC, const llvm::opt::ArgList &Args,
                 bool DiagnoseErrors = true);
 
+  bool needsSigsanRt() const { return Sanitizers.has(SanitizerKind::Signal); }
   bool needsSharedRt() const { return SharedRuntime; }
   bool needsStableAbi() const { return StableABI; }
 
diff --git a/clang/lib/Driver/ToolChains/CommonArgs.cpp 
b/clang/lib/Driver/ToolChains/CommonArgs.cpp
index ec8dcdc81db56..605a4a32aa276 100644
--- a/clang/lib/Driver/ToolChains/CommonArgs.cpp
+++ b/clang/lib/Driver/ToolChains/CommonArgs.cpp
@@ -1574,6 +1574,9 @@ collectSanitizerRuntimes(const ToolChain &TC, const 
ArgList &Args,
   const SanitizerArgs &SanArgs = TC.getSanitizerArgs(Args);
   // Collect shared runtimes.
   if (SanArgs.needsSharedRt()) {
+    if (SanArgs.needsSigsanRt()) {
+      SharedRuntimes.push_back("sigsan");
+    }
     if (SanArgs.needsAsanRt()) {
       SharedRuntimes.push_back("asan");
       if (!Args.hasArg(options::OPT_shared) && !TC.getTriple().isAndroid())
@@ -1628,6 +1631,10 @@ collectSanitizerRuntimes(const ToolChain &TC, const 
ArgList &Args,
   // Each static runtime that has a DSO counterpart above is excluded below,
   // but runtimes that exist only as static are not affected by needsSharedRt.
 
+  if (!SanArgs.needsSharedRt() && SanArgs.needsSigsanRt()) {
+    StaticRuntimes.push_back("sigsan");
+  }
+
   if (!SanArgs.needsSharedRt() && SanArgs.needsAsanRt()) {
     StaticRuntimes.push_back("asan");
     if (SanArgs.linkCXXRuntimes())
diff --git a/clang/lib/Driver/ToolChains/Linux.cpp 
b/clang/lib/Driver/ToolChains/Linux.cpp
index 94a9fe8b1a63f..745a005aed286 100644
--- a/clang/lib/Driver/ToolChains/Linux.cpp
+++ b/clang/lib/Driver/ToolChains/Linux.cpp
@@ -914,6 +914,7 @@ SanitizerMask Linux::getSupportedSanitizers() const {
   Res |= SanitizerKind::KernelAddress;
   Res |= SanitizerKind::Vptr;
   Res |= SanitizerKind::SafeStack;
+  Res |= SanitizerKind::Signal;
   if (IsX86_64 || IsMIPS64 || IsAArch64 || IsLoongArch64)
     Res |= SanitizerKind::DataFlow;
   if (IsX86_64 || IsMIPS64 || IsAArch64 || IsX86 || IsArmArch || IsPowerPC64 ||
diff --git a/compiler-rt/lib/CMakeLists.txt b/compiler-rt/lib/CMakeLists.txt
index e6158ec408895..e9960bb237fe1 100644
--- a/compiler-rt/lib/CMakeLists.txt
+++ b/compiler-rt/lib/CMakeLists.txt
@@ -42,6 +42,7 @@ if(COMPILER_RT_BUILD_SANITIZERS)
     add_subdirectory(lsan)
     # Contains RTUbsan used even without COMPILER_RT_HAS_UBSAN.
     add_subdirectory(ubsan)
+    add_subdirectory(sigsan)
   endif()
 
   foreach(sanitizer ${COMPILER_RT_SANITIZERS_TO_BUILD})
diff --git a/compiler-rt/lib/sigsan/CMakeLists.txt 
b/compiler-rt/lib/sigsan/CMakeLists.txt
new file mode 100644
index 0000000000000..1168e0fcc9973
--- /dev/null
+++ b/compiler-rt/lib/sigsan/CMakeLists.txt
@@ -0,0 +1,18 @@
+include_directories(..)
+
+# Runtime library sources and build flags.
+set(SIGSAN_SOURCES
+  sigsan.cpp
+)
+
+add_compiler_rt_component(sigsan)
+add_compiler_rt_runtime(clang_rt.sigsan
+    STATIC
+    ARCHS x86_64
+    SOURCES ${SIGSAN_SOURCES}
+    OBJECT_LIBS RTInterception
+                RTSanitizerCommon
+                RTSanitizerCommonLibc
+                RTSanitizerCommonSymbolizer
+    PARENT_TARGET sigsan
+)
diff --git a/compiler-rt/lib/sigsan/sigsan.cpp 
b/compiler-rt/lib/sigsan/sigsan.cpp
new file mode 100644
index 0000000000000..34099dcdbd405
--- /dev/null
+++ b/compiler-rt/lib/sigsan/sigsan.cpp
@@ -0,0 +1,200 @@
+#include "interception/interception.h" // for INTERCEPTOR, INTERCEPT_FUNCTION, 
REAL
+#include "sanitizer_common/sanitizer_stacktrace.h" // for 
GET_CURRENT_PC_BP_SP, BufferedStackTrace
+#include "sanitizer_common/sanitizer_report_decorator.h" // for 
SanitizerCommonDecorator
+#include "sanitizer_common/sanitizer_common.h" // for Printf, Die
+// TODO: figure out the include for Report
+
+#include <signal.h> // for siginfo_t, NSIG, struct sigaction, SIG_IGN, SIG_DFL
+#include <cstdint> // for uintptr_t
+#include <cstdarg> // for va_list, va_start, va_end
+#include <stdio.h> // for FILE
+
+using namespace __sanitizer;
+
+using signal_handler = void (*)(int);
+using extended_signal_handler = void (*)(int, siginfo_t *, void *);
+
+thread_local unsigned int __sigsan_signal_depth = 0;
+
+// TODO: handle sigvec and the other deprecated sighandler installation 
functions
+
+// TODO: Make these atomic so concurrent calls to signal for the same signum 
don't cause problems
+static uintptr_t __sigsan_handlers[NSIG];
+
+void __sigsan_handler(int signum) {
+  __sigsan_signal_depth++;
+  ((signal_handler)(__sigsan_handlers[signum]))(signum);
+  __sigsan_signal_depth--;
+}
+
+void __sigsan_extended_handler(int signum, siginfo_t *si, void *arg) {
+    __sigsan_signal_depth++;
+    ((extended_signal_handler)(__sigsan_handlers[signum]))(signum, si, arg);
+    __sigsan_signal_depth--;
+}
+
+INTERCEPTOR(signal_handler, signal, int signum, signal_handler handler) {
+  // Adapted from llvm-project/libc/src/signal/linux/signal.cpp
+  struct sigaction action, old;
+  action.sa_handler = handler;
+  action.sa_flags = SA_RESTART;
+  return sigaction(signum, &action, &old) < 0 ? SIG_ERR : old.sa_handler;
+}
+
+INTERCEPTOR(int, sigaction, int sig, struct sigaction const *__restrict act, 
struct sigaction *oldact) {
+    auto old_handler = __sigsan_handlers[sig];
+
+    int result;
+    if (!act || act->sa_handler == SIG_IGN || act->sa_handler == SIG_DFL) {
+        result = REAL(sigaction)(sig, act, oldact);
+    } else {
+        // Pass in act, but replace the sa_handler with our middleman
+        struct sigaction act_copy = *act;
+        act_copy.sa_handler = act_copy.sa_flags & SA_SIGINFO ? 
(signal_handler)(uintptr_t)__sigsan_extended_handler : __sigsan_handler;
+        result = REAL(sigaction)(sig, &act_copy, oldact);
+    }
+
+    if (result == 0) {
+        if (act) {
+            // TODO: Fix race condition.
+            // (sig gets delievered right here, causing old sighandler to be 
called)
+            __sigsan_handlers[sig] = (uintptr_t)act->sa_handler;
+        }
+
+        if (oldact) {
+            // TODO: figure out if oldact gets written to even when result != 0
+
+            // Stick in the handler from __sigsan_handlers, so the caller 
isn't aware of our trickery :)
+            oldact->sa_handler = (signal_handler)old_handler;
+        }
+    }
+
+    return result;
+}
+
+void __sanitizer::BufferedStackTrace::UnwindImpl(uptr pc, uptr bp, void 
*context, bool request_fast, u32 max_depth) {
+    uptr top = 0;
+    uptr bottom = 0;
+    GetThreadStackTopAndBottom(false, &top, &bottom);
+    Unwind(max_depth, pc, bp, context, top, bottom, request_fast);
+}
+
+#define SIGSAN_INTERCEPTOR(ret_type, func, args, ...) \
+    INTERCEPTOR(ret_type, func, ##__VA_ARGS__) { \
+        if (__sigsan_signal_depth > 0) { \
+            __sigsan_signal_depth = 0; /* To avoid having to make this 
function async-signal-safe :) */ \
+            SanitizerCommonDecorator d; \
+            Printf("%s", d.Warning()); \
+            Report("ERROR: SignalSanitizer: Async-signal unsafe function " 
#func " called from a signal handler.\n"); \
+            Printf("%s", d.Default()); \
+            BufferedStackTrace stack; \
+            GET_CURRENT_PC_BP_SP; \
+            (void)sp; \
+            stack.Unwind(pc, bp, nullptr, false); \
+            stack.Print(); \
+            Die(); \
+        } \
+        return REAL(func)args; \
+    }
+
+SIGSAN_INTERCEPTOR(void *, malloc, (size), size_t size)
+SIGSAN_INTERCEPTOR(void *, calloc, (n, size), size_t n, size_t size)
+SIGSAN_INTERCEPTOR(void *, realloc, (p, size), void *p, size_t size)
+SIGSAN_INTERCEPTOR(void *, reallocarray, (p, n, size), void *p, size_t n, 
size_t size)
+SIGSAN_INTERCEPTOR(void, free, (p), void *p)
+
+SIGSAN_INTERCEPTOR(int, fputc, (c, stream), int c, FILE *stream)
+SIGSAN_INTERCEPTOR(int, putc, (c, stream), int c, FILE *stream)
+SIGSAN_INTERCEPTOR(int, putchar, (c), int c);
+SIGSAN_INTERCEPTOR(int, fputs, (s, stream), const char *s, FILE *stream)
+SIGSAN_INTERCEPTOR(int, puts, (s), const char *s)
+
+SIGSAN_INTERCEPTOR(int, vprintf, (format, ap), const char *format, va_list ap)
+SIGSAN_INTERCEPTOR(int, vfprintf, (stream, format, ap), FILE *stream, const 
char *format, va_list ap)
+SIGSAN_INTERCEPTOR(int, vdprintf, (fd, format, ap), int fd, const char 
*format, va_list ap)
+SIGSAN_INTERCEPTOR(int, vsprintf, (str, format, ap), char *str, const char 
*format, va_list ap)
+SIGSAN_INTERCEPTOR(int, vsnprintf, (str, size, format, ap), char *str, size_t 
size, const char *format, va_list ap)
+
+INTERCEPTOR(int, printf, const char *format, ...) {
+    va_list ap;
+    va_start(ap, format);
+    auto result = REAL(vprintf)(format, ap);
+    va_end(ap);
+    return result;
+}
+INTERCEPTOR(int, fprintf, FILE *stream, const char *format, ...) {
+    va_list ap;
+    va_start(ap, format);
+    auto result = REAL(vfprintf)(stream, format, ap);
+    va_end(ap);
+    return result;
+}
+INTERCEPTOR(int, dprintf, int fd, const char *format, ...) {
+    va_list ap;
+    va_start(ap, format);
+    auto result = REAL(vdprintf)(fd, format, ap);
+    va_end(ap);
+    return result;
+}
+INTERCEPTOR(int, sprintf, char *str, const char *format, ...) {
+    va_list ap;
+    va_start(ap, format);
+    auto result = REAL(vsprintf)(str, format, ap);
+    va_end(ap);
+    return result;
+}
+INTERCEPTOR(int, snprintf, char *str, size_t size, const char *format, ...) {
+    va_list ap;
+    va_start(ap, format);
+    auto result = REAL(vsnprintf)(str, size, format, ap);
+    va_end(ap);
+    return result;
+}
+
+__attribute__((constructor)) void __sigsan_init() {
+    SetCommonFlagsDefaults();
+    InitializeCommonFlags();
+
+    INTERCEPT_FUNCTION(signal);
+    INTERCEPT_FUNCTION(sigaction);
+
+    INTERCEPT_FUNCTION(malloc);
+    INTERCEPT_FUNCTION(calloc);
+    INTERCEPT_FUNCTION(realloc);
+    INTERCEPT_FUNCTION(reallocarray);
+    INTERCEPT_FUNCTION(free);
+    INTERCEPT_FUNCTION(fputc);
+    INTERCEPT_FUNCTION(putc);
+    INTERCEPT_FUNCTION(putchar);
+    INTERCEPT_FUNCTION(fputs);
+    INTERCEPT_FUNCTION(puts);
+
+    INTERCEPT_FUNCTION(vprintf);
+    INTERCEPT_FUNCTION(vfprintf);
+    INTERCEPT_FUNCTION(vdprintf);
+    INTERCEPT_FUNCTION(vsprintf);
+    INTERCEPT_FUNCTION(vsnprintf);
+
+    INTERCEPT_FUNCTION(printf);
+    INTERCEPT_FUNCTION(fprintf);
+    INTERCEPT_FUNCTION(dprintf);
+    INTERCEPT_FUNCTION(sprintf);
+    INTERCEPT_FUNCTION(snprintf);
+
+    // TODO: Fix race conditions.
+    // (signal/sigaction called while this loop is running)
+    for (int i = 0; i < NSIG; i++) {
+        struct sigaction existing_sa;
+        if (REAL(sigaction)(i, NULL, &existing_sa) == 0) {
+            auto const existing_handler = existing_sa.sa_handler;
+            if (existing_handler != SIG_IGN && existing_handler != SIG_DFL) {
+                __sigsan_handlers[i] = (uintptr_t)existing_handler;
+                if (existing_sa.sa_flags & SA_SIGINFO) {
+                    existing_sa.sa_handler = __sigsan_handler;
+                } else {
+                    existing_sa.sa_handler = 
(signal_handler)(uintptr_t)__sigsan_extended_handler;
+                }
+            }
+        }
+    }
+}

>From 8d4d9f9bd7cebb3e24da28b5aed52a4f8e3bf6c8 Mon Sep 17 00:00:00 2001
From: Ben Kallus <[email protected]>
Date: Fri, 12 Dec 2025 09:25:33 -0500
Subject: [PATCH 2/4] format and add a few more functions

---
 compiler-rt/lib/sigsan/sigsan.cpp | 339 ++++++++++++++++++------------
 1 file changed, 208 insertions(+), 131 deletions(-)

diff --git a/compiler-rt/lib/sigsan/sigsan.cpp 
b/compiler-rt/lib/sigsan/sigsan.cpp
index 34099dcdbd405..b48583740e2b2 100644
--- a/compiler-rt/lib/sigsan/sigsan.cpp
+++ b/compiler-rt/lib/sigsan/sigsan.cpp
@@ -1,13 +1,14 @@
 #include "interception/interception.h" // for INTERCEPTOR, INTERCEPT_FUNCTION, 
REAL
-#include "sanitizer_common/sanitizer_stacktrace.h" // for 
GET_CURRENT_PC_BP_SP, BufferedStackTrace
+#include "sanitizer_common/sanitizer_common.h"           // for Printf, Die
 #include "sanitizer_common/sanitizer_report_decorator.h" // for 
SanitizerCommonDecorator
-#include "sanitizer_common/sanitizer_common.h" // for Printf, Die
+#include "sanitizer_common/sanitizer_stacktrace.h" // for 
GET_CURRENT_PC_BP_SP, BufferedStackTrace
 // TODO: figure out the include for Report
 
+#include <cstdarg>  // for va_list, va_start, va_end
+#include <cstdint>  // for uintptr_t
+#include <errno.h>  // for errno
 #include <signal.h> // for siginfo_t, NSIG, struct sigaction, SIG_IGN, SIG_DFL
-#include <cstdint> // for uintptr_t
-#include <cstdarg> // for va_list, va_start, va_end
-#include <stdio.h> // for FILE
+#include <stdio.h>  // for FILE
 
 using namespace __sanitizer;
 
@@ -16,21 +17,63 @@ using extended_signal_handler = void (*)(int, siginfo_t *, 
void *);
 
 thread_local unsigned int __sigsan_signal_depth = 0;
 
-// TODO: handle sigvec and the other deprecated sighandler installation 
functions
+// TODO: handle sigvec and the other deprecated sighandler installation
+// functions
 
-// TODO: Make these atomic so concurrent calls to signal for the same signum 
don't cause problems
+// TODO: Make these atomic so concurrent calls to signal for the same signum
+// don't cause problems
 static uintptr_t __sigsan_handlers[NSIG];
 
+[[noreturn]] void __sigsan_print_backtrace_and_die() {
+  BufferedStackTrace stack;
+  GET_CURRENT_PC_BP_SP;
+  (void)sp;
+  stack.Unwind(pc, bp, nullptr, false);
+  stack.Print();
+  Die();
+}
+
+[[noreturn]] void
+__sigsan_die_from_unsafe_function_call(char const *func_name) {
+  __sigsan_signal_depth =
+      0; /* To avoid having to make this function async-signal-safe :) */
+  SanitizerCommonDecorator d;
+  Printf("%s", d.Warning());
+  Report("ERROR: SignalSanitizer: async-signal-unsafe function %s called from "
+         "a signal handler.\n",
+         func_name);
+  Printf("%s", d.Default());
+  __sigsan_print_backtrace_and_die();
+}
+
+[[noreturn]] void __sigsan_die_from_modified_errno() {
+  __sigsan_signal_depth =
+      0; /* To avoid having to make this function async-signal-safe :) */
+  SanitizerCommonDecorator d;
+  Printf("%s", d.Warning());
+  Report("ERROR: SignalSanitizer: errno modified from a signal handler.\n");
+  Printf("%s", d.Default());
+  __sigsan_print_backtrace_and_die();
+}
+
 void __sigsan_handler(int signum) {
   __sigsan_signal_depth++;
+  int saved_errno = errno;
   ((signal_handler)(__sigsan_handlers[signum]))(signum);
+  if (errno != saved_errno) {
+    __sigsan_die_from_modified_errno();
+  }
   __sigsan_signal_depth--;
 }
 
 void __sigsan_extended_handler(int signum, siginfo_t *si, void *arg) {
-    __sigsan_signal_depth++;
-    ((extended_signal_handler)(__sigsan_handlers[signum]))(signum, si, arg);
-    __sigsan_signal_depth--;
+  __sigsan_signal_depth++;
+  int saved_errno = errno;
+  ((extended_signal_handler)(__sigsan_handlers[signum]))(signum, si, arg);
+  if (errno != saved_errno) {
+    __sigsan_die_from_modified_errno();
+  }
+  __sigsan_signal_depth--;
 }
 
 INTERCEPTOR(signal_handler, signal, int signum, signal_handler handler) {
@@ -41,160 +84,194 @@ INTERCEPTOR(signal_handler, signal, int signum, 
signal_handler handler) {
   return sigaction(signum, &action, &old) < 0 ? SIG_ERR : old.sa_handler;
 }
 
-INTERCEPTOR(int, sigaction, int sig, struct sigaction const *__restrict act, 
struct sigaction *oldact) {
-    auto old_handler = __sigsan_handlers[sig];
-
-    int result;
-    if (!act || act->sa_handler == SIG_IGN || act->sa_handler == SIG_DFL) {
-        result = REAL(sigaction)(sig, act, oldact);
-    } else {
-        // Pass in act, but replace the sa_handler with our middleman
-        struct sigaction act_copy = *act;
-        act_copy.sa_handler = act_copy.sa_flags & SA_SIGINFO ? 
(signal_handler)(uintptr_t)__sigsan_extended_handler : __sigsan_handler;
-        result = REAL(sigaction)(sig, &act_copy, oldact);
-    }
+INTERCEPTOR(int, sigaction, int sig, struct sigaction const *__restrict act,
+            struct sigaction *oldact) {
+  auto old_handler = __sigsan_handlers[sig];
 
-    if (result == 0) {
-        if (act) {
-            // TODO: Fix race condition.
-            // (sig gets delievered right here, causing old sighandler to be 
called)
-            __sigsan_handlers[sig] = (uintptr_t)act->sa_handler;
-        }
+  int result;
+  if (!act || act->sa_handler == SIG_IGN || act->sa_handler == SIG_DFL) {
+    result = REAL(sigaction)(sig, act, oldact);
+  } else {
+    // Pass in act, but replace the sa_handler with our middleman
+    struct sigaction act_copy = *act;
+    act_copy.sa_handler =
+        act_copy.sa_flags & SA_SIGINFO
+            ? (signal_handler)(uintptr_t)__sigsan_extended_handler
+            : __sigsan_handler;
+    result = REAL(sigaction)(sig, &act_copy, oldact);
+  }
 
-        if (oldact) {
-            // TODO: figure out if oldact gets written to even when result != 0
+  if (result == 0) {
+    if (act) {
+      // TODO: Fix race condition.
+      // (sig gets delievered right here, causing old sighandler to be called)
+      __sigsan_handlers[sig] = (uintptr_t)act->sa_handler;
+    }
 
-            // Stick in the handler from __sigsan_handlers, so the caller 
isn't aware of our trickery :)
-            oldact->sa_handler = (signal_handler)old_handler;
-        }
+    if (oldact) {
+      // TODO: figure out if oldact gets written to even when result != 0
+
+      // Stick in the handler from __sigsan_handlers, so the caller isn't aware
+      // of our trickery :)
+      oldact->sa_handler = (signal_handler)old_handler;
     }
+  }
 
-    return result;
+  return result;
 }
 
-void __sanitizer::BufferedStackTrace::UnwindImpl(uptr pc, uptr bp, void 
*context, bool request_fast, u32 max_depth) {
-    uptr top = 0;
-    uptr bottom = 0;
-    GetThreadStackTopAndBottom(false, &top, &bottom);
-    Unwind(max_depth, pc, bp, context, top, bottom, request_fast);
+void __sanitizer::BufferedStackTrace::UnwindImpl(uptr pc, uptr bp,
+                                                 void *context,
+                                                 bool request_fast,
+                                                 u32 max_depth) {
+  uptr top = 0;
+  uptr bottom = 0;
+  GetThreadStackTopAndBottom(false, &top, &bottom);
+  Unwind(max_depth, pc, bp, context, top, bottom, request_fast);
 }
 
-#define SIGSAN_INTERCEPTOR(ret_type, func, args, ...) \
-    INTERCEPTOR(ret_type, func, ##__VA_ARGS__) { \
-        if (__sigsan_signal_depth > 0) { \
-            __sigsan_signal_depth = 0; /* To avoid having to make this 
function async-signal-safe :) */ \
-            SanitizerCommonDecorator d; \
-            Printf("%s", d.Warning()); \
-            Report("ERROR: SignalSanitizer: Async-signal unsafe function " 
#func " called from a signal handler.\n"); \
-            Printf("%s", d.Default()); \
-            BufferedStackTrace stack; \
-            GET_CURRENT_PC_BP_SP; \
-            (void)sp; \
-            stack.Unwind(pc, bp, nullptr, false); \
-            stack.Print(); \
-            Die(); \
-        } \
-        return REAL(func)args; \
-    }
+#define SIGSAN_INTERCEPTOR(ret_type, func, args, ...)                          
\
+  INTERCEPTOR(ret_type, func, ##__VA_ARGS__) {                                 
\
+    if (__sigsan_signal_depth > 0) {                                           
\
+      __sigsan_die_from_unsafe_function_call(#func);                           
\
+    }                                                                          
\
+    return REAL(func) args;                                                    
\
+  }
 
+// malloc
 SIGSAN_INTERCEPTOR(void *, malloc, (size), size_t size)
 SIGSAN_INTERCEPTOR(void *, calloc, (n, size), size_t n, size_t size)
 SIGSAN_INTERCEPTOR(void *, realloc, (p, size), void *p, size_t size)
-SIGSAN_INTERCEPTOR(void *, reallocarray, (p, n, size), void *p, size_t n, 
size_t size)
+SIGSAN_INTERCEPTOR(void *, reallocarray, (p, n, size), void *p, size_t n,
+                   size_t size)
 SIGSAN_INTERCEPTOR(void, free, (p), void *p)
 
+// stdio
 SIGSAN_INTERCEPTOR(int, fputc, (c, stream), int c, FILE *stream)
 SIGSAN_INTERCEPTOR(int, putc, (c, stream), int c, FILE *stream)
 SIGSAN_INTERCEPTOR(int, putchar, (c), int c);
 SIGSAN_INTERCEPTOR(int, fputs, (s, stream), const char *s, FILE *stream)
 SIGSAN_INTERCEPTOR(int, puts, (s), const char *s)
-
+SIGSAN_INTERCEPTOR(int, fflush, (stream), FILE *stream)
 SIGSAN_INTERCEPTOR(int, vprintf, (format, ap), const char *format, va_list ap)
-SIGSAN_INTERCEPTOR(int, vfprintf, (stream, format, ap), FILE *stream, const 
char *format, va_list ap)
-SIGSAN_INTERCEPTOR(int, vdprintf, (fd, format, ap), int fd, const char 
*format, va_list ap)
-SIGSAN_INTERCEPTOR(int, vsprintf, (str, format, ap), char *str, const char 
*format, va_list ap)
-SIGSAN_INTERCEPTOR(int, vsnprintf, (str, size, format, ap), char *str, size_t 
size, const char *format, va_list ap)
-
+SIGSAN_INTERCEPTOR(int, vfprintf, (stream, format, ap), FILE *stream,
+                   const char *format, va_list ap)
+SIGSAN_INTERCEPTOR(int, vdprintf, (fd, format, ap), int fd, const char *format,
+                   va_list ap)
+SIGSAN_INTERCEPTOR(int, vsprintf, (str, format, ap), char *str,
+                   const char *format, va_list ap)
+SIGSAN_INTERCEPTOR(int, vsnprintf, (str, size, format, ap), char *str,
+                   size_t size, const char *format, va_list ap)
 INTERCEPTOR(int, printf, const char *format, ...) {
-    va_list ap;
-    va_start(ap, format);
-    auto result = REAL(vprintf)(format, ap);
-    va_end(ap);
-    return result;
+  va_list ap;
+  va_start(ap, format);
+  auto const result = REAL(vprintf)(format, ap);
+  va_end(ap);
+  return result;
 }
 INTERCEPTOR(int, fprintf, FILE *stream, const char *format, ...) {
-    va_list ap;
-    va_start(ap, format);
-    auto result = REAL(vfprintf)(stream, format, ap);
-    va_end(ap);
-    return result;
+  va_list ap;
+  va_start(ap, format);
+  auto const result = REAL(vfprintf)(stream, format, ap);
+  va_end(ap);
+  return result;
 }
 INTERCEPTOR(int, dprintf, int fd, const char *format, ...) {
-    va_list ap;
-    va_start(ap, format);
-    auto result = REAL(vdprintf)(fd, format, ap);
-    va_end(ap);
-    return result;
+  va_list ap;
+  va_start(ap, format);
+  auto const result = REAL(vdprintf)(fd, format, ap);
+  va_end(ap);
+  return result;
 }
 INTERCEPTOR(int, sprintf, char *str, const char *format, ...) {
-    va_list ap;
-    va_start(ap, format);
-    auto result = REAL(vsprintf)(str, format, ap);
-    va_end(ap);
-    return result;
+  va_list ap;
+  va_start(ap, format);
+  auto const result = REAL(vsprintf)(str, format, ap);
+  va_end(ap);
+  return result;
 }
 INTERCEPTOR(int, snprintf, char *str, size_t size, const char *format, ...) {
-    va_list ap;
-    va_start(ap, format);
-    auto result = REAL(vsnprintf)(str, size, format, ap);
-    va_end(ap);
-    return result;
+  va_list ap;
+  va_start(ap, format);
+  auto const result = REAL(vsnprintf)(str, size, format, ap);
+  va_end(ap);
+  return result;
+}
+SIGSAN_INTERCEPTOR(FILE *, fopen, (path, mode), const char *path,
+                   const char *mode)
+SIGSAN_INTERCEPTOR(FILE *, fdopen, (fd, mode), int fd, const char *mode)
+SIGSAN_INTERCEPTOR(FILE *, freopen, (path, mode, stream), const char *path,
+                   const char *mode, FILE *stream)
+
+// syslog
+SIGSAN_INTERCEPTOR(void, openlog, (ident, option, facility), const char *ident,
+                   int option, int facility)
+SIGSAN_INTERCEPTOR(void, closelog, ())
+SIGSAN_INTERCEPTOR(void, vsyslog, (priority, format, ap), int priority,
+                   const char *format, va_list ap)
+INTERCEPTOR(void, syslog, int priority, const char *format, ...) {
+  va_list ap;
+  va_start(ap, format);
+  REAL(vsyslog)(priority, format, ap);
+  va_end(ap);
 }
 
 __attribute__((constructor)) void __sigsan_init() {
-    SetCommonFlagsDefaults();
-    InitializeCommonFlags();
-
-    INTERCEPT_FUNCTION(signal);
-    INTERCEPT_FUNCTION(sigaction);
-
-    INTERCEPT_FUNCTION(malloc);
-    INTERCEPT_FUNCTION(calloc);
-    INTERCEPT_FUNCTION(realloc);
-    INTERCEPT_FUNCTION(reallocarray);
-    INTERCEPT_FUNCTION(free);
-    INTERCEPT_FUNCTION(fputc);
-    INTERCEPT_FUNCTION(putc);
-    INTERCEPT_FUNCTION(putchar);
-    INTERCEPT_FUNCTION(fputs);
-    INTERCEPT_FUNCTION(puts);
-
-    INTERCEPT_FUNCTION(vprintf);
-    INTERCEPT_FUNCTION(vfprintf);
-    INTERCEPT_FUNCTION(vdprintf);
-    INTERCEPT_FUNCTION(vsprintf);
-    INTERCEPT_FUNCTION(vsnprintf);
-
-    INTERCEPT_FUNCTION(printf);
-    INTERCEPT_FUNCTION(fprintf);
-    INTERCEPT_FUNCTION(dprintf);
-    INTERCEPT_FUNCTION(sprintf);
-    INTERCEPT_FUNCTION(snprintf);
-
-    // TODO: Fix race conditions.
-    // (signal/sigaction called while this loop is running)
-    for (int i = 0; i < NSIG; i++) {
-        struct sigaction existing_sa;
-        if (REAL(sigaction)(i, NULL, &existing_sa) == 0) {
-            auto const existing_handler = existing_sa.sa_handler;
-            if (existing_handler != SIG_IGN && existing_handler != SIG_DFL) {
-                __sigsan_handlers[i] = (uintptr_t)existing_handler;
-                if (existing_sa.sa_flags & SA_SIGINFO) {
-                    existing_sa.sa_handler = __sigsan_handler;
-                } else {
-                    existing_sa.sa_handler = 
(signal_handler)(uintptr_t)__sigsan_extended_handler;
-                }
-            }
+  SetCommonFlagsDefaults();
+  InitializeCommonFlags();
+
+  INTERCEPT_FUNCTION(signal);
+  INTERCEPT_FUNCTION(sigaction);
+
+  // malloc
+  INTERCEPT_FUNCTION(malloc);
+  INTERCEPT_FUNCTION(calloc);
+  INTERCEPT_FUNCTION(realloc);
+  INTERCEPT_FUNCTION(reallocarray);
+  INTERCEPT_FUNCTION(free);
+
+  // stdio
+  INTERCEPT_FUNCTION(fputc);
+  INTERCEPT_FUNCTION(putc);
+  INTERCEPT_FUNCTION(putchar);
+  INTERCEPT_FUNCTION(fputs);
+  INTERCEPT_FUNCTION(puts);
+  INTERCEPT_FUNCTION(fflush);
+  INTERCEPT_FUNCTION(vprintf);
+  INTERCEPT_FUNCTION(vfprintf);
+  INTERCEPT_FUNCTION(vdprintf);
+  INTERCEPT_FUNCTION(vsprintf);
+  INTERCEPT_FUNCTION(vsnprintf);
+  INTERCEPT_FUNCTION(printf);
+  INTERCEPT_FUNCTION(fprintf);
+  INTERCEPT_FUNCTION(dprintf);
+  INTERCEPT_FUNCTION(sprintf);
+  INTERCEPT_FUNCTION(snprintf);
+  INTERCEPT_FUNCTION(fopen);
+  INTERCEPT_FUNCTION(fdopen);
+  INTERCEPT_FUNCTION(freopen);
+
+  // syslog
+  INTERCEPT_FUNCTION(openlog);
+  INTERCEPT_FUNCTION(closelog);
+  INTERCEPT_FUNCTION(vsyslog);
+  INTERCEPT_FUNCTION(syslog);
+
+  // TODO: Fix race conditions.
+  // (signal/sigaction called while this loop is running)
+  for (int i = 0; i < NSIG; i++) {
+    struct sigaction existing_sa;
+    if (REAL(sigaction)(i, NULL, &existing_sa) == 0) {
+      auto const existing_handler = existing_sa.sa_handler;
+      if (existing_handler != SIG_IGN && existing_handler != SIG_DFL) {
+        __sigsan_handlers[i] = (uintptr_t)existing_handler;
+        if (existing_sa.sa_flags & SA_SIGINFO) {
+          existing_sa.sa_handler = __sigsan_handler;
+        } else {
+          existing_sa.sa_handler =
+              (signal_handler)(uintptr_t)__sigsan_extended_handler;
         }
+      }
     }
+  }
 }

>From c5db568b858ea325042238942a322c7e7b7c4a03 Mon Sep 17 00:00:00 2001
From: Ben Kallus <[email protected]>
Date: Fri, 12 Dec 2025 09:32:35 -0500
Subject: [PATCH 3/4] add pthread_mutex_whatever

---
 compiler-rt/lib/sigsan/sigsan.cpp | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/compiler-rt/lib/sigsan/sigsan.cpp 
b/compiler-rt/lib/sigsan/sigsan.cpp
index b48583740e2b2..39d4bd44e8ea7 100644
--- a/compiler-rt/lib/sigsan/sigsan.cpp
+++ b/compiler-rt/lib/sigsan/sigsan.cpp
@@ -216,6 +216,12 @@ INTERCEPTOR(void, syslog, int priority, const char 
*format, ...) {
   va_end(ap);
 }
 
+SIGSAN_INTERCEPTOR(int, pthread_mutex_init, (mutex, mutexattr), 
pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
+SIGSAN_INTERCEPTOR(int, pthread_mutex_lock, (mutex), pthread_mutex_t *mutex)
+SIGSAN_INTERCEPTOR(int, pthread_mutex_trylock, (mutex), pthread_mutex_t *mutex)
+SIGSAN_INTERCEPTOR(int, pthread_mutex_unlock, (mutex), pthread_mutex_t *mutex)
+SIGSAN_INTERCEPTOR(int, pthread_mutex_destroy, (mutex), pthread_mutex_t *mutex)
+
 __attribute__((constructor)) void __sigsan_init() {
   SetCommonFlagsDefaults();
   InitializeCommonFlags();
@@ -257,6 +263,13 @@ __attribute__((constructor)) void __sigsan_init() {
   INTERCEPT_FUNCTION(vsyslog);
   INTERCEPT_FUNCTION(syslog);
 
+  // pthreads
+  INTERCEPT_FUNCTION(pthread_mutex_init);
+  INTERCEPT_FUNCTION(pthread_mutex_lock);
+  INTERCEPT_FUNCTION(pthread_mutex_trylock);
+  INTERCEPT_FUNCTION(pthread_mutex_unlock);
+  INTERCEPT_FUNCTION(pthread_mutex_destroy);
+
   // TODO: Fix race conditions.
   // (signal/sigaction called while this loop is running)
   for (int i = 0; i < NSIG; i++) {

>From 174017af9ab3ce563f402b71d244f6c09d30194a Mon Sep 17 00:00:00 2001
From: Ben Kallus <[email protected]>
Date: Fri, 12 Dec 2025 16:29:00 -0500
Subject: [PATCH 4/4] Add vasprintf and saprintf checks

---
 compiler-rt/lib/sigsan/CMakeLists.txt |  2 +-
 compiler-rt/lib/sigsan/sigsan.cpp     | 14 ++++++++++++++
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/compiler-rt/lib/sigsan/CMakeLists.txt 
b/compiler-rt/lib/sigsan/CMakeLists.txt
index 1168e0fcc9973..9c446ae5227db 100644
--- a/compiler-rt/lib/sigsan/CMakeLists.txt
+++ b/compiler-rt/lib/sigsan/CMakeLists.txt
@@ -1,10 +1,10 @@
 include_directories(..)
 
-# Runtime library sources and build flags.
 set(SIGSAN_SOURCES
   sigsan.cpp
 )
 
+# TODO: support other architectures
 add_compiler_rt_component(sigsan)
 add_compiler_rt_runtime(clang_rt.sigsan
     STATIC
diff --git a/compiler-rt/lib/sigsan/sigsan.cpp 
b/compiler-rt/lib/sigsan/sigsan.cpp
index 39d4bd44e8ea7..f14ed9357636c 100644
--- a/compiler-rt/lib/sigsan/sigsan.cpp
+++ b/compiler-rt/lib/sigsan/sigsan.cpp
@@ -203,6 +203,16 @@ SIGSAN_INTERCEPTOR(FILE *, fdopen, (fd, mode), int fd, 
const char *mode)
 SIGSAN_INTERCEPTOR(FILE *, freopen, (path, mode, stream), const char *path,
                    const char *mode, FILE *stream)
 
+// GNU stdio
+SIGSAN_INTERCEPTOR(int, vasprintf, (strp, fmt, ap), char **strp, const char 
*fmt, va_list ap)
+INTERCEPTOR(int, asprintf, char **strp, const char *fmt, ...) {
+  va_list ap;
+  va_start(ap, fmt);
+  auto const result = REAL(vasprintf)(strp, fmt, ap);
+  va_end(ap);
+  return result;
+}
+
 // syslog
 SIGSAN_INTERCEPTOR(void, openlog, (ident, option, facility), const char *ident,
                    int option, int facility)
@@ -257,6 +267,10 @@ __attribute__((constructor)) void __sigsan_init() {
   INTERCEPT_FUNCTION(fdopen);
   INTERCEPT_FUNCTION(freopen);
 
+  // GNU stdio
+  INTERCEPT_FUNCTION(vasprintf);
+  INTERCEPT_FUNCTION(asprintf);
+
   // syslog
   INTERCEPT_FUNCTION(openlog);
   INTERCEPT_FUNCTION(closelog);

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to