All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH RFC v3] fuse: abort connection on /dev/fuse flush to prevent deadlock
@ 2026-07-20 18:12 syzbot
  2026-07-21  9:32 ` [syzbot ci] " syzbot ci
  0 siblings, 1 reply; 2+ messages in thread
From: syzbot @ 2026-07-20 18:12 UTC (permalink / raw)
  To: syzkaller-upstream-moderation; +Cc: kusaram, syzbot

A single-threaded FUSE daemon can deadlock if it mounts a filesystem and
opens a file on it. When the process exits or calls close_range(), the
kernel closes file descriptors in ascending numerical order.

If the /dev/fuse file descriptor (e.g., fd 5) is closed first, filp_close()
decrements the file reference count but defers the actual release (fput) to
task work. As a result, the FUSE connection remains active.

Next, when the FUSE file descriptor (e.g., fd 6) is closed, filp_close()
calls fuse_flush(), which sends a synchronous FUSE_FLUSH request to the
daemon. Since the daemon is the same thread that is currently blocked in
the close() syscall, it cannot process the request. The FUSE_FLUSH request
uses args.force = true, making the wait uninterruptible. The thread hangs
forever, eventually triggering a hung task panic:

Call Trace:
 <TASK>
 __schedule+0x17d9/0x56c0 kernel/sched/core.c:7234
 schedule+0x164/0x2b0 kernel/sched/core.c:7326
 request_wait_answer fs/fuse/dev.c:743 [inline]
 __fuse_request_send fs/fuse/dev.c:757 [inline]
 fuse_chan_send+0x1065/0x1ab0 fs/fuse/dev.c:833
 fuse_simple_request fs/fuse/fuse_i.h:1012 [inline]
 fuse_flush+0x66e/0x8b0 fs/fuse/file.c:504
 filp_flush+0xbd/0x190 fs/open.c:1471
 filp_close+0x1d/0x40 fs/open.c:1484
 __range_close fs/file.c:793 [inline]
 __do_sys_close_range fs/file.c:854 [inline]
 __se_sys_close_range+0x3d3/0x900 fs/file.c:818
 do_syscall_64+0x15f/0x560 arch/x86/entry/syscall_64.c:94

To fix this, implement a .flush method for /dev/fuse (fuse_dev_operations).
The .flush method is called synchronously by filp_close() during the
close() syscall, bypassing the fput delay.

In fuse_dev_flush(), if this is the final close of the file descriptor
(file_count(file) <= 1), we mark the device as closing. Since multiple
cloned /dev/fuse devices can exist for the same connection, we track the
closing state of each device under the channel lock. If all devices
associated with the channel are closing, we proactively abort the FUSE
connection. By aborting the connection synchronously in .flush, any
subsequent fuse_flush() calls on FUSE files will immediately fail with
-ENOTCONN instead of blocking indefinitely, avoiding the deadlock.

To handle the race where a /dev/fuse file descriptor is closed before it is
fully installed into a connection (i.e., fud->chan is still NULL), we use
cmpxchg() to atomically transition fud->chan from NULL to
FUSE_DEV_CHAN_DISCONNECTED. This prevents a concurrent fuse_dev_install()
from succeeding on a device that is already being closed.

Note that CUSE (Character Device in User Space) also shares
fuse_dev_operations and therefore inherits this new .flush callback, which
is safe and consistent with its lifecycle.

Fixes: 4a9d4b024a31 ("switch fput to task_work_add")
Assisted-by: Gemini:gemini-3.5-flash Gemini:gemini-3.1-pro-preview syzbot
Reported-by: syzbot+0dbb0d6fda088e78a4d8@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=0dbb0d6fda088e78a4d8
Link: https://syzkaller.appspot.com/ai_job?id=ea95192d-3fa6-40f6-a70f-f147aaa37d22
To: <fuse-devel@lists.linux.dev>
To: "Miklos Szeredi" <miklos@szeredi.hu>
Cc: <linux-kernel@vger.kernel.org>

---
v3:
- Added fuse_dev_all_closing() helper to check if all devices on a channel are closing.
- Updated fuse_dev_release() to abort the connection if all remaining devices are closing.
- Fixed the file reference count check in fuse_dev_flush() to check for file_count(file) != 1 instead of file_count(file) > 1.
- Updated the comment in fs/fuse/fuse_dev_i.h to clarify the lifecycle of fud->chan and when it is set to FUSE_DEV_CHAN_DISCONNECTED.

v2:
- Added `closing` flag to `struct fuse_dev` to track the closing state of each device.
- Updated `fuse_dev_flush()` to check if all associated devices are closing, properly handling cloned devices.
- Used `cmpxchg()` in `fuse_dev_flush()` to handle the race where a device is closed before being fully installed.
- Updated `fuse_dev_release()` to handle `FUSE_DEV_CHAN_DISCONNECTED` safely.
https://lore.kernel.org/all/3e4a3361-9e1b-4f46-8033-f85ada100a79@mail.kernel.org/T/

v1:
https://lore.kernel.org/all/bedbc009-8806-4913-9a03-1b25b6d33729@mail.kernel.org/T/
---
diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
index 5763a7cd3..7cf541873 100644
--- a/fs/fuse/dev.c
+++ b/fs/fuse/dev.c
@@ -2211,17 +2211,30 @@ void fuse_chan_wait_aborted(struct fuse_chan *fch)
 	fuse_uring_wait_stopped_queues(fch);
 }
 
+/* fch->lock must be held */
+static bool fuse_dev_all_closing(struct fuse_chan *fch)
+{
+	struct fuse_dev *pos;
+
+	list_for_each_entry(pos, &fch->devices, entry) {
+		if (!pos->closing)
+			return false;
+	}
+	return true;
+}
+
 int fuse_dev_release(struct inode *inode, struct file *file)
 {
 	struct fuse_dev *fud = fuse_file_to_fud(file);
 	/* Pairs with cmpxchg() in fuse_dev_install() */
 	struct fuse_chan *fch = xchg(&fud->chan, FUSE_DEV_CHAN_DISCONNECTED);
 
-	if (fch) {
+	if (fch && fch != FUSE_DEV_CHAN_DISCONNECTED) {
 		struct fuse_pqueue *fpq = &fud->pq;
 		LIST_HEAD(to_end);
 		unsigned int i;
 		bool last;
+		bool all_closing;
 
 		/* Make sure fuse_dev_install_with_pq() has finished */
 		spin_lock(&fch->lock);
@@ -2234,12 +2247,14 @@ int fuse_dev_release(struct inode *inode, struct file *file)
 		list_del(&fud->entry);
 		/* Are we the last open device? */
 		last = list_empty(&fch->devices);
+		all_closing = last || fuse_dev_all_closing(fch);
 		spin_unlock(&fch->lock);
 
 		fuse_dev_end_requests(&to_end);
 
-		if (last) {
-			WARN_ON(fch->iq.fasync != NULL);
+		if (all_closing) {
+			if (last)
+				WARN_ON(fch->iq.fasync != NULL);
 			fuse_chan_abort(fch, false);
 		}
 		fuse_conn_put(fch->conn);
@@ -2375,23 +2390,52 @@ static void fuse_dev_show_fdinfo(struct seq_file *seq, struct file *file)
 }
 #endif
 
+static int fuse_dev_flush(struct file *file, fl_owner_t id)
+{
+	struct fuse_dev *fud = fuse_file_to_fud(file);
+	struct fuse_chan *fch;
+	bool all_closing;
+
+	if (file_count(file) != 1)
+		return 0;
+
+	/*
+	 * Atomically transition fud->chan from NULL to FUSE_DEV_CHAN_DISCONNECTED
+	 * on final close to prevent a later fuse_dev_install() from succeeding.
+	 */
+	fch = cmpxchg(&fud->chan, NULL, FUSE_DEV_CHAN_DISCONNECTED);
+	if (!fch || fch == FUSE_DEV_CHAN_DISCONNECTED)
+		return 0;
+
+	spin_lock(&fch->lock);
+	fud->closing = true;
+	all_closing = fuse_dev_all_closing(fch);
+	spin_unlock(&fch->lock);
+
+	if (all_closing)
+		fuse_chan_abort(fch, false);
+
+	return 0;
+}
+
 const struct file_operations fuse_dev_operations = {
-	.owner		= THIS_MODULE,
-	.open		= fuse_dev_open,
-	.read_iter	= fuse_dev_read,
-	.splice_read	= fuse_dev_splice_read,
-	.write_iter	= fuse_dev_write,
-	.splice_write	= fuse_dev_splice_write,
-	.poll		= fuse_dev_poll,
-	.release	= fuse_dev_release,
-	.fasync		= fuse_dev_fasync,
+	.owner = THIS_MODULE,
+	.open = fuse_dev_open,
+	.read_iter = fuse_dev_read,
+	.splice_read = fuse_dev_splice_read,
+	.write_iter = fuse_dev_write,
+	.splice_write = fuse_dev_splice_write,
+	.poll = fuse_dev_poll,
+	.flush = fuse_dev_flush,
+	.release = fuse_dev_release,
+	.fasync = fuse_dev_fasync,
 	.unlocked_ioctl = fuse_dev_ioctl,
-	.compat_ioctl   = compat_ptr_ioctl,
+	.compat_ioctl = compat_ptr_ioctl,
 #ifdef CONFIG_FUSE_IO_URING
-	.uring_cmd	= fuse_uring_cmd,
+	.uring_cmd = fuse_uring_cmd,
 #endif
 #ifdef CONFIG_PROC_FS
-	.show_fdinfo	= fuse_dev_show_fdinfo,
+	.show_fdinfo = fuse_dev_show_fdinfo,
 #endif
 };
 EXPORT_SYMBOL_GPL(fuse_dev_operations);
diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h
index 668c8391d..ec6334414 100644
--- a/fs/fuse/fuse_dev_i.h
+++ b/fs/fuse/fuse_dev_i.h
@@ -310,6 +310,9 @@ struct fuse_dev {
 
 	/** @entry: list entry on fch->devices */
 	struct list_head entry;
+
+	/** @closing: is this device closing? */
+	bool closing;
 };
 
 struct fuse_copy_state {
@@ -337,8 +340,10 @@ struct fuse_copy_state {
  * Lockless access is OK, because fud->chan is set once during mount and is valid
  * until the file is released.
  *
- * fud->chan is set to FUSE_DEV_CHAN_DISCONNECTED only after the containing file is
- * released, so result is safe to dereference in most cases.  Exceptions are:
+ * Final flush replaces NULL with the disconnected sentinel only for an
+ * uninstalled device, while for an installed device, flush leaves the channel
+ * pointer intact and records closing, and release later installs the sentinel.
+ * Thus the result is safe to dereference in most cases.  Exceptions are:
  * fuse_dev_put() and fuse_fill_super_common().
  */
 static inline struct fuse_chan *fuse_dev_chan_get(struct fuse_dev *fud)


base-commit: 8cdeaa50eae8dad34885515f62559ee83e7e8dda
-- 
This is an AI-generated patch subject to moderation.
Reply with '#syz upstream' to Sign-off the patch as a human author
and send it to the upstream kernel mailing lists.
Reply with '#syz reject' to reject it ('#syz unreject' to undo).

See https://goo.gle/syzbot-ai-patches for information about AI-generated patches.
You can comment on the patch as usual, syzbot will try to address
the comments and send a new version of the patch if necessary.
syzbot engineers can be reached at syzkaller@googlegroups.com.

^ permalink raw reply related	[flat|nested] 2+ messages in thread

* [syzbot ci] Re: fuse: abort connection on /dev/fuse flush to prevent deadlock
  2026-07-20 18:12 [PATCH RFC v3] fuse: abort connection on /dev/fuse flush to prevent deadlock syzbot
@ 2026-07-21  9:32 ` syzbot ci
  0 siblings, 0 replies; 2+ messages in thread
