// Copyright (C) 2026 Viorel Munteanu // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // [integration][root]: regression test for run_bwrap()'s own automatic // post-exit straggler sweep (bwrap.cpp) -- exercises the exported // kill_via_cgroup() (kill_session.h) directly against a real cgroup, rather // than going through the full mount/bwrap pipeline. That's a deliberate // choice, not a shortcut: the actual bug this targets is a process that // daemonizes/double-forks and setsid()'s away, reparenting outside the // tracked process's own tree (e.g. no pid namespace support at all, the // real target device's own kernel) -- reproducing that specific escape // through a real bwrap sandbox would need to fake the kernel's own pid // namespace absence (not controllable from the CLI at all -- it's a // config-file-only NamespaceConfig field, see bwrap.h), whereas the actual // mechanism under test (cgroup membership surviving reparenting, and // kill_via_cgroup() reaping it) is fully exercised with two plain forked // processes and no container/image/bwrap involved at all. #include #include #include #include #include #include "kill_session.h" #include "session_cgroup.h" TEST_CASE("session cgroup sweep: kill_via_cgroup reaps a reparented straggler", "[integration][root]") { if (geteuid() != 0) { SKIP("requires root"); } const std::string container_name = "test-cgroup-sweep"; // sync_pipe makes the child wait until it's actually been moved into the // session cgroup before it forks its own straggler -- otherwise the // straggler could be forked (and thus inherit cgroup membership) before // create_session_cgroup() below has run, racing the exact same way // run_bwrap()'s own on_start callback races against bwrap forking its // own children. int sync_pipe[2]; REQUIRE(pipe(sync_pipe) == 0); pid_t tracked_pid = fork(); REQUIRE(tracked_pid >= 0); if (tracked_pid == 0) { close(sync_pipe[1]); char buf = 0; ssize_t unused = read(sync_pipe[0], &buf, 1); (void)unused; close(sync_pipe[0]); // Reset to the default disposition before forking: the straggler // below is expected to receive a real SIGTERM later (from // kill_via_cgroup()) and terminate via it -- Catch2 installs its own // fatal-signal handler around a running TEST_CASE, which a forked // child inherits (only the disposition *at fork time* matters), so // without this reset the straggler's own ordinary shutdown signal // gets caught by that inherited handler instead, producing a // spurious "FAILED ... due to a fatal error condition: SIGTERM" // report interleaved into this test's real output -- the exact same // lesson test_root_networking.cpp's tap-relay test already learned // for its own forked relay child. Confirmed cosmetic only when hit: // the assertions below still ran and passed correctly either way. signal(SIGTERM, SIG_DFL); // Mirrors a container process that daemonizes and exits successfully // (or is signaled and dies) while a backgrounded child of its own // keeps running, detached into a new session -- the exact shape // that used to escape bwrap's own pid tree with nothing left to // reap it. pid_t straggler = fork(); if (straggler == 0) { setsid(); pause(); _exit(0); } _exit(0); } close(sync_pipe[0]); // Mirrors create_session_cgroup() being called from run_bwrap()'s own // on_start callback, the instant the tracked pid is known -- before it's // had any real chance to fork further. auto cgroup = create_session_cgroup(container_name, tracked_pid); if (!cgroup) { char go = 1; ssize_t unused = write(sync_pipe[1], &go, 1); (void)unused; close(sync_pipe[1]); int status = 0; waitpid(tracked_pid, &status, 0); SKIP("could not create a session cgroup on this host (see session_cgroup.h)"); } char go = 1; REQUIRE(write(sync_pipe[1], &go, 1) == 1); close(sync_pipe[1]); int status = 0; // Mirrors run_process_foreground()'s own waitpid() on the tracked pid -- // by the time this returns, `tracked_pid` is already gone from // cgroup.procs (a dead process is removed from its cgroup // automatically), leaving only the straggler behind, exactly like a // real bwrap exit would. waitpid(tracked_pid, &status, 0); auto before = session_cgroup_pids(*cgroup); // Confirms the straggler really did escape into the cgroup in the first // place -- otherwise the kill_via_cgroup() check below would trivially // "pass" against an already-empty cgroup and prove nothing. REQUIRE_FALSE(before.empty()); CHECK(kill_via_cgroup(*cgroup, /*grace_period_seconds=*/2)); CHECK(session_cgroup_pids(*cgroup).empty()); remove_session_cgroup(*cgroup); }