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
+39 -17
View File
@@ -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