namespace jail cannot run snap-provided tools (docker breaks warded director) #183

Closed
opened 2026-07-03 02:47:34 +00:00 by coilyco-ops · 0 comments
Member

Defect

shell.Runner (via the namespace sandbox) cannot run a snap-provided tool. On a host where a wrapped or exec-target tool is a snap multiplexer (e.g. /snap/bin/docker -> /usr/bin/snap), every jailed call breaks:

error: unknown command "pull", see 'snap help'.
error: unknown flag `name'

Downstream hit: ward's warded director died because it routes docker through the jail (coilyco-flight-deck/ward#540). ward has since routed docker outside the sandbox as the object-level unblock; this issue is the general cli-guard hardening.

Root cause (two layers)

  1. installToolShim (cli/sandbox/sandbox_linux.go:155) resolves the tool with filepath.EvalSymlinks, collapsing /snap/bin/docker to /usr/bin/snap and stashing THAT as CLIGUARD_REALBIN_DOCKER. snap is a multiplexer keyed on argv[0]'s basename: exec it as /usr/bin/snap and it runs as snap itself, parsing the tool's args as snap subcommands.

  2. Deeper: even with a corrected argv[0]=docker and the uncollapsed /snap/bin/docker path, snap still cannot run inside the jail. Proven directly:

    $ unshare --user --map-root-user --mount /snap/bin/docker version
    cannot fstatat canonical snap directory: Permission denied
    

    snap-confine / snap's canonical-dir access needs privileges the jail strips (CAP_SYS_ADMIN cleared, mount seccomp-denied, private mount ns). So "stash /snap/bin/ + keep argv[0]" is NOT sufficient - a snap tool is fundamentally unjailable.

Correct fix

Detect a snap-multiplexer exec target (resolved real path basename == snap) and run it outside the jail - no-op Wrap for it, the same way Jailed() / NoSandbox() short-circuit. A snap exec target (docker) does not need gate re-entry; its descendants are isolated by the container boundary anyway.

Security note: if a snap-provided tool is ever a security-wrapped re-entry tool (in spec.Tools, meant to re-enter the gate), it cannot be jailed and silently escaping the gate would be a hole. cli-guard should log/warn loudly in that case rather than skip silently. docker is only ever an exec target, never a wrapped tool, so it takes the quiet skip path.

Repro harness

shell.Runner{Sandbox: spec}.Exec(ctx, "docker", "version", "--format", "..."):

  • with the jail -> error: unknown flag \format'` (exit 64)
  • with Sandbox: nil -> 29.3.1 (real docker)
## Defect `shell.Runner` (via the namespace sandbox) cannot run a **snap-provided** tool. On a host where a wrapped or exec-target tool is a snap multiplexer (e.g. `/snap/bin/docker -> /usr/bin/snap`), every jailed call breaks: ``` error: unknown command "pull", see 'snap help'. error: unknown flag `name' ``` Downstream hit: `ward`'s `warded director` died because it routes `docker` through the jail (coilyco-flight-deck/ward#540). ward has since routed docker outside the sandbox as the object-level unblock; this issue is the general cli-guard hardening. ## Root cause (two layers) 1. `installToolShim` (cli/sandbox/sandbox_linux.go:155) resolves the tool with `filepath.EvalSymlinks`, collapsing `/snap/bin/docker` to `/usr/bin/snap` and stashing THAT as `CLIGUARD_REALBIN_DOCKER`. `snap` is a multiplexer keyed on `argv[0]`'s basename: exec it as `/usr/bin/snap` and it runs as snap itself, parsing the tool's args as snap subcommands. 2. **Deeper**: even with a corrected `argv[0]=docker` and the uncollapsed `/snap/bin/docker` path, snap still cannot run inside the jail. Proven directly: ``` $ unshare --user --map-root-user --mount /snap/bin/docker version cannot fstatat canonical snap directory: Permission denied ``` snap-confine / snap's canonical-dir access needs privileges the jail strips (CAP_SYS_ADMIN cleared, mount seccomp-denied, private mount ns). So "stash /snap/bin/<tool> + keep argv[0]" is NOT sufficient - a snap tool is fundamentally unjailable. ## Correct fix Detect a snap-multiplexer exec target (resolved real path basename == `snap`) and run it **outside** the jail - no-op `Wrap` for it, the same way `Jailed()` / `NoSandbox()` short-circuit. A snap exec target (docker) does not need gate re-entry; its descendants are isolated by the container boundary anyway. Security note: if a snap-provided tool is ever a **security-wrapped** re-entry tool (in `spec.Tools`, meant to re-enter the gate), it cannot be jailed and silently escaping the gate would be a hole. cli-guard should `log`/warn loudly in that case rather than skip silently. docker is only ever an exec target, never a wrapped tool, so it takes the quiet skip path. ## Repro harness `shell.Runner{Sandbox: spec}.Exec(ctx, "docker", "version", "--format", "...")`: - with the jail -> `error: unknown flag \`format'` (exit 64) - with `Sandbox: nil` -> `29.3.1` (real docker)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/cli-guard#183
No description provided.