From: syzbot ci @ 2026-07-21  9:32 UTC (permalink / raw)
  To: kusaram, syzbot, syzbot, syzkaller-upstream-moderation
  Cc: syzbot, syzkaller-bugs

syzbot ci has tested the following series

[v3] fuse: abort connection on /dev/fuse flush to prevent deadlock
https://lore.kernel.org/all/40d9021a-6a93-43a1-97b0-6ec2f9cbabc6@mail.kernel.org
* [PATCH RFC v3] fuse: abort connection on /dev/fuse flush to prevent deadlock

and found the following issue:
general protection fault in fasync_remove_entry

Full report is available here:
https://ci.syzbot.org/series/a9dcb2cb-0ef5-4576-84ff-c40a48e2a25a

***

general protection fault in fasync_remove_entry

tree:      bpf-next
URL:       https://kernel.googlesource.com/pub/scm/linux/kernel/git/bpf/bpf-next.git
base:      8cdeaa50eae8dad34885515f62559ee83e7e8dda
arch:      amd64
compiler:  Debian clang version 22.1.8 (++20260613092233+e80beda6e255-1~exp1~20260613092250.77), Debian LLD 22.1.8
config:    https://ci.syzbot.org/builds/be2f3b2c-a8ca-4791-b9dd-8f1f9d24d9e9/config
syz repro: https://ci.syzbot.org/findings/0c2890de-1b7a-4718-a188-a597ff4d74a7/syz_repro

