Files
slocker-lite/CLAUDE.md
T
ceamac c0bef0d989 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>
2026-08-17 07:44:24 +00:00

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 the containers-storage CLI (import-layer, 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()/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 (or ninja -C buildDir)
  • Run the executable: ./buildDir/slocker_lite -m <image.tar> (see --help for 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) over if (ptr == nullptr) / if (ptr != nullptr).

Licensing

  • Every .c/.cpp/.h file under src/ must start with the GPLv2-or-later copyright header (see any existing file under src/ for the exact text).
  • After adding a new source file under src/, run ./add-license.sh from the repo root to prepend the header (it reads copyright-header and inserts it via sed, skipping files that already have it, so it's safe to re-run at any time).

Build configuration notes

  • meson.build sets warning_level=3 and cpp_std=c++20 — keep new code warning-clean under -Wall -Wextra -Wpedantic-equivalent settings.
  • The single Meson test() target runs slocker_lite against a fixture OCI image tar generated at build time by tests/gen_fixture.py (a custom_target) and checks its exit code (no test framework is wired in yet).