proot-next

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 the bwrap name 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 name

proot-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

On this page