[compiler-rt] [compiler-rt][sanitizer_common] Size readlink/getsockopt post-hook unpoison by bytes written (PR #209209)

Patrik Dokoupil via llvm-commits llvm-commits at lists.llvm.org
Mon Jul 20 15:54:29 PDT 2026


https://github.com/pdokoupil updated https://github.com/llvm/llvm-project/pull/209209

>From 15b664ef72d805f15a730255f4419fcbb84a1e54 Mon Sep 17 00:00:00 2001
From: Patrik Dokoupil <patrik.dokoupil1996 at gmail.com>
Date: Mon, 13 Jul 2026 14:49:15 +0000
Subject: [PATCH 1/3] [compiler-rt][sanitizer_common] Size readlink/getsockopt
 post-hook unpoison by bytes written

readlink/readlinkat return the number of bytes placed in the buffer in res and
do not NUL-terminate it, and getsockopt writes *optlen bytes of binary option
data. The post-hooks instead sized their POST_WRITE (MSan unpoison) with
internal_strlen(buf) + 1, which reads past what the kernel wrote -- over-
unpoisoning the uninitialized tail (masking real bugs) and, on a buffer with no
NUL, reading out of bounds inside the runtime. For binary option data an early
zero byte instead under-unpoisons.

Size the unpoison by the actual written length, matching the corresponding libc
interceptors (readlink unpoisons res bytes, and getsockopt unpoisons *optlen bytes).

Seems to be present since the file's 2013 import.
---
 .../sanitizer_common_syscalls.inc             |  9 ++--
 .../msan/Linux/syscalls_post_output_size.cpp  | 41 +++++++++++++++++++
 2 files changed, 45 insertions(+), 5 deletions(-)
 create mode 100644 compiler-rt/test/msan/Linux/syscalls_post_output_size.cpp

diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc b/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc
index ee3ac723e3669..5f4fba5299167 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc
@@ -1510,7 +1510,7 @@ PRE_SYSCALL(readlink)(const void *path, void *buf, long bufsiz) {
 POST_SYSCALL(readlink)(long res, const void *path, void *buf, long bufsiz) {
   if (res >= 0) {
     if (buf)
-      POST_WRITE(buf, __sanitizer::internal_strlen((const char *)buf) + 1);
+      POST_WRITE(buf, res);
   }
 }
 
@@ -1905,9 +1905,8 @@ PRE_SYSCALL(getsockopt)
 POST_SYSCALL(getsockopt)
 (long res, long fd, long level, long optname, void *optval, void *optlen) {
   if (res >= 0) {
-    if (optval)
-      POST_WRITE(optval,
-                 __sanitizer::internal_strlen((const char *)optval) + 1);
+    if (optval && optlen)
+      POST_WRITE(optval, *(unsigned int *)optlen);
     if (optlen)
       POST_WRITE(optlen, sizeof(int));
   }
@@ -2900,7 +2899,7 @@ POST_SYSCALL(readlinkat)
 (long res, long dfd, const void *path, void *buf, long bufsiz) {
   if (res >= 0) {
     if (buf)
-      POST_WRITE(buf, __sanitizer::internal_strlen((const char *)buf) + 1);
+      POST_WRITE(buf, res);
   }
 }
 
diff --git a/compiler-rt/test/msan/Linux/syscalls_post_output_size.cpp b/compiler-rt/test/msan/Linux/syscalls_post_output_size.cpp
new file mode 100644
index 0000000000000..14fb01d989e5e
--- /dev/null
+++ b/compiler-rt/test/msan/Linux/syscalls_post_output_size.cpp
@@ -0,0 +1,41 @@
+// RUN: %clangxx_msan -O0 %s -o %t && %run %t
+
+// readlink/readlinkat return the byte count in res and do not NUL-terminate;
+// getsockopt writes *optlen bytes of binary option data. The POST hooks used
+// internal_strlen() to size the unpoison, which reads past what the kernel
+// wrote. After the fix the unpoisoned span matches the real written length.
+
+#include <assert.h>
+#include <fcntl.h>
+#include <string.h>
+#include <sys/socket.h>
+
+#include <sanitizer/linux_syscall_hooks.h>
+#include <sanitizer/msan_interface.h>
+
+int main() {
+  {
+    char buf[64];
+    memset(buf, 'x', 64);
+    buf[63] = 0; // in-bounds NUL so a stray strlen would run to 63, not OOB
+    __msan_poison(buf, 64);
+    __sanitizer_syscall_post_readlink(3, "/x", buf, 64);
+    assert(__msan_test_shadow(buf, 64) == 3); // exactly res bytes
+  }
+  {
+    char buf[64];
+    memset(buf, 'x', 64);
+    buf[63] = 0;
+    __msan_poison(buf, 64);
+    __sanitizer_syscall_post_readlinkat(3, AT_FDCWD, "/x", buf, 64);
+    assert(__msan_test_shadow(buf, 64) == 3);
+  }
+  {
+    unsigned char ov[16];
+    __msan_poison(ov, 16);
+    socklen_t ol = 4;
+    __sanitizer_syscall_post_getsockopt(0, 3, 1, 2, ov, &ol);
+    assert(__msan_test_shadow(ov, 16) == 4); // exactly *optlen bytes
+  }
+  return 0;
+}

>From 7d94902a1401824fdd18e5ce6d8c26ebe8b86441 Mon Sep 17 00:00:00 2001
From: Patrik Dokoupil <patrik.dokoupil1996 at gmail.com>
Date: Mon, 20 Jul 2026 08:10:36 +0000
Subject: [PATCH 2/3] [fix up] Make getsockopt optlen null-check explicit

---
 .../sanitizer_common_syscalls.inc                |  9 ++++++---
 .../msan/Linux/syscalls_post_output_size.cpp     | 16 ++++++++++++++++
 2 files changed, 22 insertions(+), 3 deletions(-)

diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc b/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc
index 5f4fba5299167..3f0b2e560942d 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc
@@ -1905,10 +1905,13 @@ PRE_SYSCALL(getsockopt)
 POST_SYSCALL(getsockopt)
 (long res, long fd, long level, long optname, void *optval, void *optlen) {
   if (res >= 0) {
-    if (optval && optlen)
-      POST_WRITE(optval, *(unsigned int *)optlen);
-    if (optlen)
+    // getsockopt cannot succeed with a null optlen, but guard it before
+    // dereferencing since the hook is called with raw, untrusted arguments.
+    if (optlen) {
+      if (optval)
+        POST_WRITE(optval, *(unsigned int *)optlen);
       POST_WRITE(optlen, sizeof(int));
+    }
   }
 }
 
diff --git a/compiler-rt/test/msan/Linux/syscalls_post_output_size.cpp b/compiler-rt/test/msan/Linux/syscalls_post_output_size.cpp
index 14fb01d989e5e..6319ff83f02c5 100644
--- a/compiler-rt/test/msan/Linux/syscalls_post_output_size.cpp
+++ b/compiler-rt/test/msan/Linux/syscalls_post_output_size.cpp
@@ -37,5 +37,21 @@ int main() {
     __sanitizer_syscall_post_getsockopt(0, 3, 1, 2, ov, &ol);
     assert(__msan_test_shadow(ov, 16) == 4); // exactly *optlen bytes
   }
+  {
+    // optlen == NULL: the hook must not dereference it and must leave optval
+    // untouched (getsockopt cannot succeed with a NULL optlen, but the hook is
+    // called with raw arguments and must stay safe regardless).
+    unsigned char ov[16];
+    __msan_poison(ov, 16);
+    __sanitizer_syscall_post_getsockopt(0, 3, 1, 2, ov, nullptr);
+    assert(__msan_test_shadow(ov, 16) == 0); // nothing unpoisoned
+  }
+  {
+    // optval == NULL: only optlen is unpoisoned, optval is never dereferenced.
+    socklen_t ol = 4;
+    __msan_poison(&ol, sizeof(ol));
+    __sanitizer_syscall_post_getsockopt(0, 3, 1, 2, nullptr, &ol);
+    assert(__msan_test_shadow(&ol, sizeof(ol)) == -1);
+  }
   return 0;
 }

>From d447b9fbd149226cb24c29fea4b0943237125af8 Mon Sep 17 00:00:00 2001
From: Patrik Dokoupil <patrik.dokoupil1996 at gmail.com>
Date: Mon, 20 Jul 2026 22:50:36 +0000
Subject: [PATCH 3/3] [fix up] clang-format

---
 compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc b/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc
index 3f0b2e560942d..a9584f6ab5818 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_common_syscalls.inc
@@ -1909,7 +1909,7 @@ POST_SYSCALL(getsockopt)
     // dereferencing since the hook is called with raw, untrusted arguments.
     if (optlen) {
       if (optval)
-        POST_WRITE(optval, *(unsigned int *)optlen);
+        POST_WRITE(optval, *(unsigned int*)optlen);
       POST_WRITE(optlen, sizeof(int));
     }
   }



More information about the llvm-commits mailing list