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:
@@ -430,7 +430,27 @@ Source layout (all under `src/`):
|
|||||||
mixes slocker-lite's own status/log output in with the sandboxed
|
mixes slocker-lite's own status/log output in with the sandboxed
|
||||||
command's real output; fixed by having the sandboxed command bracket
|
command's real output; fixed by having the sandboxed command bracket
|
||||||
its own output between two unique markers and extracting only what's
|
its own output between two unique markers and extracting only what's
|
||||||
strictly between them.
|
strictly between them. **Also has a regression test for `run_bwrap()`'s
|
||||||
|
own automatic post-exit straggler sweep** (`bwrap.cpp`/`session_cgroup.h`'s
|
||||||
|
"Resolved" entries), reproducing the user's own reported shape end to
|
||||||
|
end with a real container: `nohup sleep 137 & exit` inside the sandbox,
|
||||||
|
then `any_process_cmdline_contains()`/`process_cmdline_gone_within()`
|
||||||
|
(`.cpp`-local, same `/proc`-scanning shape as `bwrap.cpp`'s own
|
||||||
|
`find_fuse_overlayfs_pid()`) confirm the backgrounded process is gone
|
||||||
|
from the *host's* own process table afterward — checked by cmdline
|
||||||
|
substring, not by pid, since a pid seen from 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 guarantee (covering this case for free on any kernel that
|
||||||
|
supports pid namespaces, this dev machine included) not necessarily
|
||||||
|
being synchronously complete by the instant `dispatch_command()`
|
||||||
|
returns. Passing here proves the outward, visible contract ("a stray
|
||||||
|
process never survives a session") end to end, though on a
|
||||||
|
pid-namespace-capable host it doesn't by itself prove the cgroup sweep
|
||||||
|
specifically fired — see `test_session_cleanup.cpp`'s own
|
||||||
|
`[integration][root]` test (below) for one that exercises
|
||||||
|
`kill_via_cgroup()` directly, since the real target device's own
|
||||||
|
no-pid-namespace escape shape can't be forced via the CLI at all.
|
||||||
- `tests/integration/test_root_networking.cpp` (`[integration][root][net]`)
|
- `tests/integration/test_root_networking.cpp` (`[integration][root][net]`)
|
||||||
— the persistent-netns/tap-relay/dns-resolver tests that originally
|
— the persistent-netns/tap-relay/dns-resolver tests that originally
|
||||||
lived directly in this file, ported to tagged `TEST_CASE`s (every
|
lived directly in this file, ported to tagged `TEST_CASE`s (every
|
||||||
|
|||||||
@@ -46,12 +46,17 @@
|
|||||||
// wouldn't prove anything either way, so both checks below only ever look
|
// wouldn't prove anything either way, so both checks below only ever look
|
||||||
// at types `detect_bwrap_unshare_args()` reports as real.
|
// at types `detect_bwrap_unshare_args()` reports as real.
|
||||||
|
|
||||||
|
#include <time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <system_error>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#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();
|
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
|
} // namespace
|
||||||
|
|
||||||
TEST_CASE("rootless -r/--run: network namespace is genuinely isolated, loopback present", "[integration][net]") {
|
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()));
|
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