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>...]:
- Symlinks
/run/user/%U/flatpak-info→/.flatpak-info, so portal-aware toolkits route audio/video capture through portals. fork()s: the child re-execspodbox-guest --daemon; the parent execs your command (or a login shell).- 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 withuserdel -r(logged). Either mismatch without reconciliation would leavesetuidon an id with no passwd entry ("I have no name!"). - The user joins
wheel/sudo,video,audio,renderandinputwherever 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) sosuworks on sudo-less images. - The drop itself is fail-closed:
initgroups→setgid→setuid, verified againstgetuid/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
$SHELLand then/bin/bash.
Daemon startup
- Creates
/run/podbox/bin/for interceptor symlinks. - Compares
PODBOX_HOST_VERSIONagainst its own version; warns on drift. - Connects to the host socket (3 tries, 500ms apart).
- Handshakes — sends its capabilities, gets back the accepted subset.
- Symlinks one interceptor per accepted capability.
- Prepends
/run/podbox/bintoPATHvia/etc/profile.d/podbox.shand/etc/fish/conf.d/podbox.fish. - Enters the event loop:
poll()on the socket, 0% CPU when idle. Withlifecycle.idle_timeoutset, it exits once no user processes remain past the timeout.
Event loop
| Event | Action |
|---|---|
Shutdown | Exit |
Ping | No-op (keepalive) |
CheckIdle | Scan /proc; reply Busy or IdleTimeout |
Disconnect / POLLHUP / POLLERR | Exit |
| Idle timeout expired | Send IdleTimeout, exit |
EINTR | Retry 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
Symlink dispatch
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).
| Symlink | Capability | What it does |
|---|---|---|
notify-send | notify | Forwards args; --action/-A buttons wait for the host's reply |
xdg-open | xdg_open | Sends the URI to the host |
podbox-clipboard | clipboard | set reads stdin; get writes the host clipboard to stdout |
host-exec | host_exec | Runs 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:
| Check | Rejected | Example error |
|---|---|---|
| Allowlist | Anything not in the map (guest $PATH ignored) | Permission denied: 'ls' is not in the host-exec allowlist |
| Shell metacharacters | ;, |, &, $, ` in args | host-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 git | Permission 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.