llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-lldb
Author: Jonas Devlieghere (JDevlieghere)
<details>
<summary>Changes</summary>
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
---
Full diff: https://github.com/llvm/llvm-project/pull/204688.diff
4 Files Affected:
- (added) lldb/test/API/macosx/deny-attach/Makefile (+3)
- (added) lldb/test/API/macosx/deny-attach/TestDenyAttach.py (+36)
- (added) lldb/test/API/macosx/deny-attach/main.c (+60)
- (modified) lldb/tools/debugserver/source/MacOSX/MachProcess.mm (+86-5)
``````````diff
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(
``````````
</details>
https://github.com/llvm/llvm-project/pull/204688
_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits