Add regression test: a nohup-backgrounded process doesn't survive a session
Reproduces the user's reported real-world shape end to end with a real
busybox container ("nohup sleep 137 & exit"), verifying via the host's own
/proc that the backgrounded process is actually gone afterward -- checked
by cmdline substring, not pid, since a pid seen inside an isolated pid
namespace doesn't correspond to the same-numbered host pid. A bounded 2s
poll guards against the pid namespace's own kernel collapse-on-pid-1-exit
timing (which already covers this case for free on this dev machine).
Complements test_session_cleanup.cpp's existing [integration][root] test,
which exercises kill_via_cgroup() directly -- this one instead proves the
outward, visible contract holds through the real -r/--run path.
This commit is contained in:
@@ -46,12 +46,17 @@
|
||||
// wouldn't prove anything either way, so both checks below only ever look
|
||||
// at types `detect_bwrap_unshare_args()` reports as real.
|
||||
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
@@ -131,6 +136,60 @@ std::string run_in_fixture(const std::filesystem::path& image, const std::vector
|
||||
return capture.contents();
|
||||
}
|
||||
|
||||
// Scans /proc for any process whose cmdline contains `needle` -- used below
|
||||
// to confirm a backgrounded process detached inside a container doesn't
|
||||
// survive the session, checked from the *host's* own process table. This
|
||||
// works regardless of whether the session's own pid namespace is isolated
|
||||
// or not: every process, wherever it lives namespace-wise, is still a
|
||||
// perfectly ordinary task on the host with its own real pid and
|
||||
// /proc/<pid>/cmdline entry -- only the pid *number* a process sees for
|
||||
// itself differs inside an isolated namespace, not whether it shows up
|
||||
// here at all. Same directory-scanning shape as bwrap.cpp's own
|
||||
// find_fuse_overlayfs_pid().
|
||||
bool any_process_cmdline_contains(const std::string& needle) {
|
||||
std::error_code ec;
|
||||
auto it = std::filesystem::directory_iterator("/proc", ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& entry : it) {
|
||||
const std::string name = entry.path().filename().string();
|
||||
if (!std::all_of(name.begin(), name.end(), [](unsigned char c) { return std::isdigit(c); })) {
|
||||
continue;
|
||||
}
|
||||
std::ifstream cmdline_file(entry.path() / "cmdline", std::ios::binary);
|
||||
std::string cmdline((std::istreambuf_iterator<char>(cmdline_file)), std::istreambuf_iterator<char>());
|
||||
std::replace(cmdline.begin(), cmdline.end(), '\0', ' ');
|
||||
if (cmdline.find(needle) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bounded (2s, 50ms interval) poll for `needle` to disappear from the
|
||||
// host's own process table -- a session ending via a pid namespace's own
|
||||
// kernel-guaranteed collapse-on-pid-1-exit isn't necessarily synchronously
|
||||
// complete by the instant run_in_fixture() above returns (that guarantee is
|
||||
// about eventual termination, not that every other task in the namespace
|
||||
// has already been fully reaped), so a single instantaneous check right
|
||||
// after could be flaky. The straggler sweep this test actually targets
|
||||
// (run_bwrap()'s own post-exit cgroup sweep, bwrap.cpp) already applies a
|
||||
// real grace period internally for the same reason.
|
||||
bool process_cmdline_gone_within(const std::string& needle, int timeout_ms) {
|
||||
for (int waited = 0; waited < timeout_ms; waited += 50) {
|
||||
if (!any_process_cmdline_contains(needle)) {
|
||||
return true;
|
||||
}
|
||||
struct timespec ts {
|
||||
0, 50L * 1000000L
|
||||
};
|
||||
while (nanosleep(&ts, &ts) != 0 && errno == EINTR) {
|
||||
}
|
||||
}
|
||||
return !any_process_cmdline_contains(needle);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("rootless -r/--run: network namespace is genuinely isolated, loopback present", "[integration][net]") {
|
||||
@@ -208,3 +267,37 @@ TEST_CASE("rootless -r/--run: every kernel-supported namespace type differs from
|
||||
CHECK(lines[i] != read_own_namespace_link(to_check[i].c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for run_bwrap()'s own automatic post-exit straggler sweep
|
||||
// (bwrap.cpp/session_cgroup.h's "Resolved" entries in CLAUDE.md) -- the
|
||||
// concrete real-world shape the user reported: a container backgrounds a
|
||||
// long-running process with nohup (so it survives its own parent shell's
|
||||
// exit and ignores SIGHUP) and exits immediately, and that process must not
|
||||
// keep running after the session itself has ended. Unlike
|
||||
// test_session_cleanup.cpp's own [integration][root] test (which exercises
|
||||
// kill_via_cgroup() directly against two plain forked processes, since the
|
||||
// real target device's own no-pid-namespace escape shape can't be forced
|
||||
// via the CLI), this goes through the exact real -r/--run path end to end
|
||||
// with a real container -- on a kernel that supports pid namespaces (this
|
||||
// dev machine included), the kernel's own collapse-on-pid-1-exit guarantee
|
||||
// already covers this case for free, so passing here doesn't by itself
|
||||
// prove the cgroup sweep specifically fired; it proves the outward, visible
|
||||
// contract this feature exists for ("a stray process never survives a
|
||||
// session") holds end to end regardless of which mechanism provided it.
|
||||
TEST_CASE("rootless -r/--run: a nohup-backgrounded process does not survive the session", "[integration][net]") {
|
||||
auto image = find_busybox_fixture();
|
||||
if (!image) {
|
||||
SKIP("no busybox fixture (images/busybox.tar) -- see tests/setup-tests.py");
|
||||
}
|
||||
|
||||
// A distinctive duration -- not a realistic value anything else on this
|
||||
// host would coincidentally already be sleeping for -- so scanning
|
||||
// /proc for it can't produce a false positive either way.
|
||||
const std::string marker = "sleep 137";
|
||||
REQUIRE_FALSE(any_process_cmdline_contains(marker));
|
||||
|
||||
ScratchXdgDirs scratch;
|
||||
run_in_fixture(*image, {"sh", "-c", "nohup sleep 137 >/dev/null 2>&1 & exit"});
|
||||
|
||||
CHECK(process_cmdline_gone_within(marker, 2000));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user