Allow passing `/dev/fuse` file descriptor from parent process
authorMattias Nissler <mnissler@chromium.org>
Mon, 27 Aug 2018 13:17:57 +0000 (15:17 +0200)
committerNikolaus Rath <Nikolaus@rath.org>
Tue, 9 Oct 2018 19:36:22 +0000 (20:36 +0100)
This adds support for a mode of operation in which a privileged parent
process opens `/dev/fuse` and takes care of mounting. The FUSE file
system daemon can then run as an unprivileged child that merely
processes requests on the FUSE file descriptor, which get passed using
the special `/dev/fd/%u` syntax for the mountpoint parameter.

The main benefit is that no privileged operations need to be performed
by the FUSE file system daemon itself directly or indirectly, so the
FUSE process can run with fully unprivileged and mechanisms like
securebits and no_new_privs can be used to prevent subprocesses from
re-acquiring privilege via setuid, fscaps, etc. This reduces risk in
case the FUSE file system gets exploited by malicious file system
data.

Below is an example that illustrates this. Note that I'm using shell
for presentation purposes, the expectation is that the parent process
will implement the equivalent of the `mount -i` and `capsh` commands.

```
\# example/hello can mount successfully with privilege
$ sudo sh -c "LD_LIBRARY_PATH=build/lib ./example/hello /mnt/tmp"
$ sudo cat /mnt/tmp/hello
Hello World!
$ sudo umount /mnt/tmp

\# example/hello fails to mount without privilege
$ sudo capsh --drop=all --secbits=0x2f -- -c 'LD_LIBRARY_PATH=build/lib ./example/hello -f /mnt/tmp'
fusermount3: mount failed: Operation not permitted

\# Passing FUSE file descriptor via /dev/fd/%u allows example/hello to work without privilege
$ sudo sh -c '
      exec 17<>/dev/fuse
      mount -i -o nodev,nosuid,noexec,fd=17,rootmode=40000,user_id=0,group_id=0 -t fuse hello /mnt/tmp
      capsh --drop=all --secbits=0x2f -- -c "LD_LIBRARY_PATH=build/lib example/hello /dev/fd/17"
    '
$ sudo cat /mnt/tmp/hello
Hello World!
$ sudo umount /mnt/tmp
```

ChangeLog.rst
lib/fuse_lowlevel.c
lib/helper.c
lib/mount_util.c
lib/mount_util.h

index 3d5c05bf9e198eb0c947443de5059acfe2d3f2ee..65f57d79d64d5eea03cf17a44bee9c6f2bcfc81b 100644 (file)
@@ -8,6 +8,10 @@ Unreleased Changes
 * The description of the FUSE_CAP_READDIRPLUS_AUTO flag has been
   improved.
 
+* Allow open `/dev/fuse` file descriptors to be passed via mountpoints of the
+  special format `/dev/fd/%u`. This allows mounting to be handled by the parent
+  so the FUSE filesystem process can run fully unprivileged.
+
 libfuse 3.2.6 (2018-08-31)
 ==========================
 
index e6e3d8d5e13a12ec874942e353587c300b84b39c..844e797de2fd9c0ded487c0a5e44c645f8ea6cc1 100644 (file)
@@ -16,6 +16,7 @@
 #include "fuse_kernel.h"
 #include "fuse_opt.h"
 #include "fuse_misc.h"
+#include "mount_util.h"
 
 #include <stdio.h>
 #include <stdlib.h>
@@ -2891,6 +2892,24 @@ int fuse_session_mount(struct fuse_session *se, const char *mountpoint)
                        close(fd);
        } while (fd >= 0 && fd <= 2);
 
+       /*
+        * To allow FUSE daemons to run without privileges, the caller may open
+        * /dev/fuse before launching the file system and pass on the file
+        * descriptor by specifying /dev/fd/N as the mount point. Note that the
+        * parent process takes care of performing the mount in this case.
+        */
+       fd = fuse_mnt_parse_fuse_fd(mountpoint);
+       if (fd != -1) {
+               if (fcntl(fd, F_GETFD) == -1) {
+                       fprintf(stderr,
+                               "fuse: Invalid file descriptor /dev/fd/%u\n",
+                               fd);
+                       return -1;
+               }
+               se->fd = fd;
+               return 0;
+       }
+
        /* Open channel */
        fd = fuse_kern_mount(mountpoint, se->mo);
        if (fd == -1)
@@ -2916,9 +2935,11 @@ int fuse_session_fd(struct fuse_session *se)
 
 void fuse_session_unmount(struct fuse_session *se)
 {
-       fuse_kern_unmount(se->mountpoint, se->fd);
-       free(se->mountpoint);
-       se->mountpoint = NULL;
+       if (se->mountpoint != NULL) {
+               fuse_kern_unmount(se->mountpoint, se->fd);
+               free(se->mountpoint);
+               se->mountpoint = NULL;
+       }
 }
 
 #ifdef linux
index 07cef8177c5269f09b3f75a9c2675e005eadef87..e1de362ee87eda09a8d4950836847f25fee2e011 100644 (file)
@@ -15,6 +15,7 @@
 #include "fuse_misc.h"
 #include "fuse_opt.h"
 #include "fuse_lowlevel.h"
+#include "mount_util.h"
 
 #include <stdio.h>
 #include <stdlib.h>
@@ -147,7 +148,11 @@ static int fuse_helper_opt_proc(void *data, const char *arg, int key,
        switch (key) {
        case FUSE_OPT_KEY_NONOPT:
                if (!opts->mountpoint) {
-                       char mountpoint[PATH_MAX];
+                       if (fuse_mnt_parse_fuse_fd(arg) != -1) {
+                               return fuse_opt_add_opt(&opts->mountpoint, arg);
+                       }
+
+                       char mountpoint[PATH_MAX] = "";
                        if (realpath(arg, mountpoint) == NULL) {
                                fprintf(stderr,
                                        "fuse: bad mount point `%s': %s\n",
index 56ed85a4e08990e7694296584c5663bdcc71a948..95e038f61d9a99b5c20c1964426b98aadd27e83d 100644 (file)
@@ -352,3 +352,16 @@ int fuse_mnt_check_fuseblk(void)
        fclose(f);
        return 0;
 }
+
+int fuse_mnt_parse_fuse_fd(const char *mountpoint)
+{
+       int fd = -1;
+       int len = 0;
+
+       if (sscanf(mountpoint, "/dev/fd/%u%n", &fd, &len) == 1 &&
+           len == strlen(mountpoint)) {
+               return fd;
+       }
+
+       return -1;
+}
index 55c6c5e491d265e18e79ceb413b84799428a982c..0ef0fbe818b7385bb93d571e8470d89a5c82210f 100644 (file)
@@ -15,3 +15,4 @@ int fuse_mnt_umount(const char *progname, const char *abs_mnt,
                    const char *rel_mnt, int lazy);
 char *fuse_mnt_resolve_path(const char *progname, const char *orig);
 int fuse_mnt_check_fuseblk(void);
+int fuse_mnt_parse_fuse_fd(const char *mountpoint);