Fix -e/--exec namespace resolution on kernels without CONFIG_CHECKPOINT_RESTORE

/proc/<pid>/task/<pid>/children doesn't exist on every kernel (confirmed
missing on a real Android target), so resolve_namespace_pid() fell back to
the outer bwrap pid itself and nsenter ended up with no namespace flags at
all. Add a portable fallback that scans /proc/<n>/stat for the child whose
ppid matches, the same information pstree uses to build its tree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-08-29 07:42:02 +00:00
parent fa0bf13732
commit 1c186b0365
2 changed files with 82 additions and 7 deletions
+67 -4
View File
@@ -18,9 +18,11 @@
#include <algorithm>
#include <array>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <optional>
#include <sstream>
#include <system_error>
#include <fmt/core.h>
@@ -31,6 +33,64 @@
namespace {
// Fast path: /proc/<pid>/task/<pid>/children lists direct children with no
// scanning needed. Requires CONFIG_CHECKPOINT_RESTORE, which not every kernel
// enables -- absent (not just unreadable), confirmed by direct testing on a
// real Android target, where the file simply doesn't exist.
std::optional<pid_t> find_child_via_children_file(pid_t pid) {
std::ifstream children(fmt::format("/proc/{}/task/{}/children", pid, pid));
pid_t child = 0;
if (children >> child && child > 0) {
return child;
}
return std::nullopt;
}
// Portable fallback for kernels without the children file: scans /proc/<n>/stat
// for any process whose ppid field (the first whitespace-separated field after
// comm's closing ')' -- comm itself is parenthesized and may contain spaces or
// parens, so it can't just be split on whitespace) equals `pid`. This is the
// same information `pstree` itself reads to build its tree -- confirmed by
// direct testing: `pstree -p <bwrap_pid>` found the real sandboxed child on a
// device where the children file was missing. Picks the lowest matching pid if
// more than one child exists, for a deterministic result.
std::optional<pid_t> find_child_by_scanning_proc(pid_t pid) {
std::error_code ec;
std::optional<pid_t> found;
for (const auto& entry : std::filesystem::directory_iterator("/proc", ec)) {
if (ec) {
break;
}
const std::string name = entry.path().filename().string();
if (name.empty() || !std::all_of(name.begin(), name.end(),
[](unsigned char c) { return std::isdigit(c) != 0; })) {
continue;
}
std::ifstream stat_file(entry.path() / "stat");
std::string line;
if (!std::getline(stat_file, line)) {
continue;
}
auto close_paren = line.rfind(')');
if (close_paren == std::string::npos) {
continue;
}
std::istringstream rest(line.substr(close_paren + 1));
std::string state;
pid_t ppid = 0;
if (!(rest >> state >> ppid) || ppid != pid) {
continue;
}
pid_t candidate = std::stoi(name);
if (!found || candidate < *found) {
found = candidate;
}
}
return found;
}
// bwrap's own outer process (the one tracked in the session pid file) sets up the
// mount/user namespaces itself, then clone()s the actual sandboxed command into
// fresh pid/uts/ipc/cgroup/net namespaces -- clone()'s namespace-creation flags
@@ -43,10 +103,13 @@ namespace {
// child can't be determined -- callers still get a mount/user-namespace join out
// of that, just not the rest.
pid_t resolve_namespace_pid(pid_t pid) {
std::ifstream children(fmt::format("/proc/{}/task/{}/children", pid, pid));
pid_t child = 0;
if (children >> child && child > 0) {
return child;
if (auto child = find_child_via_children_file(pid)) {
return *child;
}
if (auto child = find_child_by_scanning_proc(pid)) {
spdlog::debug("pid {}: found sandboxed child {} by scanning /proc (no .../task/.../children file)",
pid, *child);
return *child;
}
spdlog::debug("could not determine pid {}'s sandboxed child process; joining its own namespaces only",
pid);