Container images
Why proot-next exists and how the selkies images use it.
proot-next was written for the selkies desktop images and for proot-apps, which runs desktop applications from container images inside those images. They run on hosts the image author does not control: a default Docker install, Kubernetes, a NAS. Three things are usually missing there, and this project is the answer to all three.
- No user namespaces. Docker's default seccomp and AppArmor profiles stop
unshare(CLONE_NEWUSER), so bubblewrap cannot create a sandbox, and Steam's pressure-vessel refuses to launch a game without one. proot-next answers to thebwrapname and runs the game's container the way bubblewrap would, see bwrap mode. - No FUSE and no
chroot. The application user cannot mount anything, so a rootfs pulled from a registry has to be entered by path translation. - Browsers. Chromium-based applications open tens of thousands of files at start-up from several threads at once. PRoot's two ptrace stops per syscall made them slow to start and visibly slow to run. The notify pool serves those threads concurrently, see how it works.
What the images ship
One binary and one symlink in the application user's ~/.local/bin:
proot the static proot-next binary for the architecture
bwrap -> proot the same binary under the bubblewrap nameproot-apps calls proot -R <image> /entrypoint with -b bindings and that is
the supported surface. The images export BWRAP=$HOME/.local/bin/bwrap, which
pressure-vessel and steam-runtime-check-requirements honour, so Steam finds
the stand-in without any change to Steam.
Environment the images already provide
Nothing new is required. Docker's default seccomp profile allows the seccomp
listener, ADDFD and process_vm_readv, and ptrace is allowed on kernels
since 4.8. The docker-default AppArmor profile is fine. A kernel of 5.14 or
newer is needed for the single-step descriptor injection; older kernels fall
back to a two-step injection automatically.
A hardened profile that removes the ptrace syscall is fine too: proot-next
detects it and serves the few syscalls the tracer handled through
notifications instead, see how it works. What cannot
be worked around is a host that denies the memory access check itself (Yama
ptrace_scope 2 or 3, or an LSM policy): nothing can read a path out of a
tracee there.
Checking a build
Inside a running image, as the application user:
~/.local/bin/proot --version
~/.local/bin/bwrap --version # bubblewrap 0.9.0 (proot-next)
~/.local/bin/bwrap --bind / / true # the probe Steam runs