From: Helg Subject: fuse: change termination behaviour To: tech@openbsd.org Date: Wed, 10 Sep 2025 02:18:03 +0200 This patch addresses incompatibilities in the way that FUSE handles terminating a FUSE session. The primary change is: The Linux libfuse implementation of fuse_loop(3) terminates either when the kernel sends FUSE_DESTROY or, if fuse_set_signal_handlers(3) has been called, when it catches one of SIGINT, SIGHUP, or SIGTERM. The OpenBSD implementation behaves similarly when the file system is unmounted with umount(8). However, it tries to unmount the file system automatically when one of the above signals is caught. It should instead just terminate and rely on fuse_unmount(3) being called later by the FUSE file system daemon as part of its termination. Additional changes: - The FUSE file system daemon's destroy handler is the last operation called after the file system is unmounted. Before, it was incorrectly being called when FBT_DESTROY is received by fuse_loop(3). The destroy handler is now called in fuse_destroy(3). - The file system is no longer unmounted when the device is closed. fuse_unmount(3) now closes the FUSE device before unmount(2) is called to prevent deadlocks due to the kernel trying to send FBT_DESTROY when fuse_loop(3) is no longer active and there listening for messages from the kernel. The side effect of this change is that if a FUSE file system daemon crashes, the FUSE device is automatically closed but the file system is not unmounted. It must be unmounted manually with umount(8). This will likely raise some eyebrows, but's it how it works on Linux and FreeBSD (I haven't looked at other platforms). - Man page updates to reflect this change and correct a few other minor errors. OK? Index: lib/libfuse/fuse.c =================================================================== RCS file: /cvs/src/lib/libfuse/fuse.c,v diff -u -p -r1.54 fuse.c --- lib/libfuse/fuse.c 6 Sep 2025 06:15:52 -0000 1.54 +++ lib/libfuse/fuse.c 10 Sep 2025 18:06:05 -0000 @@ -113,42 +113,6 @@ static struct fuse_opt fuse_mount_opts[] FUSE_OPT_END }; -static void -ifuse_try_unmount(struct fuse *f) -{ - pid_t child; - - /* unmount in another thread so fuse_loop() doesn't deadlock */ - child = fork(); - - if (child == -1) { - DPERROR(__func__); - return; - } - - if (child == 0) { - fuse_remove_signal_handlers(fuse_get_session(f)); - errno = 0; - fuse_unmount(f->fc->dir, f->fc); - _exit(errno); - } -} - -static void -ifuse_child_exit(const struct fuse *f) -{ - int status; - - if (waitpid(WAIT_ANY, &status, WNOHANG) == -1) - fprintf(stderr, "fuse: %s\n", strerror(errno)); - - if (WIFEXITED(status) && (WEXITSTATUS(status) != 0)) - fprintf(stderr, "fuse: %s: %s\n", - f->fc->dir, strerror(WEXITSTATUS(status))); - - return; -} - int fuse_loop(struct fuse *fuse) { @@ -159,7 +123,7 @@ fuse_loop(struct fuse *fuse) struct iovec iov[2]; size_t fb_dat_size = FUSEBUFMAXSIZE; ssize_t n; - int ret; + int ret, intr; if (fuse == NULL) return (-1); @@ -189,7 +153,8 @@ fuse_loop(struct fuse *fuse) iov[0].iov_len = sizeof(fbuf.fb_hdr) + sizeof(fbuf.FD); iov[1].iov_base = fbuf.fb_dat; - while (!fuse->fc->dead) { + intr = 0; + while (!intr && !fuse->fc->dead) { ret = kevent(fuse->fc->kq, &event[0], 5, &ev, 1, NULL); if (ret == -1) { if (errno != EINTR) @@ -197,23 +162,21 @@ fuse_loop(struct fuse *fuse) } else if (ret > 0 && ev.filter == EVFILT_SIGNAL) { int signum = ev.ident; switch (signum) { - case SIGCHLD: - ifuse_child_exit(fuse); - break; case SIGHUP: case SIGINT: case SIGTERM: - ifuse_try_unmount(fuse); + DPRINTF("%s: %s\n", __func__, + strsignal(signum)); + intr = 1; break; default: fprintf(stderr, "%s: %s\n", __func__, - strsignal(signum)); + strsignal(signum)); } } else if (ret > 0) { iov[1].iov_len = fb_dat_size; n = readv(fuse->fc->fd, iov, 2); if (n == -1) { - perror("fuse_loop"); fprintf(stderr, "%s: bad fusebuf read: %s\n", __func__, strerror(errno)); free(fbuf.fb_dat); @@ -251,7 +214,6 @@ fuse_loop(struct fuse *fuse) ctx.umask = fbuf.fb_umask; ctx.private_data = fuse->private_data; ictx = &ctx; - ret = ifuse_exec_opcode(fuse, &fbuf); if (ret) { ictx = NULL; @@ -353,11 +315,19 @@ DEF(fuse_mount); void fuse_unmount(const char *dir, struct fuse_chan *ch) { - if (ch == NULL || ch->dead) + if (ch == NULL) return; - if (unmount(dir, MNT_UPDATE) == -1) + /* + * Close the device before unmounting to prevent deadlocks with + * FBT_DESTROY if fuse_loop() has already terminated. + */ + if (close(ch->fd) == -1) DPERROR(__func__); + + if (!ch->dead) + if (unmount(dir, MNT_FORCE) == -1) + DPERROR(__func__); } DEF(fuse_unmount); @@ -463,20 +433,32 @@ fuse_daemonize(int foreground) DEF(fuse_daemonize); void -fuse_destroy(struct fuse *f) +fuse_destroy(struct fuse *fuse) { - if (f == NULL) + struct fuse_context ctx; + + if (fuse == NULL) return; + if (fuse->fc->init && fuse->op.destroy) { + /* setup a basic fuse context for the callback */ + memset(&ctx, 0, sizeof(ctx)); + ctx.fuse = fuse; + ctx.private_data = fuse->private_data; + ictx = &ctx; + + fuse->op.destroy(fuse->private_data); + + ictx = NULL; + } + /* * Even though these were allocated in fuse_mount(), we can't free them - * in fuse_unmount() since fuse_loop() will not have terminated yet so - * we free them here. + * in fuse_unmount() since they are still needed, so we free them here. */ - close(f->fc->fd); - free(f->fc->dir); - free(f->fc); - free(f); + free(fuse->fc->dir); + free(fuse->fc); + free(fuse); } DEF(fuse_destroy); @@ -532,11 +514,6 @@ fuse_set_signal_handlers(unused struct f if (old_sa.sa_handler == SIG_DFL) signal(SIGPIPE, SIG_IGN); - if (sigaction(SIGCHLD, NULL, &old_sa) == -1) - return (-1); - if (old_sa.sa_handler == SIG_DFL) - signal(SIGCHLD, SIG_IGN); - return (0); } @@ -723,10 +700,15 @@ int fuse_main(int argc, char **argv, const struct fuse_operations *ops, void *data) { struct fuse *fuse; + char *mp; + int ret; - fuse = fuse_setup(argc, argv, ops, sizeof(*ops), NULL, NULL, data); + fuse = fuse_setup(argc, argv, ops, sizeof(*ops), &mp, NULL, data); if (fuse == NULL) return (-1); - return (fuse_loop(fuse)); + ret = fuse_loop(fuse); + fuse_teardown(fuse, mp); + + return (ret == -1 ? 1 : 0); } Index: lib/libfuse/fuse_destroy.3 =================================================================== RCS file: /cvs/src/lib/libfuse/fuse_destroy.3,v diff -u -p -r1.3 fuse_destroy.3 --- lib/libfuse/fuse_destroy.3 10 Jun 2025 12:55:33 -0000 1.3 +++ lib/libfuse/fuse_destroy.3 10 Sep 2025 18:06:05 -0000 @@ -27,9 +27,12 @@ .Fn fuse_destroy "struct fuse *f" .Sh DESCRIPTION .Fn fuse_destroy -closes the FUSE device and frees memory associated with the FUSE channel -and FUSE handle specified by +Frees memory associated with the FUSE channel and FUSE handle specified by .Fa f . +The file system's destroy operation will be called if +.Xr fuse_loop 3 +did not receive the FBT_DESTROY message. +Usually due to terminating from a signal. .Pp This function does not unmount the file system, which should be done with Index: lib/libfuse/fuse_loop.3 =================================================================== RCS file: /cvs/src/lib/libfuse/fuse_loop.3,v diff -u -p -r1.3 fuse_loop.3 --- lib/libfuse/fuse_loop.3 10 Jun 2025 12:55:33 -0000 1.3 +++ lib/libfuse/fuse_loop.3 10 Sep 2025 18:06:05 -0000 @@ -47,7 +47,7 @@ the file system is being unmounted. If FUSE signaler handlers have been installed and either SIGHUP, SIGINT or SIGTERM is received then .Fn fuse_loop -will attempt to unmount the file system. +will terminate. See .Xr fuse_set_signal_handlers 3 . .Pp Index: lib/libfuse/fuse_main.3 =================================================================== RCS file: /cvs/src/lib/libfuse/fuse_main.3,v diff -u -p -r1.8 fuse_main.3 --- lib/libfuse/fuse_main.3 10 Jun 2025 12:55:33 -0000 1.8 +++ lib/libfuse/fuse_main.3 10 Sep 2025 18:06:05 -0000 @@ -137,6 +137,7 @@ main(int argc, char **argv) .Xr fuse_new 3 , .Xr fuse_parse_cmdline 3 , .Xr fuse_setup 3 , +.Xr fuse_teardown 3 , .Xr fuse 4 .Sh STANDARDS The Index: lib/libfuse/fuse_mount.3 =================================================================== RCS file: /cvs/src/lib/libfuse/fuse_mount.3,v diff -u -p -r1.3 fuse_mount.3 --- lib/libfuse/fuse_mount.3 10 Jun 2025 12:55:33 -0000 1.3 +++ lib/libfuse/fuse_mount.3 10 Sep 2025 18:06:05 -0000 @@ -77,15 +77,13 @@ Can also be specified by itself with .El .Pp .Fn fuse_unmount -will attempt to unmount the file system mounted at +will close the FUSE device and attempt to unmount the file system mounted at .Fa dir by calling the .Xr unmount 2 system call. -If this is successful, the kernel will send the -FBT_DESTROY message to the file system, causing -.Xr fuse_loop 3 -to terminate. +To avoid a deadlock, the kernel will not send the +FBT_DESTROY message to the file system. There is no way to determine whether this call was successful. .Pp Only the super user can mount and unmount FUSE file systems. Index: lib/libfuse/fuse_new.3 =================================================================== RCS file: /cvs/src/lib/libfuse/fuse_new.3,v diff -u -p -r1.9 fuse_new.3 --- lib/libfuse/fuse_new.3 9 Sep 2025 16:46:55 -0000 1.9 +++ lib/libfuse/fuse_new.3 10 Sep 2025 18:06:05 -0000 @@ -43,8 +43,8 @@ FUSE will return ENOSYS if any operation fsyncdir is not implemented. .Pp The first parameter to each of these operations (except for init and -terminate) is a NULL terminated string representing the full path to -the file or directory, relative to the root of this file system, that +destroy) is a NULL terminated string representing the full path to +the file or directory, relative to the root of the file system, that is being operated on. .Bd -literal struct fuse_operations { Index: lib/libfuse/fuse_ops.c =================================================================== RCS file: /cvs/src/lib/libfuse/fuse_ops.c,v diff -u -p -r1.39 fuse_ops.c --- lib/libfuse/fuse_ops.c 9 Sep 2025 16:46:55 -0000 1.39 +++ lib/libfuse/fuse_ops.c 10 Sep 2025 18:06:05 -0000 @@ -74,6 +74,9 @@ ifuse_ops_init(struct fuse *f) f->op.init(&fci); } + + f->fc->init = 1; + return (0); } @@ -969,15 +972,7 @@ ifuse_ops_rename(struct fuse *f, struct static int ifuse_ops_destroy(struct fuse *f) { - struct fuse_context *ctx; - DPRINTF("Opcode: destroy\n"); - - if (f->op.destroy) { - ctx = fuse_get_context(); - - f->op.destroy((ctx)?ctx->private_data:NULL); - } f->fc->dead = 1; Index: lib/libfuse/fuse_private.h =================================================================== RCS file: /cvs/src/lib/libfuse/fuse_private.h,v diff -u -p -r1.23 fuse_private.h --- lib/libfuse/fuse_private.h 20 Sep 2024 02:00:46 -0000 1.23 +++ lib/libfuse/fuse_private.h 10 Sep 2025 18:06:05 -0000 @@ -65,6 +65,7 @@ struct fuse_chan { struct fuse_args *args; int fd; + int init; int dead; /* kqueue stuff */ Index: sys/miscfs/fuse/fuse_device.c =================================================================== RCS file: /cvs/src/sys/miscfs/fuse/fuse_device.c,v diff -u -p -r1.45 fuse_device.c --- sys/miscfs/fuse/fuse_device.c 9 Sep 2025 16:46:55 -0000 1.45 +++ sys/miscfs/fuse/fuse_device.c 10 Sep 2025 18:06:19 -0000 @@ -165,6 +165,7 @@ fuse_device_cleanup(dev_t dev) wakeup(f); lprev = f; } + knote_locked(&fd->fd_rklist, 0); rw_exit_write(&fd->fd_lock); /* clear FIFO WAIT*/ @@ -247,26 +248,22 @@ int fuseclose(dev_t dev, int flags, int fmt, struct proc *p) { struct fuse_d *fd; - int error; fd = fuse_lookup(minor(dev)); if (fd == NULL) return (EINVAL); - if (fd->fd_fmp) { - printf("fuse: device close without umount\n"); + fuse_device_cleanup(dev); + + /* + * Let fusefs_unmount know the device is closed so it doesn't try and + * send FBT_DESTROY to a dead file system daemon. + */ + if (fd->fd_fmp) fd->fd_fmp->sess_init = 0; - fuse_device_cleanup(dev); - if ((vfs_busy(fd->fd_fmp->mp, VB_WRITE | VB_NOWAIT)) != 0) - goto end; - error = dounmount(fd->fd_fmp->mp, MNT_FORCE, p); - if (error) - printf("fuse: unmount failed with error %d\n", error); - fd->fd_fmp = NULL; - } -end: LIST_REMOVE(fd, fd_list); + free(fd, M_DEVBUF, sizeof(*fd)); stat_opened_fusedev--; return (0);