Re: [PATCH RFC v8] fuse: abort connection on /dev/fuse flush to prevent deadlock
Kusaram Devineni <[email protected]> Mon, 27 Jul 2026 22:57:18 +0530
| Newsgroups | dev.linux.lists.syzbot |
|---|---|
| Message-ID | <[email protected]> |
On 27-07-2026 06:57 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:
>
> 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), the device is marked as closing. Since multiple
> cloned /dev/fuse devices can exist for the same connection, the closing
> state of each device is tracked under the channel lock. If all devices
> associated with the channel are closing, the FUSE connection is proactively
> aborted. 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),
> cmpxchg() is used 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. Furthermore,
> because the VFS may invoke .fasync after .flush but before .release,
> fuse_get_dev() and __fuse_get_dev() must reject the
> FUSE_DEV_CHAN_DISCONNECTED sentinel to prevent .fasync from dereferencing
> it.
>
> 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
> Tested-by: syzbot <[email protected]>
> Reported-by: [email protected]
> Closes: https://syzkaller.appspot.com/bug?extid=0dbb0d6fda088e78a4d8
> Link: https://syzkaller.appspot.com/ai_job?id=fdcc0473-0605-4428-aad0-dd002b5f15aa
> To: <[email protected]>
> To: "Miklos Szeredi" <[email protected]>
> Cc: <[email protected]>
>
> ---
> v8:
> - Avoided first-person pronouns ('we') in the commit description to use impersonal wording.
>
> v7:
> - Removed the unintended `---` separator and the abridged changelog from the commit message.
> https://lore.kernel.org/all/[email protected]/T/
>
> v6:
> - Clarified fud->chan state transitions and lockless access rules in comments.
> https://lore.kernel.org/all/[email protected]/T/
>
> v5:
> - Moved `closing` next to `sync_init` in `struct fuse_dev` to preserve compact placement of boolean members.
> - Restored formatting of `fuse_dev_operations`.
> - Updated commit message to explain why `fuse_get_dev()` and `__fuse_get_dev()` reject `FUSE_DEV_CHAN_DISCONNECTED` due to `.fasync` invocation.
> - Removed duplicate `Tested-by` tag.
> https://lore.kernel.org/all/[email protected]/T/
>
> v4:
> - Changed `(file_count(file) <= 1)` to `(file_count(file) == 1)` in the commit message.
> - Added `Tested-by: [email protected]` tag.
> - Added lockdep assertion in `fuse_dev_all_closing()`.
> - Updated `fuse_get_dev()` and `__fuse_get_dev()` to reject `FUSE_DEV_CHAN_DISCONNECTED`.
> - Checked `fch->connected` before aborting in `fuse_dev_release()` and `fuse_dev_flush()`.
> https://lore.kernel.org/all/[email protected]/T/
>
> 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.
> https://lore.kernel.org/all/[email protected]/T/
>
> 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/[email protected]/T/
>
> v1:
> https://lore.kernel.org/all/[email protected]/T/
> ---
> diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
> index 5763a7cd3..8b14c7d57 100644
> --- a/fs/fuse/dev.c
> +++ b/fs/fuse/dev.c
> @@ -1662,18 +1662,23 @@ static int fuse_dev_open(struct inode *inode, struct file *file)
> struct fuse_dev *fuse_get_dev(struct file *file)
> {
> struct fuse_dev *fud = fuse_file_to_fud(file);
> + struct fuse_chan *fch = fuse_dev_chan_get(fud);
> int err;
>
> - if (unlikely(!fuse_dev_chan_get(fud))) {
> + if (unlikely(!fch)) {
> /* only block waiting for mount if sync init was requested */
> if (!fud->sync_init)
> return ERR_PTR(-EPERM);
>
> - err = wait_event_interruptible(fuse_dev_waitq, fuse_dev_chan_get(fud) != NULL);
> + err = wait_event_interruptible(fuse_dev_waitq,
> + (fch = fuse_dev_chan_get(fud)) != NULL);
> if (err)
> return ERR_PTR(err);
> }
>
> + if (unlikely(fch == FUSE_DEV_CHAN_DISCONNECTED))
> + return ERR_PTR(-EPERM);
> +
> return fud;
> }
>
> @@ -2211,17 +2216,31 @@ void fuse_chan_wait_aborted(struct fuse_chan *fch)
> fuse_uring_wait_stopped_queues(fch);
> }
>
> +static bool fuse_dev_all_closing(struct fuse_chan *fch)
> +{
> + struct fuse_dev *pos;
> +
> + lockdep_assert_held(&fch->lock);
> +
> + 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 abort;
>
> /* Make sure fuse_dev_install_with_pq() has finished */
> spin_lock(&fch->lock);
> @@ -2234,14 +2253,15 @@ 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);
> + abort = fch->connected && fuse_dev_all_closing(fch);
> spin_unlock(&fch->lock);
>
> fuse_dev_end_requests(&to_end);
>
> - if (last) {
> + if (last)
> WARN_ON(fch->iq.fasync != NULL);
> + if (abort)
> fuse_chan_abort(fch, false);
> - }
> fuse_conn_put(fch->conn);
> }
> fuse_dev_put(fud);
> @@ -2375,6 +2395,34 @@ 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 abort;
> +
> + 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;
> + abort = fch->connected && fuse_dev_all_closing(fch);
> + spin_unlock(&fch->lock);
> +
> + if (abort)
> + fuse_chan_abort(fch, false);
> +
> + return 0;
> +}
> +
> const struct file_operations fuse_dev_operations = {
> .owner = THIS_MODULE,
> .open = fuse_dev_open,
> @@ -2383,6 +2431,7 @@ const struct file_operations fuse_dev_operations = {
> .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,
> diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h
> index 668c8391d..2833a0350 100644
> --- a/fs/fuse/fuse_dev_i.h
> +++ b/fs/fuse/fuse_dev_i.h
> @@ -302,6 +302,9 @@ struct fuse_dev {
> /** @sync_init: Issue FUSE_INIT synchronously */
> bool sync_init;
>
> + /** @closing: Final close has started, protected by chan->lock */
> + bool closing;
> +
> /** @chan: Fuse channel for this device */
> struct fuse_chan *chan;
>
> @@ -330,17 +333,25 @@ struct fuse_copy_state {
> } ring;
> };
>
> -/* fud->chan gets assigned to this value when /dev/fuse is closed */
> -#define FUSE_DEV_CHAN_DISCONNECTED ((struct fuse_chan *) 1)
> -
> /*
> - * Lockless access is OK, because fud->chan is set once during mount and is valid
> - * until the file is released.
> + * fud->chan has the following monotonic state transitions:
> + *
> + * NULL -> valid channel -> FUSE_DEV_CHAN_DISCONNECTED
> + * NULL -----------------> FUSE_DEV_CHAN_DISCONNECTED
> *
> - * 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:
> - * fuse_dev_put() and fuse_fill_super_common().
> + * A valid channel pointer remains valid until the file is released. Final
> + * flush can install FUSE_DEV_CHAN_DISCONNECTED directly for an uninstalled
> + * device, preventing any later channel installation. The sentinel therefore
> + * means that the channel can no longer be installed or accessed through this
> + * file; it does not imply that release has already run and it must never be
> + * dereferenced.
> + *
> + * fuse_dev_chan_get() reads this state locklessly. Callers must distinguish
> + * NULL, a valid channel pointer, and FUSE_DEV_CHAN_DISCONNECTED before
> + * dereferencing the result.
> */
> +#define FUSE_DEV_CHAN_DISCONNECTED ((struct fuse_chan *) 1)
> +
> static inline struct fuse_chan *fuse_dev_chan_get(struct fuse_dev *fud)
> {
> /* Pairs with xchg() in fuse_dev_install() */
> @@ -355,8 +366,9 @@ static inline struct fuse_dev *fuse_file_to_fud(struct file *file)
> static inline struct fuse_dev *__fuse_get_dev(struct file *file)
> {
> struct fuse_dev *fud = fuse_file_to_fud(file);
> + struct fuse_chan *fch = fuse_dev_chan_get(fud);
>
> - if (!fuse_dev_chan_get(fud))
> + if (!fch || fch == FUSE_DEV_CHAN_DISCONNECTED)
> return NULL;
>
> return fud;
>
>
> base-commit: 8cdeaa50eae8dad34885515f62559ee83e7e8dda
#syz upstream