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
|
||||
|
||||
Reference in New Issue
Block a user