Skip to content

Commit 4bcadd6

Browse files
committed
[lldb/driver] Fix SIGTSTP handling
Our SIGTSTP handler was working, but that was mostly accidental. The reason it worked is because lldb is multithreaded for most of its lifetime and the OS is reasonably fast at responding to signals. So, what happened was that the kill(SIGTSTP) which we sent from inside the handler was delivered to another thread while the handler was still set to SIG_DFL (which then correctly put the entire process to sleep). Sometimes it happened that the other thread got the second signal after the first thread had already restored the handler, in which case the signal handler would run again, and it would again attempt to send the SIGTSTP signal back to itself. Normally it didn't take many iterations for the signal to be delivered quickly enough. However, if you were unlucky (or were playing around with pexpect) you could get SIGTSTP while lldb was single-threaded, and in that case, lldb would go into an endless loop because the second SIGTSTP could only be handled on the main thread, and only after the handler for the first signal returned (and re-installed itself). In that situation the handler would keep re-sending the signal to itself. This patch fixes the issue by implementing the handler the way it supposed to be done: - before sending the second SIGTSTP, we unblock the signal (it gets automatically blocked upon entering the handler) - we use raise to send the signal, which makes sure it gets delivered to the thread which is running the handler This also means we don't need the SIGCONT handler, as our TSTP handler resumes right after the entire process is continued, and we can do the required work there. I also include a test case for the SIGTSTP flow. It uses pexpect, but it includes a couple of extra twists. Specifically, I needed to create an extra process on top of lldb, which will run lldb in a separate process group and simulate the role of the shell. This is needed because SIGTSTP is not effective on a session leader (the signal gets delivered, but it does not cause a stop) -- normally there isn't anyone to notice the stop. Differential Revision: https://reviews.llvm.org/D120320
1 parent 8f6ee17 commit 4bcadd6

File tree

4 files changed

+91
-12
lines changed

4 files changed

+91
-12
lines changed

lldb/packages/Python/lldbsuite/test/lldbpexpect.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@ class PExpectTest(TestBase):
2323
def expect_prompt(self):
2424
self.child.expect_exact(self.PROMPT)
2525

26-
def launch(self, executable=None, extra_args=None, timeout=60, dimensions=None):
26+
def launch(self, executable=None, extra_args=None, timeout=60,
27+
dimensions=None, run_under=None, post_spawn=None):
2728
logfile = getattr(sys.stdout, 'buffer',
2829
sys.stdout) if self.TraceOn() else None
2930

30-
args = ['--no-lldbinit', '--no-use-colors']
31+
args = []
32+
if run_under is not None:
33+
args += run_under
34+
args += [lldbtest_config.lldbExec, '--no-lldbinit', '--no-use-colors']
3135
for cmd in self.setUpCommands():
3236
args += ['-O', cmd]
3337
if executable is not None:
@@ -41,8 +45,11 @@ def launch(self, executable=None, extra_args=None, timeout=60, dimensions=None):
4145

