Commit Graph

67 Commits

Author SHA1 Message Date
ceamac d69b408d14 Add -i/--inspect to display an image's declared config
Prints user/group, exposed ports, env, volumes, and default command
from OciImageConfig without mounting or running the image. Extend
inspect_image_command() whenever OciImageConfig gains a new field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-08-21 13:39:29 +00:00
ceamac abf268e950 Add --delete-volume and --delete-volume-full
--delete-volume <name> removes a volume's config entry only.
--delete-volume-full <name> also recursively deletes its host
directory -- fails before touching the config if that deletion errors,
so a failed directory delete never silently drops the config entry;
warns instead of failing if the directory was already gone.

Both are long-only, following the --list-volumes precedent, and slot
into the existing multi-mode getopt dispatch the same way -v/--volume
already does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-08-21 13:21:46 +00:00
ceamac afca0f7ce3 Add --list-volumes to list named volumes
Long-only (no free short letter left), lists each volume's name and
host directory, tab-aligned the same way -l/--list-images already
aligns name:tag output -- list_volumes_command() reuses that scheme.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-08-21 13:15:28 +00:00
ceamac aac656be31 Add -v/--volume to create a named volume mapped to a host directory
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
2026-08-21 12:59:37 +00:00
ceamac be5a0b8202 Add a local YAML config file for persistent settings
Reads $XDG_CONFIG_HOME/slocker-lite/config.yaml (falling back to
$HOME/.config/slocker-lite/config.yaml), organized into sections. Only
the "global" section's log-level is supported for now -- other options
are one-shot flags, not standing preferences. An explicit --log-level
on the command line always overrides the config file, the same way
SPDLOG_LEVEL already does.

Uses libyaml directly (yaml_dep was already declared in meson.build but
unused). A missing config file isn't an error; unknown sections/keys
are ignored for forward-compatibility; malformed YAML syntax is a hard
error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-08-21 12:31:40 +00:00
ceamac f7e7faed19 Point CLAUDE.md at the new README instead of noting its absence
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-08-21 12:10:25 +00:00
ceamac 0b1698dc97 Rename the executable from slocker_lite to slocker-lite
Matches the project name and the slocker-lite-priv-drop helper's own
naming convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-08-21 12:06:03 +00:00
ceamac e64f885c2e Default -r's user/group to the image's own declared user
When --user isn't given, run the sandboxed process as whatever user the
image's own config.User declares (already parsed into OciImageConfig::user/
group), instead of always defaulting to root. An explicit --user/--group on
the command line still takes precedence.

