Skip to content

podman stub: snug's own binary replaces the /bin/sh script (#691) - #695

Merged
vyskocilm merged 8 commits into
mainfrom
feat/691-podman-wrapper
Oct 23, 2026
Merged

vyskocilm merged 8 commits into
mainfrom
feat/691-podman-wrapper

Conversation

@vyskocilm

Copy link
Copy Markdown
Contributor

Closes #691.

/snug/bin/podman was KindData /bin/sh dispatcher: needed shell inside, baked shim host path into heredoc. Now KindSymlink -> /snug/lib/snug/wrapper, wrapper bind staged with it. Dispatcher in Go: internal/wrapper/podman.go.

Dispatch

  • Main: stub when filepath.Base(argv[0]) == "podman" AND os.Executable() == WrapperGuest. bwrap execvp / shell PATH lookup pass bare podman; image check keeps host snug installed as podman the CLI (TestHostSnugNamedPodmanIsTheCLI).
  • Client: first of docker, then podman-remote (policy.PodmanStubClients) on PATH, every dir for docker before any for podman-remote. Skips /snug/bin and relative entries.
  • docker: subcommand allowlist + podman-only reasons as before. podman-remote: argv verbatim, proxy filters.
  • Refused, exit 125, both paths: --remote --url --connection --root --runroot --runtime anywhere; before subcommand also docker -H --host -c --context --config, podman-remote -r -c --identity (after subcommand -c is --cpu-shares).
  • Neither client: 125 naming both. Shim path no longer in any message.

--dry-run

  • FILESYSTEM: link /snug/bin/podman -> /snug/lib/snug/wrapper + wrapper bind.
  • COMMANDS: This sandbox: docker, at /usr/bin/docker. / NEITHER; BUT <dir> is on PATH and WRITABLE from inside when PATH dir or client path searched first is a shadow slot.
  • JSON containers.podman_stub, podman_stub_client, podman_stub_client_path, podman_stub_writable (Policy.PodmanStubClient).

Golden diff

internal/policy/testdata/podman-socket.bwrap.txt: --perms 0755 --ro-bind-data 14 /snug/bin/podman -> --symlink /snug/lib/snug/wrapper /snug/bin/podman + --ro-bind-fd 18 /snug/lib/snug/wrapper (+ /snug/lib/snug dirs, fd renumber).

Red team (2 rounds, no escape)

  • R1: dry-run named on-disk client while writable PATH dir lets payload plant docker -> fixed, TestPodmanStubClientNamesAWritablePATHDirectory.
  • R2 F1 (sev:low): same via client PATH only — host link under ro bind into /tmp, rw file in ro dir -> fixed, TestPodmanStubClientNamesAWritableClientPath.
  • R2 F2 (sev:low): podman -H=tcp://10.0.0.1:2375 ps reached docker while dry-run says connection flags refused -> fixed, TestRefusedFlag.
  • Held: symlink into wrapper (argv0 = client name, falls to CLI), PATH entries cleaning to /snug/bin, hidden __ verbs inside (all refuse; no userns, fork/exec /proc/self/exe: operation not permitted), wrapper ro/nosuid, env only CONTAINER_HOST/DOCKER_HOST, no fd leak.
  • Not fixed: binary carries build paths (same exposure as door/login already; -trimpath separate change).

Tests

  • make gate green.
  • make integration: sandbox suite 61 engine tests (floor 46), all pass except TestOfflineHasOnlyLoopback, TestDoctorRunsCleanOnAHostThatCanRunSnug — host has ip_gre loaded, fb_tunnels_only_for_init_net=0, every netns gets gre0 gretap0 erspan0 (unshare -Urn ip -br link shows same outside snug). Host state, not this change. integration-signals green.
  • make verify: 5 passed, 4 skipped, 0 failed.
  • New integration: TestPodmanStubForwardsToDockerInsideTheSandbox, TestPodmanStubRefusesInsideTheSandbox, TestHostSnugNamedPodmanIsTheCLI.
  • podman-remote path unit-tested only: host @sys exposes real /usr/bin/docker, which always wins inside.

🤖 Generated with Claude Code

vyskocilm and others added 8 commits October 23, 2026 09:04
Issue #691. /snug/bin/podman was a KindData /bin/sh dispatcher, so it needed a
shell inside and baked the shim's host path into a heredoc. Now it is a
KindSymlink to /snug/lib/snug/wrapper, staged with the wrapper bind, and
internal/wrapper/podman.go is the dispatcher.

Main runs the stub when argv[0]'s basename is podman AND os.Executable() is
WrapperGuest: bwrap's execvp and a shell pass the bare word, and the image
check keeps a host snug installed as podman the CLI.

The stub execs the first of docker, then podman-remote (policy.PodmanStubClients)
on PATH, skipping /snug/bin and relative entries. docker keeps the subcommand
allowlist and podman-only refusals; podman-remote gets argv verbatim, the proxy
filtering it. Connection-shaping flags refused on both. Neither found: 125
naming both. The shim path is no longer in any message.

--dry-run COMMANDS block and JSON containers.podman_stub* name the client this
sandbox's PATH reaches (Policy.PodmanStubClient), or that it reaches neither.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…client

Red team, issue #691: a profile putting a payload-writable directory on PATH,
even after /usr/bin, lets the payload drop a docker the stub execs, while
--dry-run named the client on disk now. PodmanStubClient now also returns the
first such directory searched before the answer (IsShadowSlot), and the
COMMANDS block and JSON containers.podman_stub_writable say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…on flags

F1 (sev:low): --dry-run named a client the payload could replace when the
client PATH itself was writable though its directory was not — a host link
under a ro bind pointing into /tmp, or a file granted rw in a ro directory.
PodmanStubClient now asks IsShadowSlot of each searched client path too.

F2 (sev:low): podman -H=tcp://... ps reached docker, while --dry-run says
connection-selecting flags are refused. Each client's own global spellings
(docker -H --host -c --context --config; podman-remote -r -c --identity) are
now refused before the subcommand, not after, where -c is --cpu-shares.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Section 3 claimed podman-remote is refused at the libpod create and pull
routes; libpodcreate.go examines both, and the stub now execs podman-remote.
Section 8's client order, flag lists, dispatch rule and --dry-run fields
restated comments in internal/wrapper/podman.go and podmanstub.go; they are
now pointers. Section 9's docker cp concern duplicated section 4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vyskocilm
vyskocilm merged commit 19f3189 into main Oct 23, 2026
5 checks passed
@vyskocilm
vyskocilm deleted the feat/691-podman-wrapper branch October 23, 2026 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

podman stub: replace /bin/sh script with snug's wrapper, like BROWSER (#669)

1 participant