4246
import pexpect
4347
self.child = pexpect.spawn(
44-
lldbtest_config.lldbExec, args=args, logfile=logfile,
48+
args[0], args=args[1:], logfile=logfile,
4549
timeout=timeout, dimensions=dimensions, env=env)
50+
51+
if post_spawn is not None:
52+
post_spawn()
4653
self.expect_prompt()
4754
for cmd in self.setUpCommands():
4855
self.child.expect_exact(cmd)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
Test lldb's handling of job control signals (SIGTSTP, SIGCONT).
3+
"""
4+
5+
6+
from lldbsuite.test.lldbtest import *
7+
from lldbsuite.test.lldbpexpect import PExpectTest
8+
9+
10+
class JobControlTest(PExpectTest):
11+
12+
mydir = TestBase.compute_mydir(__file__)
13+
14+
def test_job_control(self):
15+
def post_spawn():
16+
self.child.expect("PID=([0-9]+)")
17+
self.lldb_pid = int(self.child.match[1])
18+
19+
run_under = [sys.executable, self.getSourcePath('shell.py')]
20+
self.launch(run_under=run_under, post_spawn=post_spawn)
21+
22+
os.kill(self.lldb_pid, signal.SIGTSTP)
23+
self.child.expect("STATUS=([0-9]+)")
24+
status = int(self.child.match[1])
25+
26+
self.assertTrue(os.WIFSTOPPED(status))
27+
self.assertEquals(os.WSTOPSIG(status), signal.SIGTSTP)
28+
29+
os.kill(self.lldb_pid, signal.SIGCONT)
30+
31+
self.child.sendline("quit")
32+
self.child.expect("RETURNCODE=0")
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
Launch a process (given through argv) similar to how a shell would do it.
3+
"""
4+
5+
import signal
6+
import subprocess
7+
import sys
8+
import os
9+
10+
11+
def preexec_fn():
12+
# Block SIGTTOU generated by the tcsetpgrp call
13+
orig_mask = signal.pthread_sigmask(signal.SIG_BLOCK, [signal.SIGTTOU])
14+
15+
# Put us in a new process group.
16+
os.setpgid(0, 0)
17+
18+
# And put it in the foreground.
19+
fd = os.open("/dev/tty", os.O_RDONLY)
20+
os.tcsetpgrp(fd, os.getpgid(0))
21+
os.close(fd)
22+
23+
signal.pthread_sigmask(signal.SIG_SETMASK, orig_mask)
24+
25+
26+
if __name__ == "__main__":
27+
child = subprocess.Popen(sys.argv[1:], preexec_fn=preexec_fn)
28+
print("PID=%d" % child.pid)
29+
30+
_, status = os.waitpid(child.pid, os.WUNTRACED)
31+
print("STATUS=%d" % status)
32+
33+
returncode = child.wait()
34+
print("RETURNCODE=%d" % returncode)

lldb/tools/driver/Driver.cpp

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -671,23 +671,30 @@ void sigint_handler(int signo) {
671671
_exit(signo);
672672
}
673673

674-
void sigtstp_handler(int signo) {
674+
#ifndef _WIN32
675+
static void sigtstp_handler(int signo) {
675676
if (g_driver != nullptr)
676677
g_driver->GetDebugger().SaveInputTerminalState();
677678

679+
// Unblock the signal and remove our handler.
680+
sigset_t set;
681+
sigemptyset(&set);
682+
sigaddset(&set, signo);
683+
pthread_sigmask(SIG_UNBLOCK, &set, nullptr);
678684
signal(signo, SIG_DFL);
679-
kill(getpid(), signo);
685+
686+
// Now re-raise the signal. We will immediately suspend...
687+
raise(signo);
688+
// ... and resume after a SIGCONT.
689+
690+
// Now undo the modifications.
691+
pthread_sigmask(SIG_BLOCK, &set, nullptr);
680692
signal(signo, sigtstp_handler);
681-
}
682693

683-
void sigcont_handler(int signo) {
684694
if (g_driver != nullptr)
685695
g_driver->GetDebugger().RestoreInputTerminalState();
686-
687-
signal(signo, SIG_DFL);
688-
kill(getpid(), signo);
689-
signal(signo, sigcont_handler);
690696
}
697+
#endif
691698

692699
static void printHelp(LLDBOptTable &table, llvm::StringRef tool_name) {
693700
std::string usage_str = tool_name.str() + " [options]";
@@ -826,7 +833,6 @@ int main(int argc, char const *argv[]) {
826833
signal(SIGPIPE, SIG_IGN);
827834
signal(SIGWINCH, sigwinch_handler);
828835
signal(SIGTSTP, sigtstp_handler);
829-
signal(SIGCONT, sigcont_handler);
830836
#endif
831837

832838
int exit_code = 0;

0 commit comments

Comments
 (0)