gVisor supports FUSE (Filesystem in Userspace), allowing userspace programs to serve filesystems inside a sandbox. There are two modes of operation:
/dev/fuse. This is the standard FUSE model.The external FUSE server feature allows a host-side process to serve a FUSE filesystem into a gVisor sandbox. The host process and the sandbox communicate over a Unix socketpair using the standard FUSE protocol. This approach avoids the performance penalty incurred by context switching through the I/O proxy mechansim that’s otherwise used to expose host filesystems.
SOCK_SEQPACKET).--pass-fd
flag on runsc run or runsc create.The host process creates a socketpair and starts the FUSE server with one end:
# Example: create a socketpair and pass FD 4 to the FUSE server.
# The FUSE server reads FUSE requests from its FD and responds with
# the standard FUSE protocol (FUSEHeaderIn/Out framing).
./my_fuse_server --fd=4 --backing-dir=/data/shared
The FUSE server must implement the FUSE kernel protocol: it reads
FUSEHeaderIn-framed requests and writes FUSEHeaderOut-framed responses. At
minimum, it should handle FUSE_INIT, FUSE_GETATTR, FUSE_LOOKUP,
FUSE_OPEN, FUSE_READ, FUSE_RELEASE, and FUSE_ACCESS. Additional opcodes
like FUSE_WRITE, FUSE_FLUSH, FUSE_STATFS, and FUSE_CREATE can be added
as needed.
Use the --pass-fd flag to map the host-side socketpair FD into the sandbox:
runsc run \
--pass-fd=3:100 \
--bundle=/path/to/bundle \
my-container
The format is --pass-fd=HOST_FD:GUEST_FD. In this example, host FD 3 becomes
FD 100 inside the sandbox. The --pass-fd flag can be specified multiple times
to pass additional file descriptors.
Inside the sandbox, the application mounts a FUSE filesystem referencing the passed FD:
// Mount using the passed file descriptor.
mount("fuse", "/mnt/shared", "fuse", MS_NODEV | MS_NOSUID,
"fd=100,user_id=0,group_id=0,rootmode=40000");
Or equivalently from a shell:
mount -t fuse fuse /mnt/shared -o fd=100,user_id=0,group_id=0,rootmode=40000
The mount options are:
fd=N: The file descriptor number inside the sandbox.user_id=UID: The UID that owns the mount.group_id=GID: The GID that owns the mount.rootmode=MODE: The permission mode of the root inode (octal). Use 40000
for a directory.Here is a complete example in Go that sets up the host side:
// Create a socketpair for FUSE communication.
fds, _ := unix.Socketpair(unix.AF_UNIX, unix.SOCK_SEQPACKET, 0)
// fds[0] goes into the sandbox, fds[1] goes to the FUSE server.
sandboxFile := os.NewFile(uintptr(fds[0]), "fuse-sandbox")
serverFD := fds[1]
// Start the FUSE server on the host with the server-side FD.
go myFuseServer.Serve(serverFD, "/data/backing")
// Launch the sandbox with the FD passed in.
cmd := exec.Command("runsc", "run",
"--pass-fd=3:100", // host FD 3 → guest FD 100
"--bundle="+bundleDir,
containerID,
)
cmd.ExtraFiles = []*os.File{sandboxFile} // FD 3 in the child process
cmd.Run()
/dev/fuse. The
application mounts FUSE using the passed socketpair FD directly./dev/fuse and may not work directly over a socketpair without adaptation.gVisor also supports the standard FUSE model where both the FUSE daemon and the
application run inside the sandbox. The daemon opens /dev/fuse, and the
application mounts a FUSE filesystem using the resulting file descriptor. This
works the same as FUSE on a regular Linux system, with the gVisor kernel
handling the FUSE protocol internally.