Page MenuHomeFreeBSD

D56165.id181765.diff
No OneTemporary

D56165.id181765.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,13 @@
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 +256,46 @@
primary mount, followed by a '#' character and the index of the secondary
mount; e.g.,
.Pa /dev/fuse0#3 .
+.Sh USER MOUNTS
+The
+.Nm
+utility has the 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
+.Xr 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
+.Va vfs.usermount
+.Xr sysctl 8
+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
@@ -40,6 +40,7 @@
#include <sys/sysctl.h>
#include <err.h>
+#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -89,6 +90,9 @@
/* 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 +119,98 @@
#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 regular user even with
+ * vfs.usermount=1.
+ */
+ if (getuid() != 0) {
+ oldeuid = geteuid();
+ oldegid = getegid();
+ if (seteuid(getuid()) < 0)
+ err(1, "failed to drop privileges");
+ if (setegid(getgid()) < 0)
+ err(1, "failed to drop privileges");
+ }
+}
+
+static void
+restore_privs(void)
+{
+ if (usermount) {
+ if (seteuid(oldeuid) < 0)
+ err(1, "failed to restore privileges");
+ if (setegid(oldegid) < 0)
+ err(1, "failed to restore privileges");
+ }
+}
+
+static void
+check_perm(const char *mnt)
+{
+ int mnt_fd;
+ struct stat stbuf;
+ struct statfs stfsbuf;
+ size_t i;
+ static const char* fs_allowlist[] = {
+ "ext2fs",
+ "fusefs",
+ "msdosfs",
+ "smbfs",
+ "tmpfs",
+ "ufs",
+ "zfs",
+ };
+
+ mnt_fd = open(mnt, O_DIRECTORY);
+ if (mnt_fd < 0)
+ err(1, "failed to open mountpoint %s", mnt);
+
+ if (fchdir(mnt_fd) < 0)
+ err(1, "failed to chdir into mountpoint %s", mnt);
+
+ if (fstat(mnt_fd, &stbuf) < 0)
+ err(1, "failed to access mountpoint %s", mnt);
+
+ if ((stbuf.st_mode & S_ISVTX) && stbuf.st_uid != getuid())
+ errx(1, "mountpoint %s not owned by user", mnt);
+
+ if (access(".", W_OK) < 0)
+ errx(1, "no write access to mountpoint %s", mnt);
+
+ /* perms are ok, but we don't allow mounting over any FS
+ * for security reasons */
+ if (fstatfs(mnt_fd, &stfsbuf) < 0)
+ err(1, "failed to access mountpoint %s", mnt);
+
+ close(mnt_fd);
+
+ /* disallow tmpfs for testing purposes, see
+ * tests/sys/fs/fusefs/usermount.sh:usermount_negative_cases_body()
+ */
+ if (getenv("MOUNT_FUSEFS_TESTING") != NULL)
+ if (strcmp("tmpfs", stfsbuf.f_fstypename) == 0)
+ goto fail;
+
+ for (i = 0; i < nitems(fs_allowlist); i++) {
+ if (strcmp(fs_allowlist[i], stfsbuf.f_fstypename) == 0)
+ return;
+ }
+
+fail:
+ errx(1, "mounting over filesystem type %s is forbidden", stfsbuf.f_fstypename);
+}
+
int
main(int argc, char *argv[])
{
@@ -122,7 +218,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 +227,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 +241,21 @@
int fd = -1, fdx;
char *ep;
char *daemon_str = NULL, *daemon_opts = NULL;
+ bool 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 +343,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 +365,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,6 +393,34 @@
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 %s was mounted by someone else (%u != %u), refusing to unmount", mountpoint, 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");
@@ -292,6 +437,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 +475,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 +576,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);
+ /* 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);
+ build_iovec(&iov, &iovlen, "nosuid", NULL, -1);
+ build_iovec(&iov, &iovlen, "nocover", NULL, -1);
+ build_iovec(&iov, &iovlen, "emptydir", NULL, -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;
MNT_IUNLOCK(mp);
/* We need this here as this slot is used by getnewvnode() */
mp->mnt_stat.f_iosize = maxbcachebuf;
diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile
--- a/tests/sys/fs/fusefs/Makefile
+++ b/tests/sys/fs/fusefs/Makefile
@@ -4,8 +4,10 @@
TESTSDIR= ${TESTSBASE}/sys/fs/fusefs
+ATF_TESTS_SH+= auto_unmount
ATF_TESTS_SH+= ctl
ATF_TESTS_SH+= ext2-misc
+ATF_TESTS_SH+= usermount
# We could simply link all of these files into a single executable. But since
# Kyua treats googletest programs as plain tests, it's better to separate them
diff --git a/tests/sys/fs/fusefs/auto_unmount.sh b/tests/sys/fs/fusefs/auto_unmount.sh
new file mode 100644
--- /dev/null
+++ b/tests/sys/fs/fusefs/auto_unmount.sh
@@ -0,0 +1,250 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2026 Gleb Popov <arrowd@FreeBSD.org>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+check_mounted()
+{
+ atf_check [ ! -z "$(mount | grep fuse-hello_ll)" ]
+}
+check_not_mounted()
+{
+ atf_check [ -z "$(mount | grep fuse-hello_ll)" ]
+}
+check_hello()
+{
+ check_mounted
+ atf_check [ "$(cat mnt/hello)" == "Hello World!" ]
+}
+common_cleanup()
+{
+ killall fuse-hello_ll || true
+ umount $PWD/mnt || true
+ atf_check -s exit:1 pgrep fuse-hello_ll
+ check_not_mounted
+}
+
+
+atf_test_case no_auto_unmount_normal_exit cleanup
+no_auto_unmount_normal_exit_head()
+{
+ atf_set "descr" "Checks that the FUSE daemon performs an unmount on its own"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll" # filesystems/fusefs-hello_ll
+}
+no_auto_unmount_normal_exit_body()
+{
+ atf_check mkdir mnt
+ atf_check fuse-hello_ll mnt
+ check_hello
+ atf_check killall fuse-hello_ll
+ sleep 1
+ check_not_mounted
+}
+no_auto_unmount_normal_exit_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case no_auto_unmount_abnormal_exit cleanup
+no_auto_unmount_abnormal_exit_head()
+{
+ atf_set "descr" "Checks that the FUSE mount lingers if daemon exits abnormally"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+no_auto_unmount_abnormal_exit_body()
+{
+ atf_check mkdir mnt
+ atf_check fuse-hello_ll mnt
+ check_hello
+ atf_check killall -KILL fuse-hello_ll
+ sleep 1
+ check_mounted
+}
+no_auto_unmount_abnormal_exit_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case no_auto_unmount_external_unmount cleanup
+no_auto_unmount_external_unmount_head()
+{
+ atf_set "descr" "Checks that unmounting via umount works when auto_unmount is disabled"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+no_auto_unmount_external_unmount_body()
+{
+ atf_check mkdir mnt
+ atf_check fuse-hello_ll mnt
+ check_hello
+ atf_check umount mnt
+ sleep 1
+ check_not_mounted
+}
+no_auto_unmount_external_unmount_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case auto_unmount_normal_exit cleanup
+auto_unmount_normal_exit_head()
+{
+ atf_set "descr" "Checks that the FUSE daemon performs an unmount on its own even if auto_unmount is enabled"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+auto_unmount_normal_exit_body()
+{
+ atf_check mkdir mnt
+ atf_check fuse-hello_ll -o auto_unmount mnt
+ check_hello
+ atf_check killall fuse-hello_ll
+ sleep 1
+ check_not_mounted
+}
+auto_unmount_normal_exit_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case auto_unmount_abnormal_exit cleanup
+auto_unmount_abnormal_exit_head()
+{
+ atf_set "descr" "Checks that the FUSE mount gets unmounted if daemon exits abnormally and auto_unmount is enabled"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+auto_unmount_abnormal_exit_body()
+{
+ atf_check mkdir mnt
+ atf_check fuse-hello_ll -o auto_unmount mnt
+ check_hello
+ atf_check killall -KILL fuse-hello_ll
+ sleep 1
+ check_not_mounted
+}
+auto_unmount_abnormal_exit_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case auto_unmount_external_unmount cleanup
+auto_unmount_external_unmount_head()
+{
+ atf_set "descr" "Checks that unmounting via umount works when auto_unmount is enabled"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+auto_unmount_external_unmount_body()
+{
+ atf_check mkdir mnt
+ atf_check fuse-hello_ll -o auto_unmount mnt
+ check_hello
+ atf_check umount mnt
+ sleep 1
+ check_not_mounted
+}
+auto_unmount_external_unmount_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case auto_unmount_no_double_unmount cleanup
+auto_unmount_no_double_unmount_head()
+{
+ atf_set "descr" "Checks that enabling auto_unmount does not cause double unmount on SIGINT"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+auto_unmount_no_double_unmount_body()
+{
+ atf_check mkdir mnt
+ atf_check fuse-hello_ll mnt
+ check_hello
+ atf_check fuse-hello_ll -o auto_unmount mnt
+ check_hello
+ # we have two daemons running
+ atf_check [ $(pgrep hello_ll | wc -l) = 2 ]
+ hello_with_auto_unmount_pid=$(pgrep -lf hello_ll | grep auto_unmount | awk '{print $1}')
+ atf_check [ ! -z "$hello_with_auto_unmount_pid" ]
+ atf_check kill $hello_with_auto_unmount_pid
+ sleep 1
+ # the second daemon auto-unmounted itself, but the first one
+ # should continue running
+ check_hello
+}
+auto_unmount_no_double_unmount_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case auto_unmount_no_double_unmount_external cleanup
+auto_unmount_no_double_unmount_external_head()
+{
+ atf_set "descr" "Checks that enabling auto_unmount does not cause double unmount when unmounting via umount"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+auto_unmount_no_double_unmount_external_body()
+{
+ atf_check mkdir mnt
+ atf_check fuse-hello_ll mnt
+ check_hello
+ atf_check fuse-hello_ll -o auto_unmount mnt
+ check_hello
+ # we have two daemons running
+ atf_check [ $(pgrep hello_ll | wc -l) = 2 ]
+ atf_check umount mnt
+ sleep 1
+ # the second daemon auto-unmounted itself, but the first one
+ # should continue running
+ check_hello
+}
+auto_unmount_no_double_unmount_external_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_init_test_cases()
+{
+ atf_add_test_case no_auto_unmount_normal_exit
+ atf_add_test_case no_auto_unmount_abnormal_exit
+ atf_add_test_case no_auto_unmount_external_unmount
+ atf_add_test_case auto_unmount_normal_exit
+ atf_add_test_case auto_unmount_abnormal_exit
+ atf_add_test_case auto_unmount_external_unmount
+ atf_add_test_case auto_unmount_no_double_unmount
+ atf_add_test_case auto_unmount_no_double_unmount_external
+ # auto_unmount + usermount combo is tested in usermount.sh
+}
diff --git a/tests/sys/fs/fusefs/usermount.sh b/tests/sys/fs/fusefs/usermount.sh
new file mode 100644
--- /dev/null
+++ b/tests/sys/fs/fusefs/usermount.sh
@@ -0,0 +1,404 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2026 Gleb Popov <arrowd@FreeBSD.org>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+check_mounted()
+{
+ atf_check [ ! -z "$(mount | grep fuse-hello_ll)" ]
+}
+check_not_mounted()
+{
+ atf_check [ -z "$(mount | grep fuse-hello_ll)" ]
+}
+check_hello()
+{
+ check_mounted
+ atf_check [ "$(su -m nobody -c 'cat mnt/hello')" == "Hello World!" ]
+}
+common_cleanup()
+{
+ killall fuse-hello_ll || true
+ umount $PWD/mnt || true
+ atf_check -s exit:1 pgrep fuse-hello_ll
+ check_not_mounted
+}
+
+
+atf_test_case usermount cleanup
+usermount_head()
+{
+ atf_set "descr" "Checks that mounting a FUSE filesystem as a regular user using the usermount functionality works"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll" # filesystems/fusefs-hello_ll
+}
+usermount_body()
+{
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check su -m nobody -c "fuse-hello_ll mnt"
+ check_hello
+ # check that usermount always mounts with nosuid
+ atf_check [ ! -z "$(mount | grep fuse-hello_ll | grep nosuid)" ]
+}
+usermount_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case usermount_negative_cases cleanup
+usermount_negative_cases_head()
+{
+ atf_set "descr" "Checks cases when usermounting is not allowed"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+usermount_negative_cases_body()
+{
+ atf_check -s exit:1 -e match:"bad mount point" \
+ su -m nobody -c "fuse-hello_ll /non/exist/ent"
+ check_not_mounted
+
+ atf_check mkdir d
+ atf_check chown nobody d
+ atf_check su -m nobody -c "touch d/file"
+ atf_check -s exit:1 -e match:"Not a directory" \
+ su -m nobody -c "fuse-hello_ll d/file"
+ check_not_mounted
+
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check chmod -x mnt
+ atf_check -s exit:1 -e match:"failed to chdir into mountpoint" \
+ su -m nobody -c "fuse-hello_ll mnt"
+ check_not_mounted
+ atf_check rmdir mnt
+
+ atf_check mkdir mnt
+ atf_check chown tests mnt
+ # if we aren't owner, having sticky bit is not acceptable
+ atf_check chmod 0777 mnt
+ atf_check chmod +t mnt
+ atf_check -s exit:1 -e match:"mountpoint .* not owned by user" \
+ su -m nobody -c "fuse-hello_ll mnt"
+ check_not_mounted
+ atf_check rmdir mnt
+
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check chmod -w mnt
+ atf_check -s exit:1 -e match:"no write access to mountpoint" \
+ su -m nobody -c "fuse-hello_ll mnt"
+ check_not_mounted
+ atf_check rmdir mnt
+
+ atf_check mkdir mnt
+ atf_check mount -t tmpfs tmpfs mnt
+ atf_check mkdir mnt/dir
+ atf_check chown -R nobody mnt
+ atf_check -s exit:1 -e match:"mounting over filesystem type tmpfs is forbidden" \
+ su -m nobody -c "env MOUNT_FUSEFS_TESTING=1 fuse-hello_ll mnt/dir"
+ check_not_mounted
+ atf_check umount mnt
+ atf_check rmdir mnt
+
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check -s exit:1 -e match:"usage is disallowed for usermount" \
+ su -m nobody -c "fuse-hello_ll -o allow_other mnt"
+ check_not_mounted
+
+ atf_check -s exit:1 -e match:"usage is disallowed for usermount" \
+ su -m nobody -c "fuse-hello_ll -o allow_root mnt"
+ check_not_mounted
+
+ atf_check -s exit:1 -e match:"usermount mode, spawning daemon not allowed" \
+ su -m nobody -c "mount_fusefs -D `which fuse-hello_ll` /dev/fuse mnt"
+ check_not_mounted
+
+ # mounting over another mountpoint is not allowed
+ atf_check mount -t tmpfs tmpfs mnt
+ atf_check -s exit:1 -e match:"Device busy" \
+ su -m nobody -c "fuse-hello_ll mnt"
+ check_not_mounted
+ atf_check umount mnt
+
+ atf_check touch mnt/file
+ atf_check -s exit:1 -e match:"Directory not empty" \
+ su -m nobody -c "fuse-hello_ll mnt"
+ check_not_mounted
+}
+usermount_negative_cases_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case userunmount cleanup
+userunmount_head()
+{
+ atf_set "descr" "Checks that the FUSE daemon can unmount on its own in the usermount mode"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+userunmount_body()
+{
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check su -m nobody -c "fuse-hello_ll mnt"
+ check_hello
+ # sending INT to the daemon will cause it to shutdown normally
+ # and call fuse_session_unmount()
+ atf_check killall fuse-hello_ll
+ sleep 1
+ check_not_mounted
+}
+userunmount_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case userunmount_external cleanup
+userunmount_external_head()
+{
+ atf_set "descr" "Checks that unmounting as a regular user works via mount_fusefs -u"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+userunmount_external_body()
+{
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check su -m nobody -c "fuse-hello_ll mnt"
+ check_hello
+ # unmounting the mountpoint will cause the daemon to shutdown normally
+ atf_check su -m nobody -c "mount_fusefs -u mnt"
+ sleep 1
+ check_not_mounted
+}
+userunmount_external_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case userunmount_external_root cleanup
+userunmount_external_root_head()
+{
+ atf_set "descr" "Checks that auto_unmount does not get in the way when unmounting via umount"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+userunmount_external_root_body()
+{
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check su -m nobody -c "fuse-hello_ll mnt"
+ check_hello
+ # unmounting the mountpoint will cause the daemon to shutdown normally
+ atf_check umount mnt
+ sleep 1
+ check_not_mounted
+}
+userunmount_external_root_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case userunmount_negative_cases cleanup
+userunmount_negative_cases_head()
+{
+ atf_set "descr" "Checks cases when userunmounting is not allowed"
+ atf_set "require.user" "root"
+}
+userunmount_negative_cases_body()
+{
+ atf_check -s exit:1 -e match:"path to unmount specified incorrectly" \
+ su -m nobody -c "mount_fusefs -u"
+
+ atf_check -s exit:1 -e match:"failed to access mountpoint" \
+ su -m nobody -c "mount_fusefs -u /non/exist/ent"
+
+ atf_check mkdir access_denied
+ atf_check chmod 0700 access_denied
+ atf_check -s exit:1 -e match:"failed to access mountpoint" \
+ su -m nobody -c "mount_fusefs -u access_denied/."
+
+ atf_check -s exit:1 -e match:"filesystem .* was mounted by someone else" \
+ su -m nobody -c "mount_fusefs -u /dev"
+
+ atf_check -s exit:1 -e match:"unmount flag only makes sense for usermount" \
+ mount_fusefs -u /dev
+
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check -o ignore sysctl vfs.usermount=1
+ atf_check su -m nobody -c "mount -t devfs devfs mnt"
+ atf_check -o ignore sysctl vfs.usermount=0
+ atf_check -s exit:1 -e match:"refusing to unmount non-FUSE filesystem" \
+ su -m nobody -c "mount_fusefs -u mnt"
+}
+userunmount_negative_cases_cleanup()
+{
+ sysctl vfs.usermount=0 || true
+ common_cleanup
+}
+
+
+atf_test_case rootunmount cleanup
+rootunmount_head()
+{
+ atf_set "descr" "Checks that unmounting as root works"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+rootunmount_body()
+{
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check su -m nobody -c "fuse-hello_ll mnt"
+ check_hello
+ atf_check umount mnt
+ sleep 1
+ check_not_mounted
+}
+rootunmount_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case auto_unmount_normal_exit cleanup
+auto_unmount_normal_exit_head()
+{
+ atf_set "descr" "Checks that the FUSE daemon can unmount on its own in the usermount mode"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+auto_unmount_normal_exit_body()
+{
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check su -m nobody -c "fuse-hello_ll -o auto_unmount mnt"
+ check_hello
+ atf_check killall fuse-hello_ll
+ sleep 1
+ check_not_mounted
+}
+auto_unmount_normal_exit_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case auto_unmount_abnormal_exit cleanup
+auto_unmount_abnormal_exit_head()
+{
+ atf_set "descr" "Checks that the FUSE mount gets unmounted if daemon exits abnormally and auto_unmount is enabled in the usermount mode"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+auto_unmount_abnormal_exit_body()
+{
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check su -m nobody -c "fuse-hello_ll -o auto_unmount mnt"
+ check_hello
+ atf_check killall -KILL fuse-hello_ll
+ sleep 1
+ check_not_mounted
+}
+auto_unmount_abnormal_exit_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case auto_unmount_external_unmount cleanup
+auto_unmount_external_unmount_head()
+{
+ atf_set "descr" "Checks that the FUSE mount gets unmounted if auto_unmount is enabled and mount_fusefs -u is called"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+auto_unmount_external_unmount_body()
+{
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check su -m nobody -c "fuse-hello_ll -o auto_unmount mnt"
+ check_hello
+ atf_check su -m nobody -c "mount_fusefs -u mnt"
+ sleep 1
+ check_not_mounted
+}
+auto_unmount_external_unmount_cleanup()
+{
+ common_cleanup
+}
+
+
+atf_test_case usermount_under_vfs_usermount cleanup
+usermount_under_vfs_usermount_head()
+{
+ atf_set "descr" "Checks that the usermount mode does not kick in when vfs.usermount=1"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-hello_ll"
+}
+usermount_under_vfs_usermount_body()
+{
+ atf_check mkdir mnt
+ atf_check chown nobody mnt
+ atf_check -o ignore sysctl vfs.usermount=1
+ atf_check su -m nobody -c "fuse-hello_ll mnt"
+ check_hello
+ # it is currently impossible to tell which way the fs got mounted,
+ # but it is observable during unmounting
+ atf_check -s exit:1 -e match:"unmount flag only makes sense for usermount" \
+ su -m nobody -c "mount_fusefs -u mnt"
+ atf_check -o ignore sysctl vfs.usermount=0
+}
+usermount_under_vfs_usermount_cleanup()
+{
+ sysctl vfs.usermount=0
+ common_cleanup
+}
+
+
+atf_init_test_cases()
+{
+ atf_add_test_case usermount
+ atf_add_test_case usermount_negative_cases
+ atf_add_test_case userunmount
+ atf_add_test_case userunmount_external
+ atf_add_test_case userunmount_external_root
+ atf_add_test_case userunmount_negative_cases
+ atf_add_test_case rootunmount
+ atf_add_test_case auto_unmount_normal_exit
+ atf_add_test_case auto_unmount_abnormal_exit
+ atf_add_test_case auto_unmount_external_unmount
+ atf_add_test_case usermount_under_vfs_usermount
+}

File Metadata

Mime Type
text/plain
Expires
Thu, Sep 10, 7:16 AM (17 h, 36 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38634824
Default Alt Text
D56165.id181765.diff (30 KB)

Event Timeline