Files
slocker-lite/src/bwrap.cpp
T
ceamac 095ae8397a Drop --unshare-net from bwrap args, no network setup yet
Without any network setup (slirp4netns or similar), unsharing the
network namespace just leaves the sandbox with no network at all,
which isn't useful yet. build_bwrap_args() now skips --unshare-net
when assembling the real bwrap invocation; re-add once network
isolation is implemented.

detect_bwrap_unshare_args() itself is unchanged and still probes/
reports net namespace kernel support (e.g. via -t/--test), since
that's capability detection, not policy -- same pattern as the
--new-session removal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 06:17:20 +00:00

239 lines
8.1 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 "bwrap.h"
#include <sched.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <optional>
#include <fmt/core.h>
#include <spdlog/spdlog.h>
#include "process.h"
namespace {
struct NamespaceProbe {
int clone_flag;
const char* bwrap_arg;
const char* name;
};
constexpr std::array<NamespaceProbe, 6> kNamespaceProbes = {{
{CLONE_NEWUSER, "--unshare-user", "user"},
{CLONE_NEWIPC, "--unshare-ipc", "ipc"},
{CLONE_NEWPID, "--unshare-pid", "pid"},
{CLONE_NEWNET, "--unshare-net", "net"},
{CLONE_NEWUTS, "--unshare-uts", "uts"},
{CLONE_NEWCGROUP, "--unshare-cgroup", "cgroup"},
}};
// unshare(2) affects the calling process's own namespaces, so support for each
// namespace type is probed in a throwaway forked child rather than the caller.
bool kernel_supports_namespace(int clone_flag) {
pid_t pid = fork();
if (pid < 0) {
return false;
}
if (pid == 0) {
_exit(unshare(clone_flag) == 0 ? 0 : 1);
}
int status = 0;
waitpid(pid, &status, 0);
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
// Scans /proc for a fuse-overlayfs process whose command line references
// `merged_path`, mirroring `ps aux | grep fuse-overlayfs`. /proc is inherently racy
// (processes come and go while it's being scanned), so filesystem errors from a
// vanished entry are treated as "not this one" rather than propagated.
std::optional<pid_t> find_fuse_overlayfs_pid(const std::string& merged_path) {
std::error_code ec;
auto it = std::filesystem::directory_iterator("/proc", ec);
if (ec) {
return std::nullopt;
}
for (const auto& entry : it) {
const std::string name = entry.path().filename().string();
if (!std::all_of(name.begin(), name.end(),
[](unsigned char c) { return std::isdigit(c); })) {
continue;
}
std::error_code exe_ec;
auto exe_target = std::filesystem::read_symlink(entry.path() / "exe", exe_ec);
if (exe_ec || exe_target.filename() != "fuse-overlayfs") {
continue;
}
std::ifstream cmdline_file(entry.path() / "cmdline", std::ios::binary);
std::string cmdline((std::istreambuf_iterator<char>(cmdline_file)),
std::istreambuf_iterator<char>());
size_t start = 0;
while (start < cmdline.size()) {
size_t end = cmdline.find('\0', start);
if (end == std::string::npos) {
end = cmdline.size();
}
if (cmdline.compare(start, end - start, merged_path) == 0) {
return static_cast<pid_t>(std::stoi(name));
}
start = end + 1;
}
}
return std::nullopt;
}
} // namespace
std::vector<std::string> detect_bwrap_unshare_args() {
std::vector<std::string> args;
bool user_ns_supported = kernel_supports_namespace(CLONE_NEWUSER);
spdlog::debug("namespace user: {}", user_ns_supported ? "supported" : "not supported");
// Root already has full privilege without a new user namespace, and bwrap's own
// uid/gid mapping into one (a single trivial mapping, since we don't pass
// --uid/--gid/subuid ranges) forces the kernel's unprivileged-userns
// setgroups() restriction: every other supplementary group collapses to the
// overflow gid (65534/"nobody"), and setgroups() calls inside the sandbox (e.g.
// `su`) fail with "Operation not permitted". Observed directly: `id` inside a
// root-launched sandbox showed "nobody" repeated once per real supplementary
// group, and `su git` failed exactly this way. So only request --unshare-user
// (and only combine the other probes with it) when not root.
bool combine_with_user_ns = user_ns_supported && geteuid() != 0;
if (combine_with_user_ns) {
args.push_back("--unshare-user");
}
for (const auto& probe : kNamespaceProbes) {
if (probe.clone_flag == CLONE_NEWUSER) {
continue;
}
// As a regular (non-root) user, most namespace types can only be unshared
// together with a fresh user namespace (which supplies the capabilities
// needed), not in isolation -- so combine them here whenever it applies.
int flags = probe.clone_flag | (combine_with_user_ns ? CLONE_NEWUSER : 0);
bool supported = kernel_supports_namespace(flags);
spdlog::debug("namespace {}: {}", probe.name, supported ? "supported" : "not supported");
if (supported) {
args.push_back(probe.bwrap_arg);
}
}
return args;
}
std::vector<std::string> build_bwrap_args(const std::string& root,
const std::vector<std::string>& command) {
// --new-session detaches from the controlling terminal, which breaks job
// control for an interactive foreground shell ("can't access tty"). Re-enable
// once background/daemonized runs are implemented, where that's the point.
std::vector<std::string> args = {"bwrap", "--die-with-parent"};
auto unshare_args = detect_bwrap_unshare_args();
bool has_pid_ns = false;
for (const auto& arg : unshare_args) {
// Not requested yet: without any network setup (slirp4netns or similar),
// unsharing it just leaves the sandbox with no network at all. Re-add once
// network isolation is implemented. (detect_bwrap_unshare_args() still
// probes/reports it, e.g. for -t/--test, since that's kernel capability, not
// policy.)
if (arg == "--unshare-net") {
continue;
}
args.push_back(arg);
if (arg == "--unshare-pid") {
has_pid_ns = true;
}
}
if (has_pid_ns) {
args.push_back("--as-pid-1");
}
std::vector<std::string> filesystem_args = {
"--bind",
root,
"/",
"--proc",
"/proc",
"--dev",
"/dev",
"--perms",
"01777",
"--tmpfs",
"/dev/shm",
"--perms",
"01777",
"--tmpfs",
"/tmp",
"--chdir",
"/",
"--clearenv",
"--setenv",
"PATH",
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"--setenv",
"HOME",
"/root",
};
args.insert(args.end(), filesystem_args.begin(), filesystem_args.end());
if (const char* term = std::getenv("TERM")) {
args.insert(args.end(), {"--setenv", "TERM", term});
}
args.push_back("--");
args.insert(args.end(), command.begin(), command.end());
return args;
}
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter) {
std::vector<std::string> argv;
if (use_nsenter) {
auto pid = find_fuse_overlayfs_pid(root);
if (!pid) {
spdlog::error("could not find the fuse-overlayfs process serving {}", root);
return -1;
}
if (!find_in_path("nsenter")) {
spdlog::error("nsenter not found in PATH");
return -1;
}
argv = {"nsenter", fmt::format("--user=/proc/{}/ns/user", *pid),
fmt::format("--mount=/proc/{}/ns/mnt", *pid), "--"};
}
auto bwrap_args = build_bwrap_args(root, command);
argv.insert(argv.end(), bwrap_args.begin(), bwrap_args.end());
return run_process_foreground(argv);
}