Skip to content

Commit 5205a87

Browse files
committed
test: check SpawnProcess post-fork safety
This test fails without the previous commit.
1 parent 69652f0 commit 5205a87

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ if(BUILD_TESTING AND TARGET CapnProto::kj-test)
2626
${MP_PROXY_HDRS}
2727
mp/test/foo-types.h
2828
mp/test/foo.h
29+
mp/test/spawn_tests.cpp
2930
mp/test/test.cpp
3031
)
3132
include(${PROJECT_SOURCE_DIR}/cmake/TargetCapnpSources.cmake)

test/mp/test/spawn_tests.cpp

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <mp/util.h>
6+
7+
#include <kj/test.h>
8+
9+
#include <chrono>
10+
#include <compare>
11+
#include <condition_variable>
12+
#include <csignal>
13+
#include <cstdlib>
14+
#include <mutex>
15+
#include <string>
16+
#include <sys/wait.h>
17+
#include <thread>
18+
#include <unistd.h>
19+
#include <vector>
20+
21+
namespace {
22+
23+
// Poll for child process exit using waitpid(..., WNOHANG) until the child exits
24+
// or timeout expires. Returns true if the child exited and status_out was set.
25+
// Returns false on timeout or error.
26+
static bool WaitPidWithTimeout(int pid, std::chrono::milliseconds timeout, int& status_out)
27+
{
28+
const auto deadline = std::chrono::steady_clock::now() + timeout;
29+
while (std::chrono::steady_clock::now() < deadline) {
30+
const int r = ::waitpid(pid, &status_out, WNOHANG);
31+
if (r == pid) return true;
32+
if (r == 0) {
33+
std::this_thread::sleep_for(std::chrono::milliseconds{1});
34+
continue;
35+
}
36+
// waitpid error
37+
return false;
38+
}
39+
return false;
40+
}
41+
42+
} // namespace
43+
44+
KJ_TEST("SpawnProcess does not run callback in child")
45+
{
46+
// This test is designed to fail deterministically if fd_to_args is invoked
47+
// in the post-fork child: a mutex held by another parent thread at fork
48+
// time appears locked forever in the child.
49+
std::mutex target_mutex;
50+
std::mutex control_mutex;
51+
std::condition_variable control_cv;
52+
bool locked{false};
53+
bool release{false};
54+
55+
// Holds target_mutex until the releaser thread updates release
56+
std::thread locker([&] {
57+
std::unique_lock<std::mutex> target_lock(target_mutex);
58+
{
59+
std::lock_guard<std::mutex> g(control_mutex);
60+
locked = true;
61+
}
62+
control_cv.notify_one();
63+
64+
std::unique_lock<std::mutex> control_lock(control_mutex);
65+
control_cv.wait(control_lock, [&] { return release; });
66+
});
67+
68+
// Wait for target_mutex to be held by the locker thread.
69+
{
70+
std::unique_lock<std::mutex> l(control_mutex);
71+
control_cv.wait(l, [&] { return locked; });
72+
}
73+
74+
// Release the lock shortly after SpawnProcess starts.
75+
std::thread releaser([&] {
76+
// In the unlikely event a CI machine overshoots this delay, a
77+
// regression could be missed. This is preferable to spurious
78+
// test failures.
79+
std::this_thread::sleep_for(std::chrono::milliseconds{50});
80+
{
81+
std::lock_guard<std::mutex> g(control_mutex);
82+
release = true;
83+
}
84+
control_cv.notify_one();
85+
});
86+
87+
int pid{-1};
88+
const int fd{mp::SpawnProcess(pid, [&](int child_fd) -> std::vector<std::string> {
89+
// If this callback runs in the post-fork child, target_mutex appears
90+
// locked forever (the owning thread does not exist), so this deadlocks.
91+
std::lock_guard<std::mutex> g(target_mutex);
92+
return {"true", std::to_string(child_fd)};
93+
})};
94+
::close(fd);
95+
96+
int status{0};
97+
// Give the child up to 1 second to exit. If it does not, terminate it and
98+
// reap it to avoid leaving a zombie behind.
99+
const bool exited{WaitPidWithTimeout(pid, std::chrono::milliseconds{1000}, status)};
100+
if (!exited) {
101+
::kill(pid, SIGKILL);
102+
::waitpid(pid, &status, /*options=*/0);
103+
}
104+
105+
releaser.join();
106+
locker.join();
107+
108+
KJ_EXPECT(exited, "Timeout waiting for child process to exit");
109+
KJ_EXPECT(WIFEXITED(status) && WEXITSTATUS(status) == 0);
110+
}

0 commit comments

Comments
 (0)