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:
@@ -6,29 +6,51 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
`slocker-lite` (C++20, built with Meson) mounts an OCI Image Layout tar (`oci-layout` +
|
||||
`index.json` + `blobs/sha256/*`, as produced by `skopeo`/`podman save --format
|
||||
oci-archive`/modern `docker save`) using `containers-storage` and `fuse-overlayfs`. Given
|
||||
an image tar path, it validates the layout, imports each layer into containers-storage's
|
||||
layer store in order (chained by parent), mounts the assembled top layer, and prints the
|
||||
resulting merged path. There is no README or broader architecture doc yet — treat this
|
||||
repo as still early-stage.
|
||||
oci-archive`/modern `docker save`) using `containers-storage` and `fuse-overlayfs`, then
|
||||
(via `-r/--run`) runs a sandboxed command against it with `bwrap`. The real deployment
|
||||
target is Android with a stock kernel, where `podman`/`docker` don't run (missing
|
||||
namespace support) and there's no kernel overlayfs (hence `fuse-overlayfs`); `bwrap` is
|
||||
invoked in "degraded mode" using only whichever `--unshare-xxx` namespaces the running
|
||||
kernel actually supports. There is no README or broader architecture doc yet — treat
|
||||
this repo as still early-stage.
|
||||
|
||||
Source layout (all under `src/`):
|
||||
- `main.cpp` — CLI entry point, dependency checks, orchestration.
|
||||
- `main.cpp` — CLI entry point, dependency checks, orchestration (`mount_image()`,
|
||||
`run_container()`, `cleanup_image()`, `unmount_image()`).
|
||||
- `oci_image.{h,cpp}` — validates/parses the OCI Image Layout tar (libarchive +
|
||||
nlohmann_json) and extracts layer blobs.
|
||||
- `containers_storage.{h,cpp}` — wraps the `containers-storage` CLI (`import-layer`,
|
||||
`mount`), forcing `fuse-overlayfs` as the overlay `mount_program`.
|
||||
- `process.{h,cpp}` — argv-based subprocess helper (fork/execvp/pipe, no shell).
|
||||
`mount`, `unmount`, `layer --json`, `delete-layer`), forcing `fuse-overlayfs` as the
|
||||
overlay `mount_program`. `cleanup_layer_chain()` walks a layer's parent chain
|
||||
(children before parents) deleting each one.
|
||||
- `bwrap.{h,cpp}` — `detect_bwrap_unshare_args()` probes the kernel (via a forked
|
||||
`unshare(2)` per namespace type) for which `--unshare-xxx` flags `bwrap` can actually
|
||||
use; `build_bwrap_args()`/`run_bwrap()` assemble and run the sandboxed command.
|
||||
- `process.{h,cpp}` — argv-based subprocess helpers (fork/execvp, no shell):
|
||||
`run_process()` captures stdout (used for `containers-storage` calls),
|
||||
`run_process_foreground()` inherits all of stdio (used for the interactive `bwrap`
|
||||
run). Also `find_in_path()`, a shared `$PATH` lookup.
|
||||
|
||||
Errors are logged via `spdlog::error`; every external command is also traced at debug
|
||||
level in `run_process()` (`src/process.cpp`) — visible via `SPDLOG_LEVEL=debug`, since
|
||||
spdlog's default level is `info` — and a failed external command additionally logs a
|
||||
`spdlog::warn`, which is visible by default (no env var needed). The final "mounted
|
||||
image at: ..." success line is direct stdout program output, not a log.
|
||||
level in `run_process()`/`run_process_foreground()` (`src/process.cpp`) — visible via
|
||||
`SPDLOG_LEVEL=debug`, since spdlog's default level is `info` — and a failed external
|
||||
command additionally logs a `spdlog::warn`, which is visible by default (no env var
|
||||
needed). The final "mounted image at: ..." success line is direct stdout program
|
||||
output, not a log.
|
||||
|
||||
Because `containers-storage mount` runs rootless, the resulting mount lives in a private
|
||||
user+mount namespace; the printed path is only directly usable from within that same
|
||||
namespace (e.g. via `containers-storage unshare`), not from an arbitrary external shell.
|
||||
Because `containers-storage mount` runs rootless, it reexecs itself into a private
|
||||
user+mount namespace to gain the privilege it needs for the overlay mount — which
|
||||
leaves the result invisible to a plain shell or child process outside that namespace.
|
||||
Confirmed `containers-storage unshare` does **not** rejoin an already-running mount's
|
||||
namespace; only `nsenter` targeting the live `fuse-overlayfs` daemon's PID does.
|
||||
`-r/--run` handles this automatically by locating that PID and running `bwrap` via
|
||||
`nsenter` into its namespaces. **Running as root sidesteps all of this**: no privilege
|
||||
reexec is needed, so the mount is already directly visible in the current namespace,
|
||||
and `nsenter --user=...` into it then fails ("reassociate to namespace 'ns/user'
|
||||
failed: Invalid argument") since the caller is already in that same user namespace.
|
||||
`-r/--run` detects `geteuid() == 0` and skips `nsenter` automatically in that case;
|
||||
`-n/--no-nsenter` forces it off manually for any other situation where the mount turns
|
||||
out to already be directly visible.
|
||||
|
||||
## Build & test commands
|
||||
|
||||
@@ -37,8 +59,8 @@ Build directory is `buildDir/` (already configured).
|
||||
- Configure (only needed if `buildDir/` is missing or deleted): `meson setup buildDir`
|
||||
- Build: `meson compile -C buildDir` (or `ninja -C buildDir`)
|
||||
- Run the executable: `./buildDir/slocker_lite -m <image.tar>` (see `--help` for the
|
||||
full flag list: `-m/--mount`, `-u/--umount`, `-t/--test`, `-l/--log-level`,
|
||||
`-h/--help`, `-V/--version`)
|
||||
full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`,
|
||||
`-n/--no-nsenter`, `-t/--test`, `-l/--log-level`, `-h/--help`, `-V/--version`)
|
||||
- Run tests: `meson test -C buildDir`
|
||||
|
||||
## Code style
|
||||
|
||||
+1
-1
@@ -27,4 +27,4 @@ fixture_tar = custom_target('oci-fixture',
|
||||
output : 'fixture.tar',
|
||||
command : [find_program('python3'), files('tests/gen_fixture.py'), '@OUTPUT@'])
|
||||
|
||||
test('test', slocker_lite, args : ['-m', fixture_tar])
|
||||
test('test', find_program('python3'), args : [files('tests/run_test.py'), slocker_lite, fixture_tar])
|
||||
|
||||
+130
@@ -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);
|
||||
}
|
||||
|
||||
+17
@@ -25,3 +25,20 @@
|
||||
// support (e.g. stock Android kernels), where blindly passing every
|
||||
// --unshare-xxx flag to bwrap would make it fail outright.
|
||||
std::vector<std::string> detect_bwrap_unshare_args();
|
||||
|
||||
// Assembles the full bwrap argv (program name included) to run `command` with
|
||||
// `root` bound as the sandbox's filesystem root, using whichever --unshare-xxx
|
||||
// flags the kernel supports (see detect_bwrap_unshare_args()).
|
||||
std::vector<std::string> build_bwrap_args(const std::string& root,
|
||||
const std::vector<std::string>& command);
|
||||
|
||||
// Runs bwrap against `root` (the merged mount path from mount_layer()) in the
|
||||
// foreground and waits for it to exit. If `use_nsenter` is true, first locates the
|
||||
// fuse-overlayfs process serving `root` and runs bwrap via nsenter into that
|
||||
// process's user+mount namespaces -- needed because containers-storage mount
|
||||
// (rootless) creates the overlay mount inside a private namespace invisible to a
|
||||
// plain child process on kernels where fuse-overlayfs isolates it that way. Pass
|
||||
// use_nsenter=false on kernels where the mount is already directly visible
|
||||
// (observed on kernels older than 4.18, per fuse-overlayfs's own release notes).
|
||||
// Returns bwrap's exit code, or -1 on failure to launch.
|
||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter);
|
||||
|
||||
@@ -16,10 +16,15 @@
|
||||
|
||||
#include "containers_storage.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "process.h"
|
||||
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
std::string trim(const std::string& s) {
|
||||
size_t begin = s.find_first_not_of(" \t\r\n");
|
||||
if (begin == std::string::npos) {
|
||||
@@ -69,3 +74,47 @@ bool unmount_layer(const std::string& layer_id) {
|
||||
ProcessResult result = run_process(argv);
|
||||
return result.exit_code == 0;
|
||||
}
|
||||
|
||||
std::optional<std::string> get_layer_parent(const std::string& layer_id) {
|
||||
std::vector<std::string> argv = {"containers-storage", "layer", "--json", layer_id};
|
||||
|
||||
ProcessResult result = run_process(argv);
|
||||
if (result.exit_code != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
try {
|
||||
json layers = json::parse(result.stdout_output);
|
||||
if (!layers.is_array() || layers.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return layers[0].value("parent", "");
|
||||
} catch (const json::parse_error& e) {
|
||||
spdlog::error("failed to parse layer info for {}: {}", layer_id, e.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool delete_layer(const std::string& layer_id) {
|
||||
std::vector<std::string> argv = {"containers-storage", "delete-layer", layer_id};
|
||||
|
||||
ProcessResult result = run_process(argv);
|
||||
return result.exit_code == 0;
|
||||
}
|
||||
|
||||
bool cleanup_layer_chain(const std::string& top_layer_id) {
|
||||
std::string layer_id = top_layer_id;
|
||||
while (!layer_id.empty()) {
|
||||
auto parent = get_layer_parent(layer_id);
|
||||
if (!parent) {
|
||||
spdlog::error("failed to look up parent of layer {}", layer_id);
|
||||
return false;
|
||||
}
|
||||
if (!delete_layer(layer_id)) {
|
||||
spdlog::error("failed to delete layer {}", layer_id);
|
||||
return false;
|
||||
}
|
||||
layer_id = *parent;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -37,3 +37,17 @@ std::optional<std::string> mount_layer(const std::string& layer_id);
|
||||
|
||||
// Runs `containers-storage unmount <layer_id>`. Returns true on success.
|
||||
bool unmount_layer(const std::string& layer_id);
|
||||
|
||||
// Runs `containers-storage layer --json <layer_id>` and returns the layer's parent
|
||||
// ID, or an empty string if it has none (i.e. it's a base layer). Returns nullopt on
|
||||
// failure (layer not found, command failed, or unparsable output).
|
||||
std::optional<std::string> get_layer_parent(const std::string& layer_id);
|
||||
|
||||
// Runs `containers-storage delete-layer <layer_id>`. Returns true on success.
|
||||
bool delete_layer(const std::string& layer_id);
|
||||
|
||||
// Deletes layer_id and, walking parent by parent, every ancestor layer (children
|
||||
// before parents, so delete-layer's has-children safety check never trips). Stops at
|
||||
// the first layer with no parent, or the first deletion/lookup failure. Returns true
|
||||
// only if every layer in the chain was deleted successfully.
|
||||
bool cleanup_layer_chain(const std::string& top_layer_id);
|
||||
|
||||
+158
-77
@@ -22,6 +22,7 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <spdlog/cfg/env.h>
|
||||
@@ -31,36 +32,53 @@
|
||||
#include "config.h"
|
||||
#include "containers_storage.h"
|
||||
#include "oci_image.h"
|
||||
#include "process.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::string_view, 2> kRequiredTools = {"containers-storage", "bwrap"};
|
||||
|
||||
enum class Mode { kNone, kMount, kUnmount, kTest };
|
||||
enum class Mode { kNone, kMount, kUnmount, kTest, kRun, kCleanup };
|
||||
|
||||
constexpr std::array<struct option, 7> kLongOptions = {{
|
||||
constexpr std::array<struct option, 10> kLongOptions = {{
|
||||
{"help", no_argument, nullptr, 'h'},
|
||||
{"version", no_argument, nullptr, 'V'},
|
||||
{"test", no_argument, nullptr, 't'},
|
||||
{"log-level", required_argument, nullptr, 'l'},
|
||||
{"mount", required_argument, nullptr, 'm'},
|
||||
{"umount", required_argument, nullptr, 'u'},
|
||||
{"run", required_argument, nullptr, 'r'},
|
||||
{"cleanup", required_argument, nullptr, 'c'},
|
||||
{"no-nsenter", no_argument, nullptr, 'n'},
|
||||
{nullptr, 0, nullptr, 0},
|
||||
}};
|
||||
|
||||
void print_usage(const char* prog) {
|
||||
fmt::print(
|
||||
"usage: {0} -m|--mount <image.tar>\n"
|
||||
" {0} -r|--run <image.tar> [-- <command> [args...]]\n"
|
||||
" {0} -u|--umount <layer-id>\n"
|
||||
" {0} -c|--cleanup <layer-id>\n"
|
||||
" {0} -t|--test\n"
|
||||
" {0} -h|--help\n"
|
||||
" {0} -V|--version\n"
|
||||
"\n"
|
||||
"options:\n"
|
||||
" -m, --mount <image.tar> validate and mount an OCI Image Layout tar\n"
|
||||
" -r, --run <image.tar> mount, run bwrap in the foreground (default\n"
|
||||
" command: /bin/sh; pass -- <command> [args...]\n"
|
||||
" to override), then unmount and clean up when\n"
|
||||
" it exits\n"
|
||||
" -u, --umount <layer-id> unmount a previously mounted image layer (the\n"
|
||||
" ID printed by --mount, or from\n"
|
||||
" ID printed by --mount/--run, or from\n"
|
||||
" `containers-storage layers`)\n"
|
||||
" -c, --cleanup <layer-id> delete a layer and its ancestor chain from\n"
|
||||
" local storage (unmount it first with --umount)\n"
|
||||
" -n, --no-nsenter with --run, bind the mount directly instead of\n"
|
||||
" nsenter-ing into fuse-overlayfs's namespace\n"
|
||||
" (this is automatic when running as root, where\n"
|
||||
" the mount is already directly visible; pass\n"
|
||||
" this to force it off otherwise)\n"
|
||||
" -t, --test run the test suite\n"
|
||||
" -l, --log-level <level> set log verbosity (trace, debug, info, warn,\n"
|
||||
" error, critical, off)\n"
|
||||
@@ -98,34 +116,6 @@ int run_tests() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool is_executable_file(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
return std::filesystem::is_regular_file(path, ec) && access(path.c_str(), X_OK) == 0;
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> find_in_path(std::string_view name) {
|
||||
const char* path_env = std::getenv("PATH");
|
||||
if (!path_env) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string_view path{path_env};
|
||||
for (size_t start = 0; start <= path.size();) {
|
||||
size_t end = path.find(':', start);
|
||||
if (end == std::string_view::npos) {
|
||||
end = path.size();
|
||||
}
|
||||
std::filesystem::path dir{path.substr(start, end - start)};
|
||||
if (!dir.empty()) {
|
||||
std::filesystem::path candidate = dir / name;
|
||||
if (is_executable_file(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool check_required_dependencies() {
|
||||
@@ -151,15 +141,101 @@ int unmount_image(const std::string& layer_id) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct MountedImage {
|
||||
std::string merged_path;
|
||||
std::string top_layer_id;
|
||||
};
|
||||
|
||||
std::optional<MountedImage> mount_image(const std::filesystem::path& image_tar) {
|
||||
if (!check_required_dependencies()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto mount_program = find_in_path("fuse-overlayfs");
|
||||
if (!mount_program) {
|
||||
spdlog::error("fuse-overlayfs not found in PATH");
|
||||
return std::nullopt;
|
||||
}
|
||||
kMountProgram = mount_program->string();
|
||||
|
||||
auto layers = read_oci_layers(image_tar);
|
||||
if (!layers) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string parent_id;
|
||||
for (const auto& layer : *layers) {
|
||||
std::filesystem::path tmp_file =
|
||||
std::filesystem::temp_directory_path() /
|
||||
fmt::format("slocker-lite-{}-{}", getpid(), oci_digest_hex(layer.digest));
|
||||
|
||||
if (!extract_blob_to_file(image_tar, oci_digest_hex(layer.digest), tmp_file)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto layer_id = import_layer(tmp_file, parent_id);
|
||||
std::filesystem::remove(tmp_file);
|
||||
if (!layer_id) {
|
||||
spdlog::error("failed to import layer {}", layer.digest);
|
||||
return std::nullopt;
|
||||
}
|
||||
parent_id = *layer_id;
|
||||
}
|
||||
|
||||
auto merged = mount_layer(parent_id);
|
||||
if (!merged) {
|
||||
spdlog::error("failed to mount assembled image");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return MountedImage{*merged, parent_id};
|
||||
}
|
||||
|
||||
int cleanup_image(const std::string& layer_id) {
|
||||
if (!check_required_dependencies()) {
|
||||
return 1;
|
||||
}
|
||||
if (!cleanup_layer_chain(layer_id)) {
|
||||
spdlog::error("failed to clean up layer {}", layer_id);
|
||||
return 1;
|
||||
}
|
||||
fmt::print("cleaned up layer {}\n", layer_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int run_container(const std::filesystem::path& image_tar, const std::vector<std::string>& command,
|
||||
bool use_nsenter) {
|
||||
auto mounted = mount_image(image_tar);
|
||||
if (!mounted) {
|
||||
return 1;
|
||||
}
|
||||
fmt::print("mounted image at: {} (layer {})\n", mounted->merged_path, mounted->top_layer_id);
|
||||
|
||||
int exit_code = run_bwrap(mounted->merged_path, command, use_nsenter);
|
||||
if (exit_code < 0) {
|
||||
spdlog::error("failed to run bwrap");
|
||||
}
|
||||
|
||||
if (!unmount_layer(mounted->top_layer_id)) {
|
||||
spdlog::error("failed to unmount layer {}", mounted->top_layer_id);
|
||||
}
|
||||
if (!cleanup_layer_chain(mounted->top_layer_id)) {
|
||||
spdlog::error("failed to clean up layer {}", mounted->top_layer_id);
|
||||
}
|
||||
|
||||
return exit_code < 0 ? 1 : exit_code;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
spdlog::cfg::load_env_levels();
|
||||
|
||||
Mode mode = Mode::kNone;
|
||||
std::string mode_arg;
|
||||
bool disable_nsenter = false;
|
||||
|
||||
opterr = 0;
|
||||
int opt;
|
||||
while ((opt = getopt_long(argc, argv, ":hVtl:m:u:", kLongOptions.data(), nullptr)) != -1) {
|
||||
while ((opt = getopt_long(argc, argv, ":hVtl:m:u:r:c:n", kLongOptions.data(), nullptr)) != -1) {
|
||||
switch (opt) {
|
||||
case 'h':
|
||||
print_usage(argv[0]);
|
||||
@@ -169,8 +245,27 @@ int main(int argc, char* argv[]) {
|
||||
return 0;
|
||||
case 't':
|
||||
case 'm':
|
||||
case 'u': {
|
||||
Mode requested = opt == 't' ? Mode::kTest : (opt == 'm' ? Mode::kMount : Mode::kUnmount);
|
||||
case 'u':
|
||||
case 'r':
|
||||
case 'c': {
|
||||
Mode requested;
|
||||
switch (opt) {
|
||||
case 't':
|
||||
requested = Mode::kTest;
|
||||
break;
|
||||
case 'm':
|
||||
requested = Mode::kMount;
|
||||
break;
|
||||
case 'u':
|
||||
requested = Mode::kUnmount;
|
||||
break;
|
||||
case 'r':
|
||||
requested = Mode::kRun;
|
||||
break;
|
||||
default:
|
||||
requested = Mode::kCleanup;
|
||||
break;
|
||||
}
|
||||
if (mode != Mode::kNone && mode != requested) {
|
||||
spdlog::error("multiple actions specified");
|
||||
print_usage(argv[0]);
|
||||
@@ -182,6 +277,9 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'n':
|
||||
disable_nsenter = true;
|
||||
break;
|
||||
case 'l':
|
||||
if (!apply_log_level(optarg)) {
|
||||
return 1;
|
||||
@@ -199,7 +297,11 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == Mode::kNone || optind != argc) {
|
||||
if (mode == Mode::kNone) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (mode != Mode::kRun && optind != argc) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
@@ -210,50 +312,29 @@ int main(int argc, char* argv[]) {
|
||||
if (mode == Mode::kUnmount) {
|
||||
return unmount_image(mode_arg);
|
||||
}
|
||||
if (mode == Mode::kCleanup) {
|
||||
return cleanup_image(mode_arg);
|
||||
}
|
||||
if (mode == Mode::kRun) {
|
||||
std::vector<std::string> command(argv + optind, argv + argc);
|
||||
if (command.empty()) {
|
||||
command = {"/bin/sh"};
|
||||
}
|
||||
// As root, containers-storage mount doesn't need to reexec into a private
|
||||
// user namespace to gain privilege, so the mount is already directly
|
||||
// visible; nsenter into it then fails ("reassociate to namespace 'ns/user'
|
||||
// failed: Invalid argument") since we're already in that same namespace.
|
||||
bool use_nsenter = !disable_nsenter && geteuid() != 0;
|
||||
if (geteuid() == 0 && !disable_nsenter) {
|
||||
spdlog::debug("running as root; skipping nsenter (the mount is already directly visible)");
|
||||
}
|
||||
return run_container(mode_arg, command, use_nsenter);
|
||||
}
|
||||
|
||||
const std::filesystem::path image_tar = mode_arg;
|
||||
|
||||
if (!check_required_dependencies()) {
|
||||
auto mounted = mount_image(mode_arg);
|
||||
if (!mounted) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto mount_program = find_in_path("fuse-overlayfs");
|
||||
if (!mount_program) {
|
||||
spdlog::error("fuse-overlayfs not found in PATH");
|
||||
return 1;
|
||||
}
|
||||
kMountProgram = mount_program->string();
|
||||
|
||||
auto layers = read_oci_layers(image_tar);
|
||||
if (!layers) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string parent_id;
|
||||
for (const auto& layer : *layers) {
|
||||
std::filesystem::path tmp_file =
|
||||
std::filesystem::temp_directory_path() /
|
||||
fmt::format("slocker-lite-{}-{}", getpid(), oci_digest_hex(layer.digest));
|
||||
|
||||
if (!extract_blob_to_file(image_tar, oci_digest_hex(layer.digest), tmp_file)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto layer_id = import_layer(tmp_file, parent_id);
|
||||
std::filesystem::remove(tmp_file);
|
||||
if (!layer_id) {
|
||||
spdlog::error("failed to import layer {}", layer.digest);
|
||||
return 1;
|
||||
}
|
||||
parent_id = *layer_id;
|
||||
}
|
||||
|
||||
auto merged = mount_layer(parent_id);
|
||||
if (!merged) {
|
||||
spdlog::error("failed to mount assembled image");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fmt::print("mounted image at: {} (layer {})\n", *merged, parent_id);
|
||||
fmt::print("mounted image at: {} (layer {})\n", mounted->merged_path, mounted->top_layer_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
+70
-7
@@ -19,11 +19,31 @@
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <fmt/ranges.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<char*> to_c_argv(const std::vector<std::string>& argv) {
|
||||
std::vector<char*> c_argv;
|
||||
c_argv.reserve(argv.size() + 1);
|
||||
for (const auto& arg : argv) {
|
||||
c_argv.push_back(const_cast<char*>(arg.c_str()));
|
||||
}
|
||||
c_argv.push_back(nullptr);
|
||||
return c_argv;
|
||||
}
|
||||
|
||||
bool is_executable_file(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
return std::filesystem::is_regular_file(path, ec) && access(path.c_str(), X_OK) == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProcessResult run_process(const std::vector<std::string>& argv) {
|
||||
spdlog::debug("running external command: {}", fmt::join(argv, " "));
|
||||
|
||||
@@ -44,13 +64,7 @@ ProcessResult run_process(const std::vector<std::string>& argv) {
|
||||
dup2(stdout_pipe[1], STDOUT_FILENO);
|
||||
close(stdout_pipe[1]);
|
||||
|
||||
std::vector<char*> c_argv;
|
||||
c_argv.reserve(argv.size() + 1);
|
||||
for (const auto& arg : argv) {
|
||||
c_argv.push_back(const_cast<char*>(arg.c_str()));
|
||||
}
|
||||
c_argv.push_back(nullptr);
|
||||
|
||||
auto c_argv = to_c_argv(argv);
|
||||
execvp(c_argv[0], c_argv.data());
|
||||
const char* msg = "run_process: execvp failed\n";
|
||||
write(STDERR_FILENO, msg, std::strlen(msg));
|
||||
@@ -76,3 +90,52 @@ ProcessResult run_process(const std::vector<std::string>& argv) {
|
||||
}
|
||||
return {exit_code, output};
|
||||
}
|
||||
|
||||
int run_process_foreground(const std::vector<std::string>& argv) {
|
||||
spdlog::debug("running external command: {}", fmt::join(argv, " "));
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
auto c_argv = to_c_argv(argv);
|
||||
execvp(c_argv[0], c_argv.data());
|
||||
const char* msg = "run_process_foreground: execvp failed\n";
|
||||
write(STDERR_FILENO, msg, std::strlen(msg));
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
waitpid(pid, &status, 0);
|
||||
|
||||
int exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
||||
if (exit_code != 0) {
|
||||
spdlog::warn("external command failed (exit code {}): {}", exit_code, fmt::join(argv, " "));
|
||||
}
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> find_in_path(std::string_view name) {
|
||||
const char* path_env = std::getenv("PATH");
|
||||
if (!path_env) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string_view path{path_env};
|
||||
for (size_t start = 0; start <= path.size();) {
|
||||
size_t end = path.find(':', start);
|
||||
if (end == std::string_view::npos) {
|
||||
end = path.size();
|
||||
}
|
||||
std::filesystem::path dir{path.substr(start, end - start)};
|
||||
if (!dir.empty()) {
|
||||
std::filesystem::path candidate = dir / name;
|
||||
if (is_executable_file(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
struct ProcessResult {
|
||||
@@ -27,3 +30,12 @@ struct ProcessResult {
|
||||
// Runs argv[0] with the given arguments via fork/execvp, capturing stdout.
|
||||
// stderr is inherited so the child's own error messages reach the user directly.
|
||||
ProcessResult run_process(const std::vector<std::string>& argv);
|
||||
|
||||
// Runs argv[0] with the given arguments via fork/execvp, with stdin/stdout/stderr
|
||||
// all inherited from the caller (no output capture) for interactive/foreground use.
|
||||
// Returns the exit code, or -1 if fork or exec failed.
|
||||
int run_process_foreground(const std::vector<std::string>& argv);
|
||||
|
||||
// Searches $PATH for an executable regular file named `name`, in PATH order.
|
||||
// Returns its full path, or nullopt if not found.
|
||||
std::optional<std::filesystem::path> find_in_path(std::string_view name);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Runs the mount -> umount -> cleanup cycle against the fixture image and checks
|
||||
that every step succeeds, so `meson test` doesn't leak a layer on every run.
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def run(slocker_lite: str, *args: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run([slocker_lite, *args], capture_output=True, text=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
slocker_lite, fixture_tar = sys.argv[1], sys.argv[2]
|
||||
|
||||
mount = run(slocker_lite, "-m", fixture_tar)
|
||||
if mount.returncode != 0:
|
||||
print(mount.stderr, file=sys.stderr)
|
||||
return mount.returncode
|
||||
|
||||
match = re.search(r"\(layer (\S+)\)", mount.stdout)
|
||||
if not match:
|
||||
print(f"could not find layer ID in mount output: {mount.stdout!r}", file=sys.stderr)
|
||||
return 1
|
||||
layer_id = match.group(1)
|
||||
|
||||
for args in (("-u", layer_id), ("-c", layer_id)):
|
||||
result = run(slocker_lite, *args)
|
||||
if result.returncode != 0:
|
||||
print(result.stderr, file=sys.stderr)
|
||||
return result.returncode
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user