Skip to main content
On this page

Guest daemon

podbox-guest runs inside the container. It bridges notifications, URI opening, clipboard, and host execution to the host over a Unix socket.

Entry point

The container starts with podbox-guest --entry [<command>...]:

  1. Symlinks /run/user/%U/flatpak-info → /.flatpak-info, so portal-aware toolkits route audio/video capture through portals.
  2. fork()s: the child re-execs podbox-guest --daemon; the parent execs your command (or a login shell).
  3. The daemon idles in the background with a 5-minute timeout.

User setup and privilege drop

When running as root with HOST_USER/HOST_UID/HOST_GID set, the entrypoint creates (or reconciles) the container user before exec:

  • An existing name with a different UID/GID is moved onto the host's ids with usermod; a UID taken by another name is freed with userdel -r (logged). Either mismatch without reconciliation would leave setuid on an id with no passwd entry ("I have no name!").
  • The user joins wheel/sudo, video, audio, render and input wherever those groups exist, so GPU and device access granted by group keeps working.
  • Sudo goes to /etc/sudoers.d/podbox-<user> (mode 0440), and the password is deleted (passwd -d) so su works on sudo-less images.
  • The drop itself is fail-closed: initgroups → setgid → setuid, verified against getuid/geteuid/getgid. Any failure aborts the entrypoint instead of running the shell as root.
  • The login shell comes from passwd after reconciliation, falling back to $SHELL and then /bin/bash.

Daemon startup

  1. Creates /run/podbox/bin/ for interceptor symlinks.
  2. Compares PODBOX_HOST_VERSION against its own version; warns on drift.
  3. Connects to the host socket (3 tries, 500ms apart).
  4. Handshakes — sends its capabilities, gets back the accepted subset.
  5. Symlinks one interceptor per accepted capability.
  6. Prepends /run/podbox/bin to PATH via /etc/profile.d/podbox.sh and /etc/fish/conf.d/podbox.fish.
  7. Enters the event loop: poll() on the socket, 0% CPU when idle. With lifecycle.idle_timeout set, it exits once no user processes remain past the timeout.

Event loop

EventAction
ShutdownExit
PingNo-op (keepalive)
CheckIdleScan /proc; reply Busy or IdleTimeout
Disconnect / POLLHUP / POLLERRExit
Idle timeout expiredSend IdleTimeout, exit
EINTRRetry poll()

User processes are tracked via pidfds (Linux 5.3+). 0% CPU when idle.

Socket protocol

Length-prefixed JSON over $XDG_RUNTIME_DIR/podbox/<container>.sock. Full wire format: protocol.md.

→ {"type":"hello","version":"0.1.0","container":"myenv","capabilities":["notify","xdg_open","clipboard","host_exec"]}
← {"type":"hello_ack","accepted":["notify","xdg_open"],"rejected":["clipboard","host_exec"]}

Interceptors

Symlinks in /run/podbox/bin/, one per accepted capability. The binary reads argv[0] to know which interceptor it is. They shadow system binaries via PATH (/etc/profile.d/podbox.sh, /etc/fish/conf.d/podbox.fish).

SymlinkCapabilityWhat it does
notify-sendnotifyForwards args; --action/-A buttons wait for the host's reply
xdg-openxdg_openSends the URI to the host
podbox-clipboardclipboardset reads stdin; get writes the host clipboard to stdout
host-exechost_execRuns the command on the host, relays output as it arrives, exits with its code

Each opens its own short-lived socket connection, sends one message, waits for the reply, exits. xdg-open is the one that waits for the host to close the connection: a clean close exits 0, and a refusal exits 1 with the reason.

xdg-open scheme allowlist

The host opens only http, https and mailto URIs by default. Anything else is refused with an error the guest prints, exit 1. Refused include file:, smb:, sftp:, trash:, archive: and app handlers such as vscode: or steam:. A bare hostname such as example.com is opened as https://. Control characters, URIs over 8 KiB and http(s) URLs without a host are refused too.

The allowlist is a footgun by design: a container's xdg-open is the host declining to run unknown handlers on URLs a container found. If you widen integration.xdg_open.allowed_schemes, a file: or vscode: URL from inside the container now reaches the host's handler for that scheme — opening local files or launching editor windows. Only add schemes you understand the host-side side effects of (for example smb: paired with an allowlisted talk daemon like a VFS client); the web schemes and mailto stay refused when they would otherwise not be, and trust in a URI inside the container lives and dies with this list.

Host-exec security

Off by default. When enabled, the host validates every command:

CheckRejectedExample error
AllowlistAnything not in the map (guest $PATH ignored)Permission denied: 'ls' is not in the host-exec allowlist
Shell metacharacters;, |, &, $, ` in argshost-exec: failed to execute 'echo $HOME'
Dangerous flags--exec-path, --config, -o, …Security violation: argument "--exec-path=/tmp/x" …
Absolute-path bypass/usr/bin/git when the key is gitPermission denied: '/usr/bin/git' is not in the host-exec allowlist

See config.md for the allowlist shape — and prefer wrapper scripts over general-purpose tools.