Add -k/--kill <pid> to fully stop a running session

A plain `kill <tracked_bwrap_pid>` doesn't kill everything a container
started: on a kernel without pid namespace support, a service that
daemonizes (double-forks and detaches) before the entrypoint execs into its
main command reparents all the way to the *host's* own pid 1, completely
disconnected from the sandboxed session -- confirmed via a real session log
from the user's own Android target device, where php-fpm and caddy both
kept running as orphans after killing the tracked pid.

kill_session() (src/kill_session.{h,cpp}) picks between three
independently-named strategies per session, based on what's actually
available for it:
- kill_via_cgroup(): preferred when the session has a dedicated cgroup
  (src/session_cgroup.{h,cpp}, set up at -r/--run time in run_bwrap()'s
  on_start callback). Reaches every process the session ever started,
  daemonized/reparented or not, regardless of pid namespace support.
- kill_via_pid_namespace(): used when --unshare-pid was genuinely in effect
  for the session (src/sandbox_process.{h,cpp}, shared with exec_session.cpp,
  which already needed resolve_namespace_pid()). Relies on the kernel's own
  guarantee that killing a pid namespace's pid 1 tears down everything in it.
- kill_via_tracked_pid(): fallback, signals the tracked pid directly -- no
  worse than today's manual kill. This is what the user's real target
  device currently falls back to (no pid namespace support there).

Each strategy sends SIGTERM, waits up to a 10s grace period, then forces a
SIGKILL. Verified locally (rootless dev machine, which does support pid
namespaces): a daemonizing test session was fully cleaned up via
kill_via_pid_namespace(), including the forced-SIGKILL escalation path,
with no leftover processes, mounts, or layers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-08-29 09:43:12 +00:00
parent 247e61d9d8
commit 8c4afb82e8
16 changed files with 852 additions and 114 deletions
+18
View File
@@ -59,6 +59,7 @@ slocker-lite -c|--cleanup <layer-id>
slocker-lite -l|--list-images <directory>
slocker-lite -i|--inspect <image.tar>
slocker-lite -e|--exec <pid> [-- <command> [args...]]
slocker-lite -k|--kill <pid>
slocker-lite -v|--volume <name> <directory>
slocker-lite --list-volumes
slocker-lite --delete-volume <name>
@@ -86,6 +87,7 @@ slocker-lite -V|--version
| `-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. |
| `-e, --exec <pid>` | Join an already-running `--run` session (`<pid>` must be one `--list-processes` shows as `running`) and run a command inside its container. Pass `-- <command> [args...]` to specify it. |
| `-k, --kill <pid>` | Stop a running `--run` session (`<pid>` must be one `--list-processes` shows as `running`): sends `SIGTERM`, waits up to 10s, then forces it with `SIGKILL`. Reaches every process the session started — including daemonized/reparented ones a plain `kill <pid>` would leave behind — via a dedicated cgroup when available, or the sandboxed pid namespace's own collapse-on-kill guarantee when not, falling back to signaling the tracked pid alone if neither applies. |
| `-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. |
@@ -212,6 +214,22 @@ rather than joining the outer `bwrap` process's own namespaces, so the joined
command sees the container's process tree and hostname too, not just its
filesystem.
`-k/--kill <pid>` stops a running session and everything it started — not just
the tracked `bwrap` process. A plain `kill <pid>` can leave processes behind: a
container whose entrypoint daemonizes a service (double-forks and detaches)
before `exec`-ing its main command can end up with that service reparented
somewhere `bwrap` dying never reaches, especially on a kernel without pid
namespace support, where it reparents all the way to the *host's* own pid 1.
`-k/--kill` picks between three mechanisms depending on what's actually
available for that session: a dedicated cgroup (set up at `-r/--run` time,
reliably includes every process the session ever started regardless of
daemonizing or pid namespace support — the most complete option, when the
kernel and permissions allow it), the sandboxed pid namespace's own
collapse-on-kill guarantee (when `--unshare-pid` was genuinely in effect for
that session), or, failing both, signaling the tracked process directly (no
worse than today's manual `kill`). Either way it sends `SIGTERM` first,
waits up to 10 seconds, then escalates to `SIGKILL`.
`-D/--daemonize` forks and detaches into the background by calling `setsid()`
itself, rather than re-enabling `bwrap`'s own `--new-session` — that flag only
detaches the deeply-nested sandboxed command, leaving `bwrap`/`nsenter` still