https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/204688
>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 1/2] [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( >From 53f412087cc94bf24282b77c6cbd37251a036a3d Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <[email protected]> Date: Thu, 18 Jun 2026 15:04:07 -0700 Subject: [PATCH 2/2] Spice up error message --- lldb/tools/debugserver/source/MacOSX/MachProcess.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lldb/tools/debugserver/source/MacOSX/MachProcess.mm b/lldb/tools/debugserver/source/MacOSX/MachProcess.mm index 0eb9640b98c91..3374267f7bde8 100644 --- a/lldb/tools/debugserver/source/MacOSX/MachProcess.mm +++ b/lldb/tools/debugserver/source/MacOSX/MachProcess.mm @@ -3066,7 +3066,8 @@ int PTraceAttachExcDenyAttachSafe(pid_t pid, bool &denied_attach) { // 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)", + "via ptrace(PT_DENY_ATTACH). Attach earlier, put a breakpoint " + "on ptrace and return 0.", pid); DNBLogError("[LaunchAttach] (%d) MachProcess::AttachForDebug pid %d " "denied attach via ptrace(PT_DENY_ATTACH)", _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
