Creates the host directory if missing (warning if it already exists,
another if it's non-empty) and records name -> directory in the config
file's new "volumes" section. Fails if the name or directory is already
used by an existing volume.
This is a distinct concept from OciImageConfig::volumes (an image's own
declared mount points, still unconsumed) -- a user-defined volume, meant
to be referenced by name once -r/--run starts actually mounting volumes.
config_file.{h,cpp} gains write_config_file(), symmetric to the existing
load_config_file(), built on libyaml's document-building/emitter API.
Rewrites the whole file each time; global.log-level round-trips
untouched alongside the new volumes section.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
slocker-lite
slocker-lite mounts an OCI Image Layout
tar (the format produced by skopeo, podman save --format oci-archive, or a modern
docker save) and runs a sandboxed command against it — without podman, docker,
or kernel overlayfs.
Why
The target environment is Android with a stock kernel: podman/docker don't run
there (missing namespace support), and there's no kernel overlayfs. slocker-lite
works around both: it imports image layers into containers-storage and mounts them
with fuse-overlayfs (userspace, no kernel overlayfs needed), then sandboxes the run
with bwrap in "degraded mode" — using only whichever --unshare-xxx namespaces the
running kernel actually supports, instead of requiring the full set.
Status
Early-stage. Mounting, running, and dropping privileges to a specific user/group all
work. Named volumes (-v/--volume) can be created and are persisted in the config
file, but aren't consumed by -r/--run yet. Image-declared networking
(ExposedPorts/Env from the image config, and the image's own separately-declared
Volumes) are parsed but not yet applied, and there's no background/daemonized run
mode yet.
Requirements
Build-time:
- Meson + a C++20 compiler
fmt,libarchive,nlohmann_json,yaml-0.1(libyaml)spdlog(uses the system package if found, otherwise fetched automatically via the vendoredsubprojects/spdlog.wrap)catch2, only if theenable_testsMeson option is on (default: on)
Runtime:
containers-storagefuse-overlayfsbwrap(bubblewrap)nsenter(only needed for non-root runs — see "How it works" below)
Build
meson setup buildDir
meson compile -C buildDir
meson test -C buildDir
This also builds buildDir/slocker-lite-priv-drop, a small statically-linked helper
that -r --user/--group needs at runtime (see "How it works").
Usage
slocker-lite -m|--mount <image.tar>
slocker-lite -r|--run <image.tar> [-- <command> [args...]]
slocker-lite -u|--umount <layer-id>
slocker-lite -c|--cleanup <layer-id>
slocker-lite -l|--list-images <directory>
slocker-lite -v|--volume <name> <directory>
slocker-lite -t|--test
slocker-lite -h|--help
slocker-lite -V|--version
| Flag | Description |
|---|---|
-m, --mount <image.tar> |
Validate and mount an OCI Image Layout tar. |
-r, --run <image.tar> |
Mount, run bwrap in the foreground, then unmount and clean up on exit. Defaults to the image's own Entrypoint/Cmd (or /bin/sh if neither is set); pass -- <command> [args...] to override. |
-u, --umount <layer-id> |
Unmount a previously mounted layer (the ID printed by --mount/--run, or from containers-storage layers). |
-c, --cleanup <layer-id> |
Delete a layer and its ancestor chain from local storage (unmount it first). |
-n, --no-nsenter |
With --run, bind the mount directly instead of nsenter-ing into fuse-overlayfs's namespace. Automatic when running as root; use this to force it off otherwise. |
--user <user> |
With --run, run the command as this user (name or numeric uid) instead of the image's own declared user (or root, if it declares none). Resolved against the image's own /etc/passwd. Only takes effect when --run executes as root. |
--group <group> |
With --user, use this group (name or numeric gid) instead of the user's primary group. |
-l, --list-images <dir> |
List OCI Image Layout tars (*.tar, *.tar.*) found directly in <dir>, with their name:tag. |
-v, --volume <name> <dir> |
Create a named volume mapped to a host directory (created if missing), recorded in the config file's volumes section. Fails if the name or directory is already used by an existing volume. |
-t, --test |
Print which bwrap --unshare-xxx namespaces the running kernel supports. |
--log-level <level> |
Set log verbosity (trace, debug, info, warn, error, critical, off). |
-h, --help |
Print usage and exit. |
-V, --version |
Print version information and exit. |
Examples
# Mount an image and inspect it (prints the merged mount path)
./buildDir/slocker-lite -m myimage.tar
# Mount, run the image's default command, then unmount and clean up
./buildDir/slocker-lite -r myimage.tar
# Run a specific command instead
./buildDir/slocker-lite -r myimage.tar -- /bin/sh -c 'echo hello'
# Run as a specific user (as root only)
sudo ./buildDir/slocker-lite -r myimage.tar --user git
# List every OCI image tar in a directory
./buildDir/slocker-lite -l ./images
# Create a named volume backed by a host directory
./buildDir/slocker-lite -v mydata ~/slocker-volumes/mydata
Configuration
Persistent settings can be kept in a local YAML config file at
$XDG_CONFIG_HOME/slocker-lite/config.yaml (falling back to
$HOME/.config/slocker-lite/config.yaml if XDG_CONFIG_HOME isn't set). The file is
organized into sections:
global:
log-level: debug
volumes:
mydata: /home/user/slocker-volumes/mydata
global.log-level is the only standing preference supported today (one-shot
commands like --mount/--run/--user don't belong in a config file). An explicit
--log-level on the command line always overrides the config file. The volumes
section is managed by -v/--volume (see above) rather than hand-edited — it's not
consumed by -r/--run yet. A missing config file is fine either way (nothing is
overridden, and one gets created the first time -v/--volume is used).
How it works
Image layers are imported into containers-storage (parent-chained) and the
resulting top layer is mounted via fuse-overlayfs; -r/--run then sandboxes the
requested command with bwrap. Because containers-storage mount runs rootless by
re-execing into a private user+mount namespace, -r/--run normally has to nsenter
into that namespace to reach the mount — except when running as root, where the mount
is already directly visible and --unshare-user is skipped entirely (a fresh user
namespace isn't needed for root's own privilege, and forces an unrelated
supplementary-group bug in that case). Running as root also unlocks --user/
--group: since bwrap --uid/--gid require a user namespace that isn't available
there, slocker-lite instead bind-mounts a separate, statically-linked helper
(slocker-lite-priv-drop) into the sandbox and routes the command through it to drop
privileges before exec.
See CLAUDE.md for the full architecture writeup (file-by-file breakdown, the
reasoning behind each of the above, and known gaps).
License
GPL-2.0-or-later. See COPYING.