From 7297044ada625da583211f0a574410cddb4f7d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20G=C3=B6rgens?= Date: Wed, 12 Apr 2023 15:39:32 +0800 Subject: Fuse mount: make auto_unmount compatible with suid/dev mount options (#762) * Fuse mount: make auto_unmount compatible with suid/dev mount options > When you run as root, fuse normally does not call fusermount but uses > the mount system call directly. When you specify auto_unmount, it goes > through fusermount instead. However, fusermount is a setuid binary that > is normally called by regular users, so it cannot in general accept suid > or dev options. In this patch, we split up how fuse mounts as root when `auto_unmount` is specified. First, we mount using system calls directly, then we reach out to fusermount to set up auto_unmount only (with no actual mounting done in fusermount). Fixes: #148 --- example/passthrough_ll.c | 9 ++++-- lib/mount.c | 73 +++++++++++++++++++++++++++++++++++++++++++----- test/test_examples.py | 31 ++++++++++++++++++++ util/fusermount.c | 19 +++++++++++-- 4 files changed, 120 insertions(+), 12 deletions(-) diff --git a/example/passthrough_ll.c b/example/passthrough_ll.c index 8b2eb4b..070cef1 100644 --- a/example/passthrough_ll.c +++ b/example/passthrough_ll.c @@ -89,7 +89,7 @@ struct lo_data { int writeback; int flock; int xattr; - const char *source; + char *source; double timeout; int cache; int timeout_set; @@ -1240,7 +1240,11 @@ int main(int argc, char *argv[]) } } else { - lo.source = "/"; + lo.source = strdup("/"); + if(!lo.source) { + fuse_log(FUSE_LOG_ERR, "fuse: memory allocation failed\n"); + exit(1); + } } if (!lo.timeout_set) { switch (lo.cache) { @@ -1302,5 +1306,6 @@ err_out1: if (lo.root.fd >= 0) close(lo.root.fd); + free(lo.source); return ret ? 1 : 0; } diff --git a/lib/mount.c b/lib/mount.c index 3990243..9c233a3 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -322,6 +322,65 @@ void fuse_kern_unmount(const char *mountpoint, int fd) waitpid(pid, NULL, 0); } +static int setup_auto_unmount(const char *mountpoint, int quiet) +{ + int fds[2], pid; + int res; + + if (!mountpoint) { + fuse_log(FUSE_LOG_ERR, "fuse: missing mountpoint parameter\n"); + return -1; + } + + res = socketpair(PF_UNIX, SOCK_STREAM, 0, fds); + if(res == -1) { + perror("fuse: socketpair() failed"); + return -1; + } + + pid = fork(); + if(pid == -1) { + perror("fuse: fork() failed"); + close(fds[0]); + close(fds[1]); + return -1; + } + + if(pid == 0) { + char env[10]; + const char *argv[32]; + int a = 0; + + if (quiet) { + int fd = open("/dev/null", O_RDONLY); + if (fd != -1) { + dup2(fd, 1); + dup2(fd, 2); + } + } + + argv[a++] = FUSERMOUNT_PROG; + argv[a++] = "--auto-unmount"; + argv[a++] = "--"; + argv[a++] = mountpoint; + argv[a++] = NULL; + + close(fds[1]); + fcntl(fds[0], F_SETFD, 0); + snprintf(env, sizeof(env), "%i", fds[0]); + setenv(FUSE_COMMFD_ENV, env, 1); + exec_fusermount(argv); + perror("fuse: failed to exec fusermount3"); + _exit(1); + } + + close(fds[0]); + + // Now fusermount3 will only exit when fds[1] closes automatically when our + // process exits. + return 0; +} + static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo, const char *opts, int quiet) { @@ -422,12 +481,6 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo, return -1; } - if (mo->auto_unmount) { - /* Tell the caller to fallback to fusermount3 because - auto-unmount does not work otherwise. */ - return -2; - } - fd = open(devname, O_RDWR | O_CLOEXEC); if (fd == -1) { if (errno == ENODEV || errno == ENOENT) @@ -590,7 +643,13 @@ int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo) goto out; res = fuse_mount_sys(mountpoint, mo, mnt_opts); - if (res == -2) { + if (res >= 0 && mo->auto_unmount) { + if(0 > setup_auto_unmount(mountpoint, 0)) { + // Something went wrong, let's umount like in fuse_mount_sys. + umount2(mountpoint, MNT_DETACH); /* lazy umount */ + res = -1; + } + } else if (res == -2) { if (mo->fusermount_opts && fuse_opt_add_opt(&mnt_opts, mo->fusermount_opts) == -1) goto out; diff --git a/test/test_examples.py b/test/test_examples.py index a7ba998..f0aa63d 100755 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -372,6 +372,37 @@ def test_notify_inval_entry(tmpdir, only_expire, notify, output_checker): else: umount(mount_process, mnt_dir) +@pytest.mark.parametrize("intended_user", ('root', 'non_root')) +def test_dev_auto_unmount(short_tmpdir, output_checker, intended_user): + """Check that root can mount with dev and auto_unmount + (but non-root cannot). + Split into root vs non-root, so that the output of pytest + makes clear what functionality is being tested.""" + if os.getuid() == 0 and intended_user == 'non_root': + pytest.skip('needs to run as non-root') + if os.getuid() != 0 and intended_user == 'root': + pytest.skip('needs to run as root') + mnt_dir = str(short_tmpdir.mkdir('mnt')) + src_dir = str('/dev') + cmdline = base_cmdline + \ + [ pjoin(basename, 'example', 'passthrough_ll'), + '-o', f'source={src_dir},dev,auto_unmount', + '-f', mnt_dir ] + mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, + stderr=output_checker.fd) + try: + wait_for_mount(mount_process, mnt_dir) + if os.getuid() == 0: + open(pjoin(mnt_dir, 'null')).close() + else: + with pytest.raises(PermissionError): + open(pjoin(mnt_dir, 'null')).close() + except: + cleanup(mount_process, mnt_dir) + raise + else: + umount(mount_process, mnt_dir) + @pytest.mark.skipif(os.getuid() != 0, reason='needs to run as root') def test_cuse(output_checker): diff --git a/util/fusermount.c b/util/fusermount.c index 32d3fbd..034383e 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -1356,9 +1356,13 @@ int main(int argc, char *argv[]) int cfd; const char *opts = ""; const char *type = NULL; + int setup_auto_unmount_only = 0; static const struct option long_opts[] = { {"unmount", no_argument, NULL, 'u'}, + // Note: auto-unmount deliberately does not have a short version. + // It's meant for internal use by mount.c's setup_auto_unmount. + {"auto-unmount", no_argument, NULL, 'U'}, {"lazy", no_argument, NULL, 'z'}, {"quiet", no_argument, NULL, 'q'}, {"help", no_argument, NULL, 'h'}, @@ -1390,7 +1394,11 @@ int main(int argc, char *argv[]) case 'u': unmount = 1; break; - + case 'U': + unmount = 1; + auto_unmount = 1; + setup_auto_unmount_only = 1; + break; case 'z': lazy = 1; break; @@ -1434,7 +1442,7 @@ int main(int argc, char *argv[]) exit(1); umask(033); - if (unmount) + if (!setup_auto_unmount_only && unmount) goto do_unmount; commfd = getenv(FUSE_COMMFD_ENV); @@ -1444,11 +1452,15 @@ int main(int argc, char *argv[]) goto err_out; } + cfd = atoi(commfd); + + if (setup_auto_unmount_only) + goto wait_for_auto_unmount; + fd = mount_fuse(mnt, opts, &type); if (fd == -1) goto err_out; - cfd = atoi(commfd); res = send_fd(cfd, fd); if (res == -1) goto err_out; @@ -1459,6 +1471,7 @@ int main(int argc, char *argv[]) return 0; } +wait_for_auto_unmount: /* Become a daemon and wait for the parent to exit or die. ie For the control socket to get closed. btw We don't want to use daemon() function here because -- cgit v1.2.1