From: Claudio Jeker Subject: Re: fuse: change termination behaviour To: Helg Cc: tech@openbsd.org Date: Fri, 12 Sep 2025 14:04:58 +0200 On Wed, Sep 10, 2025 at 02:18:03AM +0200, Helg wrote: > 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? Diff looks generally OK. If this is expected behaviour we should probably follow it. My question is what happens when you kill the fuse userland process but end up with the FS still mounted. Is there a chance that this will lockup the machine (similar to unreachable NFS servers)? I assume that the idea is that you can restart the FUSE file system daemon without remounting the file system (and keeping dirty buffers accross such a restart). Is this correct? > 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); > -- :wq Claudio