Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F150472735
D56165.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
11 KB
Referenced Files
None
Subscribers
None
D56165.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D56165: mount_fusefs: Implement the fusermount functionality
Attached
Detach File
Event Timeline
Log In to Comment