Skip to content

Commit 5f3e106

Browse files
authored
[lldb/linux] Make sure the process continues running after a detach (#88494)
Fixes #85084 Whenever an inferior thread stops, lldb-server sends a SIGSTOP to all other threads in the process to force them to stop as well. If those threads stop on their own before they get a signal, this SIGSTOP will remain pending and be delivered the next time the process resumes. Normally, this is not a problem, because lldb-server will detect this stale SIGSTOP and resume the process. However, if we detach from the process while it has these SIGSTOPs pending, they will get immediately delivered, and the process will remain stopped (most likely forever). This patch fixes that by sending a SIGCONT just before detaching from the process. This signal cancels out any pending SIGSTOPs, and ensures it is able to run after we detach. It does have one somewhat unfortunate side-effect that in that the process's SIGCONT handler (if it has one) will get executed spuriously (from the process's POV). This could be _sometimes_ avoided by tracking which threads got send a SIGSTOP, and whether those threads stopped due to it. From what I could tell by observing its behavior, this is what gdb does. I have not tried to replicate that behavior here because it adds a nontrivial amount of complexity and the result is still uncertain -- we still need to send a SIGCONT (and execute the handler) when any thread stops for some other reason (and leaves our SIGSTOP hanging). Furthermore, since SIGSTOPs don't stack, it's also possible that our SIGSTOP/SIGCONT combination will cancel a genuine SIGSTOP being sent to the debugger application (by someone else), and there is nothing we can do about that. For this reason I think it's simplest and most predictible to just always send a SIGCONT when detaching, but if it turns out this is breaking something, we can consider implementing something more elaborate. One alternative I did try is to use PTRACE_INTERRUPT to suspend the threads instead of a SIGSTOP. PTRACE_INTERUPT requires using PTRACE_SEIZE to attach to the process, which also made this solution somewhat complicated, but the main problem with that approach is that PTRACE_INTERRUPT is not considered to be a signal-delivery-stop, which means it's not possible to resume it while injecting another signal to the inferior (which some of our tests expect to be able to do). This limitation could be worked around by forcing the thread into a signal delivery stop whenever we need to do this, but this additional complication is what made me think this approach is also not worthwhile. This patch should fix (at least some of) the problems with TestConcurrentVFork, but I've also added a dedicated test for checking that a process keeps running after we detach. Although the problem I'm fixing here is linux-specific, the core functinoality of not stopping after a detach should function the same way everywhere.
1 parent 792d437 commit 5f3e106

File tree

5 files changed

+115
-16
lines changed

5 files changed

+115
-16
lines changed

lldb/source/Plugins/Process/Linux/NativeProcessLinux.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,10 @@ Status NativeProcessLinux::Detach() {
10891089
if (GetID() == LLDB_INVALID_PROCESS_ID)
10901090
return error;
10911091

1092+
// Cancel out any SIGSTOPs we may have sent while stopping the process.
1093+
// Otherwise, the process may stop as soon as we detach from it.
1094+
kill(GetID(), SIGCONT);
1095+
10921096
for (const auto &thread : m_threads) {
10931097
Status e = Detach(thread->GetID());
10941098
if (e.Fail())
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CXX_SOURCES := main.cpp
2+
ENABLE_THREADS := YES
3+
4+
include Makefile.rules
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""
2+
Test that the process continues running after we detach from it.
3+
"""
4+
5+
import lldb
6+
import time
7+
from lldbsuite.test.decorators import *
8+
from lldbsuite.test.lldbtest import *
9+
from lldbsuite.test import lldbutil
10+
11+
12+
class DetachResumesTestCase(TestBase):
13+
NO_DEBUG_INFO_TESTCASE = True
14+
15+
def test_detach_resumes(self):
16+
self.build()
17+
exe = self.getBuildArtifact()
18+
19+
# The inferior will use this file to let us know it is ready to be
20+
# attached.
21+
sync_file_path = lldbutil.append_to_process_working_directory(
22+
self, "sync_file_%d" % (int(time.time()))
23+
)
24+
25+
# And this one to let us know it is running after we've detached from
26+
# it.
27+
exit_file_path = lldbutil.append_to_process_working_directory(
28+
self, "exit_file_%d" % (int(time.time()))
29+
)
30+
31+
popen = self.spawnSubprocess(
32+
self.getBuildArtifact(exe), [sync_file_path, exit_file_path]
33+
)
34+
lldbutil.wait_for_file_on_target(self, sync_file_path)
35+
36+
self.runCmd("process attach -p " + str(popen.pid))
37+
38+
# Set a breakpoint at a place that will be called by multiple threads
39+
# simultaneously. On systems (e.g. linux) where the debugger needs to
40+
# send signals to suspend threads, these signals will race with threads
41+
# hitting the breakpoint (and stopping on their own).
42+
bpno = lldbutil.run_break_set_by_symbol(self, "break_here")
43+
44+
# And let the inferior know it can call the function.
45+
self.runCmd("expr -- wait_for_debugger_flag = false")
46+
47+
self.runCmd("continue")
48+
49+
self.expect(
50+
"thread list",
51+
STOPPED_DUE_TO_BREAKPOINT,
52+
substrs=["stopped", "stop reason = breakpoint"],
53+
)
54+
55+
# Detach, the process should keep running after this, and not be stopped
56+
# by the signals that the debugger may have used to suspend the threads.
57+
self.runCmd("detach")
58+
59+
lldbutil.wait_for_file_on_target(self, exit_file_path)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#include "pseudo_barrier.h"
2+
#include <chrono>
3+
#include <fcntl.h>
4+
#include <fstream>
5+
#include <stdio.h>
6+
#include <thread>
7+
#include <vector>
8+
9+
pseudo_barrier_t barrier;
10+
11+
constexpr size_t nthreads = 5;
12+
volatile bool wait_for_debugger_flag = true;
13+
14+
void break_here() {}
15+
16+
void tfunc() {
17+
pseudo_barrier_wait(barrier);
18+
19+
break_here();
20+
}
21+
22+
int main(int argc, char const *argv[]) {
23+
lldb_enable_attach();
24+
25+
if (argc < 3)
26+
return 1;
27+
28+
// Create a file to signal that this process has started up.
29+
std::ofstream(argv[1]).close();
30+
31+
// And wait for it to attach.
32+
for (int i = 0; i < 100 && wait_for_debugger_flag; ++i)
33+
std::this_thread::sleep_for(std::chrono::seconds(1));
34+
35+
// Fire up the threads and have them call break_here() simultaneously.
36+
pseudo_barrier_init(barrier, nthreads);
37+
std::vector<std::thread> threads;
38+
for (size_t i = 0; i < nthreads; ++i)
39+
threads.emplace_back(tfunc);
40+
41+
for (std::thread &t : threads)
42+
t.join();
43+
44+
// Create the file to let the debugger know we're running.
45+
std::ofstream(argv[2]).close();
46+
47+
return 0;
48+
}

lldb/test/API/functionalities/fork/concurrent_vfork/TestConcurrentVFork.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ def follow_child_helper(self, use_fork, call_exec):
4848
self.expect("continue", patterns=[r"exited with status = 1[0-4]"])
4949

5050
@skipUnlessPlatform(["linux"])
51-
# See https://github.com/llvm/llvm-project/issues/85084.
52-
@skipIf(oslist=["linux"], archs=["aarch64", "arm"])
5351
def test_follow_parent_vfork_no_exec(self):
5452
"""
5553
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-parent.
@@ -58,8 +56,6 @@ def test_follow_parent_vfork_no_exec(self):
5856
self.follow_parent_helper(use_fork=False, call_exec=False)
5957

6058
@skipUnlessPlatform(["linux"])
61-
# See https://github.com/llvm/llvm-project/issues/85084.
62-
@skipIf(oslist=["linux"], archs=["aarch64", "arm"])
6359
def test_follow_parent_fork_no_exec(self):
6460
"""
6561
Make sure that debugging concurrent fork() from multiple threads won't crash lldb during follow-parent.
@@ -68,8 +64,6 @@ def test_follow_parent_fork_no_exec(self):
6864
self.follow_parent_helper(use_fork=True, call_exec=False)
6965

7066
@skipUnlessPlatform(["linux"])
71-
# See https://github.com/llvm/llvm-project/issues/85084.
72-
@skipIf(oslist=["linux"], archs=["aarch64", "arm"])
7367
def test_follow_parent_vfork_call_exec(self):
7468
"""
7569
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-parent.
@@ -78,8 +72,6 @@ def test_follow_parent_vfork_call_exec(self):
7872
self.follow_parent_helper(use_fork=False, call_exec=True)
7973

8074
@skipUnlessPlatform(["linux"])
81-
# See https://github.com/llvm/llvm-project/issues/85084.
82-
@skipIf(oslist=["linux"], archs=["aarch64", "arm"])
8375
def test_follow_parent_fork_call_exec(self):
8476
"""
8577
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-parent.
@@ -88,8 +80,6 @@ def test_follow_parent_fork_call_exec(self):
8880
self.follow_parent_helper(use_fork=True, call_exec=True)
8981

9082
@skipUnlessPlatform(["linux"])
91-
# See https://github.com/llvm/llvm-project/issues/85084.
92-
@skipIf(oslist=["linux"], archs=["aarch64", "arm"])
9383
def test_follow_child_vfork_no_exec(self):
9484
"""
9585
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-child.
@@ -98,8 +88,6 @@ def test_follow_child_vfork_no_exec(self):
9888
self.follow_child_helper(use_fork=False, call_exec=False)
9989

10090
@skipUnlessPlatform(["linux"])
101-
# See https://github.com/llvm/llvm-project/issues/85084.
102-
@skipIf(oslist=["linux"], archs=["aarch64", "arm"])
10391
def test_follow_child_fork_no_exec(self):
10492
"""
10593
Make sure that debugging concurrent fork() from multiple threads won't crash lldb during follow-child.
@@ -108,8 +96,6 @@ def test_follow_child_fork_no_exec(self):
10896
self.follow_child_helper(use_fork=True, call_exec=False)
10997

11098
@skipUnlessPlatform(["linux"])
111-
# See https://github.com/llvm/llvm-project/issues/85084.
112-
@skipIf(oslist=["linux"], archs=["aarch64", "arm"])
11399
def test_follow_child_vfork_call_exec(self):
114100
"""
115101
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-child.
@@ -118,8 +104,6 @@ def test_follow_child_vfork_call_exec(self):
118104
self.follow_child_helper(use_fork=False, call_exec=True)
119105

120106
@skipUnlessPlatform(["linux"])
121-
# See https://github.com/llvm/llvm-project/issues/85084.
122-
@skipIf(oslist=["linux"], archs=["aarch64", "arm"])
123107
def test_follow_child_fork_call_exec(self):
124108
"""
125109
Make sure that debugging concurrent fork() from multiple threads won't crash lldb during follow-child.

0 commit comments

Comments
 (0)