-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>
4.8 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project state
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, 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 (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 thecontainers-storageCLI (import-layer,mount,unmount,layer --json,delete-layer), forcingfuse-overlayfsas the overlaymount_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 forkedunshare(2)per namespace type) for which--unshare-xxxflagsbwrapcan 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 forcontainers-storagecalls),run_process_foreground()inherits all of stdio (used for the interactivebwraprun). Alsofind_in_path(), a shared$PATHlookup.
Errors are logged via spdlog::error; every external command is also traced at debug
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, 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
Build directory is buildDir/ (already configured).
- Configure (only needed if
buildDir/is missing or deleted):meson setup buildDir - Build:
meson compile -C buildDir(orninja -C buildDir) - Run the executable:
./buildDir/slocker_lite -m <image.tar>(see--helpfor the 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
- Null-pointer checks: prefer
if (!ptr)/if (ptr)overif (ptr == nullptr)/if (ptr != nullptr).
Licensing
- Every
.c/.cpp/.hfile undersrc/must start with the GPLv2-or-later copyright header (see any existing file undersrc/for the exact text). - After adding a new source file under
src/, run./add-license.shfrom the repo root to prepend the header (it readscopyright-headerand inserts it viased, skipping files that already have it, so it's safe to re-run at any time).
Build configuration notes
meson.buildsetswarning_level=3andcpp_std=c++20— keep new code warning-clean under-Wall -Wextra -Wpedantic-equivalent settings.- The single Meson
test()target runsslocker_liteagainst a fixture OCI image tar generated at build time bytests/gen_fixture.py(acustom_target) and checks its exit code (no test framework is wired in yet).