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:
@@ -422,9 +422,21 @@ Source layout (all under `src/`):
|
|||||||
`/proc/<outer_pid>/ns/{pid,uts,ipc,cgroup}` against this process's own showed
|
`/proc/<outer_pid>/ns/{pid,uts,ipc,cgroup}` against this process's own showed
|
||||||
them identical, while only `mnt`/`user` differed. `resolve_namespace_pid()`
|
them identical, while only `mnt`/`user` differed. `resolve_namespace_pid()`
|
||||||
reads `/proc/<pid>/task/<pid>/children` (the direct-children list `procfs`
|
reads `/proc/<pid>/task/<pid>/children` (the direct-children list `procfs`
|
||||||
exposes) to find that real inner process and joins *its* namespaces instead —
|
exposes) to find that real inner process and joins *its* namespaces instead.
|
||||||
falls back to `pid` itself (best-effort, not fatal) if that file can't be
|
**Real bug found via testing on a real target device, not assumed**: that
|
||||||
read. For each of `{mnt→--mount, uts→--uts, ipc→--ipc, pid→--pid,
|
file requires `CONFIG_CHECKPOINT_RESTORE`, which not every kernel enables —
|
||||||
|
confirmed absent (not just unreadable — the file doesn't exist at all) on a
|
||||||
|
real Android device, where `-e/--exec` then fell back to the outer `bwrap`
|
||||||
|
pid itself and failed outright (`nsenter: no namespace specified`, since
|
||||||
|
every namespace type either matched the outer process's own or couldn't be
|
||||||
|
read at all). Fixed by adding `find_child_by_scanning_proc()`, a portable
|
||||||
|
fallback used only when the children file is missing/empty: scans
|
||||||
|
`/proc/<n>/stat` for any process whose ppid field equals `pid` — the same
|
||||||
|
information `pstree` itself reads to build its tree, which is how the actual
|
||||||
|
sandboxed child was located and confirmed correct on the same device via a
|
||||||
|
manual `nsenter -t <child_pid> -a -- /bin/sh` before the fix was written.
|
||||||
|
Picks the lowest matching pid if more than one child exists, for a
|
||||||
|
deterministic result. For each of `{mnt→--mount, uts→--uts, ipc→--ipc, pid→--pid,
|
||||||
cgroup→--cgroup, user→--user}` (`net` deliberately excluded — this project
|
cgroup→--cgroup, user→--user}` (`net` deliberately excluded — this project
|
||||||
never isolates networking, see `bwrap.cpp` below), `readlink()`s both
|
never isolates networking, see `bwrap.cpp` below), `readlink()`s both
|
||||||
`/proc/<ns_pid>/ns/<type>` and `/proc/self/ns/<type>` and only passes
|
`/proc/<ns_pid>/ns/<type>` and `/proc/self/ns/<type>` and only passes
|
||||||
|
|||||||
+67
-4
@@ -18,9 +18,11 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cctype>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <sstream>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
|
|
||||||
#include <fmt/core.h>
|
#include <fmt/core.h>
|
||||||
@@ -31,6 +33,64 @@
|
|||||||
|
|
||||||
namespace {
|
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
|
// 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
|
// mount/user namespaces itself, then clone()s the actual sandboxed command into
|
||||||
// fresh pid/uts/ipc/cgroup/net namespaces -- clone()'s namespace-creation flags
|
// 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
|
// child can't be determined -- callers still get a mount/user-namespace join out
|
||||||
// of that, just not the rest.
|
// of that, just not the rest.
|
||||||
pid_t resolve_namespace_pid(pid_t pid) {
|
pid_t resolve_namespace_pid(pid_t pid) {
|
||||||
std::ifstream children(fmt::format("/proc/{}/task/{}/children", pid, pid));
|
if (auto child = find_child_via_children_file(pid)) {
|
||||||
pid_t child = 0;
|
return *child;
|
||||||
if (children >> child && child > 0) {
|
}
|
||||||
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",
|
spdlog::debug("could not determine pid {}'s sandboxed child process; joining its own namespaces only",
|
||||||
pid);
|
pid);
|
||||||
|
|||||||
Reference in New Issue
Block a user