https://github.com/JDevlieghere created 
https://github.com/llvm/llvm-project/pull/204688

A process can opt out of being debugged with ptrace(PT_DENY_ATTACH). The XNU 
kernel enforces this by delivering SIGSEGV to the *attaching* process while it 
is still inside the ptrace(PT_ATTACHEXC) syscall. This means debugserver gets 
killed before it can inspect the result. LLDB only sees the dropped connection 
("error: attach failed: lost connection").

The condition can't be detected up front: the target's P_LNOATTACH flag is not 
exposed to userspace. To work around this, install a temporary SIGSEGV handler 
around the ptrace(PT_ATTACHEXC) call in AttachForDebug and siglongjmp back out 
if it fires, turning the fatal signal into an EPERM that propagates to lldb as 
a clear message:

```
error: attach failed: cannot attach to process N because it has
disabled debugging via ptrace(PT_DENY_ATTACH)
```

The handler only acts on a SIGSEGV that arrives on the attaching thread while 
the guard is armed. Any other SIGSEGV restores the default handler and 
re-raises. Genuine, unrelated, crashes still report normally.

rdar://31935404

Assisted-by: Claude

>From de955d0dc720c827dd43d150a37a2ed2b01e863a Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <[email protected]>
Date: Thu, 18 Jun 2026 14:52:59 -0700
Subject: [PATCH] [lldb] Survive ptrace(PT_DENY_ATTACH) when attaching

