-
Notifications
You must be signed in to change notification settings - Fork 14.3k
Fix lldb crash while handling concurrent vfork() #81564
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d65900f
b4c60c3
2e7bf23
7340372
6a528dd
bc36011
f06d81a
476511f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
CXX_SOURCES := main.cpp | ||
ENABLE_THREADS := YES | ||
|
||
include Makefile.rules |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
""" | ||
Make sure that the concurrent vfork() from multiple threads works correctly. | ||
""" | ||
|
||
import lldb | ||
import lldbsuite.test.lldbutil as lldbutil | ||
from lldbsuite.test.lldbtest import * | ||
from lldbsuite.test.decorators import * | ||
|
||
|
||
class TestConcurrentVFork(TestBase): | ||
NO_DEBUG_INFO_TESTCASE = True | ||
|
||
def build_run_to_breakpoint(self, use_fork, call_exec): | ||
self.build() | ||
|
||
args = [] | ||
if use_fork: | ||
args.append("--fork") | ||
if call_exec: | ||
args.append("--exec") | ||
launch_info = lldb.SBLaunchInfo(args) | ||
launch_info.SetWorkingDirectory(self.getBuildDir()) | ||
|
||
return lldbutil.run_to_source_breakpoint( | ||
self, "// break here", lldb.SBFileSpec("main.cpp") | ||
) | ||
|
||
def follow_parent_helper(self, use_fork, call_exec): | ||
(target, process, thread, bkpt) = self.build_run_to_breakpoint( | ||
use_fork, call_exec | ||
) | ||
|
||
parent_pid = target.FindFirstGlobalVariable("g_pid").GetValueAsUnsigned() | ||
self.runCmd("settings set target.process.follow-fork-mode parent") | ||
self.runCmd("settings set target.process.stop-on-exec False", check=False) | ||
self.expect( | ||
"continue", substrs=[f"Process {parent_pid} exited with status = 0"] | ||
) | ||
|
||
def follow_child_helper(self, use_fork, call_exec): | ||
self.build_run_to_breakpoint(use_fork, call_exec) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (target, process, thread, bkpt) = self.build_run_to_breakpoint(use_fork, call_exec) |
||
|
||
self.runCmd("settings set target.process.follow-fork-mode child") | ||
self.runCmd("settings set target.process.stop-on-exec False", check=False) | ||
# Child process exits with code "index + 10" since index is [0-4] | ||
# so the exit code should be 1[0-4] | ||
self.expect("continue", patterns=[r"exited with status = 1[0-4]"]) | ||
|
||
@skipUnlessPlatform(["linux"]) | ||
def test_follow_parent_vfork_no_exec(self): | ||
""" | ||
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-parent. | ||
And follow-parent successfully detach all child processes and exit debugger without calling exec. | ||
""" | ||
self.follow_parent_helper(use_fork=False, call_exec=False) | ||
|
||
@skipUnlessPlatform(["linux"]) | ||
def test_follow_parent_fork_no_exec(self): | ||
""" | ||
Make sure that debugging concurrent fork() from multiple threads won't crash lldb during follow-parent. | ||
And follow-parent successfully detach all child processes and exit debugger without calling exec | ||
""" | ||
self.follow_parent_helper(use_fork=True, call_exec=False) | ||
|
||
@skipUnlessPlatform(["linux"]) | ||
def test_follow_parent_vfork_call_exec(self): | ||
""" | ||
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-parent. | ||
And follow-parent successfully detach all child processes and exit debugger after calling exec. | ||
""" | ||
self.follow_parent_helper(use_fork=False, call_exec=True) | ||
|
||
@skipUnlessPlatform(["linux"]) | ||
def test_follow_parent_fork_call_exec(self): | ||
""" | ||
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-parent. | ||
And follow-parent successfully detach all child processes and exit debugger after calling exec. | ||
""" | ||
self.follow_parent_helper(use_fork=True, call_exec=True) | ||
|
||
@skipUnlessPlatform(["linux"]) | ||
def test_follow_child_vfork_no_exec(self): | ||
""" | ||
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-child. | ||
And follow-child successfully detach parent process and exit child process with correct exit code without calling exec. | ||
""" | ||
self.follow_child_helper(use_fork=False, call_exec=False) | ||
|
||
@skipUnlessPlatform(["linux"]) | ||
def test_follow_child_fork_no_exec(self): | ||
""" | ||
Make sure that debugging concurrent fork() from multiple threads won't crash lldb during follow-child. | ||
And follow-child successfully detach parent process and exit child process with correct exit code without calling exec. | ||
""" | ||
self.follow_child_helper(use_fork=True, call_exec=False) | ||
|
||
@skipUnlessPlatform(["linux"]) | ||
def test_follow_child_vfork_call_exec(self): | ||
""" | ||
Make sure that debugging concurrent vfork() from multiple threads won't crash lldb during follow-child. | ||
And follow-child successfully detach parent process and exit child process with correct exit code after calling exec. | ||
""" | ||
self.follow_child_helper(use_fork=False, call_exec=True) | ||
|
||
@skipUnlessPlatform(["linux"]) | ||
def test_follow_child_fork_call_exec(self): | ||
""" | ||
Make sure that debugging concurrent fork() from multiple threads won't crash lldb during follow-child. | ||
And follow-child successfully detach parent process and exit child process with correct exit code after calling exec. | ||
""" | ||
self.follow_child_helper(use_fork=True, call_exec=True) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
#include <assert.h> | ||
#include <iostream> | ||
#include <mutex> | ||
#include <sys/wait.h> | ||
#include <thread> | ||
#include <unistd.h> | ||
#include <vector> | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be a good idea to create a global
I will comment below where to use it. |
||
pid_t g_pid = 0; | ||
std::mutex g_child_pids_mutex; | ||
std::vector<pid_t> g_child_pids; | ||
|
||
const char *g_program = nullptr; | ||
bool g_use_vfork = true; // Use vfork by default. | ||
bool g_call_exec = false; // Does not call exec by default. | ||
|
||
int call_vfork(int index) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably want to test with both
|
||
pid_t child_pid = 0; | ||
if (g_use_vfork) { | ||
child_pid = vfork(); | ||
} else { | ||
child_pid = fork(); | ||
} | ||
|
||
if (child_pid == -1) { | ||
// Error handling | ||
perror("vfork"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return 1; | ||
} else if (child_pid == 0) { | ||
// This code is executed by the child process | ||
g_pid = getpid(); | ||
printf("Child process: %d\n", g_pid); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add code to call
|
||
|
||
if (g_call_exec) { | ||
std::string child_exit_code = std::to_string(index + 10); | ||
execl(g_program, g_program, "--child", child_exit_code.c_str(), NULL); | ||
} else { | ||
_exit(index + 10); | ||
} | ||
} else { | ||
// This code is executed by the parent process | ||
printf("[Parent] Forked process id: %d\n", child_pid); | ||
} | ||
return 0; | ||
} | ||
|
||
void wait_all_children_to_exit() { | ||
std::lock_guard<std::mutex> Lock(g_child_pids_mutex); | ||
for (pid_t child_pid : g_child_pids) { | ||
int child_status = 0; | ||
pid_t pid = waitpid(child_pid, &child_status, 0); | ||
if (child_status != 0) { | ||
int exit_code = WEXITSTATUS(child_status); | ||
if (exit_code > 15 || exit_code < 10) { | ||
printf("Error: child process exits with unexpected code %d\n", | ||
exit_code); | ||
_exit(1); // This will let our program know that some child processes | ||
// didn't exist with an expected exit status. | ||
} | ||
} | ||
if (pid != child_pid) | ||
_exit(2); // This will let our program know it didn't succeed | ||
} | ||
} | ||
|
||
void create_threads(int num_threads) { | ||
std::vector<std::thread> threads; | ||
for (int i = 0; i < num_threads; ++i) { | ||
threads.emplace_back(std::thread(call_vfork, i)); | ||
} | ||
printf("Created %d threads, joining...\n", | ||
num_threads); // end_of_create_threads | ||
for (auto &thread : threads) { | ||
thread.join(); | ||
} | ||
wait_all_children_to_exit(); | ||
} | ||
|
||
// Can be called in various ways: | ||
// 1. [program]: use vfork and not call exec | ||
// 2. [program] --fork: use fork and not call exec | ||
// 3. [program] --fork --exec: use fork and call exec | ||
// 4. [program] --exec: use vfork and call exec | ||
// 5. [program] --child [exit_code]: child process | ||
int main(int argc, char *argv[]) { | ||
g_pid = getpid(); | ||
g_program = argv[0]; | ||
|
||
for (int i = 1; i < argc; ++i) { | ||
if (strcmp(argv[i], "--child") == 0) { | ||
assert(i + 1 < argc); | ||
int child_exit_code = std::stoi(argv[i + 1]); | ||
printf("Child process: %d, exiting with code %d\n", g_pid, | ||
child_exit_code); | ||
_exit(child_exit_code); | ||
} else if (strcmp(argv[i], "--fork") == 0) | ||
g_use_vfork = false; | ||
else if (strcmp(argv[i], "--exec") == 0) | ||
g_call_exec = true; | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. parse args manually to check if we should use
|
||
int num_threads = 5; // break here | ||
create_threads(num_threads); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pass
|
||
return 0; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now reap the children you spawned:
This will ensure that if we stayed with the parent, then we successfully resumed all of the child processes and that they didn't crash and that they exited. If we don't resume the child processes correctly, they can get caught in limbo if they weren't resumed and this test will deadlock waiting for the child process to exit. There are some options you can do that allows you to not hang where instead of zero passed as the last parameter of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return all of the info from
lldbutil.run_to_source_breakpoint
from this function: