Page MenuHomeFreeBSD

D56165.diff
No OneTemporary

D56165.diff

diff --git a/sbin/mount_fusefs/Makefile b/sbin/mount_fusefs/Makefile
--- a/sbin/mount_fusefs/Makefile
+++ b/sbin/mount_fusefs/Makefile
@@ -22,5 +22,8 @@
PROG= mount_fusefs
MAN= mount_fusefs.8
LIBADD= util
+# this program is suid to support the mount-as-user functionality in libfuse
+BINOWN= root
+BINMODE=4555
.include <bsd.prog.mk>
diff --git a/sbin/mount_fusefs/mount_fusefs.8 b/sbin/mount_fusefs/mount_fusefs.8
--- a/sbin/mount_fusefs/mount_fusefs.8
+++ b/sbin/mount_fusefs/mount_fusefs.8
@@ -49,6 +49,7 @@
.Op Fl m Ar node
.Op Fl h
.Op Fl V
+.Op Fl u
.Op Fl o Ar option ...
.Ar special node
.Op Ar fuse_daemon ...
@@ -132,6 +133,12 @@
Show help.
.It Fl V , Ic --version
Show version information.
+.It Fl u , Ic --unmount
+Perform an unprivileged unmount. The path to unmount is taken from
+.Ar node
+or
+.Ar special
+if the former was not passed on command line.
.It Fl o
Mount options are specified via
.Fl o .
@@ -248,6 +255,42 @@
primary mount, followed by a '#' character and the index of the secondary
mount; e.g.,
.Pa /dev/fuse0#3 .
+.Sh USER MOUNTS
+The
+.Nm
+program has SUID bit set to allow mounting FUSE filesystems by an unprivileged
+user.
+This is an important feature for AppImage bundles or file managers that use
+FUSE to access remote files.
+The "usermount" mode is activated automatically when
+.Nm
+is invoked by unprivileged user.
+When running in this mode, a couple of additional restrictions are imposed to
+prevent privilege escalation:
+.Bl -enum
+.It
+The filesystem is mounted
+.Cm nosuid .
+.It
+The mount point should not shadow another mount point.
+.It
+The mount point should be an empty directory that the user has write access to,
+without the sticky(7) bit set.
+.It
+The filesystem could not be mounted over arbitrary file systems like "procfs" or
+"devfs", but only over those present in the allow list.
+Allowed file systems are "ext2fs", "fusefs", "msdosfs", "smbfs", "tmpfs", "ufs",
+and "zfs".
+.It
+The
+.Cm allow_other
+and
+.Cm allow_root
+options are prohibited.
+.El
+.Pp
+The "usermount" functionality is disabled if the vfs.usermount sysctl is set
+to 1.
.Sh SECURITY
System administrators might want to use a custom mount policy (ie., one going
beyond the
diff --git a/sbin/mount_fusefs/mount_fusefs.c b/sbin/mount_fusefs/mount_fusefs.c
--- a/sbin/mount_fusefs/mount_fusefs.c
+++ b/sbin/mount_fusefs/mount_fusefs.c
@@ -89,6 +89,8 @@
/* Linux specific options, we silently ignore them */
{ "fd=", 0, 0x00, 1 },
{ "rootmode=", 0, 0x00, 1 },
+ /* We actually do support user_id in kernel, but the user who calls
+ mount_fusefs is not supposed to pass it */
{ "user_id=", 0, 0x00, 1 },
{ "group_id=", 0, 0x00, 1 },
{ "large_read", 0, 0x00, 1 },
@@ -115,6 +117,85 @@
#define DEFAULT_MOUNT_FLAGS ALTF_PRIVATE
+static uid_t oldeuid;
+static gid_t oldegid;
+static int usermount;
+
+static void drop_privs(void)
+{
+ /* Drop privileges regardless of the usermount flag.
+ * When we run as regular user, but with vfs.usermount=1, we still
+ * want to drop privileges to end up with mount owner being the regular
+ * user.
+ * If we don't drop privs, the mount owner would be geteuid(), root.
+ * This later prevents unmounting by the refular user.
+ */
+ if (getuid() != 0) {
+ oldeuid = geteuid();
+ oldegid = getegid();
+ seteuid(getuid());
+ setegid(getgid());
+ }
+}
+
+static void restore_privs(void)
+{
+ if (usermount) {
+ seteuid(oldeuid);
+ setegid(oldegid);
+ }
+}
+
+static void check_perm(const char *mnt)
+{
+ const char *origmnt = mnt;
+ struct stat stbuf;
+ struct statfs stfsbuf;
+ size_t i;
+
+ if (lstat(mnt, &stbuf) < 0)
+ err(1, "failed to access mountpoint %s", mnt);
+
+ if (!S_ISDIR(stbuf.st_mode))
+ errx(1, "mountpoint %s is not a directory", mnt);
+
+ if (chdir(mnt) < 0)
+ err(1, "failed to chdir into mountpoint %s", mnt);
+
+ mnt = "."; /* TODO: fusermount performs lstat again after chdir for some reason */
+ if (lstat(mnt, &stbuf) < 0)
+ err(1, "failed to access mountpoint %s", origmnt);
+
+ if ((stbuf.st_mode & S_ISVTX) && stbuf.st_uid != getuid())
+ errx(1, "mountpoint %s not owned by user", origmnt);
+
+ if (access(mnt, W_OK) < 0)
+ errx(1, "no write access to mountpoint %s", origmnt);
+
+ /* perms are ok, but we don't allow mounting over any FS
+ * for security reasons */
+ if (statfs(mnt, &stfsbuf) < 0)
+ err(1, "failed to access mountpoint %s", origmnt);
+
+ const char* fs_allowlist[] = {
+ "ext2fs",
+ "fusefs",
+ "msdosfs",
+ "smbfs",
+ "tmpfs",
+ "ufs",
+ "zfs",
+ };
+
+ for (i = 0; i < sizeof(fs_allowlist)/sizeof(fs_allowlist[0]); i++) {
+ int len = strlen(fs_allowlist[i]);
+ if (strncmp(fs_allowlist[i], stfsbuf.f_fstypename, len) == 0)
+ return;
+ }
+
+ errx(1, "mounting over filesystem type %s is forbidden", stfsbuf.f_fstypename);
+}
+
int
main(int argc, char *argv[])
{
@@ -122,7 +203,7 @@
int mntflags, iovlen, verbose = 0;
char *dev = NULL, *dir = NULL, mntpath[MAXPATHLEN];
char *devo = NULL, *diro = NULL;
- char ndev[128], fdstr[15];
+ char ndev[128], fdstr[15], uidstr[32];
int i, done = 0, reject_allow_other = 0, safe_level = 0;
int altflags = DEFAULT_MOUNT_FLAGS;
int __altflags = DEFAULT_MOUNT_FLAGS;
@@ -131,6 +212,7 @@
struct mntval *mv;
static struct option longopts[] = {
{"reject-allow_other", no_argument, NULL, 'A'},
+ {"unmount", no_argument, NULL, 'u'},
{"safe", no_argument, NULL, 'S'},
{"daemon", required_argument, NULL, 'D'},
{"daemon_opts", required_argument, NULL, 'O'},
@@ -144,6 +226,21 @@
int fd = -1, fdx;
char *ep;
char *daemon_str = NULL, *daemon_opts = NULL;
+ int do_unmount = 0;
+ int usermount_sysctl;
+ size_t usermount_sysctl_size = sizeof(usermount_sysctl);
+
+ usermount = getuid() != 0;
+
+ if (sysctlbyname("vfs.usermount", &usermount_sysctl,
+ &usermount_sysctl_size, NULL, 0) == 0) {
+ /* There is no point in usermount mode if vfs.usermount=1 */
+ if (usermount_sysctl)
+ usermount = 0;
+ }
+
+ /* Drop SUID privileges */
+ drop_privs();
/*
* We want a parsing routine which is not sensitive to
@@ -231,6 +328,11 @@
errx(1, "mount path specified inconsistently");
diro = optarg;
break;
+ case 'u':
+ if (!usermount)
+ errx(1, "unmount flag only makes sense for usermount");
+ do_unmount = 1;
+ break;
case 'v':
verbose = 1;
break;
@@ -248,7 +350,7 @@
}
if (done)
break;
- } while ((ch = getopt_long(argc, argv, "AvVho:SD:O:s:m:", longopts, NULL)) != -1);
+ } while ((ch = getopt_long(argc, argv, "AvVuho:SD:O:s:m:", longopts, NULL)) != -1);
argc -= optind;
argv += optind;
@@ -276,13 +378,40 @@
argc--;
}
+ if (do_unmount)
+ {
+ const char *mountpoint = dir ? dir : dev;
+ if (!mountpoint)
+ errx(1, "path to unmount specified incorrectly");
+
+ /* We should not allow an unprivileged user to unmount
+ * whatever he wants, but only FUSE mounts owned by him.
+ */
+ struct statfs fs_buf;
+ // TODO: do checkpath() & rmslashes() here too?
+ if (statfs(mountpoint, &fs_buf) == -1)
+ err(1, "failed to access mountpoint %s", mountpoint);
+
+ if (fs_buf.f_owner != getuid())
+ errx(1, "filesystem was mounted by someone else (%u != %u), refusing to unmount", fs_buf.f_owner, getuid());
+
+ if (strncmp(fs_buf.f_fstypename, "fusefs", 6) != 0)
+ errx(1, "refusing to unmount non-FUSE filesystem");
+
+ restore_privs();
+
+ if (unmount(mountpoint, 0) != 0)
+ err(1, "failed to unmount %s", mountpoint);
+
+ return 0;
+ }
+
if (! (dev && dir))
errx(1, "missing special and/or mountpoint");
for (mo = mopts; mo->m_flag; ++mo) {
if (altflags & mo->m_flag) {
int iov_done = 0;
-
if (reject_allow_other &&
strcmp(mo->m_option, "allow_other") == 0)
/*
@@ -292,6 +421,14 @@
*/
errx(1, "\"allow_other\" usage is banned by respective option");
+ if (usermount &&
+ strcmp(mo->m_option, "allow_other") == 0)
+ errx(1, "\"allow_other\" usage is disallowed for usermount");
+
+ if (usermount &&
+ strcmp(mo->m_option, "allow_root") == 0)
+ errx(1, "\"allow_root\" usage is disallowed for usermount");
+
for (mv = mvals; mv->mv_flag; ++mv) {
if (mo->m_flag != mv->mv_flag)
continue;
@@ -322,10 +459,16 @@
if (safe_level > 0 && (argc > 0 || daemon_str || daemon_opts))
errx(1, "safe mode, spawning daemon not allowed");
+ if (usermount && (argc > 0 || daemon_str || daemon_opts))
+ errx(1, "usermount mode, spawning daemon not allowed");
+
if ((argc > 0 && (daemon_str || daemon_opts)) ||
(daemon_opts && ! daemon_str))
errx(1, "daemon specified inconsistently");
+ if (usermount) /* TODO: fusermount does this, why is it necessary */
+ umask(033);
+
/*
* Resolve the mountpoint with realpath(3) and remove unnecessary
* slashes from the devicename if there are any.
@@ -417,6 +560,30 @@
}
}
+ if (usermount) {
+ /* Allowing an unprivileged user to mount wherever he likes to
+ * is a security issue. To make it safe, we perform checks
+ * as described in
+ * https://github.com/libfuse/libfuse/blob/22d0fcd4c757a86377bc258296e55e2900af2c3c/doc/kernel.txt#L175
+ * The code of the check_perm() function follows the same
+ * function from Linux fusermount:
+ * https://github.com/libfuse/libfuse/blob/22d0fcd4c757a86377bc258296e55e2900af2c3c/util/fusermount.c#L1094
+ */
+ check_perm(mntpath);
+ mntflags |= MNT_NOSUID;
+ mntflags |= MNT_NOCOVER;
+ mntflags |= MNT_EMPTYDIR;
+ /* To allow for later unmounting by the same unprivileged user
+ * we pass the UID to the kernel, which ends up being saved in
+ * mp->mnt_stat.f_owner
+ * We later use this value in the do_unmount block.
+ */
+ sprintf(uidstr, "%u", getuid());
+ build_iovec(&iov, &iovlen, "user_id=", uidstr, -1);
+ }
+
+ restore_privs();
+
/* Prepare the options vector for nmount(). build_iovec() is declared
* in mntopts.h. */
sprintf(fdstr, "%d", fd);
diff --git a/sys/fs/fuse/fuse_device.c b/sys/fs/fuse/fuse_device.c
--- a/sys/fs/fuse/fuse_device.c
+++ b/sys/fs/fuse/fuse_device.c
@@ -178,6 +178,15 @@
if (fdata->mp && fdata->dataflags & FSESS_AUTO_UNMOUNT) {
vfs_ref(fdata->mp);
+ /* If FUSE daemon runs as an unprivileged user
+ * ("fusermount" case) and requested auto_unmount,
+ * and then exits abnormally, then we get here with
+ * curthread->td_ucred->cr_uid != 0 .
+ * This makes dounmount() to return EPERM, which is usually
+ * a correct thing to do, but not in this specific case.
+ * We want unmounting to happen, so let's lie about the uid.
+ */
+ fdata->mp->mnt_cred->cr_uid = curthread->td_ucred->cr_uid;
dounmount(fdata->mp, MNT_FORCE, curthread);
}
diff --git a/sys/fs/fuse/fuse_vfsops.c b/sys/fs/fuse/fuse_vfsops.c
--- a/sys/fs/fuse/fuse_vfsops.c
+++ b/sys/fs/fuse/fuse_vfsops.c
@@ -299,6 +299,7 @@
uint64_t mntopts, __mntopts;
uint32_t max_read;
+ uid_t user_id;
int linux_errnos;
int daemon_timeout;
int fd;
@@ -313,6 +314,7 @@
subtype = NULL;
max_read = ~0;
+ user_id = 0;
linux_errnos = 0;
err = 0;
mntopts = 0;
@@ -340,6 +342,7 @@
FUSE_FLAGOPT(auto_unmount, FSESS_AUTO_UNMOUNT);
(void)vfs_scanopt(opts, "max_read=", "%u", &max_read);
+ (void)vfs_scanopt(opts, "user_id=", "%u", &user_id);
(void)vfs_scanopt(opts, "linux_errnos", "%d", &linux_errnos);
if (vfs_scanopt(opts, "timeout=", "%u", &daemon_timeout) == 1) {
if (daemon_timeout < FUSE_MIN_DAEMON_TIMEOUT)
@@ -440,6 +443,8 @@
* the FUSE server.
*/
mp->mnt_kern_flag |= MNTK_NULL_NOCACHE;
+ if (user_id != 0)
+ mp->mnt_stat.f_owner = user_id; // TODO: is this safe?
MNT_IUNLOCK(mp);
/* We need this here as this slot is used by getnewvnode() */
mp->mnt_stat.f_iosize = maxbcachebuf;

File Metadata

Mime Type
text/plain
Expires
Thu, Apr 2, 12:39 PM (9 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
30722909
Default Alt Text
D56165.diff (11 KB)

Event Timeline