Skip to content

Commit 97aeeec

Browse files
committed
test: check SpawnProcess post-fork safety
This test fails without the previous commit.
1 parent 3659fce commit 97aeeec

File tree

2 files changed

+98
-0
lines changed

2 files changed

+98
-0
lines changed

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_stress.cpp
2930
mp/test/test.cpp
3031
)
3132
include(${PROJECT_SOURCE_DIR}/cmake/TargetCapnpSources.cmake)

test/mp/test/spawn_stress.cpp

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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 <kj/test.h>
6+
7+
#include <chrono>
8+
#include <condition_variable>
9+
#include <csignal>
10+
#include <mutex>
11+
#include <mp/util.h>
12+
#include <string>
13+
#include <thread>
14+
#include <unistd.h>
15+
#include <vector>
16+
#include <sys/wait.h>
17+
18+
namespace {
19+
20+
using namespace std::chrono_literals;
21+
22+
static bool WaitPidWithTimeout(int pid, std::chrono::milliseconds timeout, int& status_out)
23+
{
24+
const auto deadline = std::chrono::steady_clock::now() + timeout;
25+
while (std::chrono::steady_clock::now() < deadline) {
26+
const int r = ::waitpid(pid, &status_out, WNOHANG);
27+
if (r == pid) return true;
28+
if (r == 0) {
29+
std::this_thread::sleep_for(1ms);
30+
continue;
31+
}
32+
// waitpid error
33+
return false;
34+
}
35+
return false;
36+
}
37+
38+
} // namespace
39+
40+
KJ_TEST("SpawnProcess does not run callback in child")
41+
{
42+
// This test is designed to fail deterministically if fd_to_args is invoked
43+
// in the post-fork child: a mutex held by another parent thread at fork
44+
// time appears locked forever in the child.
45+
std::mutex target_mutex;
46+
std::mutex control_mutex;
47+
std::condition_variable control_cv;
48+
bool locked = false;
49+
bool release = false;
50+
51+
std::thread locker([&] {
52+
std::unique_lock<std::mutex> target_lock(target_mutex);
53+
{
54+
std::lock_guard<std::mutex> g(control_mutex);
55+
locked = true;
56+
}
57+
control_cv.notify_one();
58+
59+
std::unique_lock<std::mutex> control_lock(control_mutex);
60+
control_cv.wait(control_lock, [&] { return release; });
61+
});
62+
63+
{
64+
std::unique_lock<std::mutex> l(control_mutex);
65+
control_cv.wait(l, [&] { return locked; });
66+
}
67+
68+
// Release the lock shortly after SpawnProcess starts.
69+
std::thread releaser([&] {
70+
std::this_thread::sleep_for(50ms);
71+
{
72+
std::lock_guard<std::mutex> g(control_mutex);
73+
release = true;
74+
}
75+
control_cv.notify_one();
76+
});
77+
78+
int pid = -1;
79+
const int fd = mp::SpawnProcess(pid, [&](int child_fd) -> std::vector<std::string> {
80+
std::lock_guard<std::mutex> g(target_mutex);
81+
return {"/usr/bin/true", std::to_string(child_fd)};
82+
});
83+
::close(fd);
84+
85+
int status = 0;
86+
const bool exited = WaitPidWithTimeout(pid, 1000ms, status);
87+
if (!exited) {
88+
::kill(pid, SIGKILL);
89+
::waitpid(pid, &status, 0);
90+
}
91+
92+
releaser.join();
93+
locker.join();
94+
95+
KJ_EXPECT(exited, "Timeout waiting for child process to exit");
96+
KJ_EXPECT(WIFEXITED(status) && WEXITSTATUS(status) == 0);
97+
}

0 commit comments

Comments
 (0)