ceamac 355675b43f Resolve HOME to the target user's home directory instead of /root
ResolvedUser now carries home, looked up from the image's own
/etc/passwd entry for the final resolved uid (falling back to /root
for uid 0 or / otherwise when there's no matching row). bwrap's HOME
now uses this whenever a user override applies (--user/--group or an
image-declared default user); the plain /root default is kept only
when no override applies at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-08-22 06:47:16 +00:00

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, and can be mounted into -r/--run (repeatably), along with ad hoc host directories. 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 vendored subprojects/spdlog.wrap)
  • catch2, only if the enable_tests Meson option is on (default: on)

Runtime:

  • containers-storage
  • fuse-overlayfs
  • bwrap (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> [-v <name-or-dir> <container-path>]... [-- <command> [args...]]
slocker-lite -u|--umount <layer-id>
slocker-lite -c|--cleanup <layer-id>
slocker-lite -l|--list-images <directory>
slocker-lite -i|--inspect <image.tar>
slocker-lite -v|--volume <name> <directory>
slocker-lite --list-volumes
slocker-lite --delete-volume <name>
slocker-lite --delete-volume-full <name>
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.
-i, --inspect <image.tar> Print an image's declared user, exposed ports, env, volumes, and default command, without mounting or running it.
-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. Volume names can't contain /. With --run, instead mounts a volume into the sandbox (repeatable): <name> is an existing named volume, or, if it contains /, a host directory path (created if missing); <dir> is the absolute path inside the container to mount it at. If the host directory is empty and the image already has content there, that content is copied in first, preserving numeric ownership/permissions/links and, where the host filesystem supports them, extended attributes/ACLs (skipped with a warning otherwise).
--list-volumes List all named volumes (see -v/--volume) with their host directory.
--delete-volume <name> Remove a named volume from the config. The host directory is left untouched.
--delete-volume-full <name> Like --delete-volume, but also recursively deletes the volume's host directory.
-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

# Inspect an image's declared config without mounting or running it
./buildDir/slocker-lite -i myimage.tar

# Create a named volume backed by a host directory
./buildDir/slocker-lite -v mydata ~/slocker-volumes/mydata

# Run, mounting that named volume plus an ad hoc host directory
./buildDir/slocker-lite -r myimage.tar -v mydata /data -v ~/scratch /scratch

# List all named volumes
./buildDir/slocker-lite --list-volumes

# Remove a named volume (keeps its host directory)
./buildDir/slocker-lite --delete-volume mydata

# Remove a named volume and delete its host directory too
./buildDir/slocker-lite --delete-volume-full 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 what -r/--run's own -v usage looks named volumes up in. 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.

S
Description
Run a docker image with bubblewrap on low end devices
Readme 772 KiB
Languages
C++ 97.9%
Python 1%
Meson 0.7%
C 0.3%