Add --user/--group support to -e/--exec, matching -r/--run
Previously -e/--exec always ran its command as the host's own invoking
credentials, ignoring whatever uid/gid the session's own -r/--run resolved
it to. Now it defaults to whatever the session's sandboxed command is
already running as (read from /proc/<pid>/status), and --user/--group can
override that, resolved against the session's own /etc/passwd/group
(fetched via nsenter, since the mount namespace isn't otherwise reachable).
Either way it's applied by reusing the already bind-mounted
slocker-lite-priv-drop helper from the original -r/--run, not a second copy.
Refactored resolve_user_and_group() (user_spec.{h,cpp}) to take passwd/group
*content* instead of a filesystem path, so both callers -- run_container()
(local file read) and exec_in_session() (nsenter + cat) -- can share it.
Exported priv_drop::path/helper_name and find_priv_drop_helper() from
bwrap.h so exec_session.cpp can reuse the same helper.
Caught and fixed a second bug during testing on a rootless dev machine:
under -r/--run's own --unshare-user, "root inside the container" is a uid
mapping, not a real privilege drop, so /proc/<pid>/status's uid/gid (read
from outside that namespace) is the host-mapped id, not the container's
own view -- defaulting to a priv-drop there was wrong and failed outright.
Fixed by skipping the default-identity lookup whenever --exec is already
joining a differing user namespace, since nsenter --preserve-credentials
alone already reproduces the container's view via that same kernel mapping.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -270,22 +270,34 @@ Source layout (all under `src/`):
|
||||
working code). Usage: `slocker-lite-priv-drop <uid>:<gid> -- <command> [args...]`;
|
||||
does `setgroups(0,…)` → `setgid()` → `setuid()` → `execvp()`, in that order
|
||||
(dropping the group needs `CAP_SETGID`, which is lost once `setuid()` drops root).
|
||||
`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`.
|
||||
`find_priv_drop_helper()` and the `priv_drop::path`/`priv_drop::helper_name`
|
||||
constants live in `bwrap.h` (not just internal to `bwrap.cpp`) specifically so
|
||||
`exec_in_session()` (`exec_session.cpp`, see below) can reuse the exact same
|
||||
already-bind-mounted helper for `-e/--exec`'s own `--user`/`--group` support,
|
||||
instead of a second copy needing to be bind-mounted for it (which wouldn't even
|
||||
be possible — `-e/--exec` joins an *already-running* session's mount namespace,
|
||||
it doesn't get to add bind mounts to it). `find_priv_drop_helper()` itself only
|
||||
checks this binary's own host-side existence; it says nothing about whether a
|
||||
given session actually has it bind-mounted (only true when that session's
|
||||
`-r/--run` resolved a user in the first place).
|
||||
- `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. `ResolvedUser` (`bwrap.h`) also
|
||||
carries `home`, looked up by the final resolved uid's `/etc/passwd` entry (field 5)
|
||||
regardless of whether `user` was given as a name or a number; falls back to
|
||||
`"/root"` for uid 0 or `"/"` otherwise when there's no matching row.
|
||||
`build_sandbox_env()` (`bwrap.cpp`) sets the sandboxed process's `HOME` from
|
||||
this — `"/root"` only when no user override applies at all (no `--user`, no
|
||||
image-declared `config.User`). `run_container()` (`commands.cpp`) calls `resolve_user_and_group()`
|
||||
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
|
||||
a name or numeric id) against the *container's own* `/etc/passwd`/`/etc/group`
|
||||
**content** (not the host's, and not a path — callers own reading it, since the
|
||||
two current callers get that content two different ways: `run_container()`
|
||||
reads it directly off the merged mount path, while `exec_in_session()` fetches
|
||||
it over `nsenter`, since a running session's mount namespace isn't otherwise
|
||||
reachable from this process — see below). `nullopt` content for either file
|
||||
means "unreadable/absent"; a numeric user with no group still resolves fine
|
||||
without it (defaults gid to the same numeric value as the uid) but a named one
|
||||
doesn't. `ResolvedUser` (`bwrap.h`) also carries `home`, looked up by the final
|
||||
resolved uid's `/etc/passwd` entry (field 5) regardless of whether `user` was
|
||||
given as a name or a number; falls back to `"/root"` for uid 0 or `"/"`
|
||||
otherwise when there's no matching row. `build_sandbox_env()` (`bwrap.cpp`)
|
||||
sets the sandboxed process's `HOME` from this — `"/root"` only when no user
|
||||
override applies at all (no `--user`, no image-declared `config.User`).
|
||||
`run_container()` (`commands.cpp`) calls `resolve_user_and_group()` 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):
|
||||
@@ -456,6 +468,51 @@ Source layout (all under `src/`):
|
||||
`nsenter ... -- <command>` via the existing `run_process_foreground()`
|
||||
(`process.h`) — same inherited stdio and SIGINT/SIGTERM forwarding as every
|
||||
other foreground external command, no new process-running logic needed.
|
||||
`exec_in_session()` also takes optional `--user`/`--group` (mirroring `-r/--run`'s
|
||||
own): given, they resolve against the session's own `/etc/passwd`/`/etc/group`
|
||||
(fetched via `cat` run through the same `nsenter` join, since this process can't
|
||||
otherwise see into that namespace, then handed to `resolve_user_and_group()` —
|
||||
`user_spec.h`, see below); if unset, defaults to whatever uid/gid the session's
|
||||
own sandboxed command is *already* running as (read from `/proc/<ns_pid>/status`),
|
||||
rather than root/the caller — fixing a real bug (reported after this project's
|
||||
own `-e/--exec` and priv-drop features had both shipped separately): without this,
|
||||
`-e/--exec` always ran as whatever the *host* invocation was, ignoring any
|
||||
`--user`/`--group` the session itself was started with. Either way, the resolved
|
||||
identity is applied by running `command` through the session's already
|
||||
bind-mounted `slocker-lite-priv-drop` helper (`priv_drop::path`, `bwrap.h`) —
|
||||
reused as-is, not bind-mounted again (`-e/--exec` can't add bind mounts to an
|
||||
already-running session's namespace anyway). Skipped entirely when the resolved
|
||||
uid *and* gid are both 0: a session that was never given a resolvable user at
|
||||
`-r/--run` time never got the helper bind-mounted at all, and dropping to 0:0
|
||||
would be a no-op regardless; a missing helper for a genuinely non-root
|
||||
resolution instead surfaces as `nsenter`'s own "No such file or directory" once
|
||||
it tries to exec `priv_drop::path`, diagnostic enough on its own. **Second real
|
||||
bug, caught by direct testing on a rootless dev machine before this shipped**:
|
||||
when the session's own `-r/--run` used `--unshare-user` (i.e. ran rootless —
|
||||
see the root-vs-rootless paragraph below), "root inside the container" is
|
||||
achieved purely through the kernel's own uid mapping for that namespace, not a
|
||||
real privilege drop — so `/proc/<ns_pid>/status`'s uid/gid, read from *outside*
|
||||
that namespace, shows the host-mapped id (e.g. `1000`), not the
|
||||
container-relative one (`0`). Treating that as "needs a priv-drop to 1000" is
|
||||
wrong two ways: the helper is typically never bind-mounted for a session with
|
||||
no resolved `--user`, and even when it is, `setuid()` fails outright under the
|
||||
single-entry uid map an unprivileged user namespace gets (confirmed directly:
|
||||
`failed to drop privileges to 0:0: Operation not permitted`). Fixed by tracking
|
||||
whether the `user` namespace type was actually one of the ones joined (it only
|
||||
is when it differs from this process's own, i.e. exactly when `-r/--run` used
|
||||
`--unshare-user`) and, when so, leaving the default identity unresolved (no
|
||||
priv-drop) for that case — joining that same user namespace with
|
||||
`--preserve-credentials` (already done regardless) already reproduces the
|
||||
container's own view correctly via that same kernel mapping, with nothing
|
||||
further needed. Verified end-to-end on this same rootless dev machine: a
|
||||
daemonized `-r --run` busybox session with no declared user, `--exec`'d with no
|
||||
`--user`, now correctly shows `uid=0(root)` (previously would have attempted,
|
||||
and failed, a priv-drop to the host-mapped uid); an explicit `--exec --user 0`
|
||||
against the same session correctly resolves to `0:0` and skips the priv-drop
|
||||
step; `--exec --user portage` against it correctly resolves the name to its
|
||||
real `250:250` via the fetched `/etc/passwd` and then fails clearly (helper not
|
||||
bind-mounted, since the session itself had no declared user) rather than
|
||||
silently running as the wrong identity.
|
||||
- `config_file.{h,cpp}` — `load_config_file()` reads and parses (via libyaml's
|
||||
document API, `<yaml.h>`) the `global` and `volumes` sections of the local YAML
|
||||
config file located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`,
|
||||
|
||||
Reference in New Issue
Block a user