Skip to contentSkip to conversation

FAILED: patch "[PATCH] fuse: wait for FR_FINISHED on abort_on_kill to prevent" failed to apply to 6.18-stable tree

PATCH In review [email protected] 3 participants 6 messages Aug 31 – Sep 9

Subsystems (2)
lore
Share & tools
lore

[email protected]

author maintainer
#0 lore raw

The patch below does not apply to the 6.18-stable tree.
If someone wants it applied there, or to any other stable or longterm tree, then please email the backport, including the original git commit id to <[email protected]>.

To reproduce the conflict and resubmit, you may use the following commands:

git fetch https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/ linux-6.18.y
git checkout FETCH_HEAD
git cherry-pick -x 64b0b5cacbd2fea88001464cb712c9dfc795b26e
# <resolve conflicts, build, test, etc.>
git commit -s
git send-email --to '<[email protected]>' --in-reply-to '2026083101-rimless-sadness-3898@gregkh' --subject-prefix 'PATCH 6.18.y' 'HEAD^..'

Possible dependencies:

thanks,

greg k-h

------------------ original commit in Linus's tree ------------------

From 64b0b5cacbd2fea88001464cb712c9dfc795b26e Mon Sep 17 00:00:00 2001
From: Rochan Avlur <[email protected]>
Date: Wed, 12 Aug 2026 20:58:36 -0700
Subject: [PATCH] fuse: wait for FR_FINISHED on abort_on_kill to prevent
use-after-free

The abort_on_kill path in request_wait_answer() calls fuse_abort_conn() and returns without waiting for FR_FINISHED. If fuse_dev_do_write() is concurrently processing the same request (FR_LOCKED set), the caller frees req->args while it is still being accessed, causing a use-after-free.

Fix this by jumping to the existing wait_event(FR_FINISHED) instead of returning early. The wait will not hang because fuse_abort_conn() ensures all requests are ended.

Reported-by [email protected]
Fixes 204aa22a686b ("fuse: abort on fatal signal during sync init")
Assisted-by Claude:claude-opus-4-6
fs/fuse/dev.c +2 -1
index 90dceb7da571..34106f6e66a0 100644
@@ -724,7 +724,7 @@ static void request_wait_answer(struct fuse_req *req)
 
 		if (req->args->abort_on_kill) {
 			fuse_chan_abort(fch, false);
-			return;
+			goto wait_for_finish;
 		}
 
 		if (test_bit(FR_URING, &req->flags))
@@ -735,6 +735,7 @@ static void request_wait_answer(struct fuse_req *req)
 			return;
 	}
 
