gVisor provides a Bubblewrap (bwrap) command-line personality for runsc. The
goal is to simplify configurations by allowing users to use a bwrap alias and
familiar flags to quickly start a secure gVisor sandbox without manually
creating OCI configuration files.
You can run an application using the bwrap command-line personality by
invoking runsc bwrap:
sudo runsc bwrap --ro-bind / / --tmpfs /tmp --unshare-net --hostname gvisor-sentry -- /bin/sh
Alternatively, if runsc is symlinked as bwrap in your $PATH, you can
invoke it directly:
ln -s /usr/local/bin/runsc /usr/local/bin/bwrap
sudo bwrap --ro-bind / / --tmpfs /tmp -- /bin/sh
gVisor translates bwrap flags into OCI specification settings (spec.Mounts,
spec.Process.Env, spec.Process.Capabilities, etc.) and, where no
specification field fits, into runsc global flags such as --network. Below
is the reference of supported flags and their behavior.
The last column of each table calls out anything that behaves unlike native
bwrap. A dash there means you can carry the flag over from a bwrap command
line unchanged.
bwrap |--bind | <SRC> <DEST> | Bind mounts SRC | — |DEST. When : :SRC is the host : :/) and : :DEST is not, the : :. : :
| –ro-bind | SRC to : :DEST. Sets : : if DEST` : :/. : :
| --tmpfs | <DEST> | Mounts a fresh | — |tmpfs filesystem : :DEST. : :
| --proc | <DEST> | Mounts a new | /proc is mounted in |procfs filesystem : every sandbox whether :DEST. : or not you pass the :DEST on one : :NOTE: A mount whose
DESTis/becomes the sandbox root filesystem. Only its source and its read-only bit carry over, so therbind, rprivate, nosuid, nodevoptions above do not apply to--bind / /.
NOTE:
SRCmust exist on the host. A missing source aborts the run withCan't find source pathbefore the sandbox starts.
NOTE: Every sandbox gets
/proc,/sys,/dev,/dev/pts,/sys/fs/cgroup, and/tmp, all backed by gVisor’s own implementations of those filesystems. These defaults are applied first and the command line is layered on top of them, so a mount you ask for at one of those paths wins. If no root mount (/) is specified via--bindor--ro-bind, gVisor initializes a freshtmpfsroot filesystem at/.
Native bwrap builds a sandbox by unsharing namespaces from the host kernel, so
each --unshare-* flag decides whether the application keeps using a host
namespace or gets a fresh one. gVisor works differently: the Sentry is a kernel,
and the application always runs inside it. The Sentry implements its own process
tree, mount table, hostname, System V IPC, and cgroup hierarchy for every
sandbox, whatever the command line says. The application never holds a handle on
a host namespace, and no flag can give it one.
The flags below therefore do not choose between “host namespace” and “new namespace”. They configure either the host-side sandbox process or the Sentry subsystem that backs an already-isolated resource.
Networking is the one subsystem whose backing can be the host, and even then the
application does not join the host network namespace. Every socket call goes to
the Sentry. Under --network=none the sandbox has no networking at all: the
Sentry brings up a loopback interface and nothing else. Under --network=host
the Sentry makes the call on the host through hostinet. What changes is how
the Sentry serves the call on the application’s behalf, never whether the
application itself sits in a host namespace.
bwrap :
| :—————– | :——– | :———————- | :—————- |
| --unshare-user | None | Host-side only. Runs | Nothing implies |specs.UserNamespace) : namespaces at :--unshare-net | None | Disables networking | — |--network=none). The : :--unshare-all | None | Implies | Only those two |--unshare-user and : parts do :--unshare-net. : anything. Every :bwrap is :--unshare-ipc | None | The Sentry always | No-op. The |--unshare-pid | None | The Sentry always runs | No-op, as above. |--unshare-uts | None | The Sentry always owns | No-op, as above. |--hostname to set : :--unshare-cgroup | None | The Sentry always | No-op, as above. |cgroupfs at : :/sys/fs/cgroup. : :NOTE: Without
--unshare-netthe sandbox runs withrunsc --network=host, which forwards the application’s socket calls to host sockets.
NOTE: The no-op flags exist so that current
bwrapcommand lines keep working. They are no-ops because gVisor already provides at least the isolation they ask for, never because the isolation is missing.
bwrap :
| :———– | :——– | :————————- | :————- |
| --chdir | <DIR> | Sets the initial working | DIR is a |spec.Process.Cwd). : translated to :/work. A :/. :
| --hostname | <NAME> | Sets the hostname the | — |spec.Hostname). : :
| --uid | <UID> | Custom UID in the sandbox | — |spec.Process.User.UID). : :--unshare-user. : :
| --gid | <GID> | Custom GID in the sandbox | — |spec.Process.User.GID). : :--unshare-user. : :
| --argv0 | <VALUE> | Runs the program named by | — |VALUE as its argv[0]. : :NOTE: Without
--uidand--gid, the sandbox UID depends on--unshare-user. With it, the command runs as the UID/GID that invokedrunsc, or as theSUDO_UID/SUDO_GIDowner undersudo. Without it, the command runs as UID/GID 0. That is root as gVisor’s kernel sees it, and it says nothing about the privileges the sandbox holds on the host.
bwrap :
| :—————— | :——– | :—————– | :——————– |
| --new-session | None | No-op. The flag | No-op. The protection |TIOCSTI input : already in place. :TIOCSTI : :--die-with-parent | None | No-op. The sandbox | Accepted and ignored. |runsc’s parent :PR_SET_PDEATHSIG : :WARNING:
--die-with-parentcarries no cleanup guarantee here.runsctears the sandbox down when it exits normally, but arunsckilled withSIGKILLleaves the sandbox running.
By default the command holds every capability, in all five sets. Use
--cap-drop to narrow that down.
bwrap :
| :———– | :——– | :————————— | :—————– |
| --cap-drop | <CAP> | Removes CAP from all five | — |spec.Process.Capabilities. : :ALL clears : :CAP_ prefix is optional. : :
| --cap-add | <CAP> | Adds CAP back to all five | CAP_NET_RAW is |ALL restores the full set. : because raw :Capability operations apply in command-line order, so --cap-drop ALL --cap-add
NET_ADMIN leaves the sandbox with exactly one capability.
NOTE: gVisor’s kernel enforces these capabilities inside the sandbox. They grant no privilege on the host: the Sentry never passes a capability through to a host syscall.
NOTE:
CAP_NET_RAWis the one exception to the table.runscstrips it from every set whenever raw sockets are disabled, and they are disabled by default, so--cap-add NET_RAWhas no effect unless you also passrunsc --net-raw.
The sandbox inherits the host environment, and the personality adds nothing of its own to it.
bwrap :
| :———– | :————– | :—————— | :————– |
| --setenv | <VAR> <VALUE> | Appends VAR=VALUE | — |spec.Process.Env. : :
| --unsetenv | <VAR> | Removes an entry | — |VAR from : :spec.Process.Env. : :
| --clearenv | None | Clears the | — |PWD=<cwd>, and : :--unsetenv. : :NOTE: One combination does not carry over. Native
bwrapapplies--setenvand--unsetenvin command-line order, while this personality collects every--setenvfirst and resolves--unsetenvafterwards. That is only observable when a single command line both sets and unsets the same variable, so don’t rely on the order of the two in that case. Every other combination behaves as it does under nativebwrap,--clearenvincluded: it takes effect where it appears, wiping what comes before it and keeping what comes after.