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

Kusaram Devineni <[email protected]> Mon, 27 Jul 2026 18:22:53 +0530
Newsgroups dev.linux.lists.syzbot
Message-ID <[email protected]>
On 25-07-2026 01:34 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), 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.
> 

please use impersonal wording instead of “we” in the above paragraph.

> 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()

please use impersonal wording instead of “we” in the above sentence.

> 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=754cb784-1492-448f-863d-81ac84d6c0fb
> To: <[email protected]>
> To: "Miklos Szeredi" <[email protected]>
> Cc: <[email protected]>
> 
> ---
> v7:
> - Removed the unintended `---` separator and the abridged changelog from the commit message.
> 
> 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