A process can opt out of being debugged with ptrace(PT_DENY_ATTACH). The
XNU kernel enforces this by delivering SIGSEGV to the *attaching*
process while it is still inside the ptrace(PT_ATTACHEXC) syscall. This
means debugserver gets killed before it can inspect the result. LLDB
only sees the dropped connection ("error: attach failed: lost
connection").

The condition can't be detected up front: the target's P_LNOATTACH flag
is not exposed to userspace. To work around this, install a temporary
SIGSEGV handler around the ptrace(PT_ATTACHEXC) call in AttachForDebug
and siglongjmp back out if it fires, turning the fatal signal into an
EPERM that propagates to lldb as a clear message:

```
error: attach failed: cannot attach to process N because it has
disabled debugging via ptrace(PT_DENY_ATTACH)
```

The handler only acts on a SIGSEGV that arrives on the attaching thread
while the guard is armed. Any other SIGSEGV restores the default handler
and re-raises. Genuine, unrelated, crashes still report normally.

rdar://31935404

Assisted-by: Claude
---
 lldb/test/API/macosx/deny-attach/Makefile     |  3 +
 .../API/macosx/deny-attach/TestDenyAttach.py  | 36 ++++++++
 lldb/test/API/macosx/deny-attach/main.c       | 60 ++++++++++++
 .../debugserver/source/MacOSX/MachProcess.mm  | 91 ++++++++++++++++++-
 4 files changed, 185 insertions(+), 5 deletions(-)
 create mode 100644 lldb/test/API/macosx/deny-attach/Makefile
 create mode 100644 lldb/test/API/macosx/deny-attach/TestDenyAttach.py
 create mode 100644 lldb/test/API/macosx/deny-attach/main.c

diff --git a/lldb/test/API/macosx/deny-attach/Makefile 
b/lldb/test/API/macosx/deny-attach/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/macosx/deny-attach/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/macosx/deny-attach/TestDenyAttach.py 
b/lldb/test/API/macosx/deny-attach/TestDenyAttach.py
new file mode 100644
index 0000000000000..f061bee51c31f
--- /dev/null
+++ b/lldb/test/API/macosx/deny-attach/TestDenyAttach.py
@@ -0,0 +1,36 @@
+import time
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class DenyAttachTestCase(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    @skipUnlessDarwin
+    @skipIfDarwinEmbedded  # PT_DENY_ATTACH attach behavior differs on 
ios/tvos/etc
+    @skipIfAsan  # Attach tests time out inconsistently under asan.
+    def test_attach_to_deny_attach_process(self):
+        """Attaching to a PT_DENY_ATTACH process reports an error, not a 
crash."""
+        self.build()
+        exe = self.getBuildArtifact("a.out")
+
+        # Use a file as a synchronization point between test and inferior: the
+        # inferior writes its pid only after it has called PT_DENY_ATTACH.
+        pid_file_path = lldbutil.append_to_process_working_directory(
+            self, "pid_file_%d" % (int(time.time()))
+        )
+        self.addTearDownHook(
+            lambda: self.run_platform_command("rm %s" % (pid_file_path))
+        )
+
+        popen = self.spawnSubprocess(exe, [pid_file_path])
+        pid = lldbutil.wait_for_file_on_target(self, pid_file_path)
+
+        self.expect(
+            "process attach -p " + pid,
+            startstr="error: attach failed:",
+            substrs=["PT_DENY_ATTACH"],
+            error=True,
+        )
diff --git a/lldb/test/API/macosx/deny-attach/main.c 
b/lldb/test/API/macosx/deny-attach/main.c
new file mode 100644
index 0000000000000..1f19f6928604a
--- /dev/null
+++ b/lldb/test/API/macosx/deny-attach/main.c
@@ -0,0 +1,60 @@
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/ptrace.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+// Write our pid into file_name atomically (write to a temp file, then rename)
+// so the test never observes a partially written pid.
+static int write_pid(const char *file_name) {
+  char tmp_name[1024];
+  snprintf(tmp_name, sizeof(tmp_name), "%s_tmp", file_name);
+
+  int fd = open(tmp_name, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);
+  if (fd == -1) {
+    fprintf(stderr, "open(%s) failed: %s\n", tmp_name, strerror(errno));
+    return 1;
+  }
+
+  char buffer[64];
+  int len = snprintf(buffer, sizeof(buffer), "%ld", (long)getpid());
+  int result = 0;
+  if (write(fd, buffer, len) == -1) {
+    fprintf(stderr, "write failed: %s\n", strerror(errno));
+    result = 1;
+  }
+  close(fd);
+
+  if (rename(tmp_name, file_name) == -1) {
+    fprintf(stderr, "rename failed: %s\n", strerror(errno));
+    result = 1;
+  }
+  return result;
+}
+
+int main(int argc, char const *argv[]) {
+  if (argc < 2) {
+    fprintf(stderr, "invalid number of command line arguments\n");
+    return 1;
+  }
+
+  // Tell the kernel to refuse all debugger attachments to this process. Any
+  // subsequent ptrace(PT_ATTACHEXC) against us makes the kernel deliver 
SIGSEGV
+  // to the attaching process (debugserver).
+  if (ptrace(PT_DENY_ATTACH, 0, 0, 0) == -1) {
+    fprintf(stderr, "ptrace(PT_DENY_ATTACH) failed: %s\n", strerror(errno));
+    return 1;
+  }
+
+  if (write_pid(argv[1]) != 0)
+    return 1;
+
+  // Wait for the debugger to try (and fail) to attach.
+  while (1)
+    sleep(60);
+
+  return 0;
+}
diff --git a/lldb/tools/debugserver/source/MacOSX/MachProcess.mm 
b/lldb/tools/debugserver/source/MacOSX/MachProcess.mm
index d79313846438b..0eb9640b98c91 100644
--- a/lldb/tools/debugserver/source/MacOSX/MachProcess.mm
+++ b/lldb/tools/debugserver/source/MacOSX/MachProcess.mm
@@ -19,6 +19,7 @@
 #include <mach/mach.h>
 #include <mach/task.h>
 #include <pthread.h>
+#include <setjmp.h>
 #include <signal.h>
 #include <spawn.h>
 #include <sys/fcntl.h>
@@ -2936,11 +2937,79 @@ static uint64_t bits(uint64_t value, uint32_t msbit, 
uint32_t lsbit) {
   return NULL;
 }
 
+namespace {
+// The XNU kernel enforces ptrace(PT_DENY_ATTACH) by delivering a SIGSEGV to 
the
+// process that tries to attach, while it is still inside the ptrace() syscall.
+// That kills debugserver outright instead of failing the call with an error.
+// This leaves lldb unable to tell the user why the attach failed. The 
condition
+// can't be detected up front because the target's P_LNOATTACH flag isn't
+// exposed to userspace, so instead we install a temporary SIGSEGV handler
+// around the ptrace() call and jump back out of it if the signal fires, 
turning
+// the fatal signal into a clean error.
+
+sigjmp_buf g_deny_attach_jmpbuf;
+// Only act on the SIGSEGV if it arrives on the thread that armed the guard
+// while a PT_ATTACHEXC call is in flight; anything else is a genuine crash.
+volatile sig_atomic_t g_deny_attach_armed = 0;
+pthread_t g_deny_attach_thread;
+
+void DenyAttachSIGSEGVHandler(int signo) {
+  if (g_deny_attach_armed &&
+      pthread_equal(pthread_self(), g_deny_attach_thread)) {
+    g_deny_attach_armed = 0;
+    siglongjmp(g_deny_attach_jmpbuf, 1);
+  }
+  // Not the deny-attach case: restore the default disposition and re-raise so 
a
+  // real crash is still reported the usual way.
+  signal(signo, SIG_DFL);
+  raise(signo);
+}
+
+// Wrapper around ptrace(PT_ATTACHEXC, pid) that survives the SIGSEGV the 
kernel
+// sends when `pid` has called ptrace(PT_DENY_ATTACH). On a normal attach it
+// behaves exactly like ptrace() (returning its result with errno set). If the
+// attach is rejected via the deny-attach signal it sets `denied_attach` and
+// returns -1 with errno set to EPERM.
+int PTraceAttachExcDenyAttachSafe(pid_t pid, bool &denied_attach) {
+  denied_attach = false;
+
+  struct sigaction new_action = {};
+  struct sigaction old_action = {};
+  new_action.sa_handler = DenyAttachSIGSEGVHandler;
+  sigemptyset(&new_action.sa_mask);
+  // SA_NODEFER so a genuine fault inside the handler crashes normally instead
+  // of deadlocking with SIGSEGV blocked.
+  new_action.sa_flags = SA_NODEFER;
+
+  if (::sigaction(SIGSEGV, &new_action, &old_action) != 0) {
+    // Couldn't install the handler; fall back to the unguarded call.
+    return ::ptrace(PT_ATTACHEXC, pid, 0, 0);
+  }
+
+  g_deny_attach_thread = pthread_self();
+  int result;
+  int saved_errno;
+  if (sigsetjmp(g_deny_attach_jmpbuf, 1) == 0) {
+    g_deny_attach_armed = 1;
+    result = ::ptrace(PT_ATTACHEXC, pid, 0, 0);
+    saved_errno = errno;
+    g_deny_attach_armed = 0;
+  } else {
+    // The kernel delivered SIGSEGV: the target denied the attach.
+    denied_attach = true;
+    result = -1;
+    saved_errno = EPERM;
+  }
+
+  ::sigaction(SIGSEGV, &old_action, nullptr);
+  errno = saved_errno;
+  return result;
+}
+} // namespace
+
 pid_t MachProcess::AttachForDebug(
-    pid_t pid, 
-    const RNBContext::IgnoredExceptions &ignored_exceptions, 
-    char *err_str,
-    size_t err_len) {
+    pid_t pid, const RNBContext::IgnoredExceptions &ignored_exceptions,
+    char *err_str, size_t err_len) {
   // Clear out and clean up from any current state
   Clear();
   if (pid != 0) {
@@ -2973,7 +3042,8 @@ static uint64_t bits(uint64_t value, uint32_t msbit, 
uint32_t lsbit) {
     DNBLog("[LaunchAttach] (%d) About to ptrace(PT_ATTACHEXC, %d)...", 
getpid(),
            pid);
     errno = 0;
-    int ptrace_result = ::ptrace(PT_ATTACHEXC, pid, 0, 0);
+    bool denied_attach = false;
+    int ptrace_result = PTraceAttachExcDenyAttachSafe(pid, denied_attach);
     int ptrace_errno = errno;
     DNBLog("[LaunchAttach] (%d) Completed ptrace(PT_ATTACHEXC, %d) == %d",
            getpid(), pid, ptrace_result);
@@ -2990,6 +3060,17 @@ static uint64_t bits(uint64_t value, uint32_t msbit, 
uint32_t lsbit) {
       m_flags |= eMachProcessFlagsAttached;
       DNBLogThreadedIf(LOG_PROCESS, "successfully attached to pid %d", pid);
       return m_pid;
+    } else if (denied_attach) {
+      // The target denied being debugged via ptrace(PT_DENY_ATTACH). The 
kernel
+      // would normally kill debugserver for attempting this; we caught the
+      // signal instead, so report a useful error rather than crashing.
+      snprintf(err_str, err_len,
+               "cannot attach to process %d because it has disabled debugging "
+               "via ptrace(PT_DENY_ATTACH)",
+               pid);
+      DNBLogError("[LaunchAttach] (%d) MachProcess::AttachForDebug pid %d "
+                  "denied attach via ptrace(PT_DENY_ATTACH)",
+                  getpid(), pid);
     } else {
       ::snprintf(err_str, err_len, "%s", err.AsString());
       DNBLogError(

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

Reply via email to