1c186b0365
/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
211 lines
8.0 KiB
C++
211 lines
8.0 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.
|
|
|
|
#include "exec_session.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cctype>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <system_error>
|
|
|
|
#include <fmt/core.h>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include "pid_file.h"
|
|
#include "process.h"
|
|
|
|
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
|
|
// only ever affect the newly created child, never the caller, so the outer
|
|
// process itself never actually enters those namespaces (confirmed by direct
|
|
// testing: /proc/<outer_pid>/ns/{pid,uts,ipc,cgroup,net} all matched this
|
|
// process's own, while only mnt/user differed). The real sandboxed command is
|
|
// that direct child, which is what actually needs to be nsenter-target for a
|
|
// faithful join. Returns `pid` itself (best-effort fallback, not fatal) if the
|
|
// 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) {
|
|
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);
|
|
return pid;
|
|
}
|
|
|
|
struct JoinableNamespace {
|
|
const char* proc_name; // matches /proc/<pid>/ns/<proc_name>
|
|
const char* nsenter_flag;
|
|
bool required; // fatal (not just skipped) if it can't be read
|
|
};
|
|
|
|
// net is deliberately excluded: this project never isolates networking either
|
|
// (see build_bwrap_args() dropping --unshare-net), so there's nothing meaningful
|
|
// to join there.
|
|
constexpr std::array<JoinableNamespace, 6> joinable_namespaces = {{
|
|
{"mnt", "--mount", true},
|
|
{"uts", "--uts", false},
|
|
{"ipc", "--ipc", false},
|
|
{"pid", "--pid", false},
|
|
{"cgroup", "--cgroup", false},
|
|
{"user", "--user", false},
|
|
}};
|
|
|
|
std::optional<std::string> read_ns_link(const std::filesystem::path& path) {
|
|
std::error_code ec;
|
|
auto target = std::filesystem::read_symlink(path, ec);
|
|
if (ec) {
|
|
return std::nullopt;
|
|
}
|
|
return target.string();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int exec_in_session(pid_t pid, const std::vector<std::string>& command) {
|
|
if (pid <= 0) {
|
|
spdlog::error("invalid pid: {}", pid);
|
|
return 1;
|
|
}
|
|
|
|
auto sessions = list_sessions();
|
|
auto it = std::find_if(sessions.begin(), sessions.end(),
|
|
[&](const SessionInfo& session) { return session.pid == pid; });
|
|
if (it == sessions.end()) {
|
|
spdlog::error("no tracked slocker-lite session with pid {}", pid);
|
|
return 1;
|
|
}
|
|
if (!it->running) {
|
|
spdlog::error("session '{}' (pid {}) is no longer running", it->container_name, pid);
|
|
return 1;
|
|
}
|
|
|
|
pid_t ns_pid = resolve_namespace_pid(pid);
|
|
|
|
std::vector<std::string> argv = {"nsenter"};
|
|
for (const auto& ns : joinable_namespaces) {
|
|
auto target_ns = read_ns_link(fmt::format("/proc/{}/ns/{}", ns_pid, ns.proc_name));
|
|
if (!target_ns) {
|
|
if (ns.required) {
|
|
spdlog::error(
|
|
"could not access pid {}'s {} namespace (permission denied, or it has since exited)",
|
|
ns_pid, ns.proc_name);
|
|
return 1;
|
|
}
|
|
spdlog::debug("skipping {} namespace for pid {}: not accessible", ns.proc_name, ns_pid);
|
|
continue;
|
|
}
|
|
|
|
auto own_ns = read_ns_link(fmt::format("/proc/self/ns/{}", ns.proc_name));
|
|
if (own_ns && *own_ns == *target_ns) {
|
|
// Already in the same namespace -- nsenter can fail ("Invalid
|
|
// argument") if asked to re-enter it, so skip rather than risk that.
|
|
continue;
|
|
}
|
|
|
|
argv.push_back(fmt::format("{}=/proc/{}/ns/{}", ns.nsenter_flag, ns_pid, ns.proc_name));
|
|
}
|
|
|
|
if (!find_in_path("nsenter")) {
|
|
spdlog::error("nsenter not found in PATH");
|
|
return 1;
|
|
}
|
|
|
|
// Without this, nsenter --user tries to setgroups()/setuid()/setgid() to the
|
|
// target's identity within the new user namespace, which fails outright
|
|
// ("setgroups failed: Operation not permitted") when that namespace has
|
|
// setgroups denied -- the kernel-enforced default for any unprivileged user
|
|
// namespace, which is exactly what bwrap creates when run_container() isn't
|
|
// root. We don't want nsenter changing our credentials anyway -- just join
|
|
// the namespaces and keep running as whatever this process already is.
|
|
argv.push_back("--preserve-credentials");
|
|
|
|
argv.push_back("--");
|
|
argv.insert(argv.end(), command.begin(), command.end());
|
|
|
|
return run_process_foreground(argv);
|
|
}
|