From 980bb372de52744ce678c2cccaba0ba6d55d90a2 Mon Sep 17 00:00:00 2001 From: Saleem Abdulrasool Date: Mon, 27 Jul 2026 16:13:35 -0700 Subject: [PATCH] POSIX: detach every thread, not just the main pid Process::detach() only issued PTRACE_DETACH for _pid, the main thread. ptrace() traces each thread of a multithreaded process as its own tracee: threads created after the initial attach arrive via PTRACE_EVENT_CLONE rather than a fresh PTRACE_ATTACH, but each one still needs its own PTRACE_DETACH. Detaching only the main pid leaves every other thread parked in its last ptrace-stop indefinitely, so a detached multithreaded inferior can appear to hang rather than resume. This reproduces directly in lldb's TestDetachResumes: a program spawns five threads that all hit a breakpoint at once, the debugger detaches, and the test waits for the process to finish running on its own. The other four threads never got continued, so the process never exits. Enumerate every tracked thread and detach the rest before the existing, error-checked detach of the main pid. When LLDB sends D1, DebugSessionImplBase::onDetach suspends every thread first because the target is expected to remain stopped after detaching. PTRACE_DETACH always resumes the tracee as part of detaching regardless of any signal already pending from that earlier suspend, so the new per-thread detach calls need to carry that intent explicitly rather than rely on it as a side effect: pass SIGSTOP as the detach signal for every thread, main included, when the caller asked for the process to stay stopped, and 0 otherwise. detach() picks up a stopped parameter for this, threaded through from onDetach; the Windows override ignores it, since DebugActiveProcessStop always resumes the debuggee regardless. --- Headers/DebugServer2/Host/POSIX/PTrace.h | 2 +- Headers/DebugServer2/Target/POSIX/Process.h | 2 +- Headers/DebugServer2/Target/ProcessBase.h | 2 +- Headers/DebugServer2/Target/Windows/Process.h | 2 +- Sources/GDBRemote/DebugSessionImpl.cpp | 2 +- Sources/Host/POSIX/PTrace.cpp | 7 +++-- Sources/Target/POSIX/Process.cpp | 28 +++++++++++++++++-- Sources/Target/Windows/Process.cpp | 9 ++++-- 8 files changed, 42 insertions(+), 12 deletions(-) diff --git a/Headers/DebugServer2/Host/POSIX/PTrace.h b/Headers/DebugServer2/Host/POSIX/PTrace.h index 1e5085ec..a8786281 100644 --- a/Headers/DebugServer2/Host/POSIX/PTrace.h +++ b/Headers/DebugServer2/Host/POSIX/PTrace.h @@ -38,7 +38,7 @@ class PTrace { public: virtual ErrorCode attach(ProcessId pid); - virtual ErrorCode detach(ProcessId pid); + virtual ErrorCode detach(ProcessId pid, int signal = 0); public: virtual ErrorCode kill(ProcessThreadId const &ptid, int signal) = 0; diff --git a/Headers/DebugServer2/Target/POSIX/Process.h b/Headers/DebugServer2/Target/POSIX/Process.h index 4bed1e8c..182abdd5 100644 --- a/Headers/DebugServer2/Target/POSIX/Process.h +++ b/Headers/DebugServer2/Target/POSIX/Process.h @@ -39,7 +39,7 @@ class Process : public ds2::Target::ProcessBase { bool checkInterrupt(ThreadId tid, int waitStatus); public: - ErrorCode detach() override; + ErrorCode detach(bool stopped) override; ErrorCode interrupt() override; ErrorCode terminate() override; bool isAlive() const override; diff --git a/Headers/DebugServer2/Target/ProcessBase.h b/Headers/DebugServer2/Target/ProcessBase.h index c37b5ceb..9809c433 100644 --- a/Headers/DebugServer2/Target/ProcessBase.h +++ b/Headers/DebugServer2/Target/ProcessBase.h @@ -107,7 +107,7 @@ class ProcessBase { virtual void cleanup(); public: - virtual ErrorCode detach() = 0; + virtual ErrorCode detach(bool stopped) = 0; public: virtual ErrorCode suspend(); diff --git a/Headers/DebugServer2/Target/Windows/Process.h b/Headers/DebugServer2/Target/Windows/Process.h index abd7c921..81c77992 100644 --- a/Headers/DebugServer2/Target/Windows/Process.h +++ b/Headers/DebugServer2/Target/Windows/Process.h @@ -38,7 +38,7 @@ class Process : public ds2::Target::ProcessBase, ErrorCode writeDebugBreakCode(uint64_t address); public: - ErrorCode detach() override; + ErrorCode detach(bool stopped) override; ErrorCode interrupt() override; ErrorCode terminate() override; bool isAlive() const override; diff --git a/Sources/GDBRemote/DebugSessionImpl.cpp b/Sources/GDBRemote/DebugSessionImpl.cpp index 169e986f..66e2f2c6 100644 --- a/Sources/GDBRemote/DebugSessionImpl.cpp +++ b/Sources/GDBRemote/DebugSessionImpl.cpp @@ -1173,7 +1173,7 @@ ErrorCode DebugSessionImplBase::onDetach(Session &, ProcessId pid, CHK(_process->suspend()); } - return _process->detach(); + return _process->detach(stopped); } ErrorCode DebugSessionImplBase::onTerminate(Session &session, diff --git a/Sources/Host/POSIX/PTrace.cpp b/Sources/Host/POSIX/PTrace.cpp index 14775ded..8ded8e5a 100644 --- a/Sources/Host/POSIX/PTrace.cpp +++ b/Sources/Host/POSIX/PTrace.cpp @@ -76,13 +76,14 @@ ErrorCode PTrace::attach(ProcessId pid) { return kSuccess; } -ErrorCode PTrace::detach(ProcessId pid) { +ErrorCode PTrace::detach(ProcessId pid, int signal) { if (pid <= kAnyProcessId) return kErrorProcessNotFound; - DS2LOG(Debug, "detaching from pid %" PRIu64, (uint64_t)pid); + DS2LOG(Debug, "detaching from pid %" PRIu64 " with signal %d", (uint64_t)pid, + signal); - if (wrapPtrace(PTCMD(DETACH), pid, nullptr, nullptr) < 0) + if (wrapPtrace(PTCMD(DETACH), pid, nullptr, signal) < 0) return Platform::TranslateError(); return kSuccess; diff --git a/Sources/Target/POSIX/Process.cpp b/Sources/Target/POSIX/Process.cpp index beddde02..685b6ebb 100644 --- a/Sources/Target/POSIX/Process.cpp +++ b/Sources/Target/POSIX/Process.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -50,10 +51,33 @@ ErrorCode Process::initialize(ProcessId pid, uint32_t flags) { return kSuccess; } -ErrorCode Process::detach() { +ErrorCode Process::detach(bool stopped) { prepareForDetach(); - CHK(ptrace().detach(_pid)); + // PTRACE_DETACH resumes the tracee as it stops being traced, so a plain + // detach with no signal leaves it running. When the caller wants the + // target left stopped (LLDB's GDB-remote D1 detach-and-stay-stopped + // mode), SIGSTOP has to be delivered as part of the detach itself rather + // than relied on as a side effect of an earlier suspend, since suspending + // a thread beforehand only leaves a signal pending and does not by + // itself survive detaching. + int const signal = stopped ? SIGSTOP : 0; + + // ptrace() traces each thread of a multithreaded process as its own + // tracee. Threads created after the initial attach show up via + // PTRACE_EVENT_CLONE rather than a fresh PTRACE_ATTACH, but they still + // need their own PTRACE_DETACH. Detaching only the main pid would leave + // every other thread parked in its last ptrace-stop forever, so this + // best-effort detaches the rest of the threads first. + std::vector tids; + getThreadIds(tids); + for (ThreadId tid : tids) { + if (tid != _pid) { + ptrace().detach(tid, signal); + } + } + + CHK(ptrace().detach(_pid, signal)); cleanup(); _flags &= ~kFlagAttachedProcess; diff --git a/Sources/Target/Windows/Process.cpp b/Sources/Target/Windows/Process.cpp index 265c053f..7924ba7e 100644 --- a/Sources/Target/Windows/Process.cpp +++ b/Sources/Target/Windows/Process.cpp @@ -72,7 +72,7 @@ Process::~Process() { // exit, so we detach at this point. This is required because otherwise the // debugged winphone process might stay alive in an unclosable state. If // detached, the winphone process dies gracefully. - detach(); + detach(false); ::CloseHandle(_handle); } @@ -125,9 +125,14 @@ Target::Process *Process::Attach(ProcessId pid) { return process.release(); } -ErrorCode Process::detach() { +ErrorCode Process::detach(bool stopped) { prepareForDetach(); + // DebugActiveProcessStop has no equivalent of POSIX's detach-and-leave- + // stopped: the debuggee always resumes once debugging is stopped, so + // there is nothing here to do differently when stopped is requested. + (void)stopped; + BOOL result = DebugActiveProcessStop(_pid); if (!result) { return Platform::TranslateError();