From e64f885c2ed4a1237cd85c55a32be4569bda5778 Mon Sep 17 00:00:00 2001 From: Viorel Munteanu Date: Fri, 21 Aug 2026 07:28:14 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz --- CLAUDE.md | 24 +++++++++++++++++------- src/main.cpp | 25 ++++++++++++++++++------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e6cc62c..ff8aa24 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,11 @@ 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()`, `list_images_command()`). + `run_container()` unconditionally calls `read_oci_image_config()` and reuses the + result for two independent defaults: the command to run (`Entrypoint ++ Cmd`) when + none is given on the command line, and, when `--user` wasn't given, the sandboxed + process's user/group (`config.User`, split into `OciImageConfig::user`/`group`) — + an explicit `--user`/`--group` on the command line always takes precedence. - `oci_image.{h,cpp}` — validates/parses the OCI Image Layout tar (libarchive + nlohmann_json) and extracts layer blobs. `list_oci_images()` scans a directory (non-recursively) for `*.tar`/`*.tar.*` files and, for each valid OCI archive, @@ -24,10 +29,11 @@ Source layout (all under `src/`): (`io.containerd.image.name` preferred, else `org.opencontainers.image.ref.name`), falling back to the archive's filename and `"latest"` respectively. `read_oci_image_config()` reads the image config blob referenced by the manifest - and extracts `User`, `ExposedPorts`, `Env`, `Volumes`, and the effective default - command (`Entrypoint ++ Cmd`); `-r/--run` uses its command when none is given on - the command line. Only the default command is actually consumed today — the rest - is captured for when volumes/networking are implemented. + and extracts `User` (split on `:` into `OciImageConfig::user`/`group`), + `ExposedPorts`, `Env`, `Volumes`, and the effective default command + (`Entrypoint ++ Cmd`). `user`/`group` and the default command are consumed by + `-r/--run` (see `main.cpp` above) — `ExposedPorts`/`Env`/`Volumes` are still just + captured for when networking/volumes are implemented. - `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 @@ -71,11 +77,15 @@ Source layout (all under `src/`): `find_priv_drop_helper()` (`src/bwrap.cpp`) locates it next to `slocker_lite`'s own binary (via `/proc/self/exe`'s directory), which holds both when run straight from `buildDir/` and after a real `meson install`. -- `user_spec.{h,cpp}` — `resolve_user_and_group()` resolves `--user`/`--group` (each +- `user_spec.{h,cpp}` — `resolve_user_and_group()` resolves a user/group spec (each a name or numeric id) against the *mounted image's own* `/etc/passwd`/`/etc/group` (not the host's), since names like `git` only mean anything inside that image's own - user database. A numeric `--user` with no `--group` and no matching `/etc/passwd` - entry defaults gid to the same numeric value as the uid. + user database. A numeric user with no group and no matching `/etc/passwd` entry + defaults gid to the same numeric value as the uid. `run_container()` (`main.cpp`) + calls this with either the explicit `--user`/`--group` flags, or, when `--user` + wasn't given, the image's own declared `config.User` (`OciImageConfig::user`/ + `group`) — so a container defaults to running as whatever user the image itself + declares, not root, unless the image declares none. - `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` diff --git a/src/main.cpp b/src/main.cpp index 38f7088..ded754e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -94,10 +94,11 @@ void print_usage(const char* prog) { " the mount is already directly visible; pass\n" " this to force it off otherwise)\n" " --user with --run, run the command as this user (name\n" - " or numeric uid) instead of root, resolved\n" - " against the image's own /etc/passwd; only takes\n" - " effect when --run executes as root (no user\n" - " namespace involved)\n" + " or numeric uid) instead of the image's own\n" + " declared user (or root, if it declares none),\n" + " resolved against the image's own /etc/passwd;\n" + " only takes effect when --run executes as root\n" + " (no user namespace involved)\n" " --group with --user, use this group (name or numeric\n" " gid) instead of the user's own primary group\n" " -l, --list-images list OCI Image Layout tars (*.tar, *.tar.*) found\n" @@ -263,10 +264,21 @@ int run_container(const std::filesystem::path& image_tar, } fmt::print("mounted image at: {} (layer {})\n", mounted->merged_path, mounted->top_layer_id); + auto config = read_oci_image_config(image_tar); + + // Falls back to the image's own declared user (config.User) when --user wasn't + // given on the command line, rather than always defaulting to root. + std::optional effective_user = user; + std::optional effective_group = group; + if (!effective_user && config && !config->user.empty()) { + effective_user = config->user; + effective_group = config->group.empty() ? std::nullopt : std::optional(config->group); + } + bool ok = true; std::optional resolved_user; - if (user) { - resolved_user = resolve_user_and_group(*user, group, mounted->merged_path); + if (effective_user) { + resolved_user = resolve_user_and_group(*effective_user, effective_group, mounted->merged_path); if (!resolved_user) { ok = false; } @@ -274,7 +286,6 @@ int run_container(const std::filesystem::path& image_tar, std::vector command = requested_command; if (command.empty()) { - auto config = read_oci_image_config(image_tar); command = (config && !config->command.empty()) ? config->command : std::vector{"/bin/sh"}; }