read_oci_image_config() is now called unconditionally in run_container()
(it was previously gated behind "no command given") and shared for both the
default command and the default user/group, rather than growing another
special-case guard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-08-21 07:28:14 +00:00
ceamac 69df728e09 Add --user/--group to run as a different uid/gid as root
bwrap --uid/--gid require --unshare-user, which is never requested
when running as root (since last session's fix), and user namespaces
aren't a near-term option anyway -- the actual Android target doesn't
support them.

--user <name-or-uid> / --group <name-or-gid> work around this:
user_spec.cpp resolves them against the *mounted image's own*
/etc/passwd and /etc/group (names like "git" only mean anything inside
that image's user database), and bwrap.cpp bind-mounts a small helper
into the sandbox to do the actual privilege drop before exec'ing the
real command, since bwrap itself can't switch uid/gid without a user
namespace.

The helper has to be a separate, statically-linked binary
(priv_drop_helper.cpp -> slocker-lite-priv-drop, built with -static)
rather than slocker_lite's own binary: bind-mounting a dynamically
linked executable into an arbitrary container image fails ("error
while loading shared libraries") since that image's own /lib won't
have slocker_lite's dependencies. find_priv_drop_helper() locates it
next to slocker_lite's own binary; run_bwrap() fails fast if it's
missing rather than silently running as root.

Only works without a user namespace (root): under --unshare-user the
sandbox's uid map has only one valid entry, so the helper's own
setuid() fails cleanly there instead of doing nothing silently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 07:06:13 +00:00
ceamac 095ae8397a Drop --unshare-net from bwrap args, no network setup yet
Without any network setup (slirp4netns or similar), unsharing the
network namespace just leaves the sandbox with no network at all,
which isn't useful yet. build_bwrap_args() now skips --unshare-net
when assembling the real bwrap invocation; re-add once network
isolation is implemented.

detect_bwrap_unshare_args() itself is unchanged and still probes/
reports net namespace kernel support (e.g. via -t/--test), since
that's capability detection, not policy -- same pattern as the
--new-session removal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 06:17:20 +00:00
ceamac b1a2d924e8 Don't request --unshare-user for bwrap when running as root
Root already has full privilege without a new user namespace. Creating
one anyway (bwrap's default single-mapping uid/gid setup, no
--uid/--gid/subuid ranges) forces the kernel's unprivileged-userns
setgroups() restriction: every supplementary group outside that one
mapping collapses to the overflow gid (65534/"nobody"), and
setgroups() calls inside the sandbox then fail.

Reported by the user running -r images/gitea.tar as root: `id` showed
groups=0(root),65534(nobody) repeated once per real supplementary
group, and `su git` failed with "can't set groups: Operation not
permitted". detect_bwrap_unshare_args() now skips --unshare-user (and
stops combining the other probes with CLONE_NEWUSER) whenever
geteuid() == 0 -- confirmed fixed by the user on a root-capable
machine. The non-root path is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 05:34:41 +00:00
ceamac f88b4c08e7 Handle SIGINT/SIGTERM in run_process_foreground for -r
Previously, Ctrl-C or `kill` during -r's bwrap run left slocker_lite
killed outright (default signal disposition), skipping
run_container()'s unmount/cleanup entirely and leaving the layer
imported and/or mounted as garbage.

run_process_foreground() now installs a handler around its waitpid()
that forwards the signal to the running child (kill(), async-signal-
safe) and keeps waiting (EINTR-retry loop) instead of dying. Once the
child actually exits, control returns normally so the existing
unmount/cleanup runs as usual.

Verified with a backgrounded `-r ... -- /bin/sleep 30`, sending both
SIGINT and SIGTERM to the outer slocker_lite PID: process tree exits
cleanly, unmount+delete-layer both run, containers-storage layers
returns to baseline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 05:10:46 +00:00
ceamac b8f4680745 Extract OCI image config; use it for -r's default command
read_oci_image_config() (src/oci_image.cpp) reads the image config
blob referenced by the manifest and extracts User, ExposedPorts (as
OciExposedPort{port, OciPortProtocol}, parsed from keys like
"3000/tcp"), Env, Volumes, and the effective default command
(Entrypoint ++ Cmd). Refactored the oci-layout/index.json/manifest
loading read_oci_layers() already did into a shared read_oci_manifest()
helper, since this is the first time a second "loud" (spdlog::error
on failure) caller needs the identical validation.

-r/--run now uses the image's own default command when none is given
on the command line, falling back to /bin/sh only if the image sets
neither Entrypoint nor Cmd. User/ExposedPorts/Env/Volumes are captured
but not applied anywhere yet -- that lines up with volumes/networking
still being deferred.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 16:35:38 +00:00
ceamac 94ae5b36a2 Add -l/--list-images to discover OCI archives in a directory
list_oci_images() (src/oci_image.cpp) scans a directory non-recursively
for *.tar/*.tar.* files and, for each one that's a valid OCI Image
Layout archive, derives a name:tag from its index.json manifest
annotations -- io.containerd.image.name if present (a full reference),
else org.opencontainers.image.ref.name (conventionally just a bare
tag for skopeo/podman-produced archives). Falls back to the archive's
filename (.tar and any compression suffix stripped) for the name and
"latest" for the tag. Files that aren't OCI archives are skipped
quietly, since a directory scan is expected to hit unrelated tars.

-l/--list-images <directory> wires this into the CLI, printing
"name:tag<TAB>filename" per image. This is also why --log-level lost
its short form last session: -l needed to be free for this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 15:06:32 +00:00
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
ceamac 7e1ee150f6 Add CLI option parsing and implement layer unmounting
Replace the implicit single-argument invocation with getopt_long-based
flags: -m/--mount (existing mount flow, now explicit), -u/--umount
(unmounts a layer via containers-storage), -t/--test (stub),
-l/--log-level (runtime spdlog level), -h/--help, -V/--version.
--mount now also prints the top layer's ID so it can be passed to
--umount.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 09:03:58 +00:00
ceamac a301dfb44f Initial commit: OCI image mounting via containers-storage
slocker-lite validates an OCI Image Layout tar, imports its layers into
containers-storage's layer store in order, and mounts the assembled image
using fuse-overlayfs, printing the resulting merged path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 08:12:50 +00:00