Add -r/--run and -c/--cleanup, fix nsenter under root

-r/--run mounts an image, runs a command under bwrap in the
foreground (default /bin/sh, overridable via -- <command> [args...]),
then unmounts and cleans up when it exits. -c/--cleanup deletes a
layer and its ancestor chain from local storage (containers-storage
delete-layer, walking parents via `layer --json`), since -u only ever
unmounted.

bwrap needs to see the merged mount from inside the private namespace
containers-storage mount creates when running rootless; run_bwrap()
locates the live fuse-overlayfs process and runs bwrap via nsenter
into its namespaces. When running as root no such namespace exists
(containers-storage doesn't need to reexec for privilege), so nsenter
fails with EINVAL; detect geteuid() == 0 and skip it automatically
there. -n/--no-nsenter forces it off manually for any other case.

process.cpp gains run_process_foreground() (inherited stdio, for the
interactive bwrap run) and the relocated find_in_path(), now shared
with bwrap.cpp's nsenter lookup.

Also: meson test only ran -m, leaking a layer on every run; it now
runs tests/run_test.py, which drives mount -> umount -> cleanup and
fails if any step does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 07:44:24 +00:00
parent 839551a648
commit c0bef0d989
10 changed files with 525 additions and 98 deletions
+130
View File
@@ -17,13 +17,23 @@
#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 {
@@ -56,6 +66,49 @@ bool kernel_supports_namespace(int clone_flag) {
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() {
@@ -69,3 +122,80 @@ std::vector<std::string> detect_bwrap_unshare_args() {
}
return args;
}
std::vector<std::string> build_bwrap_args(const std::string& root,
const std::vector<std::string>& command) {
std::vector<std::string> args = {"bwrap", "--die-with-parent", "--new-session"};
auto unshare_args = detect_bwrap_unshare_args();
bool has_pid_ns = false;
for (const auto& arg : unshare_args) {
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);
}