Files
slocker-lite/tests/integration/test_session_cleanup.cpp
ceamac 174b6b4c29 Use explicit test-only subnets and test- prefixed names in the test suite
Running the full device test suite on the real Android target found a
genuine test-harness gap: every extern-network test in
test_network_join_scenarios.cpp failed with "RTNETLINK answers: File
exists" adding its own uplink route. Root cause: each test runs under its
own ScratchXdgDirs, so its own persistent.yaml starts empty every time --
allocate_ipv4_subnet()'s own auto-allocation always picks the very first
slot (10.168.0.0/24) with no way to see whatever real, non-test networks
already exist on the host. The device happens to have a real, long-lived
"extern" network already occupying exactly that subnet from prior manual
testing, and since extern's uplink adds a route back into the (necessarily
shared, host-root) routing table -- unlike intern, whose routing lives
entirely inside its own isolated per-network namespace -- every extern
test collided with it. Not a production bug: a real end user only ever has
one persistent.yaml where allocation correctly sees every existing entry.

Fixed by giving each of the 10 test networks in
test_network_join_scenarios.cpp its own fixed, explicit subnet (--subnet)
in 10.169.0.0/16 -- a different /16 than production's own default
10.168.0.0/16 range, so a test run can't collide with a real network
regardless of how many the host already has.

Also renamed every network/hostname/container-name string literal used
across the test suite (test_network_join_scenarios.cpp,
test_root_networking.cpp, test_rootless_run.cpp,
test_session_cleanup.cpp) from "selftest*" to "test-*", to further reduce
the chance of colliding with anything a real invocation might already be
using. Low-level interface device literals (slkselftest0/thselftest0/
ethselftest in test_root_networking.cpp) are left as-is -- they're
internal identifiers for a throwaway unit test, not network or container
names.

Verified: clean rebuild, meson test, and 3 consecutive
[integration][root] suite runs (61 assertions, 14 test cases) with no
failures.
2026-09-05 14:03:14 +00:00

135 lines
5.7 KiB
C++

// 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 <unistd.h>
#include <csignal>
#include <string>
#include <sys/wait.h>
#include <catch2/catch_test_macros.hpp>
#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);
}