+wait_for_finish:
 	/*
 	 * Either request is already in userspace, or it was forced.
 	 * Wait it out.
maintainer
#1 lore raw

PATCH 1/4 fuse: abort related layering cleanup

From: Miklos Szeredi <[email protected]>

[ Upstream commit 4eeb5e6cb0fd89b343fcf755ae3aad76fdb5b2c2 ]

- rename fuse_abort_conn() to fuse_chan_abort(), pass fuse_chan pointer instead of fuse_conn

- pass an abort_with_err argument that tells fuse_dev_(read|write) to return with ECONNABORTED instead of ENODEV

- move fc->aborted to fch->abort_with_err

- rename fuse_wait_aborted() to fuse_chan_wait_aborted()

[ 6.18 has no "struct fuse_chan" -- the connection object still plays
that role, and the surrounding series that splits it out is not in stable. Adapted as follows:

- the renames (fuse_abort_conn() -> fuse_chan_abort(), fuse_wait_aborted() -> fuse_chan_wait_aborted(), the "abort_conn" label in fuse_check_timeout() -> "chan_abort") and the new abort_with_err argument are taken as-is, but both helpers keep taking "struct fuse_conn *".

- the "move fc->aborted to fch->abort_with_err" bullet is dropped:
there is no fuse_chan to move the flag into, so fc->aborted stays in struct fuse_conn. fuse_chan_abort() only ever *sets* it, as control.c did before, rather than assigning the new argument unconditionally the way upstream does: on this tree the flag is write-once and is what makes fuse_dev_do_read() and fuse_uring_cmd() report ECONNABORTED instead of ENODEV, so letting a later abort with abort_with_err=false clear it (umount -f, fuse_conn_destroy(), fuse_dev_release(), the timeout worker) would change what userspace sees after a sysfs abort on an abort_err mount. Consequently the fuse_dev_i.h and dev_uring.c hunks, which only rename the field access, are dropped as well.

- fs/fuse/dev.h and fs/fuse/req_timeout.c do not exist here: the declarations stay in fuse_i.h and fuse_check_timeout() lives in dev.c, so those two hunks were replayed there.

- in request_wait_answer() the connection pointer is named "fch", matching the name upstream gives it in 28d733cc3ecad ("fuse: change req->fm to req->chan"), so that the follow-up fix applies here unchanged. ]

[ sashal: Reduced backport -- upstream 4eeb5e6cb0fd8 touches 9 file(s), this
backport carries 5. Not backported here:
fs/fuse/dev.h
fs/fuse/dev_uring.c
fs/fuse/fuse_dev_i.h
fs/fuse/req_timeout.c
This note is generated from the file lists only; see the resolution record for the reasoning. ]

Stable-dep-of: 64b0b5cacbd2 ("fuse: wait for FR_FINISHED on abort_on_kill to prevent use-after-free")

---
 fs/fuse/control.c |  4 +---
 fs/fuse/cuse.c    |  4 ++--
 fs/fuse/dev.c     | 35 +++++++++++++++++++----------------
 fs/fuse/fuse_i.h  |  4 ++--
 fs/fuse/inode.c   |  6 +++---
 5 files changed, 27 insertions(+), 26 deletions(-)
fs/fuse/control.c +1 -3
index 2e300f6d646c3..5536d6ee65514 100644
@@ -37,9 +37,7 @@ static ssize_t fuse_conn_abort_write(struct file *file, const char __user *buf,
 {
 	struct fuse_conn *fc = fuse_ctl_file_conn_get(file);
 	if (fc) {
-		if (fc->abort_err)
-			fc->aborted = true;
-		fuse_abort_conn(fc);
+		fuse_chan_abort(fc, fc->abort_err);
 		fuse_conn_put(fc);
 	}
 	return count;
fs/fuse/cuse.c +2 -2
index 2bb98bf81b079..d90f138321fe9 100644
@@ -425,7 +425,7 @@ static void cuse_process_init_reply(struct fuse_mount *fm,
 err_region:
 	unregister_chrdev_region(devt, 1);
 err:
-	fuse_abort_conn(fc);
+	fuse_chan_abort(fc, false);
 	goto out;
 }
 
@@ -593,7 +593,7 @@ static ssize_t cuse_class_abort_store(struct device *dev,
 {
 	struct cuse_conn *cc = dev_get_drvdata(dev);
 
-	fuse_abort_conn(&cc->fc);
+	fuse_chan_abort(&cc->fc, false);
 	return count;
 }
 static DEVICE_ATTR(abort, 0200, NULL, cuse_class_abort_store);
fs/fuse/dev.c +19 -16
index 77c3982bca3d7..934f9ac5056ed 100644
@@ -87,13 +87,13 @@ void fuse_check_timeout(struct work_struct *work)
 	expired = fuse_request_expired(fc, &fiq->pending);
 	spin_unlock(&fiq->lock);
 	if (expired)
-		goto abort_conn;
+		goto chan_abort;
 
 	spin_lock(&fc->bg_lock);
 	expired = fuse_request_expired(fc, &fc->bg_queue);
 	spin_unlock(&fc->bg_lock);
 	if (expired)
-		goto abort_conn;
+		goto chan_abort;
 
 	spin_lock(&fc->lock);
 	if (!fc->connected) {
@@ -107,7 +107,7 @@ void fuse_check_timeout(struct work_struct *work)
 		    fuse_fpq_processing_expired(fc, fpq->processing)) {
 			spin_unlock(&fpq->lock);
 			spin_unlock(&fc->lock);
-			goto abort_conn;
+			goto chan_abort;
 		}
 
 		spin_unlock(&fpq->lock);
@@ -115,15 +115,15 @@ void fuse_check_timeout(struct work_struct *work)
 	spin_unlock(&fc->lock);
 
 	if (fuse_uring_request_expired(fc))
-	    goto abort_conn;
+	    goto chan_abort;
 
 out:
 	queue_delayed_work(system_percpu_wq, &fc->timeout.work,
 			   fuse_timeout_timer_freq);
 	return;
 
-abort_conn:
-	fuse_abort_conn(fc);
+chan_abort:
+	fuse_chan_abort(fc, false);
 }
 
 static void fuse_request_init(struct fuse_mount *fm, struct fuse_req *req)
@@ -180,7 +180,7 @@ static void fuse_drop_waiting(struct fuse_conn *fc)
 {
 	/*
 	 * lockess check of fc->connected is okay, because atomic_dec_and_test()
-	 * provides a memory barrier matched with the one in fuse_wait_aborted()
+	 * provides a memory barrier matched with the one in fuse_chan_wait_aborted()
 	 * to ensure no wake-up is missed.
 	 */
 	if (atomic_dec_and_test(&fc->num_waiting) &&
@@ -551,11 +551,11 @@ bool fuse_remove_pending_req(struct fuse_req *req, spinlock_t *lock)
 
 static void request_wait_answer(struct fuse_req *req)
 {
-	struct fuse_conn *fc = req->fm->fc;
-	struct fuse_iqueue *fiq = &fc->iq;
+	struct fuse_conn *fch = req->fm->fc;
+	struct fuse_iqueue *fiq = &fch->iq;
 	int err;
 
-	if (!fc->no_interrupt) {
+	if (!fch->no_interrupt) {
 		/* Any signal may interrupt this */
 		err = wait_event_interruptible(req->waitq,
 					test_bit(FR_FINISHED, &req->flags));
@@ -579,7 +579,7 @@ static void request_wait_answer(struct fuse_req *req)
 			return;
 
 		if (req->args->abort_on_kill) {
-			fuse_abort_conn(fc);
+			fuse_chan_abort(fch, false);
 			return;
 		}
 
@@ -1491,7 +1491,7 @@ static ssize_t fuse_dev_do_read(struct fuse_dev *fud, struct file *file,
 	spin_lock(&fpq->lock);
 	/*
 	 *  Must not put request on fpq->io queue after having been shut down by
-	 *  fuse_abort_conn()
+	 *  fuse_chan_abort()
 	 */
 	if (!fpq->connected) {
 		req->out.h.error = err = -ECONNABORTED;
@@ -2475,10 +2475,13 @@ static void end_polls(struct fuse_conn *fc)
  * is OK, the request will in that case be removed from the list before we touch
  * it.
  */
-void fuse_abort_conn(struct fuse_conn *fc)
+void fuse_chan_abort(struct fuse_conn *fc, bool abort_with_err)
 {
 	struct fuse_iqueue *fiq = &fc->iq;
 
+	if (abort_with_err)
+		fc->aborted = true;
+
 	spin_lock(&fc->lock);
 	if (fc->connected) {
 		struct fuse_dev *fud;
@@ -2547,9 +2550,9 @@ void fuse_abort_conn(struct fuse_conn *fc)
 		spin_unlock(&fc->lock);
 	}
 }
-EXPORT_SYMBOL_GPL(fuse_abort_conn);
+EXPORT_SYMBOL_GPL(fuse_chan_abort);
 
-void fuse_wait_aborted(struct fuse_conn *fc)
+void fuse_chan_wait_aborted(struct fuse_conn *fc)
 {
 	/* matches implicit memory barrier in fuse_drop_waiting() */
 	smp_mb();
@@ -2579,7 +2582,7 @@ int fuse_dev_release(struct inode *inode, struct file *file)
 		/* Are we the last open device? */
 		if (atomic_dec_and_test(&fc->dev_count)) {
 			WARN_ON(fc->iq.fasync != NULL);
-			fuse_abort_conn(fc);
+			fuse_chan_abort(fc, false);
 		}
 		fuse_dev_free(fud);
 	}
fs/fuse/fuse_i.h +2 -2
index c288f28f6c6ea..8b745045f1ab3 100644
@@ -1272,8 +1272,8 @@ void fuse_request_assign_unique(struct fuse_iqueue *fiq, struct fuse_req *req);
 void fuse_request_end(struct fuse_req *req);
 
 /* Abort all requests */
-void fuse_abort_conn(struct fuse_conn *fc);
-void fuse_wait_aborted(struct fuse_conn *fc);
+void fuse_chan_abort(struct fuse_conn *fc, bool abort_with_err);
+void fuse_chan_wait_aborted(struct fuse_conn *fc);
 
 /* Check if any requests timed out */
 void fuse_check_timeout(struct work_struct *work);
fs/fuse/inode.c +3 -3
index e9ed693fc7b37..399298816dea0 100644
@@ -621,7 +621,7 @@ static void fuse_umount_begin(struct super_block *sb)
 	if (fc->no_force_umount)
 		return;
 
-	fuse_abort_conn(fc);
+	fuse_chan_abort(fc, false);
 
 	// Only retire block-device-based superblocks.
 	if (sb->s_bdev != NULL)
@@ -2099,8 +2099,8 @@ void fuse_conn_destroy(struct fuse_mount *fm)
 	if (fc->destroy)
 		fuse_send_destroy(fm);
 
-	fuse_abort_conn(fc);
-	fuse_wait_aborted(fc);
+	fuse_chan_abort(fc, false);
+	fuse_chan_wait_aborted(fc);
 
 	if (!list_empty(&fc->entry)) {
 		mutex_lock(&fuse_mutex);
Signature · 1 line
2.53.0
maintainer
#2 lore raw

PATCH 2/4 fuse: split off fuse_args and related definitions into a separate header

From: Miklos Szeredi <[email protected]>

[ Upstream commit 229f9b9b66ab5be9e015422cf30b97740cfbdc8d ]

This is going to be used by both layers (transport and filesystem)

Stable-dep-of: 64b0b5cacbd2 ("fuse: wait for FR_FINISHED on abort_on_kill to prevent use-after-free")

---
 fs/fuse/args.h   | 65 ++++++++++++++++++++++++++++++++++++++++++++++++
 fs/fuse/fuse_i.h | 53 +--------------------------------------
 2 files changed, 66 insertions(+), 52 deletions(-)
 create mode 100644 fs/fuse/args.h
fs/fuse/args.h new file +65 -0
new file mode 100644
index 0000000000000..12c94ece569fb
@@ -0,0 +1,65 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef _FS_FUSE_ARGS_H
+#define _FS_FUSE_ARGS_H
+
+#include <linux/types.h>
+
+struct fuse_mount;
+
+/** One input argument of a request */
+struct fuse_in_arg {
+	unsigned size;
+	const void *value;
+};
+
+/** One output argument of a request */
+struct fuse_arg {
+	unsigned size;
+	void *value;
+};
+
+struct fuse_args {
+	u64 nodeid;
+	u32 opcode;
+	u32 uid;
+	u32 gid;
+	u32 pid;
+	u8 in_numargs;
+	u8 out_numargs;
+	u8 ext_idx;
+	bool force:1;
+	bool noreply:1;
+	bool nocreds:1;
+	bool in_pages:1;
+	bool out_pages:1;
+	bool user_pages:1;
+	bool out_argvar:1;
+	bool page_zeroing:1;
+	bool page_replace:1;
+	bool may_block:1;
+	bool is_ext:1;
+	bool is_pinned:1;
+	bool invalidate_vmap:1;
+	bool abort_on_kill:1;
+	struct fuse_in_arg in_args[4];
+	struct fuse_arg out_args[2];
+	void (*end)(struct fuse_mount *fm, struct fuse_args *args, int error);
+	/* Used for kvec iter backed by vmalloc address */
+	void *vmap_base;
+};
+
+/** FUSE folio descriptor */
+struct fuse_folio_desc {
+	unsigned int length;
+	unsigned int offset;
+};
+
+struct fuse_args_pages {
+	struct fuse_args args;
+	struct folio **folios;
+	struct fuse_folio_desc *descs;
+	unsigned int num_folios;
+};
+
+#endif /* _FS_FUSE_ARGS_H */
fs/fuse/fuse_i.h +1 -52

65 lines not on this page · raw message

Signature · 1 line
2.53.0
maintainer
#3 lore raw

PATCH 3/4 fuse: remove fm arg of args->end callback

In reply to Sasha Levin

From: Miklos Szeredi <[email protected]>

[ Upstream commit a697d95fcdbb3bbe25cdc29db5542ddcebb831c1 ]

Only used by FUSE_INIT and CUSE_INIT, these can store the relevant pointer in their structs derived from fuse_args.

Stable-dep-of: 64b0b5cacbd2 ("fuse: wait for FR_FINISHED on abort_on_kill to prevent use-after-free")

---
 fs/fuse/args.h  |  2 +-
 fs/fuse/cuse.c  |  7 ++++---
 fs/fuse/dev.c   |  7 +++----
 fs/fuse/file.c  | 22 +++++++++-------------
 fs/fuse/inode.c | 10 ++++++----
 5 files changed, 23 insertions(+), 25 deletions(-)
fs/fuse/args.h +1 -1
index 12c94ece569fb..ecfe51a192af0 100644
@@ -44,7 +44,7 @@ struct fuse_args {
 	bool abort_on_kill:1;
 	struct fuse_in_arg in_args[4];
 	struct fuse_arg out_args[2];
-	void (*end)(struct fuse_mount *fm, struct fuse_args *args, int error);
+	void (*end)(struct fuse_args *args, int error);
 	/* Used for kvec iter backed by vmalloc address */
 	void *vmap_base;
 };
fs/fuse/cuse.c +4 -3
index d90f138321fe9..7d9925dd2879a 100644
@@ -306,6 +306,7 @@ struct cuse_init_args {
 	struct cuse_init_out out;
 	struct folio *folio;
 	struct fuse_folio_desc desc;
+	struct fuse_conn *fc;
 };
 
 /**
@@ -319,11 +320,10 @@ struct cuse_init_args {
  * required data structures for it.  Please read the comment at the
  * top of this file for high level overview.
  */
-static void cuse_process_init_reply(struct fuse_mount *fm,
-				    struct fuse_args *args, int error)
+static void cuse_process_init_reply(struct fuse_args *args, int error)
 {
-	struct fuse_conn *fc = fm->fc;
 	struct cuse_init_args *ia = container_of(args, typeof(*ia), ap.args);
+	struct fuse_conn *fc = ia->fc;
 	struct fuse_args_pages *ap = &ia->ap;
 	struct cuse_conn *cc = fc_to_cc(fc), *pos;
 	struct cuse_init_out *arg = &ia->out;
@@ -468,6 +468,7 @@ static int cuse_send_init(struct cuse_conn *cc)
 	ap->descs = &ia->desc;
 	ia->folio = folio;
 	ia->desc.length = ap->args.out_args[1].size;
+	ia->fc = &cc->fc;
 	ap->args.end = cuse_process_init_reply;
 
 	rc = fuse_simple_background(fm, &ap->args, GFP_KERNEL);
fs/fuse/dev.c +3 -4
index 934f9ac5056ed..50012b0b1258a 100644
@@ -512,7 +512,7 @@ void fuse_request_end(struct fuse_req *req)
 	}
 
 	if (test_bit(FR_ASYNC, &req->flags))
-		req->args->end(fm, req->args, req->out.h.error);
+		req->args->end(req->args, req->out.h.error);
 put_request:
 	fuse_put_request(req);
 }
@@ -1859,8 +1859,7 @@ struct fuse_retrieve_args {
 	struct fuse_notify_retrieve_in inarg;
 };
 
-static void fuse_retrieve_end(struct fuse_mount *fm, struct fuse_args *args,
-			      int error)
+static void fuse_retrieve_end(struct fuse_args *args, int error)
 {
 	struct fuse_retrieve_args *ra =
 		container_of(args, typeof(*ra), ap.args);
@@ -1955,7 +1954,7 @@ static int fuse_retrieve(struct fuse_mount *fm, struct inode *inode,
 
 	err = fuse_simple_notify_reply(fm, args, outarg->notify_unique);
 	if (err)
-		fuse_retrieve_end(fm, args, err);
+		fuse_retrieve_end(args, err);
 
 	return err;
 }
fs/fuse/file.c +9 -13
index 585dd90361b65..f3b4d6ae0858a 100644
@@ -91,8 +91,7 @@ static struct fuse_file *fuse_file_get(struct fuse_file *ff)
 	return ff;
 }
 
-static void fuse_release_end(struct fuse_mount *fm, struct fuse_args *args,
-			     int error)
+static void fuse_release_end(struct fuse_args *args, int error)
 {
 	struct fuse_release_args *ra = container_of(args, typeof(*ra), args);
 
@@ -112,15 +111,15 @@ static void fuse_file_put(struct fuse_file *ff, bool sync)
 		if (!args) {
 			/* Do nothing when server does not implement 'opendir' */
 		} else if (args->opcode == FUSE_RELEASE && ff->fm->fc->no_open) {
-			fuse_release_end(ff->fm, args, 0);
+			fuse_release_end(args, 0);
 		} else if (sync) {
 			fuse_simple_request(ff->fm, args);
-			fuse_release_end(ff->fm, args, 0);
+			fuse_release_end(args, 0);
 		} else {
 			args->end = fuse_release_end;
 			if (fuse_simple_background(ff->fm, args,
 						   GFP_KERNEL | __GFP_NOFAIL))
-				fuse_release_end(ff->fm, args, -ENOTCONN);
+				fuse_release_end(args, -ENOTCONN);
 		}
 		kfree(ff);
 	}
@@ -709,8 +708,7 @@ static void fuse_io_free(struct fuse_io_args *ia)
 	kfree(ia);
 }
 
-static void fuse_aio_complete_req(struct fuse_mount *fm, struct fuse_args *args,
-				  int err)
+static void fuse_aio_complete_req(struct fuse_args *args, int err)
 {
 	struct fuse_io_args *ia = container_of(args, typeof(*ia), ap.args);
 	struct fuse_io_priv *io = ia->io;
@@ -758,7 +756,7 @@ static ssize_t fuse_async_req_send(struct fuse_mount *fm,
 	ia->ap.args.may_block = io->should_dirty;
 	err = fuse_simple_background(fm, &ia->ap.args, GFP_KERNEL);
 	if (err)
-		fuse_aio_complete_req(fm, &ia->ap.args, err);
+		fuse_aio_complete_req(&ia->ap.args, err);
 
 	return num_bytes;
 }
@@ -881,8 +879,7 @@ static int fuse_iomap_read_folio_range(const struct iomap_iter *iter,
 	return fuse_do_readfolio(file, folio, off, len);
 }
 
-static void fuse_readpages_end(struct fuse_mount *fm, struct fuse_args *args,
-			       int err)
+static void fuse_readpages_end(struct fuse_args *args, int err)
 {
 	int i;
 	struct fuse_io_args *ia = container_of(args, typeof(*ia), ap.args);
@@ -947,7 +944,7 @@ static void fuse_send_readpages(struct fuse_io_args *ia, struct file *file,
 		res = fuse_simple_request(fm, &ap->args);
 		err = res < 0 ? res : 0;
 	}
-	fuse_readpages_end(fm, &ap->args, err);
+	fuse_readpages_end(&ap->args, err);
 }
 
 static void fuse_readahead(struct readahead_control *rac)
@@ -1950,8 +1947,7 @@ __acquires(fi->lock)
 	}
 }
 
-static void fuse_writepage_end(struct fuse_mount *fm, struct fuse_args *args,
-			       int error)
+static void fuse_writepage_end(struct fuse_args *args, int error)
 {
 	struct fuse_writepage_args *wpa =
 		container_of(args, typeof(*wpa), ia.ap.args);
fs/fuse/inode.c +6 -4
index 399298816dea0..7464ee8622ac9 100644
@@ -1311,13 +1311,14 @@ struct fuse_init_args {
 	struct fuse_args args;
 	struct fuse_init_in in;
 	struct fuse_init_out out;
+	struct fuse_mount *fm;
 };
 
-static void process_init_reply(struct fuse_mount *fm, struct fuse_args *args,
-			       int error)
+static void process_init_reply(struct fuse_args *args, int error)
 {
-	struct fuse_conn *fc = fm->fc;
 	struct fuse_init_args *ia = container_of(args, typeof(*ia), args);
+	struct fuse_mount *fm = ia->fm;
+	struct fuse_conn *fc = fm->fc;
 	struct fuse_init_out *arg = &ia->out;
 	bool ok = true;
 
@@ -1486,6 +1487,7 @@ static struct fuse_init_args *fuse_new_init(struct fuse_mount *fm)
 
 	ia = kzalloc(sizeof(*ia), GFP_KERNEL | __GFP_NOFAIL);
 
+	ia->fm = fm;
 	ia->in.major = FUSE_KERNEL_VERSION;
 	ia->in.minor = FUSE_KERNEL_MINOR_VERSION;
 	ia->in.max_readahead = fm->sb->s_bdi->ra_pages * PAGE_SIZE;
@@ -1559,7 +1561,7 @@ int fuse_send_init(struct fuse_mount *fm)
 		if (!err)
 			return 0;
 	}
-	process_init_reply(fm, &ia->args, err);
+	process_init_reply(&ia->args, err);
 	if (fm->fc->conn_error)
 		return -ENOTCONN;
 	return 0;
Signature · 1 line
2.53.0
maintainer
#4 lore raw

PATCH 4/4 fuse: wait for FR_FINISHED on abort_on_kill to prevent use-after-free

In reply to Sasha Levin

From: Rochan Avlur <[email protected]>

[ Upstream commit 64b0b5cacbd2fea88001464cb712c9dfc795b26e ]

The abort_on_kill path in request_wait_answer() calls fuse_abort_conn() and returns without waiting for FR_FINISHED. If fuse_dev_do_write() is concurrently processing the same request (FR_LOCKED set), the caller frees req->args while it is still being accessed, causing a use-after-free.

Fix this by jumping to the existing wait_event(FR_FINISHED) instead of returning early. The wait will not hang because fuse_abort_conn() ensures all requests are ended.

Reported-by [email protected]
Fixes 204aa22a686b ("fuse: abort on fatal signal during sync init")
Assisted-by Claude:claude-opus-4-6
---
 fs/fuse/dev.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
fs/fuse/dev.c +2 -1
index 50012b0b1258a..9f1f81e66cace 100644
@@ -580,7 +580,7 @@ static void request_wait_answer(struct fuse_req *req)
 
 		if (req->args->abort_on_kill) {
 			fuse_chan_abort(fch, false);
-			return;
+			goto wait_for_finish;
 		}
 
 		if (test_bit(FR_URING, &req->flags))
@@ -591,6 +591,7 @@ static void request_wait_answer(struct fuse_req *req)
 			return;
 	}
 
+wait_for_finish:
 	/*
 	 * Either request is already in userspace, or it was forced.
 	 * Wait it out.
Signature · 1 line
2.53.0
#5 lore raw

Re: [PATCH 6.18.y 1/4] fuse: abort related layering cleanup

In reply to Sasha Levin
Quoted 273 lines · Sasha Levin

From: Miklos Szeredi <[email protected]>

[ Upstream commit 4eeb5e6cb0fd89b343fcf755ae3aad76fdb5b2c2 ]

- rename fuse_abort_conn() to fuse_chan_abort(), pass fuse_chan pointer instead of fuse_conn

- pass an abort_with_err argument that tells fuse_dev_(read|write) to return with ECONNABORTED instead of ENODEV

- move fc->aborted to fch->abort_with_err

- rename fuse_wait_aborted() to fuse_chan_wait_aborted()

[ 6.18 has no "struct fuse_chan" -- the connection object still plays
that role, and the surrounding series that splits it out is not in stable. Adapted as follows:

- the renames (fuse_abort_conn() -> fuse_chan_abort(), fuse_wait_aborted() -> fuse_chan_wait_aborted(), the "abort_conn" label in fuse_check_timeout() -> "chan_abort") and the new abort_with_err argument are taken as-is, but both helpers keep taking "struct fuse_conn *".

- the "move fc->aborted to fch->abort_with_err" bullet is dropped:
there is no fuse_chan to move the flag into, so fc->aborted stays in struct fuse_conn. fuse_chan_abort() only ever *sets* it, as control.c did before, rather than assigning the new argument unconditionally the way upstream does: on this tree the flag is write-once and is what makes fuse_dev_do_read() and fuse_uring_cmd() report ECONNABORTED instead of ENODEV, so letting a later abort with abort_with_err=false clear it (umount -f, fuse_conn_destroy(), fuse_dev_release(), the timeout worker) would change what userspace sees after a sysfs abort on an abort_err mount. Consequently the fuse_dev_i.h and dev_uring.c hunks, which only rename the field access, are dropped as well.

- fs/fuse/dev.h and fs/fuse/req_timeout.c do not exist here: the declarations stay in fuse_i.h and fuse_check_timeout() lives in dev.c, so those two hunks were replayed there.

- in request_wait_answer() the connection pointer is named "fch", matching the name upstream gives it in 28d733cc3ecad ("fuse: change req->fm to req->chan"), so that the follow-up fix applies here unchanged. ]

[ sashal: Reduced backport -- upstream 4eeb5e6cb0fd8 touches 9 file(s), this
backport carries 5. Not backported here:
fs/fuse/dev.h
fs/fuse/dev_uring.c
fs/fuse/fuse_dev_i.h
fs/fuse/req_timeout.c
This note is generated from the file lists only; see the resolution record for the reasoning. ]

Stable-dep-of: 64b0b5cacbd2 ("fuse: wait for FR_FINISHED on abort_on_kill to prevent use-after-free")

---
 fs/fuse/control.c |  4 +---
 fs/fuse/cuse.c    |  4 ++--
 fs/fuse/dev.c     | 35 +++++++++++++++++++----------------
 fs/fuse/fuse_i.h  |  4 ++--
 fs/fuse/inode.c   |  6 +++---
 5 files changed, 27 insertions(+), 26 deletions(-)
fs/fuse/control.c +1 -3
index 2e300f6d646c3..5536d6ee65514 100644
@@ -37,9 +37,7 @@ static ssize_t fuse_conn_abort_write(struct file *file, const char __user *buf,
 {
 	struct fuse_conn *fc = fuse_ctl_file_conn_get(file);
 	if (fc) {
-		if (fc->abort_err)
-			fc->aborted = true;
-		fuse_abort_conn(fc);
+		fuse_chan_abort(fc, fc->abort_err);
 		fuse_conn_put(fc);
 	}
 	return count;
fs/fuse/cuse.c +2 -2
index 2bb98bf81b079..d90f138321fe9 100644
@@ -425,7 +425,7 @@ static void cuse_process_init_reply(struct fuse_mount *fm,
 err_region:
 	unregister_chrdev_region(devt, 1);
 err:
-	fuse_abort_conn(fc);
+	fuse_chan_abort(fc, false);
 	goto out;
 }
 
@@ -593,7 +593,7 @@ static ssize_t cuse_class_abort_store(struct device *dev,
 {
 	struct cuse_conn *cc = dev_get_drvdata(dev);
 
-	fuse_abort_conn(&cc->fc);
+	fuse_chan_abort(&cc->fc, false);
 	return count;
 }
 static DEVICE_ATTR(abort, 0200, NULL, cuse_class_abort_store);
fs/fuse/dev.c +19 -16
index 77c3982bca3d7..934f9ac5056ed 100644
@@ -87,13 +87,13 @@ void fuse_check_timeout(struct work_struct *work)
 	expired = fuse_request_expired(fc, &fiq->pending);
 	spin_unlock(&fiq->lock);
 	if (expired)
-		goto abort_conn;
+		goto chan_abort;
 
 	spin_lock(&fc->bg_lock);
 	expired = fuse_request_expired(fc, &fc->bg_queue);
 	spin_unlock(&fc->bg_lock);
 	if (expired)
-		goto abort_conn;
+		goto chan_abort;
 
 	spin_lock(&fc->lock);
 	if (!fc->connected) {
@@ -107,7 +107,7 @@ void fuse_check_timeout(struct work_struct *work)
 		    fuse_fpq_processing_expired(fc, fpq->processing)) {
 			spin_unlock(&fpq->lock);
 			spin_unlock(&fc->lock);
-			goto abort_conn;
+			goto chan_abort;
 		}
 
 		spin_unlock(&fpq->lock);
@@ -115,15 +115,15 @@ void fuse_check_timeout(struct work_struct *work)
 	spin_unlock(&fc->lock);
 
 	if (fuse_uring_request_expired(fc))
-	    goto abort_conn;
+	    goto chan_abort;
 
 out:
 	queue_delayed_work(system_percpu_wq, &fc->timeout.work,
 			   fuse_timeout_timer_freq);
 	return;
 
-abort_conn:
-	fuse_abort_conn(fc);
+chan_abort:
+	fuse_chan_abort(fc, false);
 }
 
 static void fuse_request_init(struct fuse_mount *fm, struct fuse_req *req)
@@ -180,7 +180,7 @@ static void fuse_drop_waiting(struct fuse_conn *fc)
 {
 	/*
 	 * lockess check of fc->connected is okay, because atomic_dec_and_test()
-	 * provides a memory barrier matched with the one in fuse_wait_aborted()
+	 * provides a memory barrier matched with the one in fuse_chan_wait_aborted()
 	 * to ensure no wake-up is missed.
 	 */
 	if (atomic_dec_and_test(&fc->num_waiting) &&
@@ -551,11 +551,11 @@ bool fuse_remove_pending_req(struct fuse_req *req, spinlock_t *lock)
 
 static void request_wait_answer(struct fuse_req *req)
 {
-	struct fuse_conn *fc = req->fm->fc;
-	struct fuse_iqueue *fiq = &fc->iq;
+	struct fuse_conn *fch = req->fm->fc;
+	struct fuse_iqueue *fiq = &fch->iq;
 	int err;
 
-	if (!fc->no_interrupt) {
+	if (!fch->no_interrupt) {
 		/* Any signal may interrupt this */
 		err = wait_event_interruptible(req->waitq,
 					test_bit(FR_FINISHED, &req->flags));
@@ -579,7 +579,7 @@ static void request_wait_answer(struct fuse_req *req)
 			return;
 
 		if (req->args->abort_on_kill) {
-			fuse_abort_conn(fc);
+			fuse_chan_abort(fch, false);
 			return;
 		}
 
@@ -1491,7 +1491,7 @@ static ssize_t fuse_dev_do_read(struct fuse_dev *fud, struct file *file,
 	spin_lock(&fpq->lock);
 	/*
 	 *  Must not put request on fpq->io queue after having been shut down by
-	 *  fuse_abort_conn()
+	 *  fuse_chan_abort()
 	 */
 	if (!fpq->connected) {
 		req->out.h.error = err = -ECONNABORTED;
@@ -2475,10 +2475,13 @@ static void end_polls(struct fuse_conn *fc)
  * is OK, the request will in that case be removed from the list before we touch
  * it.
  */
-void fuse_abort_conn(struct fuse_conn *fc)
+void fuse_chan_abort(struct fuse_conn *fc, bool abort_with_err)
 {
 	struct fuse_iqueue *fiq = &fc->iq;
 
+	if (abort_with_err)
+		fc->aborted = true;
+
 	spin_lock(&fc->lock);
 	if (fc->connected) {
 		struct fuse_dev *fud;
@@ -2547,9 +2550,9 @@ void fuse_abort_conn(struct fuse_conn *fc)
 		spin_unlock(&fc->lock);
 	}
 }
-EXPORT_SYMBOL_GPL(fuse_abort_conn);
+EXPORT_SYMBOL_GPL(fuse_chan_abort);
 
-void fuse_wait_aborted(struct fuse_conn *fc)
+void fuse_chan_wait_aborted(struct fuse_conn *fc)
 {
 	/* matches implicit memory barrier in fuse_drop_waiting() */
 	smp_mb();
@@ -2579,7 +2582,7 @@ int fuse_dev_release(struct inode *inode, struct file *file)
 		/* Are we the last open device? */
 		if (atomic_dec_and_test(&fc->dev_count)) {
 			WARN_ON(fc->iq.fasync != NULL);
-			fuse_abort_conn(fc);
+			fuse_chan_abort(fc, false);
 		}
 		fuse_dev_free(fud);
 	}
fs/fuse/fuse_i.h +2 -2
index c288f28f6c6ea..8b745045f1ab3 100644
@@ -1272,8 +1272,8 @@ void fuse_request_assign_unique(struct fuse_iqueue *fiq, struct fuse_req *req);
 void fuse_request_end(struct fuse_req *req);
 
 /* Abort all requests */
-void fuse_abort_conn(struct fuse_conn *fc);
-void fuse_wait_aborted(struct fuse_conn *fc);
+void fuse_chan_abort(struct fuse_conn *fc, bool abort_with_err);
+void fuse_chan_wait_aborted(struct fuse_conn *fc);
 
 /* Check if any requests timed out */
 void fuse_check_timeout(struct work_struct *work);
fs/fuse/inode.c +3 -3
index e9ed693fc7b37..399298816dea0 100644
@@ -621,7 +621,7 @@ static void fuse_umount_begin(struct super_block *sb)
 	if (fc->no_force_umount)
 		return;
 
-	fuse_abort_conn(fc);
+	fuse_chan_abort(fc, false);
 
 	// Only retire block-device-based superblocks.
 	if (sb->s_bdev != NULL)
@@ -2099,8 +2099,8 @@ void fuse_conn_destroy(struct fuse_mount *fm)
 	if (fc->destroy)
 		fuse_send_destroy(fm);
 
-	fuse_abort_conn(fc);
-	fuse_wait_aborted(fc);
+	fuse_chan_abort(fc, false);
+	fuse_chan_wait_aborted(fc);
 
 	if (!list_empty(&fc->entry)) {
 		mutex_lock(&fuse_mutex);
Signature · 1 line
2.53.0

Series did not apply :(