Skip to content

Commit 8bdddcf

Browse files
jeffreytan81jeffreytan81
andauthored
Fix lldb crash while handling concurrent vfork() (llvm#81564)
We got user reporting lldb crash while the debuggee is calling vfork() concurrently from multiple threads. The crash happens because the current implementation can only handle single vfork, vforkdone protocol transaction. This diff fixes the crash by lldb-server storing forked debuggee's <pid, tid> pair in jstopinfo which will be decoded by lldb client to create StopInfoVFork for follow parent/child policy. Each StopInfoVFork will later have a corresponding vforkdone packet. So the patch also changes the `m_vfork_in_progress` to be reference counting based. Two new test cases are added which crash/assert without the changes in this patch. --------- Co-authored-by: jeffreytan81 <[email protected]>
1 parent 954f891 commit 8bdddcf

File tree

6 files changed

+243
-12
lines changed

6 files changed

+243
-12
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,9 @@ void NativeThreadLinux::SetStoppedByFork(bool is_vfork, lldb::pid_t child_pid) {
456456
m_stop_info.signo = SIGTRAP;
457457
m_stop_info.details.fork.child_pid = child_pid;
458458
m_stop_info.details.fork.child_tid = child_pid;
459+
m_stop_description = std::to_string(child_pid);
460+
m_stop_description += " ";
461+
m_stop_description += std::to_string(child_pid);
459462
}
460463

461464
void NativeThreadLinux::SetStoppedByVForkDone() {

lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,9 @@ ProcessGDBRemote::ProcessGDBRemote(lldb::TargetSP target_sp,
263263
m_continue_C_tids(), m_continue_s_tids(), m_continue_S_tids(),
264264
m_max_memory_size(0), m_remote_stub_max_memory_size(0),
265265
m_addr_to_mmap_size(), m_thread_create_bp_sp(),
266-
m_waiting_for_attach(false),
267-
m_command_sp(), m_breakpoint_pc_offset(0),
266+
m_waiting_for_attach(false), m_command_sp(), m_breakpoint_pc_offset(0),
268267
m_initial_tid(LLDB_INVALID_THREAD_ID), m_allow_flash_writes(false),
269-
m_erased_flash_ranges(), m_vfork_in_progress(false) {
268+
m_erased_flash_ranges(), m_vfork_in_progress_count(0) {
270269
m_async_broadcaster.SetEventName(eBroadcastBitAsyncThreadShouldExit,
271270
"async thread should exit");
272271
m_async_broadcaster.SetEventName(eBroadcastBitAsyncContinue,
@@ -5293,8 +5292,10 @@ class CommandObjectProcessGDBRemoteSpeedTest : public CommandObjectParsed {
52935292
(ProcessGDBRemote *)m_interpreter.GetExecutionContext()
52945293
.GetProcessPtr();
52955294
if (process) {
5296-
StreamSP output_stream_sp(
5297-
m_interpreter.GetDebugger().GetAsyncOutputStream());
5295+
StreamSP output_stream_sp = result.GetImmediateOutputStream();
5296+
if (!output_stream_sp)
5297+
output_stream_sp =
5298+
StreamSP(m_interpreter.GetDebugger().GetAsyncOutputStream());
52985299
result.SetImmediateOutputStream(output_stream_sp);
52995300

53005301
const uint32_t num_packets =
@@ -5634,8 +5635,11 @@ void ProcessGDBRemote::DidFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
56345635
void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
56355636
Log *log = GetLog(GDBRLog::Process);
56365637

5637-
assert(!m_vfork_in_progress);
5638-
m_vfork_in_progress = true;
5638+
LLDB_LOG(
5639+
log,
5640+
"ProcessGDBRemote::DidFork() called for child_pid: {0}, child_tid {1}",
5641+
child_pid, child_tid);
5642+
++m_vfork_in_progress_count;
56395643

56405644
// Disable all software breakpoints for the duration of vfork.
56415645
if (m_gdb_comm.SupportsGDBStoppointPacket(eBreakpointSoftware))
@@ -5689,8 +5693,8 @@ void ProcessGDBRemote::DidVFork(lldb::pid_t child_pid, lldb::tid_t child_tid) {
56895693
}
56905694

56915695
void ProcessGDBRemote::DidVForkDone() {
5692-
assert(m_vfork_in_progress);
5693-
m_vfork_in_progress = false;
5696+
assert(m_vfork_in_progress_count > 0);
5697+
--m_vfork_in_progress_count;
56945698

56955699
// Reenable all software breakpoints that were enabled before vfork.
56965700
if (m_gdb_comm.SupportsGDBStoppointPacket(eBreakpointSoftware))
@@ -5700,7 +5704,9 @@ void ProcessGDBRemote::DidVForkDone() {
57005704
void ProcessGDBRemote::DidExec() {
57015705
// If we are following children, vfork is finished by exec (rather than
57025706
// vforkdone that is submitted for parent).
5703-
if (GetFollowForkMode() == eFollowChild)
5704-
m_vfork_in_progress = false;
5707+
if (GetFollowForkMode() == eFollowChild) {
5708+
if (m_vfork_in_progress_count > 0)
5709+
--m_vfork_in_progress_count;
5710+
}
57055711
Process::DidExec();
57065712
}

lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ class ProcessGDBRemote : public Process,
301301
using FlashRange = FlashRangeVector::Entry;
302302
FlashRangeVector m_erased_flash_ranges;
303303

304-
bool m_vfork_in_progress;
304+
// Number of vfork() operations being handled.
305+
uint32_t m_vfork_in_progress_count;
305306

306307
// Accessors
307308
bool IsRunning(lldb::StateType state) {
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: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""
2+
Make sure that the concurrent vfork() from multiple threads works correctly.
3+
"""
4+
5+
import lldb
6+
import lldbsuite.test.lldbutil as lldbutil
7+
from lldbsuite.test.lldbtest import *
8+
from lldbsuite.test.decorators import *
9+
10+
11+
class TestConcurrentVFork(TestBase):
12+
NO_DEBUG_INFO_TESTCASE = True
13+
14+
def build_run_to_breakpoint(self, use_fork, call_exec):
15+
self.build()
16+
17+
args = []
18+
if use_fork:
19+
args.append("--fork")
20+
if call_exec:
21+
args.append("--exec")
22+
launch_info = lldb.SBLaunchInfo(args)
23+
launch_info.SetWorkingDirectory(self.getBuildDir())
24+
25+
return lldbutil.run_to_source_breakpoint(
26+
self, "// break here", lldb.SBFileSpec("main.cpp")
27+
)
28+
29+
def follow_parent_helper(self, use_fork, call_exec):
30+
(target, process, thread, bkpt) = self.build_run_to_breakpoint(
31+
use_fork, call_exec
32+
)
33+
34+
parent_pid = target.FindFirstGlobalVariable("g_pid").GetValueAsUnsigned()
35+
self.runCmd("settings set target.process.follow-fork-mode parent")
36+
self.runCmd("settings set target.process.stop-on-exec False", check=False)
37+
self.expect(
38+
"continue", substrs=[f"Process {parent_pid} exited with status = 0"]
39+
)
40+
41+
def follow_child_helper(self, use_fork, call_exec):
42+
self.build_run_to_breakpoint(use_fork, call_exec)
43+
44+
self.runCmd("settings set target.process.follow-fork-mode child")
45+
self.runCmd("settings set target.process.stop-on-exec False", check=False)
46+
# Child process exits with code "index + 10" since index is [0-4]
47+
# so the exit code should be 1[0-4]
48+
self.expect("continue", patterns=[r"exited with status = 1[0-4]"])
49+
50+
@skipUnlessPlatform(["linux"])
51+
def test_follow_parent_vfork_no_exec(self):
52+
"""
53+
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-parent.
54+
And follow-parent successfully detach all child processes and exit debugger without calling exec.
55+
"""
56+
self.follow_parent_helper(use_fork=False, call_exec=False)
57+
58+
@skipUnlessPlatform(["linux"])
59+
def test_follow_parent_fork_no_exec(self):
60+
"""
61+
Make sure that debugging concurrent fork() from multiple threads won't crash lldb during follow-parent.
62+
And follow-parent successfully detach all child processes and exit debugger without calling exec
63+
"""
64+
self.follow_parent_helper(use_fork=True, call_exec=False)
65+
66+
@skipUnlessPlatform(["linux"])
67+
def test_follow_parent_vfork_call_exec(self):
68+
"""
69+
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-parent.
70+
And follow-parent successfully detach all child processes and exit debugger after calling exec.
71+
"""
72+
self.follow_parent_helper(use_fork=False, call_exec=True)
73+
74+
@skipUnlessPlatform(["linux"])
75+
def test_follow_parent_fork_call_exec(self):
76+
"""
77+
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-parent.
78+
And follow-parent successfully detach all child processes and exit debugger after calling exec.
79+
"""
80+
self.follow_parent_helper(use_fork=True, call_exec=True)
81+
82+
@skipUnlessPlatform(["linux"])
83+
def test_follow_child_vfork_no_exec(self):
84+
"""
85+
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-child.
86+
And follow-child successfully detach parent process and exit child process with correct exit code without calling exec.
87+
"""
88+
self.follow_child_helper(use_fork=False, call_exec=False)
89+
90+
@skipUnlessPlatform(["linux"])
91+
def test_follow_child_fork_no_exec(self):
92+
"""
93+
Make sure that debugging concurrent fork() from multiple threads won't crash lldb during follow-child.
94+
And follow-child successfully detach parent process and exit child process with correct exit code without calling exec.
95+
"""
96+
self.follow_child_helper(use_fork=True, call_exec=False)
97+
98+
@skipUnlessPlatform(["linux"])
99+
def test_follow_child_vfork_call_exec(self):
100+
"""
101+
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-child.
102+
And follow-child successfully detach parent process and exit child process with correct exit code after calling exec.
103+
"""
104+
self.follow_child_helper(use_fork=False, call_exec=True)
105+
106+
@skipUnlessPlatform(["linux"])
107+
def test_follow_child_fork_call_exec(self):
108+
"""
109+
Make sure that debugging concurrent fork() from multiple threads won't crash lldb during follow-child.
110+
And follow-child successfully detach parent process and exit child process with correct exit code after calling exec.
111+
"""
112+
self.follow_child_helper(use_fork=True, call_exec=True)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#include <assert.h>
2+
#include <iostream>
3+
#include <mutex>
4+
#include <sys/wait.h>
5+
#include <thread>
6+
#include <unistd.h>
7+
#include <vector>
8+
9+
pid_t g_pid = 0;
10+
std::mutex g_child_pids_mutex;
11+
std::vector<pid_t> g_child_pids;
12+
13+
const char *g_program = nullptr;
14+
bool g_use_vfork = true; // Use vfork by default.
15+
bool g_call_exec = false; // Does not call exec by default.
16+
17+
int call_vfork(int index) {
18+
pid_t child_pid = 0;
19+
if (g_use_vfork) {
20+
child_pid = vfork();
21+
} else {
22+
child_pid = fork();
23+
}
24+
25+
if (child_pid == -1) {
26+
// Error handling
27+
perror("vfork");
28+
return 1;
29+
} else if (child_pid == 0) {
30+
// This code is executed by the child process
31+
g_pid = getpid();
32+
printf("Child process: %d\n", g_pid);
33+
34+
if (g_call_exec) {
35+
std::string child_exit_code = std::to_string(index + 10);
36+
execl(g_program, g_program, "--child", child_exit_code.c_str(), NULL);
37+
} else {
38+
_exit(index + 10);
39+
}
40+
} else {
41+
// This code is executed by the parent process
42+
printf("[Parent] Forked process id: %d\n", child_pid);
43+
}
44+
return 0;
45+
}
46+
47+
void wait_all_children_to_exit() {
48+
std::lock_guard<std::mutex> Lock(g_child_pids_mutex);
49+
for (pid_t child_pid : g_child_pids) {
50+
int child_status = 0;
51+
pid_t pid = waitpid(child_pid, &child_status, 0);
52+
if (child_status != 0) {
53+
int exit_code = WEXITSTATUS(child_status);
54+
if (exit_code > 15 || exit_code < 10) {
55+
printf("Error: child process exits with unexpected code %d\n",
56+
exit_code);
57+
_exit(1); // This will let our program know that some child processes
58+
// didn't exist with an expected exit status.
59+
}
60+
}
61+
if (pid != child_pid)
62+
_exit(2); // This will let our program know it didn't succeed
63+
}
64+
}
65+
66+
void create_threads(int num_threads) {
67+
std::vector<std::thread> threads;
68+
for (int i = 0; i < num_threads; ++i) {
69+
threads.emplace_back(std::thread(call_vfork, i));
70+
}
71+
printf("Created %d threads, joining...\n",
72+
num_threads); // end_of_create_threads
73+
for (auto &thread : threads) {
74+
thread.join();
75+
}
76+
wait_all_children_to_exit();
77+
}
78+
79+
// Can be called in various ways:
80+
// 1. [program]: use vfork and not call exec
81+
// 2. [program] --fork: use fork and not call exec
82+
// 3. [program] --fork --exec: use fork and call exec
83+
// 4. [program] --exec: use vfork and call exec
84+
// 5. [program] --child [exit_code]: child process
85+
int main(int argc, char *argv[]) {
86+
g_pid = getpid();
87+
g_program = argv[0];
88+
89+
for (int i = 1; i < argc; ++i) {
90+
if (strcmp(argv[i], "--child") == 0) {
91+
assert(i + 1 < argc);
92+
int child_exit_code = std::stoi(argv[i + 1]);
93+
printf("Child process: %d, exiting with code %d\n", g_pid,
94+
child_exit_code);
95+
_exit(child_exit_code);
96+
} else if (strcmp(argv[i], "--fork") == 0)
97+
g_use_vfork = false;
98+
else if (strcmp(argv[i], "--exec") == 0)
99+
g_call_exec = true;
100+
}
101+
102+
int num_threads = 5; // break here
103+
create_threads(num_threads);
104+
return 0;
105+
}

0 commit comments

Comments
 (0)