Skip to content

Commit dbbe02e

Browse files
committed
test: check SpawnProcess post-fork safety
This test fails without the previous commit.
1 parent 9d3c2b6 commit dbbe02e

File tree

2 files changed

+99
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)