Fix a real race: -d checked is_network_in_use() before async cleanup ran

Even after fixing is_network_in_use()'s own exit-code bug, the real device
still failed the same way: both managed networks reported "still in use"
milliseconds after their only session was confirmed stopped. Root cause:
kill_session() only waits for the *sandboxed process itself* (via its
cgroup) to die -- it says nothing about the separate, independently
scheduled daemonized process that started it, which still has its own
post-waitpid() cleanup left to run (stop_tap_relay() among it) before a
tap+relay join's host-side device is actually detached from the bridge.
Checking is_network_in_use() the instant kill_session() returns can
genuinely still see it attached; on the real device, the kill+check pair
landed single-digit milliseconds apart in the log.

network_becomes_unused() retries is_network_in_use() for up to 10s
(nanosleep()-based, matching this project's existing polling style)
instead of giving up on the first still-attached answer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-09-07 14:46:02 +00:00
parent d3108845a6
commit 5bec5c5e53
+30 -1
View File
@@ -25,6 +25,7 @@
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <time.h>
#include <unistd.h> #include <unistd.h>
#include <vector> #include <vector>
@@ -728,6 +729,34 @@ int compose_up_command(const std::string& images_directory, const std::string& c
// state file (no -u/--up ever ran for this file) isn't an error -- just // state file (no -u/--up ever ran for this file) isn't an error -- just
// nothing to stop. // nothing to stop.
// //
// A killed session's own sandboxed process (what kill_session() actually
// waits for, via the cgroup/pid-namespace/tracked-pid strategy it picks)
// isn't the same process as the one that started it: run_mounted_container()
// runs in the compose service's own daemonized process, blocked in its own
// waitpid() on bwrap -- once that returns, *that* process still has to run
// its own post-exit cleanup (stop_tap_relay() among it) before a tap+relay
// join's own host-side device is actually removed from the bridge. Since
// this happens in a completely separate, independently-scheduled process,
// checking is_network_in_use() the instant kill_session() itself returns
// can genuinely still see it as attached -- confirmed on the real Android
// target device, where the two kill+check calls above landed only single-
// digit milliseconds apart, well within the gap that process's own
// scheduling can take on a loaded device. This retries for a bounded time
// (nanosleep()-based, matching this project's own existing polling style)
// rather than giving up on the very first still-attached answer.
bool network_becomes_unused(const NetworkEntry& network, int timeout_ms, int interval_ms) {
for (int waited = 0; waited < timeout_ms; waited += interval_ms) {
if (!is_network_in_use(network)) {
return true;
}
struct timespec ts {
interval_ms / 1000, (interval_ms % 1000) * 1000000L
};
nanosleep(&ts, nullptr);
}
return !is_network_in_use(network);
}
// Networks and volumes are then handled asymmetrically, matching real // Networks and volumes are then handled asymmetrically, matching real
// Compose's own default and the user's own explicit direction: volumes // Compose's own default and the user's own explicit direction: volumes
// always persist (never touched here at all -- a compose-managed volume // always persist (never touched here at all -- a compose-managed volume
@@ -768,7 +797,7 @@ int compose_down_command(const std::string& compose_file_name, AppConfig& config
if (it == config.networks.end()) { if (it == config.networks.end()) {
continue; // never created, or already removed continue; // never created, or already removed
} }
if (is_network_in_use(*it)) { if (!network_becomes_unused(*it, 10000, 200)) {
fmt::print("network '{}' is still in use, leaving it\n", actual_name); fmt::print("network '{}' is still in use, leaving it\n", actual_name);
continue; continue;
} }