proot-next

bwrap mode

Stand in for bubblewrap under Steam's pressure-vessel without user namespaces.

The same binary speaks bubblewrap's command line when it is invoked through a name ending in bwrap, busybox style. A symlink is the whole setup:

ln -s proot bwrap
./bwrap --version
# bubblewrap 0.9.0 (proot-next)

Steam's pressure-vessel tries its bundled bwrap first, which fails without user namespaces, then uses $BWRAP or a bwrap on PATH. Point either at the symlink:

export BWRAP=$HOME/.local/bin/bwrap

Steam's own steam-runtime-check-requirements probes the same variable with --bind / / true, which is why the start-up check that insists on user namespaces is satisfied too.

What the options become

There are no namespaces, so bwrap's description of a container is materialized as a fresh private root directory under $XDG_RUNTIME_DIR and a set of bindings:

bwrapproot-next
--bind, --dev-bind, --ro-bind, the -try variants, --bind-fd, --ro-bind-fdBindings. Read-only ones refuse writes with EROFS, including mkdir, unlink, rename and chmod.
--remount-roMarks the matching binding read-only.
--proc, --devBindings of the host's /proc and /dev.
--dir, --tmpfs, --mqueue, --symlink, --file, --bind-data, --ro-bind-data, --perms, --chmodCreated in the private root. --size is ignored.
--chdir, --setenv, --unsetenv, --clearenv, --argv0, --argsAs in bwrap.
--seccomp, --add-seccomp-fdThe program is installed in the child after proot-next's own filter. ERRNO outranks notification, so a caller's denials still apply.
--info-fd, --json-status-fd{"child-pid": N} after the fork; the status fd also gets {"exit-code": N}.
--die-with-parentAlready the behavior.
--new-sessionsetsid() in the child.
--hostnameReported by uname(2). Dropped in a nested launch when the outer launch has no -k or --hostname.
--unshare-*, --share-net, --uid, --gid, --cap-*, --userns*, --pidns, --lock-file, --sync-fd, --block-fd, --as-pid-1Accepted and ignored.
--overlay-*Refused.

The private root is removed when the command exits. PROOT_BWRAP_KEEP=1 keeps it for inspection.

Inside a guest

A program running under proot-next that execs bwrap, whether the symlink above, a copy of proot-next, or the rootfs's own bubblewrap, gets the container it described as a nested view served by the supervisor already there: no second supervisor, no namespaces, the same materialized root and bindings composed through the caller's own view. GTK's glycin image loaders (every GTK 4 application, FileZilla's icons through its file chooser) run inside their sandbox root this way instead of falling back to running unsandboxed. The match on the name bwrap can be turned off with PROOT_NEST=0.

Steam

install-steam.sh sets a Debian-based amd64 image or system up for it: Steam from Valve's repository, its keys checked against their fingerprints, the 32-bit libraries the client and games need, the release binary for the architecture, checked against the SHA-256 the release publishes, in /usr/local/lib/proot-next with bwrap beside it, and Steam's launcher diverted so that every way of starting Steam exports BWRAP to it. The bwrap name stays off PATH, so nothing but Steam picks it up. Run it as root, or under fakeroot in a rootless image build:

curl -fsSL https://raw.githubusercontent.com/selkies-project/proot-next/main/install-steam.sh | sudo sh

PROOT_NEXT_VERSION=0.3.9 installs that release rather than the latest, and PREFIX moves /usr/local. Run again, it rewires an earlier install, proot-bwrap's included, to the binary it installs. The selkies KDE desktop images carry it.

With BWRAP set, the Steam client's own browser helper, every Steam Linux Runtime container and every Proton launch run through proot-next. The things that made the helper impossible under PRoot are handled: a supervisor must not look like a debugger, so TracerPid is hidden and breakpoints are resumed; and /proc/<pid>/fd/N links re-open the object they name, which Chromium's shared memory depends on.

On this page