Add -e/--exec to join a running -r/--run session
Validates the given pid against the same tracked-session liveness check --list-processes/--clean-processes already use, then joins its namespaces via nsenter and runs a command there in the foreground. Two things discovered only by testing against a live session, not assumed up front: - The tracked pid is bwrap's own outer process. It sets up the mount/user namespaces itself, then clone()s the actual sandboxed command into fresh pid/uts/ipc/cgroup namespaces -- clone()'s namespace flags only ever affect the new child, never the caller, so the outer process itself never enters those namespaces at all. exec_in_session() resolves that real child via /proc/<pid>/task/<pid>/children and joins its namespaces instead, falling back to the outer pid if that can't be read. - Rather than nsenter -a (which would hit a known "Invalid argument" failure re-entering an identical namespace -- this project already worked around exactly that once, for the containers-storage mount path), each namespace type is only joined if /proc/<pid>/ns/<type> actually differs from this process's own. nsenter also needs --preserve-credentials, or it tries to setuid/setgid/setgroups to the target's identity, which fails outright against the setgroups-denied unprivileged user namespace bwrap creates whenever -r/--run isn't root. Verified end-to-end: joined shell gets the container's own hostname, process tree (ps shows only container processes), and root filesystem; untracked/stale pids error out cleanly without touching nsenter; Ctrl-C during the joined command doesn't disturb the original session; no leftover mounts after either exits. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -29,7 +29,14 @@ Source layout (all under `src/`):
|
||||
(`pid_file.{h,cpp}`) and prints one `removed stale pid file for '<name>' (pid
|
||||
<pid>)` line per file actually removed — nothing is printed for sessions still
|
||||
running, and an empty result (nothing stale) is silent success, same
|
||||
convention as the rest of this file's list/delete commands.
|
||||
convention as the rest of this file's list/delete commands. `-e/--exec <pid>`
|
||||
(has a short form, unlike the rest of the process-tracking flags) dispatches
|
||||
straight to `exec_in_session()` (`exec_session.{h,cpp}`, see below): `pid`
|
||||
lands in `mode_arg` (parsed as a positive integer, erroring out otherwise) the
|
||||
same way `-r`'s image path does, and the trailing command
|
||||
(`argv[optind:]`, required — errors out if empty) is collected the same way
|
||||
`-r`'s own command is, sharing that mode's exemption from the "no leftover
|
||||
positional args" check.
|
||||
`inspect_image_command()` implements `-i/--inspect
|
||||
<image.tar>`: prints every `OciImageConfig` field (user/group, exposed ports, env,
|
||||
volumes, default command) without mounting or running the image — extend it
|
||||
@@ -222,6 +229,43 @@ Source layout (all under `src/`):
|
||||
a check and a later removal. Only files it actually removes are reported back
|
||||
(as `SessionInfo`s with `running=false`); still-locked (running) files are
|
||||
left untouched and not reported.
|
||||
- `exec_session.{h,cpp}` — implements `-e/--exec <pid>`: joins an already-running
|
||||
`-r/--run` session's namespaces via `nsenter` and runs a command inside it in
|
||||
the foreground. `exec_in_session()` first confirms `pid` is a tracked, running
|
||||
session via `list_sessions()` (`pid_file.h`) — same liveness check
|
||||
`--list-processes`/`--clean-processes` already use, no new logic needed there.
|
||||
**Key discovery, confirmed by direct testing, not assumed**: `pid` (the one
|
||||
`run_process_foreground()` captured and pid-file-tracked when `-r` launched
|
||||
`bwrap`) is bwrap's own *outer* process — it sets up the mount and user
|
||||
namespaces itself, then `clone()`s the actual sandboxed command into fresh
|
||||
pid/uts/ipc/cgroup namespaces, and `clone()`'s namespace-creation flags only
|
||||
ever affect the newly created child, never the caller. So the outer process
|
||||
itself never actually enters those namespaces — comparing
|
||||
`/proc/<outer_pid>/ns/{pid,uts,ipc,cgroup}` against this process's own showed
|
||||
them identical, while only `mnt`/`user` differed. `resolve_namespace_pid()`
|
||||
reads `/proc/<pid>/task/<pid>/children` (the direct-children list `procfs`
|
||||
exposes) to find that real inner process and joins *its* namespaces instead —
|
||||
falls back to `pid` itself (best-effort, not fatal) if that file can't be
|
||||
read. For each of `{mnt→--mount, uts→--uts, ipc→--ipc, pid→--pid,
|
||||
cgroup→--cgroup, user→--user}` (`net` deliberately excluded — this project
|
||||
never isolates networking, see `bwrap.cpp` below), `readlink()`s both
|
||||
`/proc/<ns_pid>/ns/<type>` and `/proc/self/ns/<type>` and only passes
|
||||
nsenter's corresponding `--type=/proc/<ns_pid>/ns/<type>` flag when they
|
||||
differ — an identical-namespace re-entry attempt can fail outright
|
||||
(`setns()`'s own `EINVAL` restriction on re-entering a namespace you're
|
||||
already in), so skipping is deliberate, not just an optimization. `mnt` is the
|
||||
one type where a *read* failure (permission denied, or the process vanished)
|
||||
is treated as fatal, since without it "joining the container" is meaningless;
|
||||
every other type just degrades to a skip. Always appends
|
||||
`--preserve-credentials`: without it, `nsenter --user` also tries to
|
||||
`setuid()`/`setgid()`/`setgroups()` to the target's identity within the new
|
||||
user namespace, which fails outright (`setgroups failed: Operation not
|
||||
permitted`) against the `setgroups`-denied unprivileged user namespace bwrap
|
||||
creates whenever `-r/--run` isn't root — confirmed by hitting this exact
|
||||
failure during manual testing before adding the flag. Runs the final
|
||||
`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.
|
||||
- `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`,
|
||||
@@ -298,7 +342,7 @@ Build directory is `buildDir/` (already configured).
|
||||
(see `priv_drop_helper.cpp` in "Project state")
|
||||
- Run the executable: `./buildDir/slocker-lite -m <image.tar>` (see `--help` for the
|
||||
full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`,
|
||||
`-l/--list-images`, `-i/--inspect`, `-n/--no-nsenter`, `--user`, `--group`,
|
||||
`-l/--list-images`, `-i/--inspect`, `-e/--exec`, `-n/--no-nsenter`, `--user`, `--group`,
|
||||
`--hostname`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
|
||||
`--delete-volume-full`, `--list-processes`, `--clean-processes`, `-t/--test`, `--log-level`,
|
||||
`-h/--help`, `-V/--version`)
|
||||
|
||||
Reference in New Issue
Block a user