Re: [PATCH RFC] fuse: abort connection on /dev/fuse flush to prevent deadlock

Kusaram Devineni <[email protected]> Mon, 20 Jul 2026 15:02:46 +0530
Newsgroups dev.linux.lists.syzbot
Message-ID <[email protected]>
On 09-07-2026 08:00 pm, syzbot wrote:
> 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:
> 
> INFO: task syz.0.17:6117 blocked for more than 143 seconds.
> ...
> 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 last reference to the file descriptor
> (file_count(file) == 1) and it is the last device for the FUSE connection,
> 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.
> 

the `list_is_singular()` test is incomplete. with two cloned `/dev/fuse` 
devices, both final `.flush` calls can run before deferred release 
removes either device; both see a non-singular list and neither aborts. 
track a per-`fuse_dev` `closing` state under `fch->lock`, and abort when 
all devices on the channel are closing, while preserving a still-open clone.

update the commit message to describe cloned-device handling, the 
fs-context install race, and that CUSE inherits the new `.flush` 
callback. avoid claiming that `file_count(file) == 1` plus a singular 
list universally identifies the last endpoint.

> Fixes: 4a9d4b024a31 ("switch fput to task_work_add")
> Assisted-by: Gemini:gemini-3.1-pro-preview Gemini:gemini-3-flash-preview syzbot
> Reported-by: [email protected]
> Closes: https://syzkaller.appspot.com/bug?extid=0dbb0d6fda088e78a4d8
> Link: https://syzkaller.appspot.com/ai_job?id=2b6a3623-6dc9-49c8-ada9-03747a9847d2
> To: <[email protected]>
> To: "Miklos Szeredi" <[email protected]>
> Cc: <[email protected]>
> 
> ---
> diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
> index 5763a7cd3..89e2dea87 100644
> --- a/fs/fuse/dev.c
> +++ b/fs/fuse/dev.c
> @@ -2375,23 +2375,43 @@ 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 = READ_ONCE(fud->chan);
> +

`READ_ONCE(fud->chan)` leaves a close/install race. an fs context holds 
a `fuse_dev` reference without holding the `struct file`, so final close 
can observe `chan == NULL` and a later `fuse_dev_install()` can still 
succeed. atomically transition `NULL` to `FUSE_DEV_CHAN_DISCONNECTED` on 
final close, or provide an equivalent state transition that makes later 
install fail. update `fuse_dev_release()` to handle that early sentinel 
safely and update the `fud->chan` state documentation.

> +	if (fch && fch != FUSE_DEV_CHAN_DISCONNECTED && file_count(file) == 1) {
> +		bool last;
> +
> +		spin_lock(&fch->lock);
> +		last = list_is_singular(&fch->devices);
> +		spin_unlock(&fch->lock);
> +
> +		if (last)
> +			fuse_chan_abort(fch, false);
> +	}
> +

keep queue removal, fasync cleanup, `fuse_conn_put()`, and normal 
resource destruction in `.release`; `.flush` should only publish closing 
state and abort the channel when every endpoint is closing. call 
`fuse_chan_abort()` after dropping `fch->lock`.

> +	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

preserve the existing aligned `fuse_dev_operations` formatting and add 
only `.flush = fuse_dev_flush,`. the initializer reformat is unrelated 
churn.

>   };
>   EXPORT_SYMBOL_GPL(fuse_dev_operations);
> 
> 
> base-commit: 8cdeaa50eae8dad34885515f62559ee83e7e8dda

try validate at least: one device; duplicated fds; one open clone; two 
clones closed in one `close_range()`; close racing mount/install; CUSE 
close; fasync; and FUSE-over-io_uring abort. do not replace this with a 
fatal-signal check in `request_wait_answer()`; a client signal does not 
prove daemon loss, and request code must never set `FR_FINISHED` outside 
`fuse_request_end()`.

-kusaram