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