Index | Thread | Search

From:
Helg <helg-openbsd@gmx.de>
Subject:
fuse: change termination behaviour
To:
tech@openbsd.org
Date:
Wed, 10 Sep 2025 02:18:03 +0200

Download raw body.

Thread
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);