Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F171155224
D56165.id181462.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
30 KB
Referenced Files
None
Subscribers
None
D56165.id181462.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,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
@@ -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,87 @@
#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();
+ seteuid(getuid());
+ setegid(getgid());
+ }
+}
+
+static void restore_privs(void)
+{
+ if (usermount) {
+ seteuid(oldeuid);
+ setegid(oldegid);
+ }
+}
+
+static void check_perm(const char *mnt)
+{
+ struct stat stbuf;
+ struct statfs stfsbuf;
+ size_t i;
+
+ int 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);
+
+ const char* fs_allowlist[] = {
+ "ext2fs",
+ "fusefs",
+ "msdosfs",
+ "smbfs",
+ "tmpfs",
+ "ufs",
+ "zfs",
+ };
+ /* disallow tmpfs for testing purposes, see
+ * tests/sys/fs/fusefs/usermount.sh
+ */
+ if (getenv("MOUNT_FUSEFS_TESTING") != NULL)
+ fs_allowlist[4] = "";
+
+ for (i = 0; i < sizeof(fs_allowlist)/sizeof(fs_allowlist[0]); i++) {
+ if (strcmp(fs_allowlist[i], stfsbuf.f_fstypename) == 0)
+ return;
+ }
+
+ errx(1, "mounting over filesystem type %s is forbidden", stfsbuf.f_fstypename);
+}
+
int
main(int argc, char *argv[])
{
@@ -122,7 +205,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 +214,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 +228,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 +330,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 +352,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 +380,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 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");
@@ -292,6 +424,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 +462,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 +563,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"
+
+ mkdir access_denied
+ 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
Details
Attached
Mime Type
text/plain
Expires
Thu, Sep 10, 1:59 AM (12 h, 18 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38625295
Default Alt Text
D56165.id181462.diff (30 KB)
Attached To
Mode
D56165: mount_fusefs: Implement the fusermount functionality
Attached
Detach File
Event Timeline
Log In to Comment