Oops: general protection fault, probably for non-canonical address 0xdffffc0000000026: 0000 [#1] SMP KASAN PTI
KASAN: null-ptr-deref in range [0x0000000000000130-0x0000000000000137]
CPU: 0 UID: 0 PID: 5854 Comm: syz.1.18 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
RIP: 0010:fasync_remove_entry+0x3e/0x1c0 fs/fcntl.c:1008
Code: 49 bd 00 00 00 00 00 fc ff df e8 fd 44 7a ff 48 89 df e8 05 2e 85 09 48 c7 c7 80 65 cd 8e e8 f9 2d 85 09 4c 89 f0 48 c1 e8 03 <42> 80 3c 28 00 74 08 4c 89 f7 e8 a3 cf e7 ff 4d 8b 3e 4d 85 ff 74
RSP: 0018:ffffc900035efd38 EFLAGS: 00010202
RAX: 0000000000000026 RBX: ffff888174b8cc00 RCX: 0000000000000001
RDX: 0000000000000001 RSI: 0000000000000004 RDI: ffffc900035efcc0
RBP: dffffc0000000000 R08: 0000000000000003 R09: 0000000000000004
R10: dffffc0000000000 R11: fffff520006bdf98 R12: ffff88810270ab80
R13: dffffc0000000000 R14: 0000000000000131 R15: ffffffff8c29b708
FS:  0000555569119500(0000) GS:ffff88818d959000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000200000000080 CR3: 0000000102f90000 CR4: 00000000000006f0
Call Trace:
 <TASK>
 __fput+0x88d/0xa50 fs/file_table.c:509
 task_work_run+0x1d9/0x270 kernel/task_work.c:233
 resume_user_mode_work include/linux/resume_user_mode.h:50 [inline]
 __exit_to_user_mode_loop kernel/entry/common.c:70 [inline]
 exit_to_user_mode_loop+0x1fa/0x730 kernel/entry/common.c:101
 __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
 syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
 syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
 do_syscall_64+0x353/0x580 arch/x86/entry/syscall_64.c:100
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7fc7bcb9de99
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007ffe628b3f98 EFLAGS: 00000246 ORIG_RAX: 00000000000001b4
RAX: 0000000000000000 RBX: 00007ffe628b4080 RCX: 00007fc7bcb9de99
RDX: 0000000000000000 RSI: 000000000000001e RDI: 0000000000000003
RBP: 000000000001302f R08: 0000000000000001 R09: 0000000000000000
R10: 0000001b32a20000 R11: 0000000000000246 R12: 00007ffe628b40c0
R13: 00007fc7bce25fac R14: 000000000001306b R15: 00007fc7bce25fa0
 </TASK>
Modules linked in:
---[ end trace 0000000000000000 ]---
RIP: 0010:fasync_remove_entry+0x3e/0x1c0 fs/fcntl.c:1008
Code: 49 bd 00 00 00 00 00 fc ff df e8 fd 44 7a ff 48 89 df e8 05 2e 85 09 48 c7 c7 80 65 cd 8e e8 f9 2d 85 09 4c 89 f0 48 c1 e8 03 <42> 80 3c 28 00 74 08 4c 89 f7 e8 a3 cf e7 ff 4d 8b 3e 4d 85 ff 74
RSP: 0018:ffffc900035efd38 EFLAGS: 00010202
RAX: 0000000000000026 RBX: ffff888174b8cc00 RCX: 0000000000000001
RDX: 0000000000000001 RSI: 0000000000000004 RDI: ffffc900035efcc0
RBP: dffffc0000000000 R08: 0000000000000003 R09: 0000000000000004
R10: dffffc0000000000 R11: fffff520006bdf98 R12: ffff88810270ab80
R13: dffffc0000000000 R14: 0000000000000131 R15: ffffffff8c29b708
FS:  0000555569119500(0000) GS:ffff88818d959000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000200000000080 CR3: 0000000102f90000 CR4: 00000000000006f0
----------------
Code disassembly (best guess):
   0:	49 bd 00 00 00 00 00 	movabs $0xdffffc0000000000,%r13
   7:	fc ff df
   a:	e8 fd 44 7a ff       	call   0xff7a450c
   f:	48 89 df             	mov    %rbx,%rdi
  12:	e8 05 2e 85 09       	call   0x9852e1c
  17:	48 c7 c7 80 65 cd 8e 	mov    $0xffffffff8ecd6580,%rdi
  1e:	e8 f9 2d 85 09       	call   0x9852e1c
  23:	4c 89 f0             	mov    %r14,%rax
  26:	48 c1 e8 03          	shr    $0x3,%rax
* 2a:	42 80 3c 28 00       	cmpb   $0x0,(%rax,%r13,1) <-- trapping instruction
  2f:	74 08                	je     0x39
  31:	4c 89 f7             	mov    %r14,%rdi
  34:	e8 a3 cf e7 ff       	call   0xffe7cfdc
  39:	4d 8b 3e             	mov    (%r14),%r15
  3c:	4d 85 ff             	test   %r15,%r15
  3f:	74                   	.byte 0x74


***

If these findings have caused you to resend the series or submit a
separate fix, please add the following tag to your commit message:
  Tested-by: syzbot@syzkaller.appspotmail.com

---
This report is generated by a bot. It may contain errors.
syzbot ci engineers can be reached at syzkaller@googlegroups.com.

To test a patch for this bug, please reply with `#syz test`
(should be on a separate line).

The patch should be attached to the email.
Note: arguments like custom git repos and branches are not supported.

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-07-21  9:32 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-20 18:12 [PATCH RFC v3] fuse: abort connection on /dev/fuse flush to prevent deadlock syzbot
2026-07-21  9:32 ` [syzbot ci] " syzbot ci

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.