From 4bd18c63694e3d36e07a1b03b131a7bd362b3487 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 14:50:16 -0500 Subject: [PATCH 01/41] fuse: rename to fuse_dev_end_requests and make non-static jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 92270d076115ae81b83f0605703602d2d5a866e2 This function is needed by fuse_uring.c to clean ring queues, so make it non static. Especially in non-static mode the function name 'end_requests' should be prefixed with fuse_ Signed-off-by: Bernd Schubert Reviewed-by: Josef Bacik Reviewed-by: Joanne Koong Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit 92270d076115ae81b83f0605703602d2d5a866e2) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 11 +++++------ fs/fuse/fuse_dev_i.h | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 fs/fuse/fuse_dev_i.h diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index a9de694d48fcd..a3dcf01e656da 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -7,6 +7,7 @@ */ #include "fuse_i.h" +#include "fuse_dev_i.h" #include #include @@ -35,8 +36,6 @@ MODULE_ALIAS("devname:fuse"); static struct kmem_cache *fuse_req_cachep; -static void end_requests(struct list_head *head); - static struct fuse_dev *fuse_get_dev(struct file *file) { /* @@ -1874,7 +1873,7 @@ static void fuse_resend(struct fuse_conn *fc) spin_unlock(&fiq->lock); list_for_each_entry(req, &to_queue, list) clear_bit(FR_PENDING, &req->flags); - end_requests(&to_queue); + fuse_dev_end_requests(&to_queue); return; } /* iq and pq requests are both oldest to newest */ @@ -2209,7 +2208,7 @@ static __poll_t fuse_dev_poll(struct file *file, poll_table *wait) } /* Abort all requests on the given list (pending or processing) */ -static void end_requests(struct list_head *head) +void fuse_dev_end_requests(struct list_head *head) { while (!list_empty(head)) { struct fuse_req *req; @@ -2312,7 +2311,7 @@ void fuse_abort_conn(struct fuse_conn *fc) wake_up_all(&fc->blocked_waitq); spin_unlock(&fc->lock); - end_requests(&to_end); + fuse_dev_end_requests(&to_end); } else { spin_unlock(&fc->lock); } @@ -2342,7 +2341,7 @@ int fuse_dev_release(struct inode *inode, struct file *file) list_splice_init(&fpq->processing[i], &to_end); spin_unlock(&fpq->lock); - end_requests(&to_end); + fuse_dev_end_requests(&to_end); /* Are we the last open device? */ if (atomic_dec_and_test(&fc->dev_count)) { diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h new file mode 100644 index 0000000000000..4fcff2223fa60 --- /dev/null +++ b/fs/fuse/fuse_dev_i.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-2.0 + * + * FUSE: Filesystem in Userspace + * Copyright (C) 2001-2008 Miklos Szeredi + */ +#ifndef _FS_FUSE_DEV_I_H +#define _FS_FUSE_DEV_I_H + +#include + +void fuse_dev_end_requests(struct list_head *head); + +#endif + From 9939c86f571f76a824d4cf25bb2947a3456946b9 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 14:50:50 -0500 Subject: [PATCH 02/41] fuse: Move fuse_get_dev to header file jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 867d93dcdede5ea453543085d1ed0bf43cde60de Another preparation patch, as this function will be needed by fuse/dev.c and fuse/dev_uring.c. Signed-off-by: Bernd Schubert Reviewed-by: Josef Bacik Reviewed-by: Joanne Koong Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit 867d93dcdede5ea453543085d1ed0bf43cde60de) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 9 --------- fs/fuse/fuse_dev_i.h | 9 +++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index a3dcf01e656da..2c37c8acbfdc0 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -36,15 +36,6 @@ MODULE_ALIAS("devname:fuse"); static struct kmem_cache *fuse_req_cachep; -static struct fuse_dev *fuse_get_dev(struct file *file) -{ - /* - * Lockless access is OK, because file->private data is set - * once during mount and is valid until the file is released. - */ - return READ_ONCE(file->private_data); -} - static void fuse_request_init(struct fuse_mount *fm, struct fuse_req *req) { INIT_LIST_HEAD(&req->list); diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h index 4fcff2223fa60..e7ea1b21c1820 100644 --- a/fs/fuse/fuse_dev_i.h +++ b/fs/fuse/fuse_dev_i.h @@ -8,6 +8,15 @@ #include +static inline struct fuse_dev *fuse_get_dev(struct file *file) +{ + /* + * Lockless access is OK, because file->private data is set + * once during mount and is valid until the file is released. + */ + return READ_ONCE(file->private_data); +} + void fuse_dev_end_requests(struct list_head *head); #endif From 8f00c471c6dcb9df757673059d739200c0ae8233 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 14:51:44 -0500 Subject: [PATCH 03/41] fuse: Move request bits jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 88be7aa98d91f5761f8764273ebd961915e4632d These are needed by fuse-over-io-uring. Signed-off-by: Bernd Schubert Reviewed-by: Josef Bacik Reviewed-by: Joanne Koong Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit 88be7aa98d91f5761f8764273ebd961915e4632d) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 4 ---- fs/fuse/fuse_dev_i.h | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index 2c37c8acbfdc0..c405a5e0b7289 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -30,10 +30,6 @@ MODULE_ALIAS_MISCDEV(FUSE_MINOR); MODULE_ALIAS("devname:fuse"); -/* Ordinary requests have even IDs, while interrupts IDs are odd */ -#define FUSE_INT_REQ_BIT (1ULL << 0) -#define FUSE_REQ_ID_STEP (1ULL << 1) - static struct kmem_cache *fuse_req_cachep; static void fuse_request_init(struct fuse_mount *fm, struct fuse_req *req) diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h index e7ea1b21c1820..08a7e88e00277 100644 --- a/fs/fuse/fuse_dev_i.h +++ b/fs/fuse/fuse_dev_i.h @@ -8,6 +8,10 @@ #include +/* Ordinary requests have even IDs, while interrupts IDs are odd */ +#define FUSE_INT_REQ_BIT (1ULL << 0) +#define FUSE_REQ_ID_STEP (1ULL << 1) + static inline struct fuse_dev *fuse_get_dev(struct file *file) { /* From ee09348d636db833d638cdc986b352a630b62da5 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 15:13:32 -0500 Subject: [PATCH 04/41] fuse: make args->in_args[0] to be always the header jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 7ccd86ba3a485a8bc33478776eb7053d9adb7816 upstream-diff | Context conflict on missing fs/fuse/fuse_i.h commit 41748675c0bf -virtiofs: use pages instead of pointer for kernel direct IO This change sets up FUSE operations to always have headers in args.in_args[0], even for opcodes without an actual header. This step prepares for a clean separation of payload from headers, initially it is used by fuse-over-io-uring. For opcodes without a header, we use a zero-sized struct as a placeholder. This approach: - Keeps things consistent across all FUSE operations - Will help with payload alignment later - Avoids future issues when header sizes change Op codes that already have an op code specific header do not need modification. Op codes that have neither payload nor op code headers are not modified either (FUSE_READLINK and FUSE_DESTROY). FUSE_BATCH_FORGET already has the header in the right place, but is not using fuse_copy_args - as -over-uring is currently not handling forgets it does not matter for now, but header separation will later need special attention for that op code. Correct the struct fuse_args->in_args array max size. Signed-off-by: Bernd Schubert Reviewed-by: Joanne Koong Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit 7ccd86ba3a485a8bc33478776eb7053d9adb7816) Signed-off-by: Jonathan Maple --- fs/fuse/dax.c | 11 ++++++----- fs/fuse/dev.c | 9 +++++---- fs/fuse/dir.c | 32 ++++++++++++++++++-------------- fs/fuse/fuse_i.h | 15 ++++++++++++++- fs/fuse/xattr.c | 7 ++++--- 5 files changed, 47 insertions(+), 27 deletions(-) diff --git a/fs/fuse/dax.c b/fs/fuse/dax.c index 12ef91d170bb3..44bd30d448e4e 100644 --- a/fs/fuse/dax.c +++ b/fs/fuse/dax.c @@ -240,11 +240,12 @@ static int fuse_send_removemapping(struct inode *inode, args.opcode = FUSE_REMOVEMAPPING; args.nodeid = fi->nodeid; - args.in_numargs = 2; - args.in_args[0].size = sizeof(*inargp); - args.in_args[0].value = inargp; - args.in_args[1].size = inargp->count * sizeof(*remove_one); - args.in_args[1].value = remove_one; + args.in_numargs = 3; + fuse_set_zero_arg0(&args); + args.in_args[1].size = sizeof(*inargp); + args.in_args[1].value = inargp; + args.in_args[2].size = inargp->count * sizeof(*remove_one); + args.in_args[2].value = remove_one; return fuse_simple_request(fm, &args); } diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index c405a5e0b7289..7f2316f48c048 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -1736,7 +1736,7 @@ static int fuse_retrieve(struct fuse_mount *fm, struct inode *inode, args = &ap->args; args->nodeid = outarg->nodeid; args->opcode = FUSE_NOTIFY_REPLY; - args->in_numargs = 2; + args->in_numargs = 3; args->in_pages = true; args->end = fuse_retrieve_end; @@ -1763,9 +1763,10 @@ static int fuse_retrieve(struct fuse_mount *fm, struct inode *inode, } ra->inarg.offset = outarg->offset; ra->inarg.size = total_len; - args->in_args[0].size = sizeof(ra->inarg); - args->in_args[0].value = &ra->inarg; - args->in_args[1].size = total_len; + fuse_set_zero_arg0(args); + args->in_args[1].size = sizeof(ra->inarg); + args->in_args[1].value = &ra->inarg; + args->in_args[2].size = total_len; err = fuse_simple_notify_reply(fm, args, outarg->notify_unique); if (err) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index f2fadaf084843..be3a6720fcba1 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -175,9 +175,10 @@ static void fuse_lookup_init(struct fuse_conn *fc, struct fuse_args *args, memset(outarg, 0, sizeof(struct fuse_entry_out)); args->opcode = FUSE_LOOKUP; args->nodeid = nodeid; - args->in_numargs = 1; - args->in_args[0].size = name->len + 1; - args->in_args[0].value = name->name; + args->in_numargs = 2; + fuse_set_zero_arg0(args); + args->in_args[1].size = name->len + 1; + args->in_args[1].value = name->name; args->out_numargs = 1; args->out_args[0].size = sizeof(struct fuse_entry_out); args->out_args[0].value = outarg; @@ -943,11 +944,12 @@ static int fuse_symlink(struct mnt_idmap *idmap, struct inode *dir, FUSE_ARGS(args); args.opcode = FUSE_SYMLINK; - args.in_numargs = 2; - args.in_args[0].size = entry->d_name.len + 1; - args.in_args[0].value = entry->d_name.name; - args.in_args[1].size = len; - args.in_args[1].value = link; + args.in_numargs = 3; + fuse_set_zero_arg0(&args); + args.in_args[1].size = entry->d_name.len + 1; + args.in_args[1].value = entry->d_name.name; + args.in_args[2].size = len; + args.in_args[2].value = link; return create_new_entry(idmap, fm, &args, dir, entry, S_IFLNK); } @@ -1007,9 +1009,10 @@ static int fuse_unlink(struct inode *dir, struct dentry *entry) args.opcode = FUSE_UNLINK; args.nodeid = get_node_id(dir); - args.in_numargs = 1; - args.in_args[0].size = entry->d_name.len + 1; - args.in_args[0].value = entry->d_name.name; + args.in_numargs = 2; + fuse_set_zero_arg0(&args); + args.in_args[1].size = entry->d_name.len + 1; + args.in_args[1].value = entry->d_name.name; err = fuse_simple_request(fm, &args); if (!err) { fuse_dir_changed(dir); @@ -1030,9 +1033,10 @@ static int fuse_rmdir(struct inode *dir, struct dentry *entry) args.opcode = FUSE_RMDIR; args.nodeid = get_node_id(dir); - args.in_numargs = 1; - args.in_args[0].size = entry->d_name.len + 1; - args.in_args[0].value = entry->d_name.name; + args.in_numargs = 2; + fuse_set_zero_arg0(&args); + args.in_args[1].size = entry->d_name.len + 1; + args.in_args[1].value = entry->d_name.name; err = fuse_simple_request(fm, &args); if (!err) { fuse_dir_changed(dir); diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 6873c5f401a27..696479fa17c9a 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -309,7 +309,7 @@ struct fuse_args { bool may_block:1; bool is_ext:1; bool is_pinned:1; - struct fuse_in_arg in_args[3]; + 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); }; @@ -941,6 +941,19 @@ struct fuse_mount { struct rcu_head rcu; }; +/* + * Empty header for FUSE opcodes without specific header needs. + * Used as a placeholder in args->in_args[0] for consistency + * across all FUSE operations, simplifying request handling. + */ +struct fuse_zero_header {}; + +static inline void fuse_set_zero_arg0(struct fuse_args *args) +{ + args->in_args[0].size = sizeof(struct fuse_zero_header); + args->in_args[0].value = NULL; +} + static inline struct fuse_mount *get_fuse_mount_super(struct super_block *sb) { return sb->s_fs_info; diff --git a/fs/fuse/xattr.c b/fs/fuse/xattr.c index 9f568d345c512..93dfb06b6cea0 100644 --- a/fs/fuse/xattr.c +++ b/fs/fuse/xattr.c @@ -164,9 +164,10 @@ int fuse_removexattr(struct inode *inode, const char *name) args.opcode = FUSE_REMOVEXATTR; args.nodeid = get_node_id(inode); - args.in_numargs = 1; - args.in_args[0].size = strlen(name) + 1; - args.in_args[0].value = name; + args.in_numargs = 2; + fuse_set_zero_arg0(&args); + args.in_args[1].size = strlen(name) + 1; + args.in_args[1].value = name; err = fuse_simple_request(fm, &args); if (err == -ENOSYS) { fm->fc->no_removexattr = 1; From 5f97f6a595885f718702b39698450d5b9a0e2219 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 15:27:53 -0500 Subject: [PATCH 05/41] fuse: {io-uring} Handle SQEs - register commands jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 24fe962c86f55347385933a1b06ca71b60854690 upstream-diff | Context diff due to missing commit on fs/fuse/Makefile 2b3933b1e0a0 - fuse: enable dynamic configuration of fuse max pages limit (FUSE_MAX_MAX_PAGES) Context difference due to future feature enabled earlier in git log order as well. This adds basic support for ring SQEs (with opcode=IORING_OP_URING_CMD). For now only FUSE_IO_URING_CMD_REGISTER is handled to register queue entries. Signed-off-by: Bernd Schubert Reviewed-by: Pavel Begunkov # io_uring Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit 24fe962c86f55347385933a1b06ca71b60854690) Signed-off-by: Jonathan Maple --- fs/fuse/Kconfig | 12 ++ fs/fuse/Makefile | 1 + fs/fuse/dev_uring.c | 326 ++++++++++++++++++++++++++++++++++++++ fs/fuse/dev_uring_i.h | 113 +++++++++++++ fs/fuse/fuse_i.h | 5 + fs/fuse/inode.c | 10 ++ include/uapi/linux/fuse.h | 75 +++++++++ 7 files changed, 542 insertions(+) create mode 100644 fs/fuse/dev_uring.c create mode 100644 fs/fuse/dev_uring_i.h diff --git a/fs/fuse/Kconfig b/fs/fuse/Kconfig index 8674dbfbe59db..ca215a3cba3e3 100644 --- a/fs/fuse/Kconfig +++ b/fs/fuse/Kconfig @@ -63,3 +63,15 @@ config FUSE_PASSTHROUGH to be performed directly on a backing file. If you want to allow passthrough operations, answer Y. + +config FUSE_IO_URING + bool "FUSE communication over io-uring" + default y + depends on FUSE_FS + depends on IO_URING + help + This allows sending FUSE requests over the io-uring interface and + also adds request core affinity. + + If you want to allow fuse server/client communication through io-uring, + answer Y diff --git a/fs/fuse/Makefile b/fs/fuse/Makefile index ce0ff7a9007b9..fcf16b1c391a9 100644 --- a/fs/fuse/Makefile +++ b/fs/fuse/Makefile @@ -14,5 +14,6 @@ fuse-y := dev.o dir.o file.o inode.o control.o xattr.o acl.o readdir.o ioctl.o fuse-y += iomode.o fuse-$(CONFIG_FUSE_DAX) += dax.o fuse-$(CONFIG_FUSE_PASSTHROUGH) += passthrough.o +fuse-$(CONFIG_FUSE_IO_URING) += dev_uring.o virtiofs-y := virtio_fs.o diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c new file mode 100644 index 0000000000000..42092a234570e --- /dev/null +++ b/fs/fuse/dev_uring.c @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * FUSE: Filesystem in Userspace + * Copyright (c) 2023-2024 DataDirect Networks. + */ + +#include "fuse_i.h" +#include "dev_uring_i.h" +#include "fuse_dev_i.h" + +#include +#include + +static bool __read_mostly enable_uring; +module_param(enable_uring, bool, 0644); +MODULE_PARM_DESC(enable_uring, + "Enable userspace communication through io-uring"); + +#define FUSE_URING_IOV_SEGS 2 /* header and payload */ + + +bool fuse_uring_enabled(void) +{ + return enable_uring; +} + +void fuse_uring_destruct(struct fuse_conn *fc) +{ + struct fuse_ring *ring = fc->ring; + int qid; + + if (!ring) + return; + + for (qid = 0; qid < ring->nr_queues; qid++) { + struct fuse_ring_queue *queue = ring->queues[qid]; + + if (!queue) + continue; + + WARN_ON(!list_empty(&queue->ent_avail_queue)); + WARN_ON(!list_empty(&queue->ent_commit_queue)); + + kfree(queue); + ring->queues[qid] = NULL; + } + + kfree(ring->queues); + kfree(ring); + fc->ring = NULL; +} + +/* + * Basic ring setup for this connection based on the provided configuration + */ +static struct fuse_ring *fuse_uring_create(struct fuse_conn *fc) +{ + struct fuse_ring *ring; + size_t nr_queues = num_possible_cpus(); + struct fuse_ring *res = NULL; + size_t max_payload_size; + + ring = kzalloc(sizeof(*fc->ring), GFP_KERNEL_ACCOUNT); + if (!ring) + return NULL; + + ring->queues = kcalloc(nr_queues, sizeof(struct fuse_ring_queue *), + GFP_KERNEL_ACCOUNT); + if (!ring->queues) + goto out_err; + + max_payload_size = max(FUSE_MIN_READ_BUFFER, fc->max_write); + max_payload_size = max(max_payload_size, fc->max_pages * PAGE_SIZE); + + spin_lock(&fc->lock); + if (fc->ring) { + /* race, another thread created the ring in the meantime */ + spin_unlock(&fc->lock); + res = fc->ring; + goto out_err; + } + + fc->ring = ring; + ring->nr_queues = nr_queues; + ring->fc = fc; + ring->max_payload_sz = max_payload_size; + + spin_unlock(&fc->lock); + return ring; + +out_err: + kfree(ring->queues); + kfree(ring); + return res; +} + +static struct fuse_ring_queue *fuse_uring_create_queue(struct fuse_ring *ring, + int qid) +{ + struct fuse_conn *fc = ring->fc; + struct fuse_ring_queue *queue; + + queue = kzalloc(sizeof(*queue), GFP_KERNEL_ACCOUNT); + if (!queue) + return NULL; + queue->qid = qid; + queue->ring = ring; + spin_lock_init(&queue->lock); + + INIT_LIST_HEAD(&queue->ent_avail_queue); + INIT_LIST_HEAD(&queue->ent_commit_queue); + + spin_lock(&fc->lock); + if (ring->queues[qid]) { + spin_unlock(&fc->lock); + kfree(queue); + return ring->queues[qid]; + } + + /* + * write_once and lock as the caller mostly doesn't take the lock at all + */ + WRITE_ONCE(ring->queues[qid], queue); + spin_unlock(&fc->lock); + + return queue; +} + +/* + * Make a ring entry available for fuse_req assignment + */ +static void fuse_uring_ent_avail(struct fuse_ring_ent *ent, + struct fuse_ring_queue *queue) +{ + WARN_ON_ONCE(!ent->cmd); + list_move(&ent->list, &queue->ent_avail_queue); + ent->state = FRRS_AVAILABLE; +} + +/* + * fuse_uring_req_fetch command handling + */ +static void fuse_uring_do_register(struct fuse_ring_ent *ent, + struct io_uring_cmd *cmd, + unsigned int issue_flags) +{ + struct fuse_ring_queue *queue = ent->queue; + + spin_lock(&queue->lock); + ent->cmd = cmd; + fuse_uring_ent_avail(ent, queue); + spin_unlock(&queue->lock); +} + +/* + * sqe->addr is a ptr to an iovec array, iov[0] has the headers, iov[1] + * the payload + */ +static int fuse_uring_get_iovec_from_sqe(const struct io_uring_sqe *sqe, + struct iovec iov[FUSE_URING_IOV_SEGS]) +{ + struct iovec __user *uiov = u64_to_user_ptr(READ_ONCE(sqe->addr)); + struct iov_iter iter; + ssize_t ret; + + if (sqe->len != FUSE_URING_IOV_SEGS) + return -EINVAL; + + /* + * Direction for buffer access will actually be READ and WRITE, + * using write for the import should include READ access as well. + */ + ret = import_iovec(WRITE, uiov, FUSE_URING_IOV_SEGS, + FUSE_URING_IOV_SEGS, &iov, &iter); + if (ret < 0) + return ret; + + return 0; +} + +static struct fuse_ring_ent * +fuse_uring_create_ring_ent(struct io_uring_cmd *cmd, + struct fuse_ring_queue *queue) +{ + struct fuse_ring *ring = queue->ring; + struct fuse_ring_ent *ent; + size_t payload_size; + struct iovec iov[FUSE_URING_IOV_SEGS]; + int err; + + err = fuse_uring_get_iovec_from_sqe(cmd->sqe, iov); + if (err) { + pr_info_ratelimited("Failed to get iovec from sqe, err=%d\n", + err); + return ERR_PTR(err); + } + + err = -EINVAL; + if (iov[0].iov_len < sizeof(struct fuse_uring_req_header)) { + pr_info_ratelimited("Invalid header len %zu\n", iov[0].iov_len); + return ERR_PTR(err); + } + + payload_size = iov[1].iov_len; + if (payload_size < ring->max_payload_sz) { + pr_info_ratelimited("Invalid req payload len %zu\n", + payload_size); + return ERR_PTR(err); + } + + err = -ENOMEM; + ent = kzalloc(sizeof(*ent), GFP_KERNEL_ACCOUNT); + if (!ent) + return ERR_PTR(err); + + INIT_LIST_HEAD(&ent->list); + + ent->queue = queue; + ent->headers = iov[0].iov_base; + ent->payload = iov[1].iov_base; + + return ent; +} + +/* + * Register header and payload buffer with the kernel and puts the + * entry as "ready to get fuse requests" on the queue + */ +static int fuse_uring_register(struct io_uring_cmd *cmd, + unsigned int issue_flags, struct fuse_conn *fc) +{ + const struct fuse_uring_cmd_req *cmd_req = io_uring_sqe_cmd(cmd->sqe); + struct fuse_ring *ring = fc->ring; + struct fuse_ring_queue *queue; + struct fuse_ring_ent *ent; + int err; + unsigned int qid = READ_ONCE(cmd_req->qid); + + err = -ENOMEM; + if (!ring) { + ring = fuse_uring_create(fc); + if (!ring) + return err; + } + + if (qid >= ring->nr_queues) { + pr_info_ratelimited("fuse: Invalid ring qid %u\n", qid); + return -EINVAL; + } + + queue = ring->queues[qid]; + if (!queue) { + queue = fuse_uring_create_queue(ring, qid); + if (!queue) + return err; + } + + /* + * The created queue above does not need to be destructed in + * case of entry errors below, will be done at ring destruction time. + */ + + ent = fuse_uring_create_ring_ent(cmd, queue); + if (IS_ERR(ent)) + return PTR_ERR(ent); + + fuse_uring_do_register(ent, cmd, issue_flags); + + return 0; +} + +/* + * Entry function from io_uring to handle the given passthrough command + * (op code IORING_OP_URING_CMD) + */ +int __maybe_unused fuse_uring_cmd(struct io_uring_cmd *cmd, + unsigned int issue_flags) +{ + struct fuse_dev *fud; + struct fuse_conn *fc; + u32 cmd_op = cmd->cmd_op; + int err; + + if (!enable_uring) { + pr_info_ratelimited("fuse-io-uring is disabled\n"); + return -EOPNOTSUPP; + } + + /* This extra SQE size holds struct fuse_uring_cmd_req */ + if (!(issue_flags & IO_URING_F_SQE128)) + return -EINVAL; + + fud = fuse_get_dev(cmd->file); + if (!fud) { + pr_info_ratelimited("No fuse device found\n"); + return -ENOTCONN; + } + fc = fud->fc; + + if (fc->aborted) + return -ECONNABORTED; + if (!fc->connected) + return -ENOTCONN; + + /* + * fuse_uring_register() needs the ring to be initialized, + * we need to know the max payload size + */ + if (!fc->initialized) + return -EAGAIN; + + switch (cmd_op) { + case FUSE_IO_URING_CMD_REGISTER: + err = fuse_uring_register(cmd, issue_flags, fc); + if (err) { + pr_info_once("FUSE_IO_URING_CMD_REGISTER failed err=%d\n", + err); + return err; + } + break; + default: + return -EINVAL; + } + + return -EIOCBQUEUED; +} diff --git a/fs/fuse/dev_uring_i.h b/fs/fuse/dev_uring_i.h new file mode 100644 index 0000000000000..ae1536355b368 --- /dev/null +++ b/fs/fuse/dev_uring_i.h @@ -0,0 +1,113 @@ +/* SPDX-License-Identifier: GPL-2.0 + * + * FUSE: Filesystem in Userspace + * Copyright (c) 2023-2024 DataDirect Networks. + */ + +#ifndef _FS_FUSE_DEV_URING_I_H +#define _FS_FUSE_DEV_URING_I_H + +#include "fuse_i.h" + +#ifdef CONFIG_FUSE_IO_URING + +enum fuse_ring_req_state { + FRRS_INVALID = 0, + + /* The ring entry received from userspace and it is being processed */ + FRRS_COMMIT, + + /* The ring entry is waiting for new fuse requests */ + FRRS_AVAILABLE, + + /* The ring entry is in or on the way to user space */ + FRRS_USERSPACE, +}; + +/** A fuse ring entry, part of the ring queue */ +struct fuse_ring_ent { + /* userspace buffer */ + struct fuse_uring_req_header __user *headers; + void __user *payload; + + /* the ring queue that owns the request */ + struct fuse_ring_queue *queue; + + /* fields below are protected by queue->lock */ + + struct io_uring_cmd *cmd; + + struct list_head list; + + enum fuse_ring_req_state state; + + struct fuse_req *fuse_req; +}; + +struct fuse_ring_queue { + /* + * back pointer to the main fuse uring structure that holds this + * queue + */ + struct fuse_ring *ring; + + /* queue id, corresponds to the cpu core */ + unsigned int qid; + + /* + * queue lock, taken when any value in the queue changes _and_ also + * a ring entry state changes. + */ + spinlock_t lock; + + /* available ring entries (struct fuse_ring_ent) */ + struct list_head ent_avail_queue; + + /* + * entries in the process of being committed or in the process + * to be sent to userspace + */ + struct list_head ent_commit_queue; +}; + +/** + * Describes if uring is for communication and holds alls the data needed + * for uring communication + */ +struct fuse_ring { + /* back pointer */ + struct fuse_conn *fc; + + /* number of ring queues */ + size_t nr_queues; + + /* maximum payload/arg size */ + size_t max_payload_sz; + + struct fuse_ring_queue **queues; +}; + +bool fuse_uring_enabled(void); +void fuse_uring_destruct(struct fuse_conn *fc); +int fuse_uring_cmd(struct io_uring_cmd *cmd, unsigned int issue_flags); + +#else /* CONFIG_FUSE_IO_URING */ + +struct fuse_ring; + +static inline void fuse_uring_create(struct fuse_conn *fc) +{ +} + +static inline void fuse_uring_destruct(struct fuse_conn *fc) +{ +} + +static inline bool fuse_uring_enabled(void) +{ + return false; +} + +#endif /* CONFIG_FUSE_IO_URING */ + +#endif /* _FS_FUSE_DEV_URING_I_H */ diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 696479fa17c9a..a50f6356e9b4c 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -917,6 +917,11 @@ struct fuse_conn { /** IDR for backing files ids */ struct idr backing_files_map; #endif + +#ifdef CONFIG_FUSE_IO_URING + /** uring connection information*/ + struct fuse_ring *ring; +#endif }; /* diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 8802111e0b478..7836e4b75907b 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -7,6 +7,7 @@ */ #include "fuse_i.h" +#include "dev_uring_i.h" #include #include @@ -960,6 +961,8 @@ static void delayed_release(struct rcu_head *p) { struct fuse_conn *fc = container_of(p, struct fuse_conn, rcu); + fuse_uring_destruct(fc); + put_user_ns(fc->user_ns); fc->release(fc); } @@ -1414,6 +1417,13 @@ void fuse_send_init(struct fuse_mount *fm) if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH)) flags |= FUSE_PASSTHROUGH; + /* + * This is just an information flag for fuse server. No need to check + * the reply - server is either sending IORING_OP_URING_CMD or not. + */ + if (fuse_uring_enabled()) + flags |= FUSE_OVER_IO_URING; + ia->in.flags = flags; ia->in.flags2 = flags >> 32; diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h index a022b3707ad6d..914aa300bcc1a 100644 --- a/include/uapi/linux/fuse.h +++ b/include/uapi/linux/fuse.h @@ -221,6 +221,16 @@ * 7.41 * - add FUSE_ALLOW_IDMAP * + * 7.42 + * - Add FUSE_OVER_IO_URING and all other io-uring related flags and data + * structures: + * - struct fuse_uring_ent_in_out + * - struct fuse_uring_req_header + * - struct fuse_uring_cmd_req + * - FUSE_URING_IN_OUT_HEADER_SZ + * - FUSE_URING_OP_IN_OUT_SZ + * - enum fuse_uring_cmd + * * 7.44 * - add FUSE_NOTIFY_INC_EPOCH */ @@ -428,6 +438,7 @@ struct fuse_file_lock { * FUSE_HAS_RESEND: kernel supports resending pending requests, and the high bit * of the request ID indicates resend requests * FUSE_ALLOW_IDMAP: allow creation of idmapped mounts + * FUSE_OVER_IO_URING: Indicate that client supports io-uring */ #define FUSE_ASYNC_READ (1 << 0) #define FUSE_POSIX_LOCKS (1 << 1) @@ -474,6 +485,7 @@ struct fuse_file_lock { /* Obsolete alias for FUSE_DIRECT_IO_ALLOW_MMAP */ #define FUSE_DIRECT_IO_RELAX FUSE_DIRECT_IO_ALLOW_MMAP #define FUSE_ALLOW_IDMAP (1ULL << 40) +#define FUSE_OVER_IO_URING (1ULL << 41) /** * CUSE INIT request/reply flags @@ -1210,4 +1222,67 @@ struct fuse_supp_groups { uint32_t groups[]; }; +/** + * Size of the ring buffer header + */ +#define FUSE_URING_IN_OUT_HEADER_SZ 128 +#define FUSE_URING_OP_IN_OUT_SZ 128 + +/* Used as part of the fuse_uring_req_header */ +struct fuse_uring_ent_in_out { + uint64_t flags; + + /* + * commit ID to be used in a reply to a ring request (see also + * struct fuse_uring_cmd_req) + */ + uint64_t commit_id; + + /* size of user payload buffer */ + uint32_t payload_sz; + uint32_t padding; + + uint64_t reserved; +}; + +/** + * Header for all fuse-io-uring requests + */ +struct fuse_uring_req_header { + /* struct fuse_in_header / struct fuse_out_header */ + char in_out[FUSE_URING_IN_OUT_HEADER_SZ]; + + /* per op code header */ + char op_in[FUSE_URING_OP_IN_OUT_SZ]; + + struct fuse_uring_ent_in_out ring_ent_in_out; +}; + +/** + * sqe commands to the kernel + */ +enum fuse_uring_cmd { + FUSE_IO_URING_CMD_INVALID = 0, + + /* register the request buffer and fetch a fuse request */ + FUSE_IO_URING_CMD_REGISTER = 1, + + /* commit fuse request result and fetch next request */ + FUSE_IO_URING_CMD_COMMIT_AND_FETCH = 2, +}; + +/** + * In the 80B command area of the SQE. + */ +struct fuse_uring_cmd_req { + uint64_t flags; + + /* entry identifier for commits */ + uint64_t commit_id; + + /* queue the command is for (queue index) */ + uint16_t qid; + uint8_t padding[6]; +}; + #endif /* _LINUX_FUSE_H */ From fc1953fce6a1275dc3faa17776f0bd3780260998 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 15:29:49 -0500 Subject: [PATCH 06/41] fuse: Make fuse_copy non static jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit d0f9c62aaf7a98412b46a91fe7daad76b316b3b7 Move 'struct fuse_copy_state' and fuse_copy_* functions to fuse_dev_i.h to make it available for fuse-io-uring. 'copy_out_args()' is renamed to 'fuse_copy_out_args'. Signed-off-by: Bernd Schubert Reviewed-by: Joanne Koong Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit d0f9c62aaf7a98412b46a91fe7daad76b316b3b7) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 30 ++++++++---------------------- fs/fuse/fuse_dev_i.h | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index 7f2316f48c048..be7eff087b2ed 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -679,22 +679,8 @@ static int unlock_request(struct fuse_req *req) return err; } -struct fuse_copy_state { - int write; - struct fuse_req *req; - struct iov_iter *iter; - struct pipe_buffer *pipebufs; - struct pipe_buffer *currbuf; - struct pipe_inode_info *pipe; - unsigned long nr_segs; - struct page *pg; - unsigned len; - unsigned offset; - unsigned move_pages:1; -}; - -static void fuse_copy_init(struct fuse_copy_state *cs, int write, - struct iov_iter *iter) +void fuse_copy_init(struct fuse_copy_state *cs, int write, + struct iov_iter *iter) { memset(cs, 0, sizeof(*cs)); cs->write = write; @@ -1045,9 +1031,9 @@ static int fuse_copy_one(struct fuse_copy_state *cs, void *val, unsigned size) } /* Copy request arguments to/from userspace buffer */ -static int fuse_copy_args(struct fuse_copy_state *cs, unsigned numargs, - unsigned argpages, struct fuse_arg *args, - int zeroing) +int fuse_copy_args(struct fuse_copy_state *cs, unsigned numargs, + unsigned argpages, struct fuse_arg *args, + int zeroing) { int err = 0; unsigned i; @@ -1938,8 +1924,8 @@ static struct fuse_req *request_find(struct fuse_pqueue *fpq, u64 unique) return NULL; } -static int copy_out_args(struct fuse_copy_state *cs, struct fuse_args *args, - unsigned nbytes) +int fuse_copy_out_args(struct fuse_copy_state *cs, struct fuse_args *args, + unsigned nbytes) { unsigned reqsize = sizeof(struct fuse_out_header); @@ -2041,7 +2027,7 @@ static ssize_t fuse_dev_do_write(struct fuse_dev *fud, if (oh.error) err = nbytes != sizeof(oh) ? -EINVAL : 0; else - err = copy_out_args(cs, req->args, nbytes); + err = fuse_copy_out_args(cs, req->args, nbytes); fuse_copy_finish(cs); spin_lock(&fpq->lock); diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h index 08a7e88e00277..21eb1bdb492d0 100644 --- a/fs/fuse/fuse_dev_i.h +++ b/fs/fuse/fuse_dev_i.h @@ -12,6 +12,23 @@ #define FUSE_INT_REQ_BIT (1ULL << 0) #define FUSE_REQ_ID_STEP (1ULL << 1) +struct fuse_arg; +struct fuse_args; + +struct fuse_copy_state { + int write; + struct fuse_req *req; + struct iov_iter *iter; + struct pipe_buffer *pipebufs; + struct pipe_buffer *currbuf; + struct pipe_inode_info *pipe; + unsigned long nr_segs; + struct page *pg; + unsigned int len; + unsigned int offset; + unsigned int move_pages:1; +}; + static inline struct fuse_dev *fuse_get_dev(struct file *file) { /* @@ -23,5 +40,13 @@ static inline struct fuse_dev *fuse_get_dev(struct file *file) void fuse_dev_end_requests(struct list_head *head); +void fuse_copy_init(struct fuse_copy_state *cs, int write, + struct iov_iter *iter); +int fuse_copy_args(struct fuse_copy_state *cs, unsigned int numargs, + unsigned int argpages, struct fuse_arg *args, + int zeroing); +int fuse_copy_out_args(struct fuse_copy_state *cs, struct fuse_args *args, + unsigned int nbytes); + #endif From 52aa7c620617727e8d949259635a36615e69d0dd Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 15:30:21 -0500 Subject: [PATCH 07/41] fuse: Add fuse-io-uring handling into fuse_copy jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit f773a7c2c3d934d00542c6471170066d150d152f Add special fuse-io-uring into the fuse argument copy handler. Signed-off-by: Bernd Schubert Reviewed-by: Joanne Koong Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit f773a7c2c3d934d00542c6471170066d150d152f) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 12 +++++++++++- fs/fuse/fuse_dev_i.h | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index be7eff087b2ed..e19f37e313993 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -787,6 +787,9 @@ static int fuse_copy_do(struct fuse_copy_state *cs, void **val, unsigned *size) *size -= ncpy; cs->len -= ncpy; cs->offset += ncpy; + if (cs->is_uring) + cs->ring.copied_sz += ncpy; + return ncpy; } @@ -1927,7 +1930,14 @@ static struct fuse_req *request_find(struct fuse_pqueue *fpq, u64 unique) int fuse_copy_out_args(struct fuse_copy_state *cs, struct fuse_args *args, unsigned nbytes) { - unsigned reqsize = sizeof(struct fuse_out_header); + + unsigned int reqsize = 0; + + /* + * Uring has all headers separated from args - args is payload only + */ + if (!cs->is_uring) + reqsize = sizeof(struct fuse_out_header); reqsize += fuse_len_args(args->out_numargs, args->out_args); diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h index 21eb1bdb492d0..4a8a4feb2df53 100644 --- a/fs/fuse/fuse_dev_i.h +++ b/fs/fuse/fuse_dev_i.h @@ -27,6 +27,10 @@ struct fuse_copy_state { unsigned int len; unsigned int offset; unsigned int move_pages:1; + unsigned int is_uring:1; + struct { + unsigned int copied_sz; /* copied size into the user buffer */ + } ring; }; static inline struct fuse_dev *fuse_get_dev(struct file *file) From c371ed10a66e531d75b2f2e50aa9017ad5d250d2 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 15:31:07 -0500 Subject: [PATCH 08/41] fuse: {io-uring} Make hash-list req unique finding functions non-static jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 3821336530616776414aa7a879640052b89def28 fuse-over-io-uring uses existing functions to find requests based on their unique id - make these functions non-static. Signed-off-by: Bernd Schubert Reviewed-by: Joanne Koong Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit 3821336530616776414aa7a879640052b89def28) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 6 +++--- fs/fuse/fuse_dev_i.h | 5 +++++ fs/fuse/fuse_i.h | 5 +++++ fs/fuse/inode.c | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index e19f37e313993..367d7895eb68c 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -221,7 +221,7 @@ u64 fuse_get_unique(struct fuse_iqueue *fiq) } EXPORT_SYMBOL_GPL(fuse_get_unique); -static unsigned int fuse_req_hash(u64 unique) +unsigned int fuse_req_hash(u64 unique) { return hash_long(unique & ~FUSE_INT_REQ_BIT, FUSE_PQ_HASH_BITS); } @@ -1915,7 +1915,7 @@ static int fuse_notify(struct fuse_conn *fc, enum fuse_notify_code code, } /* Look up request on processing list by unique ID */ -static struct fuse_req *request_find(struct fuse_pqueue *fpq, u64 unique) +struct fuse_req *fuse_request_find(struct fuse_pqueue *fpq, u64 unique) { unsigned int hash = fuse_req_hash(unique); struct fuse_req *req; @@ -1999,7 +1999,7 @@ static ssize_t fuse_dev_do_write(struct fuse_dev *fud, spin_lock(&fpq->lock); req = NULL; if (fpq->connected) - req = request_find(fpq, oh.unique & ~FUSE_INT_REQ_BIT); + req = fuse_request_find(fpq, oh.unique & ~FUSE_INT_REQ_BIT); err = -ENOENT; if (!req) { diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h index 4a8a4feb2df53..b64ab84cbc0d5 100644 --- a/fs/fuse/fuse_dev_i.h +++ b/fs/fuse/fuse_dev_i.h @@ -14,6 +14,8 @@ struct fuse_arg; struct fuse_args; +struct fuse_pqueue; +struct fuse_req; struct fuse_copy_state { int write; @@ -42,6 +44,9 @@ static inline struct fuse_dev *fuse_get_dev(struct file *file) return READ_ONCE(file->private_data); } +unsigned int fuse_req_hash(u64 unique); +struct fuse_req *fuse_request_find(struct fuse_pqueue *fpq, u64 unique); + void fuse_dev_end_requests(struct list_head *head); void fuse_copy_init(struct fuse_copy_state *cs, int write, diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index a50f6356e9b4c..d8005e56dffa4 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -1224,6 +1224,11 @@ void fuse_change_entry_timeout(struct dentry *entry, struct fuse_entry_out *o); */ struct fuse_conn *fuse_conn_get(struct fuse_conn *fc); +/** + * Initialize the fuse processing queue + */ +void fuse_pqueue_init(struct fuse_pqueue *fpq); + /** * Initialize fuse_conn */ diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 7836e4b75907b..ebf13adae51e9 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -906,7 +906,7 @@ static void fuse_iqueue_init(struct fuse_iqueue *fiq, fiq->priv = priv; } -static void fuse_pqueue_init(struct fuse_pqueue *fpq) +void fuse_pqueue_init(struct fuse_pqueue *fpq) { unsigned int i; From 2529810e95ec9dbb7dd29af0abbc0e53d1453f96 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 15:31:33 -0500 Subject: [PATCH 09/41] fuse: Add io-uring sqe commit and fetch support jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit c090c8abae4b6b77a1bee116aa6c385456ebef96 This adds support for fuse request completion through ring SQEs (FUSE_URING_CMD_COMMIT_AND_FETCH handling). After committing the ring entry it becomes available for new fuse requests. Handling of requests through the ring (SQE/CQE handling) is complete now. Fuse request data are copied through the mmaped ring buffer, there is no support for any zero copy yet. Signed-off-by: Bernd Schubert Reviewed-by: Pavel Begunkov # io_uring Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit c090c8abae4b6b77a1bee116aa6c385456ebef96) Signed-off-by: Jonathan Maple --- fs/fuse/dev_uring.c | 441 ++++++++++++++++++++++++++++++++++++++++++ fs/fuse/dev_uring_i.h | 12 ++ fs/fuse/fuse_i.h | 4 + 3 files changed, 457 insertions(+) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 42092a234570e..1030c1720990e 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -24,6 +24,17 @@ bool fuse_uring_enabled(void) return enable_uring; } +static void fuse_uring_req_end(struct fuse_ring_ent *ent, struct fuse_req *req, + int error) +{ + ent->fuse_req = NULL; + if (error) + req->out.h.error = error; + + clear_bit(FR_SENT, &req->flags); + fuse_request_end(req); +} + void fuse_uring_destruct(struct fuse_conn *fc) { struct fuse_ring *ring = fc->ring; @@ -39,8 +50,11 @@ void fuse_uring_destruct(struct fuse_conn *fc) continue; WARN_ON(!list_empty(&queue->ent_avail_queue)); + WARN_ON(!list_empty(&queue->ent_w_req_queue)); WARN_ON(!list_empty(&queue->ent_commit_queue)); + WARN_ON(!list_empty(&queue->ent_in_userspace)); + kfree(queue->fpq.processing); kfree(queue); ring->queues[qid] = NULL; } @@ -99,20 +113,34 @@ static struct fuse_ring_queue *fuse_uring_create_queue(struct fuse_ring *ring, { struct fuse_conn *fc = ring->fc; struct fuse_ring_queue *queue; + struct list_head *pq; queue = kzalloc(sizeof(*queue), GFP_KERNEL_ACCOUNT); if (!queue) return NULL; + pq = kcalloc(FUSE_PQ_HASH_SIZE, sizeof(struct list_head), GFP_KERNEL); + if (!pq) { + kfree(queue); + return NULL; + } + queue->qid = qid; queue->ring = ring; spin_lock_init(&queue->lock); INIT_LIST_HEAD(&queue->ent_avail_queue); INIT_LIST_HEAD(&queue->ent_commit_queue); + INIT_LIST_HEAD(&queue->ent_w_req_queue); + INIT_LIST_HEAD(&queue->ent_in_userspace); + INIT_LIST_HEAD(&queue->fuse_req_queue); + + queue->fpq.processing = pq; + fuse_pqueue_init(&queue->fpq); spin_lock(&fc->lock); if (ring->queues[qid]) { spin_unlock(&fc->lock); + kfree(queue->fpq.processing); kfree(queue); return ring->queues[qid]; } @@ -126,6 +154,214 @@ static struct fuse_ring_queue *fuse_uring_create_queue(struct fuse_ring *ring, return queue; } +/* + * Checks for errors and stores it into the request + */ +static int fuse_uring_out_header_has_err(struct fuse_out_header *oh, + struct fuse_req *req, + struct fuse_conn *fc) +{ + int err; + + err = -EINVAL; + if (oh->unique == 0) { + /* Not supported through io-uring yet */ + pr_warn_once("notify through fuse-io-uring not supported\n"); + goto err; + } + + if (oh->error <= -ERESTARTSYS || oh->error > 0) + goto err; + + if (oh->error) { + err = oh->error; + goto err; + } + + err = -ENOENT; + if ((oh->unique & ~FUSE_INT_REQ_BIT) != req->in.h.unique) { + pr_warn_ratelimited("unique mismatch, expected: %llu got %llu\n", + req->in.h.unique, + oh->unique & ~FUSE_INT_REQ_BIT); + goto err; + } + + /* + * Is it an interrupt reply ID? + * XXX: Not supported through fuse-io-uring yet, it should not even + * find the request - should not happen. + */ + WARN_ON_ONCE(oh->unique & FUSE_INT_REQ_BIT); + + err = 0; +err: + return err; +} + +static int fuse_uring_copy_from_ring(struct fuse_ring *ring, + struct fuse_req *req, + struct fuse_ring_ent *ent) +{ + struct fuse_copy_state cs; + struct fuse_args *args = req->args; + struct iov_iter iter; + int err; + struct fuse_uring_ent_in_out ring_in_out; + + err = copy_from_user(&ring_in_out, &ent->headers->ring_ent_in_out, + sizeof(ring_in_out)); + if (err) + return -EFAULT; + + err = import_ubuf(ITER_SOURCE, ent->payload, ring->max_payload_sz, + &iter); + if (err) + return err; + + fuse_copy_init(&cs, 0, &iter); + cs.is_uring = 1; + cs.req = req; + + return fuse_copy_out_args(&cs, args, ring_in_out.payload_sz); +} + + /* + * Copy data from the req to the ring buffer + */ +static int fuse_uring_args_to_ring(struct fuse_ring *ring, struct fuse_req *req, + struct fuse_ring_ent *ent) +{ + struct fuse_copy_state cs; + struct fuse_args *args = req->args; + struct fuse_in_arg *in_args = args->in_args; + int num_args = args->in_numargs; + int err; + struct iov_iter iter; + struct fuse_uring_ent_in_out ent_in_out = { + .flags = 0, + .commit_id = req->in.h.unique, + }; + + err = import_ubuf(ITER_DEST, ent->payload, ring->max_payload_sz, &iter); + if (err) { + pr_info_ratelimited("fuse: Import of user buffer failed\n"); + return err; + } + + fuse_copy_init(&cs, 1, &iter); + cs.is_uring = 1; + cs.req = req; + + if (num_args > 0) { + /* + * Expectation is that the first argument is the per op header. + * Some op code have that as zero size. + */ + if (args->in_args[0].size > 0) { + err = copy_to_user(&ent->headers->op_in, in_args->value, + in_args->size); + if (err) { + pr_info_ratelimited( + "Copying the header failed.\n"); + return -EFAULT; + } + } + in_args++; + num_args--; + } + + /* copy the payload */ + err = fuse_copy_args(&cs, num_args, args->in_pages, + (struct fuse_arg *)in_args, 0); + if (err) { + pr_info_ratelimited("%s fuse_copy_args failed\n", __func__); + return err; + } + + ent_in_out.payload_sz = cs.ring.copied_sz; + err = copy_to_user(&ent->headers->ring_ent_in_out, &ent_in_out, + sizeof(ent_in_out)); + return err ? -EFAULT : 0; +} + +static int fuse_uring_copy_to_ring(struct fuse_ring_ent *ent, + struct fuse_req *req) +{ + struct fuse_ring_queue *queue = ent->queue; + struct fuse_ring *ring = queue->ring; + int err; + + err = -EIO; + if (WARN_ON(ent->state != FRRS_FUSE_REQ)) { + pr_err("qid=%d ring-req=%p invalid state %d on send\n", + queue->qid, ent, ent->state); + return err; + } + + err = -EINVAL; + if (WARN_ON(req->in.h.unique == 0)) + return err; + + /* copy the request */ + err = fuse_uring_args_to_ring(ring, req, ent); + if (unlikely(err)) { + pr_info_ratelimited("Copy to ring failed: %d\n", err); + return err; + } + + /* copy fuse_in_header */ + err = copy_to_user(&ent->headers->in_out, &req->in.h, + sizeof(req->in.h)); + if (err) { + err = -EFAULT; + return err; + } + + return 0; +} + +static int fuse_uring_prepare_send(struct fuse_ring_ent *ent, + struct fuse_req *req) +{ + int err; + + err = fuse_uring_copy_to_ring(ent, req); + if (!err) + set_bit(FR_SENT, &req->flags); + else + fuse_uring_req_end(ent, req, err); + + return err; +} + +/* + * Write data to the ring buffer and send the request to userspace, + * userspace will read it + * This is comparable with classical read(/dev/fuse) + */ +static int fuse_uring_send_next_to_ring(struct fuse_ring_ent *ent, + struct fuse_req *req, + unsigned int issue_flags) +{ + struct fuse_ring_queue *queue = ent->queue; + int err; + struct io_uring_cmd *cmd; + + err = fuse_uring_prepare_send(ent, req); + if (err) + return err; + + spin_lock(&queue->lock); + cmd = ent->cmd; + ent->cmd = NULL; + ent->state = FRRS_USERSPACE; + list_move(&ent->list, &queue->ent_in_userspace); + spin_unlock(&queue->lock); + + io_uring_cmd_done(cmd, 0, 0, issue_flags); + return 0; +} + /* * Make a ring entry available for fuse_req assignment */ @@ -137,6 +373,203 @@ static void fuse_uring_ent_avail(struct fuse_ring_ent *ent, ent->state = FRRS_AVAILABLE; } +/* Used to find the request on SQE commit */ +static void fuse_uring_add_to_pq(struct fuse_ring_ent *ent, + struct fuse_req *req) +{ + struct fuse_ring_queue *queue = ent->queue; + struct fuse_pqueue *fpq = &queue->fpq; + unsigned int hash; + + req->ring_entry = ent; + hash = fuse_req_hash(req->in.h.unique); + list_move_tail(&req->list, &fpq->processing[hash]); +} + +/* + * Assign a fuse queue entry to the given entry + */ +static void fuse_uring_add_req_to_ring_ent(struct fuse_ring_ent *ent, + struct fuse_req *req) +{ + struct fuse_ring_queue *queue = ent->queue; + struct fuse_conn *fc = req->fm->fc; + struct fuse_iqueue *fiq = &fc->iq; + + lockdep_assert_held(&queue->lock); + + if (WARN_ON_ONCE(ent->state != FRRS_AVAILABLE && + ent->state != FRRS_COMMIT)) { + pr_warn("%s qid=%d state=%d\n", __func__, ent->queue->qid, + ent->state); + } + + spin_lock(&fiq->lock); + clear_bit(FR_PENDING, &req->flags); + spin_unlock(&fiq->lock); + ent->fuse_req = req; + ent->state = FRRS_FUSE_REQ; + list_move(&ent->list, &queue->ent_w_req_queue); + fuse_uring_add_to_pq(ent, req); +} + +/* Fetch the next fuse request if available */ +static struct fuse_req *fuse_uring_ent_assign_req(struct fuse_ring_ent *ent) + __must_hold(&queue->lock) +{ + struct fuse_req *req; + struct fuse_ring_queue *queue = ent->queue; + struct list_head *req_queue = &queue->fuse_req_queue; + + lockdep_assert_held(&queue->lock); + + /* get and assign the next entry while it is still holding the lock */ + req = list_first_entry_or_null(req_queue, struct fuse_req, list); + if (req) + fuse_uring_add_req_to_ring_ent(ent, req); + + return req; +} + +/* + * Read data from the ring buffer, which user space has written to + * This is comparible with handling of classical write(/dev/fuse). + * Also make the ring request available again for new fuse requests. + */ +static void fuse_uring_commit(struct fuse_ring_ent *ent, struct fuse_req *req, + unsigned int issue_flags) +{ + struct fuse_ring *ring = ent->queue->ring; + struct fuse_conn *fc = ring->fc; + ssize_t err = 0; + + err = copy_from_user(&req->out.h, &ent->headers->in_out, + sizeof(req->out.h)); + if (err) { + req->out.h.error = -EFAULT; + goto out; + } + + err = fuse_uring_out_header_has_err(&req->out.h, req, fc); + if (err) { + /* req->out.h.error already set */ + goto out; + } + + err = fuse_uring_copy_from_ring(ring, req, ent); +out: + fuse_uring_req_end(ent, req, err); +} + +/* + * Get the next fuse req and send it + */ +static void fuse_uring_next_fuse_req(struct fuse_ring_ent *ent, + struct fuse_ring_queue *queue, + unsigned int issue_flags) +{ + int err; + struct fuse_req *req; + +retry: + spin_lock(&queue->lock); + fuse_uring_ent_avail(ent, queue); + req = fuse_uring_ent_assign_req(ent); + spin_unlock(&queue->lock); + + if (req) { + err = fuse_uring_send_next_to_ring(ent, req, issue_flags); + if (err) + goto retry; + } +} + +static int fuse_ring_ent_set_commit(struct fuse_ring_ent *ent) +{ + struct fuse_ring_queue *queue = ent->queue; + + lockdep_assert_held(&queue->lock); + + if (WARN_ON_ONCE(ent->state != FRRS_USERSPACE)) + return -EIO; + + ent->state = FRRS_COMMIT; + list_move(&ent->list, &queue->ent_commit_queue); + + return 0; +} + +/* FUSE_URING_CMD_COMMIT_AND_FETCH handler */ +static int fuse_uring_commit_fetch(struct io_uring_cmd *cmd, int issue_flags, + struct fuse_conn *fc) +{ + const struct fuse_uring_cmd_req *cmd_req = io_uring_sqe_cmd(cmd->sqe); + struct fuse_ring_ent *ent; + int err; + struct fuse_ring *ring = fc->ring; + struct fuse_ring_queue *queue; + uint64_t commit_id = READ_ONCE(cmd_req->commit_id); + unsigned int qid = READ_ONCE(cmd_req->qid); + struct fuse_pqueue *fpq; + struct fuse_req *req; + + err = -ENOTCONN; + if (!ring) + return err; + + if (qid >= ring->nr_queues) + return -EINVAL; + + queue = ring->queues[qid]; + if (!queue) + return err; + fpq = &queue->fpq; + + spin_lock(&queue->lock); + /* Find a request based on the unique ID of the fuse request + * This should get revised, as it needs a hash calculation and list + * search. And full struct fuse_pqueue is needed (memory overhead). + * As well as the link from req to ring_ent. + */ + req = fuse_request_find(fpq, commit_id); + err = -ENOENT; + if (!req) { + pr_info("qid=%d commit_id %llu not found\n", queue->qid, + commit_id); + spin_unlock(&queue->lock); + return err; + } + list_del_init(&req->list); + ent = req->ring_entry; + req->ring_entry = NULL; + + err = fuse_ring_ent_set_commit(ent); + if (err != 0) { + pr_info_ratelimited("qid=%d commit_id %llu state %d", + queue->qid, commit_id, ent->state); + spin_unlock(&queue->lock); + req->out.h.error = err; + clear_bit(FR_SENT, &req->flags); + fuse_request_end(req); + return err; + } + + ent->cmd = cmd; + spin_unlock(&queue->lock); + + /* without the queue lock, as other locks are taken */ + fuse_uring_commit(ent, req, issue_flags); + + /* + * Fetching the next request is absolutely required as queued + * fuse requests would otherwise not get processed - committing + * and fetching is done in one step vs legacy fuse, which has separated + * read (fetch request) and write (commit result). + */ + fuse_uring_next_fuse_req(ent, queue, issue_flags); + return 0; +} + /* * fuse_uring_req_fetch command handling */ @@ -318,6 +751,14 @@ int __maybe_unused fuse_uring_cmd(struct io_uring_cmd *cmd, return err; } break; + case FUSE_IO_URING_CMD_COMMIT_AND_FETCH: + err = fuse_uring_commit_fetch(cmd, issue_flags, fc); + if (err) { + pr_info_once("FUSE_IO_URING_COMMIT_AND_FETCH failed err=%d\n", + err); + return err; + } + break; default: return -EINVAL; } diff --git a/fs/fuse/dev_uring_i.h b/fs/fuse/dev_uring_i.h index ae1536355b368..44bf237f0d5ab 100644 --- a/fs/fuse/dev_uring_i.h +++ b/fs/fuse/dev_uring_i.h @@ -20,6 +20,9 @@ enum fuse_ring_req_state { /* The ring entry is waiting for new fuse requests */ FRRS_AVAILABLE, + /* The ring entry got assigned a fuse req */ + FRRS_FUSE_REQ, + /* The ring entry is in or on the way to user space */ FRRS_USERSPACE, }; @@ -67,7 +70,16 @@ struct fuse_ring_queue { * entries in the process of being committed or in the process * to be sent to userspace */ + struct list_head ent_w_req_queue; struct list_head ent_commit_queue; + + /* entries in userspace */ + struct list_head ent_in_userspace; + + /* fuse requests waiting for an entry slot */ + struct list_head fuse_req_queue; + + struct fuse_pqueue fpq; }; /** diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index d8005e56dffa4..e6af5a64b4542 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -435,6 +435,10 @@ struct fuse_req { /** fuse_mount this request belongs to */ struct fuse_mount *fm; + +#ifdef CONFIG_FUSE_IO_URING + void *ring_entry; +#endif }; struct fuse_iqueue; From 68524663da96571d67d17d5ba03d140e5d5b5049 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:05:48 -0500 Subject: [PATCH 10/41] fuse: {io-uring} Handle teardown of ring entries jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 4a9bfb9b6850fec0685447aed280533cf980de70 On teardown struct file_operations::uring_cmd requests need to be completed by calling io_uring_cmd_done(). Not completing all ring entries would result in busy io-uring tasks giving warning messages in intervals and unreleased struct file. Additionally the fuse connection and with that the ring can only get released when all io-uring commands are completed. Completion is done with ring entries that are a) in waiting state for new fuse requests - io_uring_cmd_done is needed b) already in userspace - io_uring_cmd_done through teardown is not needed, the request can just get released. If fuse server is still active and commits such a ring entry, fuse_uring_cmd() already checks if the connection is active and then complete the io-uring itself with -ENOTCONN. I.e. special handling is not needed. This scheme is basically represented by the ring entry state FRRS_WAIT and FRRS_USERSPACE. Entries in state: - FRRS_INIT: No action needed, do not contribute to ring->queue_refs yet - All other states: Are currently processed by other tasks, async teardown is needed and it has to wait for the two states above. It could be also solved without an async teardown task, but would require additional if conditions in hot code paths. Also in my personal opinion the code looks cleaner with async teardown. Signed-off-by: Bernd Schubert Reviewed-by: Pavel Begunkov # io_uring Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit 4a9bfb9b6850fec0685447aed280533cf980de70) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 9 ++ fs/fuse/dev_uring.c | 207 ++++++++++++++++++++++++++++++++++++++++++ fs/fuse/dev_uring_i.h | 51 +++++++++++ 3 files changed, 267 insertions(+) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index 367d7895eb68c..94426d420ebe7 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -6,6 +6,7 @@ See the file COPYING. */ +#include "dev_uring_i.h" #include "fuse_i.h" #include "fuse_dev_i.h" @@ -2296,6 +2297,12 @@ void fuse_abort_conn(struct fuse_conn *fc) spin_unlock(&fc->lock); fuse_dev_end_requests(&to_end); + + /* + * fc->lock must not be taken to avoid conflicts with io-uring + * locks + */ + fuse_uring_abort(fc); } else { spin_unlock(&fc->lock); } @@ -2307,6 +2314,8 @@ void fuse_wait_aborted(struct fuse_conn *fc) /* matches implicit memory barrier in fuse_drop_waiting() */ smp_mb(); wait_event(fc->blocked_waitq, atomic_read(&fc->num_waiting) == 0); + + fuse_uring_wait_stopped_queues(fc); } int fuse_dev_release(struct inode *inode, struct file *file) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 1030c1720990e..1161b9aa5e11c 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -35,6 +35,37 @@ static void fuse_uring_req_end(struct fuse_ring_ent *ent, struct fuse_req *req, fuse_request_end(req); } +/* Abort all list queued request on the given ring queue */ +static void fuse_uring_abort_end_queue_requests(struct fuse_ring_queue *queue) +{ + struct fuse_req *req; + LIST_HEAD(req_list); + + spin_lock(&queue->lock); + list_for_each_entry(req, &queue->fuse_req_queue, list) + clear_bit(FR_PENDING, &req->flags); + list_splice_init(&queue->fuse_req_queue, &req_list); + spin_unlock(&queue->lock); + + /* must not hold queue lock to avoid order issues with fi->lock */ + fuse_dev_end_requests(&req_list); +} + +void fuse_uring_abort_end_requests(struct fuse_ring *ring) +{ + int qid; + struct fuse_ring_queue *queue; + + for (qid = 0; qid < ring->nr_queues; qid++) { + queue = READ_ONCE(ring->queues[qid]); + if (!queue) + continue; + + queue->stopped = true; + fuse_uring_abort_end_queue_requests(queue); + } +} + void fuse_uring_destruct(struct fuse_conn *fc) { struct fuse_ring *ring = fc->ring; @@ -94,10 +125,13 @@ static struct fuse_ring *fuse_uring_create(struct fuse_conn *fc) goto out_err; } + init_waitqueue_head(&ring->stop_waitq); + fc->ring = ring; ring->nr_queues = nr_queues; ring->fc = fc; ring->max_payload_sz = max_payload_size; + atomic_set(&ring->queue_refs, 0); spin_unlock(&fc->lock); return ring; @@ -154,6 +188,175 @@ static struct fuse_ring_queue *fuse_uring_create_queue(struct fuse_ring *ring, return queue; } +static void fuse_uring_stop_fuse_req_end(struct fuse_req *req) +{ + clear_bit(FR_SENT, &req->flags); + req->out.h.error = -ECONNABORTED; + fuse_request_end(req); +} + +/* + * Release a request/entry on connection tear down + */ +static void fuse_uring_entry_teardown(struct fuse_ring_ent *ent) +{ + struct fuse_req *req; + struct io_uring_cmd *cmd; + + struct fuse_ring_queue *queue = ent->queue; + + spin_lock(&queue->lock); + cmd = ent->cmd; + ent->cmd = NULL; + req = ent->fuse_req; + ent->fuse_req = NULL; + if (req) { + /* remove entry from queue->fpq->processing */ + list_del_init(&req->list); + } + spin_unlock(&queue->lock); + + if (cmd) + io_uring_cmd_done(cmd, -ENOTCONN, 0, IO_URING_F_UNLOCKED); + + if (req) + fuse_uring_stop_fuse_req_end(req); + + list_del_init(&ent->list); + kfree(ent); +} + +static void fuse_uring_stop_list_entries(struct list_head *head, + struct fuse_ring_queue *queue, + enum fuse_ring_req_state exp_state) +{ + struct fuse_ring *ring = queue->ring; + struct fuse_ring_ent *ent, *next; + ssize_t queue_refs = SSIZE_MAX; + LIST_HEAD(to_teardown); + + spin_lock(&queue->lock); + list_for_each_entry_safe(ent, next, head, list) { + if (ent->state != exp_state) { + pr_warn("entry teardown qid=%d state=%d expected=%d", + queue->qid, ent->state, exp_state); + continue; + } + + list_move(&ent->list, &to_teardown); + } + spin_unlock(&queue->lock); + + /* no queue lock to avoid lock order issues */ + list_for_each_entry_safe(ent, next, &to_teardown, list) { + fuse_uring_entry_teardown(ent); + queue_refs = atomic_dec_return(&ring->queue_refs); + WARN_ON_ONCE(queue_refs < 0); + } +} + +static void fuse_uring_teardown_entries(struct fuse_ring_queue *queue) +{ + fuse_uring_stop_list_entries(&queue->ent_in_userspace, queue, + FRRS_USERSPACE); + fuse_uring_stop_list_entries(&queue->ent_avail_queue, queue, + FRRS_AVAILABLE); +} + +/* + * Log state debug info + */ +static void fuse_uring_log_ent_state(struct fuse_ring *ring) +{ + int qid; + struct fuse_ring_ent *ent; + + for (qid = 0; qid < ring->nr_queues; qid++) { + struct fuse_ring_queue *queue = ring->queues[qid]; + + if (!queue) + continue; + + spin_lock(&queue->lock); + /* + * Log entries from the intermediate queue, the other queues + * should be empty + */ + list_for_each_entry(ent, &queue->ent_w_req_queue, list) { + pr_info(" ent-req-queue ring=%p qid=%d ent=%p state=%d\n", + ring, qid, ent, ent->state); + } + list_for_each_entry(ent, &queue->ent_commit_queue, list) { + pr_info(" ent-commit-queue ring=%p qid=%d ent=%p state=%d\n", + ring, qid, ent, ent->state); + } + spin_unlock(&queue->lock); + } + ring->stop_debug_log = 1; +} + +static void fuse_uring_async_stop_queues(struct work_struct *work) +{ + int qid; + struct fuse_ring *ring = + container_of(work, struct fuse_ring, async_teardown_work.work); + + /* XXX code dup */ + for (qid = 0; qid < ring->nr_queues; qid++) { + struct fuse_ring_queue *queue = READ_ONCE(ring->queues[qid]); + + if (!queue) + continue; + + fuse_uring_teardown_entries(queue); + } + + /* + * Some ring entries might be in the middle of IO operations, + * i.e. in process to get handled by file_operations::uring_cmd + * or on the way to userspace - we could handle that with conditions in + * run time code, but easier/cleaner to have an async tear down handler + * If there are still queue references left + */ + if (atomic_read(&ring->queue_refs) > 0) { + if (time_after(jiffies, + ring->teardown_time + FUSE_URING_TEARDOWN_TIMEOUT)) + fuse_uring_log_ent_state(ring); + + schedule_delayed_work(&ring->async_teardown_work, + FUSE_URING_TEARDOWN_INTERVAL); + } else { + wake_up_all(&ring->stop_waitq); + } +} + +/* + * Stop the ring queues + */ +void fuse_uring_stop_queues(struct fuse_ring *ring) +{ + int qid; + + for (qid = 0; qid < ring->nr_queues; qid++) { + struct fuse_ring_queue *queue = READ_ONCE(ring->queues[qid]); + + if (!queue) + continue; + + fuse_uring_teardown_entries(queue); + } + + if (atomic_read(&ring->queue_refs) > 0) { + ring->teardown_time = jiffies; + INIT_DELAYED_WORK(&ring->async_teardown_work, + fuse_uring_async_stop_queues); + schedule_delayed_work(&ring->async_teardown_work, + FUSE_URING_TEARDOWN_INTERVAL); + } else { + wake_up_all(&ring->stop_waitq); + } +} + /* * Checks for errors and stores it into the request */ @@ -525,6 +728,9 @@ static int fuse_uring_commit_fetch(struct io_uring_cmd *cmd, int issue_flags, return err; fpq = &queue->fpq; + if (!READ_ONCE(fc->connected) || READ_ONCE(queue->stopped)) + return err; + spin_lock(&queue->lock); /* Find a request based on the unique ID of the fuse request * This should get revised, as it needs a hash calculation and list @@ -652,6 +858,7 @@ fuse_uring_create_ring_ent(struct io_uring_cmd *cmd, ent->headers = iov[0].iov_base; ent->payload = iov[1].iov_base; + atomic_inc(&ring->queue_refs); return ent; } diff --git a/fs/fuse/dev_uring_i.h b/fs/fuse/dev_uring_i.h index 44bf237f0d5ab..a4316e118cbd8 100644 --- a/fs/fuse/dev_uring_i.h +++ b/fs/fuse/dev_uring_i.h @@ -11,6 +11,9 @@ #ifdef CONFIG_FUSE_IO_URING +#define FUSE_URING_TEARDOWN_TIMEOUT (5 * HZ) +#define FUSE_URING_TEARDOWN_INTERVAL (HZ/20) + enum fuse_ring_req_state { FRRS_INVALID = 0, @@ -80,6 +83,8 @@ struct fuse_ring_queue { struct list_head fuse_req_queue; struct fuse_pqueue fpq; + + bool stopped; }; /** @@ -97,12 +102,51 @@ struct fuse_ring { size_t max_payload_sz; struct fuse_ring_queue **queues; + + /* + * Log ring entry states on stop when entries cannot be released + */ + unsigned int stop_debug_log : 1; + + wait_queue_head_t stop_waitq; + + /* async tear down */ + struct delayed_work async_teardown_work; + + /* log */ + unsigned long teardown_time; + + atomic_t queue_refs; }; bool fuse_uring_enabled(void); void fuse_uring_destruct(struct fuse_conn *fc); +void fuse_uring_stop_queues(struct fuse_ring *ring); +void fuse_uring_abort_end_requests(struct fuse_ring *ring); int fuse_uring_cmd(struct io_uring_cmd *cmd, unsigned int issue_flags); +static inline void fuse_uring_abort(struct fuse_conn *fc) +{ + struct fuse_ring *ring = fc->ring; + + if (ring == NULL) + return; + + if (atomic_read(&ring->queue_refs) > 0) { + fuse_uring_abort_end_requests(ring); + fuse_uring_stop_queues(ring); + } +} + +static inline void fuse_uring_wait_stopped_queues(struct fuse_conn *fc) +{ + struct fuse_ring *ring = fc->ring; + + if (ring) + wait_event(ring->stop_waitq, + atomic_read(&ring->queue_refs) == 0); +} + #else /* CONFIG_FUSE_IO_URING */ struct fuse_ring; @@ -120,6 +164,13 @@ static inline bool fuse_uring_enabled(void) return false; } +static inline void fuse_uring_abort(struct fuse_conn *fc) +{ +} + +static inline void fuse_uring_wait_stopped_queues(struct fuse_conn *fc) +{ +} #endif /* CONFIG_FUSE_IO_URING */ #endif /* _FS_FUSE_DEV_URING_I_H */ From 0a3b444dd0334bca26750be37d3566a699b00422 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:06:12 -0500 Subject: [PATCH 11/41] fuse: {io-uring} Make fuse_dev_queue_{interrupt,forget} non-static jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit ba74ba571189668697a8d8da906ad6d44762ebc6 These functions are also needed by fuse-over-io-uring. Signed-off-by: Bernd Schubert Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit ba74ba571189668697a8d8da906ad6d44762ebc6) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 5 +++-- fs/fuse/fuse_dev_i.h | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index 94426d420ebe7..e33fdc49019a5 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -238,7 +238,8 @@ __releases(fiq->lock) spin_unlock(&fiq->lock); } -static void fuse_dev_queue_forget(struct fuse_iqueue *fiq, struct fuse_forget_link *forget) +void fuse_dev_queue_forget(struct fuse_iqueue *fiq, + struct fuse_forget_link *forget) { spin_lock(&fiq->lock); if (fiq->connected) { @@ -251,7 +252,7 @@ static void fuse_dev_queue_forget(struct fuse_iqueue *fiq, struct fuse_forget_li } } -static void fuse_dev_queue_interrupt(struct fuse_iqueue *fiq, struct fuse_req *req) +void fuse_dev_queue_interrupt(struct fuse_iqueue *fiq, struct fuse_req *req) { spin_lock(&fiq->lock); if (list_empty(&req->intr_entry)) { diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h index b64ab84cbc0d5..3b2bfe1248d35 100644 --- a/fs/fuse/fuse_dev_i.h +++ b/fs/fuse/fuse_dev_i.h @@ -16,6 +16,8 @@ struct fuse_arg; struct fuse_args; struct fuse_pqueue; struct fuse_req; +struct fuse_iqueue; +struct fuse_forget_link; struct fuse_copy_state { int write; @@ -56,6 +58,9 @@ int fuse_copy_args(struct fuse_copy_state *cs, unsigned int numargs, int zeroing); int fuse_copy_out_args(struct fuse_copy_state *cs, struct fuse_args *args, unsigned int nbytes); +void fuse_dev_queue_forget(struct fuse_iqueue *fiq, + struct fuse_forget_link *forget); +void fuse_dev_queue_interrupt(struct fuse_iqueue *fiq, struct fuse_req *req); #endif From 73850102299e1fd5b1adce22c3bc39fd4e4c1e57 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:06:49 -0500 Subject: [PATCH 12/41] fuse: Allow to queue fg requests through io-uring jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit c2c9af9a0b13261c36909036057a116f2edb5e1a This prepares queueing and sending foreground requests through io-uring. Signed-off-by: Bernd Schubert Reviewed-by: Pavel Begunkov # io_uring Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit c2c9af9a0b13261c36909036057a116f2edb5e1a) Signed-off-by: Jonathan Maple --- fs/fuse/dev_uring.c | 180 ++++++++++++++++++++++++++++++++++++++++++ fs/fuse/dev_uring_i.h | 8 ++ 2 files changed, 188 insertions(+) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 1161b9aa5e11c..728000434589b 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -24,6 +24,29 @@ bool fuse_uring_enabled(void) return enable_uring; } +struct fuse_uring_pdu { + struct fuse_ring_ent *ent; +}; + +static const struct fuse_iqueue_ops fuse_io_uring_ops; + +static void uring_cmd_set_ring_ent(struct io_uring_cmd *cmd, + struct fuse_ring_ent *ring_ent) +{ + struct fuse_uring_pdu *pdu = + io_uring_cmd_to_pdu(cmd, struct fuse_uring_pdu); + + pdu->ent = ring_ent; +} + +static struct fuse_ring_ent *uring_cmd_to_ring_ent(struct io_uring_cmd *cmd) +{ + struct fuse_uring_pdu *pdu = + io_uring_cmd_to_pdu(cmd, struct fuse_uring_pdu); + + return pdu->ent; +} + static void fuse_uring_req_end(struct fuse_ring_ent *ent, struct fuse_req *req, int error) { @@ -776,6 +799,31 @@ static int fuse_uring_commit_fetch(struct io_uring_cmd *cmd, int issue_flags, return 0; } +static bool is_ring_ready(struct fuse_ring *ring, int current_qid) +{ + int qid; + struct fuse_ring_queue *queue; + bool ready = true; + + for (qid = 0; qid < ring->nr_queues && ready; qid++) { + if (current_qid == qid) + continue; + + queue = ring->queues[qid]; + if (!queue) { + ready = false; + break; + } + + spin_lock(&queue->lock); + if (list_empty(&queue->ent_avail_queue)) + ready = false; + spin_unlock(&queue->lock); + } + + return ready; +} + /* * fuse_uring_req_fetch command handling */ @@ -784,11 +832,23 @@ static void fuse_uring_do_register(struct fuse_ring_ent *ent, unsigned int issue_flags) { struct fuse_ring_queue *queue = ent->queue; + struct fuse_ring *ring = queue->ring; + struct fuse_conn *fc = ring->fc; + struct fuse_iqueue *fiq = &fc->iq; spin_lock(&queue->lock); ent->cmd = cmd; fuse_uring_ent_avail(ent, queue); spin_unlock(&queue->lock); + + if (!ring->ready) { + bool ready = is_ring_ready(ring, queue->qid); + + if (ready) { + WRITE_ONCE(fiq->ops, &fuse_io_uring_ops); + WRITE_ONCE(ring->ready, true); + } + } } /* @@ -972,3 +1032,123 @@ int __maybe_unused fuse_uring_cmd(struct io_uring_cmd *cmd, return -EIOCBQUEUED; } + +static void fuse_uring_send(struct fuse_ring_ent *ent, struct io_uring_cmd *cmd, + ssize_t ret, unsigned int issue_flags) +{ + struct fuse_ring_queue *queue = ent->queue; + + spin_lock(&queue->lock); + ent->state = FRRS_USERSPACE; + list_move(&ent->list, &queue->ent_in_userspace); + ent->cmd = NULL; + spin_unlock(&queue->lock); + + io_uring_cmd_done(cmd, ret, 0, issue_flags); +} + +/* + * This prepares and sends the ring request in fuse-uring task context. + * User buffers are not mapped yet - the application does not have permission + * to write to it - this has to be executed in ring task context. + */ +static void fuse_uring_send_in_task(struct io_uring_cmd *cmd, + unsigned int issue_flags) +{ + struct fuse_ring_ent *ent = uring_cmd_to_ring_ent(cmd); + struct fuse_ring_queue *queue = ent->queue; + int err; + + if (!(issue_flags & IO_URING_F_TASK_DEAD)) { + err = fuse_uring_prepare_send(ent, ent->fuse_req); + if (err) { + fuse_uring_next_fuse_req(ent, queue, issue_flags); + return; + } + } else { + err = -ECANCELED; + } + + fuse_uring_send(ent, cmd, err, issue_flags); +} + +static struct fuse_ring_queue *fuse_uring_task_to_queue(struct fuse_ring *ring) +{ + unsigned int qid; + struct fuse_ring_queue *queue; + + qid = task_cpu(current); + + if (WARN_ONCE(qid >= ring->nr_queues, + "Core number (%u) exceeds nr queues (%zu)\n", qid, + ring->nr_queues)) + qid = 0; + + queue = ring->queues[qid]; + WARN_ONCE(!queue, "Missing queue for qid %d\n", qid); + + return queue; +} + +static void fuse_uring_dispatch_ent(struct fuse_ring_ent *ent) +{ + struct io_uring_cmd *cmd = ent->cmd; + + uring_cmd_set_ring_ent(cmd, ent); + io_uring_cmd_complete_in_task(cmd, fuse_uring_send_in_task); +} + +/* queue a fuse request and send it if a ring entry is available */ +void fuse_uring_queue_fuse_req(struct fuse_iqueue *fiq, struct fuse_req *req) +{ + struct fuse_conn *fc = req->fm->fc; + struct fuse_ring *ring = fc->ring; + struct fuse_ring_queue *queue; + struct fuse_ring_ent *ent = NULL; + int err; + + err = -EINVAL; + queue = fuse_uring_task_to_queue(ring); + if (!queue) + goto err; + + if (req->in.h.opcode != FUSE_NOTIFY_REPLY) + req->in.h.unique = fuse_get_unique(fiq); + + spin_lock(&queue->lock); + err = -ENOTCONN; + if (unlikely(queue->stopped)) + goto err_unlock; + + ent = list_first_entry_or_null(&queue->ent_avail_queue, + struct fuse_ring_ent, list); + if (ent) + fuse_uring_add_req_to_ring_ent(ent, req); + else + list_add_tail(&req->list, &queue->fuse_req_queue); + spin_unlock(&queue->lock); + + if (ent) + fuse_uring_dispatch_ent(ent); + + return; + +err_unlock: + spin_unlock(&queue->lock); +err: + req->out.h.error = err; + clear_bit(FR_PENDING, &req->flags); + fuse_request_end(req); +} + +static const struct fuse_iqueue_ops fuse_io_uring_ops = { + /* should be send over io-uring as enhancement */ + .send_forget = fuse_dev_queue_forget, + + /* + * could be send over io-uring, but interrupts should be rare, + * no need to make the code complex + */ + .send_interrupt = fuse_dev_queue_interrupt, + .send_req = fuse_uring_queue_fuse_req, +}; diff --git a/fs/fuse/dev_uring_i.h b/fs/fuse/dev_uring_i.h index a4316e118cbd8..0517a6eafc917 100644 --- a/fs/fuse/dev_uring_i.h +++ b/fs/fuse/dev_uring_i.h @@ -117,6 +117,8 @@ struct fuse_ring { unsigned long teardown_time; atomic_t queue_refs; + + bool ready; }; bool fuse_uring_enabled(void); @@ -124,6 +126,7 @@ void fuse_uring_destruct(struct fuse_conn *fc); void fuse_uring_stop_queues(struct fuse_ring *ring); void fuse_uring_abort_end_requests(struct fuse_ring *ring); int fuse_uring_cmd(struct io_uring_cmd *cmd, unsigned int issue_flags); +void fuse_uring_queue_fuse_req(struct fuse_iqueue *fiq, struct fuse_req *req); static inline void fuse_uring_abort(struct fuse_conn *fc) { @@ -147,6 +150,11 @@ static inline void fuse_uring_wait_stopped_queues(struct fuse_conn *fc) atomic_read(&ring->queue_refs) == 0); } +static inline bool fuse_uring_ready(struct fuse_conn *fc) +{ + return fc->ring && fc->ring->ready; +} + #else /* CONFIG_FUSE_IO_URING */ struct fuse_ring; From 086211fc0748ff6fe054201b6ce0fad6ed9709c9 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:06:56 -0500 Subject: [PATCH 13/41] fuse: Allow to queue bg requests through io-uring jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 857b0263f30eebe13ab4b6a65156a0d6c8fc2210 This prepares queueing and sending background requests through io-uring. Signed-off-by: Bernd Schubert Reviewed-by: Pavel Begunkov # io_uring Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit 857b0263f30eebe13ab4b6a65156a0d6c8fc2210) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 26 +++++++++++- fs/fuse/dev_uring.c | 99 +++++++++++++++++++++++++++++++++++++++++++ fs/fuse/dev_uring_i.h | 12 ++++++ 3 files changed, 136 insertions(+), 1 deletion(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index e33fdc49019a5..31f97123a10ce 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -569,7 +569,25 @@ ssize_t __fuse_simple_request(struct mnt_idmap *idmap, return ret; } -static bool fuse_request_queue_background(struct fuse_req *req) +#ifdef CONFIG_FUSE_IO_URING +static bool fuse_request_queue_background_uring(struct fuse_conn *fc, + struct fuse_req *req) +{ + struct fuse_iqueue *fiq = &fc->iq; + + req->in.h.unique = fuse_get_unique(fiq); + req->in.h.len = sizeof(struct fuse_in_header) + + fuse_len_args(req->args->in_numargs, + (struct fuse_arg *) req->args->in_args); + + return fuse_uring_queue_bq_req(req); +} +#endif + +/* + * @return true if queued + */ +static int fuse_request_queue_background(struct fuse_req *req) { struct fuse_mount *fm = req->fm; struct fuse_conn *fc = fm->fc; @@ -581,6 +599,12 @@ static bool fuse_request_queue_background(struct fuse_req *req) atomic_inc(&fc->num_waiting); } __set_bit(FR_ISREPLY, &req->flags); + +#ifdef CONFIG_FUSE_IO_URING + if (fuse_uring_ready(fc)) + return fuse_request_queue_background_uring(fc, req); +#endif + spin_lock(&fc->bg_lock); if (likely(fc->connected)) { fc->num_background++; diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 728000434589b..27bc103c17c89 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -47,10 +47,53 @@ static struct fuse_ring_ent *uring_cmd_to_ring_ent(struct io_uring_cmd *cmd) return pdu->ent; } +static void fuse_uring_flush_bg(struct fuse_ring_queue *queue) +{ + struct fuse_ring *ring = queue->ring; + struct fuse_conn *fc = ring->fc; + + lockdep_assert_held(&queue->lock); + lockdep_assert_held(&fc->bg_lock); + + /* + * Allow one bg request per queue, ignoring global fc limits. + * This prevents a single queue from consuming all resources and + * eliminates the need for remote queue wake-ups when global + * limits are met but this queue has no more waiting requests. + */ + while ((fc->active_background < fc->max_background || + !queue->active_background) && + (!list_empty(&queue->fuse_req_bg_queue))) { + struct fuse_req *req; + + req = list_first_entry(&queue->fuse_req_bg_queue, + struct fuse_req, list); + fc->active_background++; + queue->active_background++; + + list_move_tail(&req->list, &queue->fuse_req_queue); + } +} + static void fuse_uring_req_end(struct fuse_ring_ent *ent, struct fuse_req *req, int error) { + struct fuse_ring_queue *queue = ent->queue; + struct fuse_ring *ring = queue->ring; + struct fuse_conn *fc = ring->fc; + + lockdep_assert_not_held(&queue->lock); + spin_lock(&queue->lock); ent->fuse_req = NULL; + if (test_bit(FR_BACKGROUND, &req->flags)) { + queue->active_background--; + spin_lock(&fc->bg_lock); + fuse_uring_flush_bg(queue); + spin_unlock(&fc->bg_lock); + } + + spin_unlock(&queue->lock); + if (error) req->out.h.error = error; @@ -78,6 +121,7 @@ void fuse_uring_abort_end_requests(struct fuse_ring *ring) { int qid; struct fuse_ring_queue *queue; + struct fuse_conn *fc = ring->fc; for (qid = 0; qid < ring->nr_queues; qid++) { queue = READ_ONCE(ring->queues[qid]); @@ -85,6 +129,13 @@ void fuse_uring_abort_end_requests(struct fuse_ring *ring) continue; queue->stopped = true; + + WARN_ON_ONCE(ring->fc->max_background != UINT_MAX); + spin_lock(&queue->lock); + spin_lock(&fc->bg_lock); + fuse_uring_flush_bg(queue); + spin_unlock(&fc->bg_lock); + spin_unlock(&queue->lock); fuse_uring_abort_end_queue_requests(queue); } } @@ -190,6 +241,7 @@ static struct fuse_ring_queue *fuse_uring_create_queue(struct fuse_ring *ring, INIT_LIST_HEAD(&queue->ent_w_req_queue); INIT_LIST_HEAD(&queue->ent_in_userspace); INIT_LIST_HEAD(&queue->fuse_req_queue); + INIT_LIST_HEAD(&queue->fuse_req_bg_queue); queue->fpq.processing = pq; fuse_pqueue_init(&queue->fpq); @@ -1141,6 +1193,53 @@ void fuse_uring_queue_fuse_req(struct fuse_iqueue *fiq, struct fuse_req *req) fuse_request_end(req); } +bool fuse_uring_queue_bq_req(struct fuse_req *req) +{ + struct fuse_conn *fc = req->fm->fc; + struct fuse_ring *ring = fc->ring; + struct fuse_ring_queue *queue; + struct fuse_ring_ent *ent = NULL; + + queue = fuse_uring_task_to_queue(ring); + if (!queue) + return false; + + spin_lock(&queue->lock); + if (unlikely(queue->stopped)) { + spin_unlock(&queue->lock); + return false; + } + + list_add_tail(&req->list, &queue->fuse_req_bg_queue); + + ent = list_first_entry_or_null(&queue->ent_avail_queue, + struct fuse_ring_ent, list); + spin_lock(&fc->bg_lock); + fc->num_background++; + if (fc->num_background == fc->max_background) + fc->blocked = 1; + fuse_uring_flush_bg(queue); + spin_unlock(&fc->bg_lock); + + /* + * Due to bg_queue flush limits there might be other bg requests + * in the queue that need to be handled first. Or no further req + * might be available. + */ + req = list_first_entry_or_null(&queue->fuse_req_queue, struct fuse_req, + list); + if (ent && req) { + fuse_uring_add_req_to_ring_ent(ent, req); + spin_unlock(&queue->lock); + + fuse_uring_dispatch_ent(ent); + } else { + spin_unlock(&queue->lock); + } + + return true; +} + static const struct fuse_iqueue_ops fuse_io_uring_ops = { /* should be send over io-uring as enhancement */ .send_forget = fuse_dev_queue_forget, diff --git a/fs/fuse/dev_uring_i.h b/fs/fuse/dev_uring_i.h index 0517a6eafc917..0182be61778b2 100644 --- a/fs/fuse/dev_uring_i.h +++ b/fs/fuse/dev_uring_i.h @@ -82,8 +82,13 @@ struct fuse_ring_queue { /* fuse requests waiting for an entry slot */ struct list_head fuse_req_queue; + /* background fuse requests */ + struct list_head fuse_req_bg_queue; + struct fuse_pqueue fpq; + unsigned int active_background; + bool stopped; }; @@ -127,6 +132,7 @@ void fuse_uring_stop_queues(struct fuse_ring *ring); void fuse_uring_abort_end_requests(struct fuse_ring *ring); int fuse_uring_cmd(struct io_uring_cmd *cmd, unsigned int issue_flags); void fuse_uring_queue_fuse_req(struct fuse_iqueue *fiq, struct fuse_req *req); +bool fuse_uring_queue_bq_req(struct fuse_req *req); static inline void fuse_uring_abort(struct fuse_conn *fc) { @@ -179,6 +185,12 @@ static inline void fuse_uring_abort(struct fuse_conn *fc) static inline void fuse_uring_wait_stopped_queues(struct fuse_conn *fc) { } + +static inline bool fuse_uring_ready(struct fuse_conn *fc) +{ + return false; +} + #endif /* CONFIG_FUSE_IO_URING */ #endif /* _FS_FUSE_DEV_URING_I_H */ From 514c299fe08f4f3e0a849f41073338c8d3b3ceb1 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:07:00 -0500 Subject: [PATCH 14/41] fuse: {io-uring} Prevent mount point hang on fuse-server termination jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit b6236c8407cba5d7a108facb1bcfab24994d3814 When the fuse-server terminates while the fuse-client or kernel still has queued URING_CMDs, these commands retain references to the struct file used by the fuse connection. This prevents fuse_dev_release() from being invoked, resulting in a hung mount point. This patch addresses the issue by making queued URING_CMDs cancelable, allowing fuse_dev_release() to proceed as expected and preventing the mount point from hanging. Signed-off-by: Bernd Schubert Reviewed-by: Pavel Begunkov # io_uring Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit b6236c8407cba5d7a108facb1bcfab24994d3814) Signed-off-by: Jonathan Maple --- fs/fuse/dev_uring.c | 69 +++++++++++++++++++++++++++++++++++++++++-- fs/fuse/dev_uring_i.h | 9 ++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 27bc103c17c89..fa0451176385e 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -150,6 +150,7 @@ void fuse_uring_destruct(struct fuse_conn *fc) for (qid = 0; qid < ring->nr_queues; qid++) { struct fuse_ring_queue *queue = ring->queues[qid]; + struct fuse_ring_ent *ent, *next; if (!queue) continue; @@ -159,6 +160,12 @@ void fuse_uring_destruct(struct fuse_conn *fc) WARN_ON(!list_empty(&queue->ent_commit_queue)); WARN_ON(!list_empty(&queue->ent_in_userspace)); + list_for_each_entry_safe(ent, next, &queue->ent_released, + list) { + list_del_init(&ent->list); + kfree(ent); + } + kfree(queue->fpq.processing); kfree(queue); ring->queues[qid] = NULL; @@ -242,6 +249,7 @@ static struct fuse_ring_queue *fuse_uring_create_queue(struct fuse_ring *ring, INIT_LIST_HEAD(&queue->ent_in_userspace); INIT_LIST_HEAD(&queue->fuse_req_queue); INIT_LIST_HEAD(&queue->fuse_req_bg_queue); + INIT_LIST_HEAD(&queue->ent_released); queue->fpq.processing = pq; fuse_pqueue_init(&queue->fpq); @@ -289,6 +297,15 @@ static void fuse_uring_entry_teardown(struct fuse_ring_ent *ent) /* remove entry from queue->fpq->processing */ list_del_init(&req->list); } + + /* + * The entry must not be freed immediately, due to access of direct + * pointer access of entries through IO_URING_F_CANCEL - there is a risk + * of race between daemon termination (which triggers IO_URING_F_CANCEL + * and accesses entries without checking the list state first + */ + list_move(&ent->list, &queue->ent_released); + ent->state = FRRS_RELEASED; spin_unlock(&queue->lock); if (cmd) @@ -296,9 +313,6 @@ static void fuse_uring_entry_teardown(struct fuse_ring_ent *ent) if (req) fuse_uring_stop_fuse_req_end(req); - - list_del_init(&ent->list); - kfree(ent); } static void fuse_uring_stop_list_entries(struct list_head *head, @@ -318,6 +332,7 @@ static void fuse_uring_stop_list_entries(struct list_head *head, continue; } + ent->state = FRRS_TEARDOWN; list_move(&ent->list, &to_teardown); } spin_unlock(&queue->lock); @@ -432,6 +447,46 @@ void fuse_uring_stop_queues(struct fuse_ring *ring) } } +/* + * Handle IO_URING_F_CANCEL, typically should come on daemon termination. + * + * Releasing the last entry should trigger fuse_dev_release() if + * the daemon was terminated + */ +static void fuse_uring_cancel(struct io_uring_cmd *cmd, + unsigned int issue_flags) +{ + struct fuse_ring_ent *ent = uring_cmd_to_ring_ent(cmd); + struct fuse_ring_queue *queue; + bool need_cmd_done = false; + + /* + * direct access on ent - it must not be destructed as long as + * IO_URING_F_CANCEL might come up + */ + queue = ent->queue; + spin_lock(&queue->lock); + if (ent->state == FRRS_AVAILABLE) { + ent->state = FRRS_USERSPACE; + list_move(&ent->list, &queue->ent_in_userspace); + need_cmd_done = true; + ent->cmd = NULL; + } + spin_unlock(&queue->lock); + + if (need_cmd_done) { + /* no queue lock to avoid lock order issues */ + io_uring_cmd_done(cmd, -ENOTCONN, 0, issue_flags); + } +} + +static void fuse_uring_prepare_cancel(struct io_uring_cmd *cmd, int issue_flags, + struct fuse_ring_ent *ring_ent) +{ + uring_cmd_set_ring_ent(cmd, ring_ent); + io_uring_cmd_mark_cancelable(cmd, issue_flags); +} + /* * Checks for errors and stores it into the request */ @@ -839,6 +894,7 @@ static int fuse_uring_commit_fetch(struct io_uring_cmd *cmd, int issue_flags, spin_unlock(&queue->lock); /* without the queue lock, as other locks are taken */ + fuse_uring_prepare_cancel(cmd, issue_flags, ent); fuse_uring_commit(ent, req, issue_flags); /* @@ -888,6 +944,8 @@ static void fuse_uring_do_register(struct fuse_ring_ent *ent, struct fuse_conn *fc = ring->fc; struct fuse_iqueue *fiq = &fc->iq; + fuse_uring_prepare_cancel(cmd, issue_flags, ent); + spin_lock(&queue->lock); ent->cmd = cmd; fuse_uring_ent_avail(ent, queue); @@ -1038,6 +1096,11 @@ int __maybe_unused fuse_uring_cmd(struct io_uring_cmd *cmd, return -EOPNOTSUPP; } + if ((unlikely(issue_flags & IO_URING_F_CANCEL))) { + fuse_uring_cancel(cmd, issue_flags); + return 0; + } + /* This extra SQE size holds struct fuse_uring_cmd_req */ if (!(issue_flags & IO_URING_F_SQE128)) return -EINVAL; diff --git a/fs/fuse/dev_uring_i.h b/fs/fuse/dev_uring_i.h index 0182be61778b2..2102b3d0c1aed 100644 --- a/fs/fuse/dev_uring_i.h +++ b/fs/fuse/dev_uring_i.h @@ -28,6 +28,12 @@ enum fuse_ring_req_state { /* The ring entry is in or on the way to user space */ FRRS_USERSPACE, + + /* The ring entry is in teardown */ + FRRS_TEARDOWN, + + /* The ring entry is released, but not freed yet */ + FRRS_RELEASED, }; /** A fuse ring entry, part of the ring queue */ @@ -79,6 +85,9 @@ struct fuse_ring_queue { /* entries in userspace */ struct list_head ent_in_userspace; + /* entries that are released */ + struct list_head ent_released; + /* fuse requests waiting for an entry slot */ struct list_head fuse_req_queue; From 3f56a8029b1feaf4192731759e413c65296ea4e1 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:09:28 -0500 Subject: [PATCH 15/41] fuse: block request allocation until io-uring init is complete jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 3393ff964e0fa5def66570c54a4612bf9df06b76 upstream-diff | Context conflict on missing fs/fuse/fuse_i.h commit 41748675c0bf -virtiofs: use pages instead of pointer for kernel direct IO Avoid races and block request allocation until io-uring queues are ready. This is a especially important for background requests, as bg request completion might cause lock order inversion of the typical queue->lock and then fc->bg_lock fuse_request_end spin_lock(&fc->bg_lock); flush_bg_queue fuse_send_one fuse_uring_queue_fuse_req spin_lock(&queue->lock); Signed-off-by: Bernd Schubert Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit 3393ff964e0fa5def66570c54a4612bf9df06b76) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 3 ++- fs/fuse/dev_uring.c | 3 +++ fs/fuse/fuse_i.h | 3 +++ fs/fuse/inode.c | 2 ++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index 31f97123a10ce..00bb8dc453408 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -77,7 +77,8 @@ void fuse_set_initialized(struct fuse_conn *fc) static bool fuse_block_alloc(struct fuse_conn *fc, bool for_background) { - return !fc->initialized || (for_background && fc->blocked); + return !fc->initialized || (for_background && fc->blocked) || + (fc->io_uring && !fuse_uring_ready(fc)); } static void fuse_drop_waiting(struct fuse_conn *fc) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index fa0451176385e..ea197ccd4c51f 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -957,6 +957,7 @@ static void fuse_uring_do_register(struct fuse_ring_ent *ent, if (ready) { WRITE_ONCE(fiq->ops, &fuse_io_uring_ops); WRITE_ONCE(ring->ready, true); + wake_up_all(&fc->blocked_waitq); } } } @@ -1130,6 +1131,8 @@ int __maybe_unused fuse_uring_cmd(struct io_uring_cmd *cmd, if (err) { pr_info_once("FUSE_IO_URING_CMD_REGISTER failed err=%d\n", err); + fc->io_uring = 0; + wake_up_all(&fc->blocked_waitq); return err; } break; diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index e6af5a64b4542..b8f2200f884fe 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -864,6 +864,9 @@ struct fuse_conn { /** Passthrough support for read/write IO */ unsigned int passthrough:1; + /* Use io_uring for communication */ + unsigned int io_uring; + /** Maximum stack depth for passthrough backing files */ int max_stack_depth; diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index ebf13adae51e9..9a8b3bc214fd1 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -1358,6 +1358,8 @@ static void process_init_reply(struct fuse_mount *fm, struct fuse_args *args, else ok = false; } + if (flags & FUSE_OVER_IO_URING && fuse_uring_enabled()) + fc->io_uring = 1; } else { ra_pages = fc->max_read / PAGE_SIZE; fc->no_lock = 1; From 437308ac8fc63bd649327988bff9a841983c5ff3 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:10:54 -0500 Subject: [PATCH 16/41] fuse: enable fuse-over-io-uring jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 786412a73e7ee5b00ef3437bbf2f3a250759b2ae All required parts are handled now, fuse-io-uring can be enabled. Signed-off-by: Bernd Schubert Reviewed-by: Pavel Begunkov # io_uring Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit 786412a73e7ee5b00ef3437bbf2f3a250759b2ae) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 3 +++ fs/fuse/dev_uring.c | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index 00bb8dc453408..e39a4435b2b81 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -2511,6 +2511,9 @@ const struct file_operations fuse_dev_operations = { .fasync = fuse_dev_fasync, .unlocked_ioctl = fuse_dev_ioctl, .compat_ioctl = compat_ptr_ioctl, +#ifdef CONFIG_FUSE_IO_URING + .uring_cmd = fuse_uring_cmd, +#endif #ifdef CONFIG_PROC_FS .show_fdinfo = fuse_dev_show_fdinfo, #endif diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index ea197ccd4c51f..3bdc75518e5b1 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -1084,8 +1084,7 @@ static int fuse_uring_register(struct io_uring_cmd *cmd, * Entry function from io_uring to handle the given passthrough command * (op code IORING_OP_URING_CMD) */ -int __maybe_unused fuse_uring_cmd(struct io_uring_cmd *cmd, - unsigned int issue_flags) +int fuse_uring_cmd(struct io_uring_cmd *cmd, unsigned int issue_flags) { struct fuse_dev *fud; struct fuse_conn *fc; From 088406799d3876af127c2897cc0242f06b5e4adb Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:10:58 -0500 Subject: [PATCH 17/41] fuse: prevent disabling io-uring on active connections jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 2d4fde59fd502a65c1698b61ad4d0f10a9ab665a The enable_uring module parameter allows administrators to enable/disable io-uring support for FUSE at runtime. However, disabling io-uring while connections already have it enabled can lead to an inconsistent state. Fix this by keeping io-uring enabled on connections that were already using it, even if the module parameter is later disabled. This ensures active FUSE mounts continue to function correctly. Signed-off-by: Bernd Schubert Reviewed-by: Luis Henriques Signed-off-by: Miklos Szeredi (cherry picked from commit 2d4fde59fd502a65c1698b61ad4d0f10a9ab665a) Signed-off-by: Jonathan Maple --- fs/fuse/dev_uring.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 3bdc75518e5b1..ebd2931b4f2ac 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -1091,11 +1091,6 @@ int fuse_uring_cmd(struct io_uring_cmd *cmd, unsigned int issue_flags) u32 cmd_op = cmd->cmd_op; int err; - if (!enable_uring) { - pr_info_ratelimited("fuse-io-uring is disabled\n"); - return -EOPNOTSUPP; - } - if ((unlikely(issue_flags & IO_URING_F_CANCEL))) { fuse_uring_cancel(cmd, issue_flags); return 0; @@ -1112,6 +1107,12 @@ int fuse_uring_cmd(struct io_uring_cmd *cmd, unsigned int issue_flags) } fc = fud->fc; + /* Once a connection has io-uring enabled on it, it can't be disabled */ + if (!enable_uring && !fc->io_uring) { + pr_info_ratelimited("fuse-io-uring is disabled\n"); + return -EOPNOTSUPP; + } + if (fc->aborted) return -ECONNABORTED; if (!fc->connected) From cf28bc2610d142557cca4031495283ccc1ab8b20 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:11:34 -0500 Subject: [PATCH 18/41] fuse: fix uring race condition for null dereference of fc jira SECO-478 RFE: FUSE_IO_URING commit-author Joanne Koong commit d9ecc77193cad25402ff5517fb26fb22b4db0e10 There is a race condition leading to a kernel crash from a null dereference when attemping to access fc->lock in fuse_uring_create_queue(). fc may be NULL in the case where another thread is creating the uring in fuse_uring_create() and has set fc->ring but has not yet set ring->fc when fuse_uring_create_queue() reads ring->fc. There is another race condition as well where in fuse_uring_register(), ring->nr_queues may still be 0 and not yet set to the new value when we compare qid against it. This fix sets fc->ring only after ring->fc and ring->nr_queues have been set, which guarantees now that ring->fc is a proper pointer when any queues are created and ring->nr_queues reflects the right number of queues if ring is not NULL. We must use smp_store_release() and smp_load_acquire() semantics to ensure the ordering will remain correct where fc->ring is assigned only after ring->fc and ring->nr_queues have been assigned. Signed-off-by: Joanne Koong Link: https://lore.kernel.org/r/20250318003028.3330599-1-joannelkoong@gmail.com Fixes: 24fe962c86f5 ("fuse: {io-uring} Handle SQEs - register commands") Acked-by: Miklos Szeredi Reviewed-by: Bernd Schubert Signed-off-by: Christian Brauner (cherry picked from commit d9ecc77193cad25402ff5517fb26fb22b4db0e10) Signed-off-by: Jonathan Maple --- fs/fuse/dev_uring.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index ebd2931b4f2ac..82bf458fa9db5 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -208,11 +208,11 @@ static struct fuse_ring *fuse_uring_create(struct fuse_conn *fc) init_waitqueue_head(&ring->stop_waitq); - fc->ring = ring; ring->nr_queues = nr_queues; ring->fc = fc; ring->max_payload_sz = max_payload_size; atomic_set(&ring->queue_refs, 0); + smp_store_release(&fc->ring, ring); spin_unlock(&fc->lock); return ring; @@ -1041,7 +1041,7 @@ static int fuse_uring_register(struct io_uring_cmd *cmd, unsigned int issue_flags, struct fuse_conn *fc) { const struct fuse_uring_cmd_req *cmd_req = io_uring_sqe_cmd(cmd->sqe); - struct fuse_ring *ring = fc->ring; + struct fuse_ring *ring = smp_load_acquire(&fc->ring); struct fuse_ring_queue *queue; struct fuse_ring_ent *ent; int err; From f35ecae79e6ec51b0c028be29f7394f5d2befb6d Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:11:16 -0500 Subject: [PATCH 19/41] fuse: fix possible deadlock if rings are never initialized jira SECO-478 RFE: FUSE_IO_URING commit-author Luis Henriques commit d55011469b41d9da6c06cb1c4a4da7a87fe155bc When mounting a user-space filesystem using io_uring, the initialization of the rings is done separately in the server side. If for some reason (e.g. a server bug) this step is not performed it will be impossible to unmount the filesystem if there are already requests waiting. This issue is easily reproduced with the libfuse passthrough_ll example, if the queue depth is set to '0' and a request is queued before trying to unmount the filesystem. When trying to force the unmount, fuse_abort_conn() will try to wake up all tasks waiting in fc->blocked_waitq, but because the rings were never initialized, fuse_uring_ready() will never return 'true'. Fixes: 3393ff964e0f ("fuse: block request allocation until io-uring init is complete") Signed-off-by: Luis Henriques Link: https://lore.kernel.org/r/20250306111218.13734-1-luis@igalia.com Acked-by: Miklos Szeredi Reviewed-by: Bernd Schubert Signed-off-by: Christian Brauner (cherry picked from commit d55011469b41d9da6c06cb1c4a4da7a87fe155bc) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index e39a4435b2b81..fb591bb6d8e9a 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -78,7 +78,7 @@ void fuse_set_initialized(struct fuse_conn *fc) static bool fuse_block_alloc(struct fuse_conn *fc, bool for_background) { return !fc->initialized || (for_background && fc->blocked) || - (fc->io_uring && !fuse_uring_ready(fc)); + (fc->io_uring && fc->connected && !fuse_uring_ready(fc)); } static void fuse_drop_waiting(struct fuse_conn *fc) From 030b25494c1f28ec5e6b7cc1c96ac37004100b96 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:11:01 -0500 Subject: [PATCH 20/41] fuse: removed unused function fuse_uring_create() from header jira SECO-478 RFE: FUSE_IO_URING commit-author Luis Henriques commit 841c7b812c038661e4f659d1b9c9a366c6d24b71 Function fuse_uring_create() is used only from dev_uring.c and does not need to be exposed in the header file. Furthermore, it has the wrong signature. While there, also remove the 'struct fuse_ring' forward declaration. Signed-off-by: Luis Henriques Reviewed-by: Bernd Schubert Signed-off-by: Miklos Szeredi (cherry picked from commit 841c7b812c038661e4f659d1b9c9a366c6d24b71) Signed-off-by: Jonathan Maple --- fs/fuse/dev_uring_i.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/fs/fuse/dev_uring_i.h b/fs/fuse/dev_uring_i.h index 2102b3d0c1aed..0d67738850c5a 100644 --- a/fs/fuse/dev_uring_i.h +++ b/fs/fuse/dev_uring_i.h @@ -172,12 +172,6 @@ static inline bool fuse_uring_ready(struct fuse_conn *fc) #else /* CONFIG_FUSE_IO_URING */ -struct fuse_ring; - -static inline void fuse_uring_create(struct fuse_conn *fc) -{ -} - static inline void fuse_uring_destruct(struct fuse_conn *fc) { } From 95ea39804f9f9e5f28f9e8953a3f91b8a49dc188 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:11:56 -0500 Subject: [PATCH 21/41] fuse: {io-uring} Fix a possible req cancellation race jira SECO-478 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 09098e62e4be8f0755e58d6078aaf27cbd9a3a8d task-A (application) might be in request_wait_answer and try to remove the request when it has FR_PENDING set. task-B (a fuse-server io-uring task) might handle this request with FUSE_IO_URING_CMD_COMMIT_AND_FETCH, when fetching the next request and accessed the req from the pending list in fuse_uring_ent_assign_req(). That code path was not protected by fiq->lock and so might race with task-A. For scaling reasons we better don't use fiq->lock, but add a handler to remove canceled requests from the queue. This also removes usage of fiq->lock from fuse_uring_add_req_to_ring_ent() altogether, as it was there just to protect against this race and incomplete. Also added is a comment why FR_PENDING is not cleared. Fixes: c090c8abae4b ("fuse: Add io-uring sqe commit and fetch support") Cc: # v6.14 Reported-by: Joanne Koong Closes: https://lore.kernel.org/all/CAJnrk1ZgHNb78dz-yfNTpxmW7wtT88A=m-zF0ZoLXKLUHRjNTw@mail.gmail.com/ Signed-off-by: Bernd Schubert Reviewed-by: Joanne Koong Signed-off-by: Miklos Szeredi (cherry picked from commit 09098e62e4be8f0755e58d6078aaf27cbd9a3a8d) Signed-off-by: Jonathan Maple --- fs/fuse/dev.c | 34 +++++++++++++++++++++++++--------- fs/fuse/dev_uring.c | 15 +++++++++++---- fs/fuse/dev_uring_i.h | 6 ++++++ fs/fuse/fuse_dev_i.h | 1 + fs/fuse/fuse_i.h | 3 +++ 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index fb591bb6d8e9a..d327e93837ea4 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -408,6 +408,24 @@ static int queue_interrupt(struct fuse_req *req) return 0; } +bool fuse_remove_pending_req(struct fuse_req *req, spinlock_t *lock) +{ + spin_lock(lock); + if (test_bit(FR_PENDING, &req->flags)) { + /* + * FR_PENDING does not get cleared as the request will end + * up in destruction anyway. + */ + list_del(&req->list); + spin_unlock(lock); + __fuse_put_request(req); + req->out.h.error = -EINTR; + return true; + } + spin_unlock(lock); + return false; +} + static void request_wait_answer(struct fuse_req *req) { struct fuse_conn *fc = req->fm->fc; @@ -429,22 +447,20 @@ static void request_wait_answer(struct fuse_req *req) } if (!test_bit(FR_FORCE, &req->flags)) { + bool removed; + /* Only fatal signals may interrupt this */ err = wait_event_killable(req->waitq, test_bit(FR_FINISHED, &req->flags)); if (!err) return; - spin_lock(&fiq->lock); - /* Request is not yet in userspace, bail out */ - if (test_bit(FR_PENDING, &req->flags)) { - list_del(&req->list); - spin_unlock(&fiq->lock); - __fuse_put_request(req); - req->out.h.error = -EINTR; + if (test_bit(FR_URING, &req->flags)) + removed = fuse_uring_remove_pending_req(req); + else + removed = fuse_remove_pending_req(req, &fiq->lock); + if (removed) return; - } - spin_unlock(&fiq->lock); } /* diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 82bf458fa9db5..5f1566c69ddcc 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -726,8 +726,6 @@ static void fuse_uring_add_req_to_ring_ent(struct fuse_ring_ent *ent, struct fuse_req *req) { struct fuse_ring_queue *queue = ent->queue; - struct fuse_conn *fc = req->fm->fc; - struct fuse_iqueue *fiq = &fc->iq; lockdep_assert_held(&queue->lock); @@ -737,9 +735,7 @@ static void fuse_uring_add_req_to_ring_ent(struct fuse_ring_ent *ent, ent->state); } - spin_lock(&fiq->lock); clear_bit(FR_PENDING, &req->flags); - spin_unlock(&fiq->lock); ent->fuse_req = req; ent->state = FRRS_FUSE_REQ; list_move(&ent->list, &queue->ent_w_req_queue); @@ -1238,6 +1234,8 @@ void fuse_uring_queue_fuse_req(struct fuse_iqueue *fiq, struct fuse_req *req) if (unlikely(queue->stopped)) goto err_unlock; + set_bit(FR_URING, &req->flags); + req->ring_queue = queue; ent = list_first_entry_or_null(&queue->ent_avail_queue, struct fuse_ring_ent, list); if (ent) @@ -1276,6 +1274,8 @@ bool fuse_uring_queue_bq_req(struct fuse_req *req) return false; } + set_bit(FR_URING, &req->flags); + req->ring_queue = queue; list_add_tail(&req->list, &queue->fuse_req_bg_queue); ent = list_first_entry_or_null(&queue->ent_avail_queue, @@ -1306,6 +1306,13 @@ bool fuse_uring_queue_bq_req(struct fuse_req *req) return true; } +bool fuse_uring_remove_pending_req(struct fuse_req *req) +{ + struct fuse_ring_queue *queue = req->ring_queue; + + return fuse_remove_pending_req(req, &queue->lock); +} + static const struct fuse_iqueue_ops fuse_io_uring_ops = { /* should be send over io-uring as enhancement */ .send_forget = fuse_dev_queue_forget, diff --git a/fs/fuse/dev_uring_i.h b/fs/fuse/dev_uring_i.h index 0d67738850c5a..d87d0a55cb5f8 100644 --- a/fs/fuse/dev_uring_i.h +++ b/fs/fuse/dev_uring_i.h @@ -142,6 +142,7 @@ void fuse_uring_abort_end_requests(struct fuse_ring *ring); int fuse_uring_cmd(struct io_uring_cmd *cmd, unsigned int issue_flags); void fuse_uring_queue_fuse_req(struct fuse_iqueue *fiq, struct fuse_req *req); bool fuse_uring_queue_bq_req(struct fuse_req *req); +bool fuse_uring_remove_pending_req(struct fuse_req *req); static inline void fuse_uring_abort(struct fuse_conn *fc) { @@ -194,6 +195,11 @@ static inline bool fuse_uring_ready(struct fuse_conn *fc) return false; } +static inline bool fuse_uring_remove_pending_req(struct fuse_req *req) +{ + return false; +} + #endif /* CONFIG_FUSE_IO_URING */ #endif /* _FS_FUSE_DEV_URING_I_H */ diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h index 3b2bfe1248d35..2481da3388c5f 100644 --- a/fs/fuse/fuse_dev_i.h +++ b/fs/fuse/fuse_dev_i.h @@ -61,6 +61,7 @@ int fuse_copy_out_args(struct fuse_copy_state *cs, struct fuse_args *args, void fuse_dev_queue_forget(struct fuse_iqueue *fiq, struct fuse_forget_link *forget); void fuse_dev_queue_interrupt(struct fuse_iqueue *fiq, struct fuse_req *req); +bool fuse_remove_pending_req(struct fuse_req *req, spinlock_t *lock); #endif diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index b8f2200f884fe..7cd634f497411 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -375,6 +375,7 @@ struct fuse_io_priv { * FR_FINISHED: request is finished * FR_PRIVATE: request is on private list * FR_ASYNC: request is asynchronous + * FR_URING: request is handled through fuse-io-uring */ enum fuse_req_flag { FR_ISREPLY, @@ -389,6 +390,7 @@ enum fuse_req_flag { FR_FINISHED, FR_PRIVATE, FR_ASYNC, + FR_URING, }; /** @@ -438,6 +440,7 @@ struct fuse_req { #ifdef CONFIG_FUSE_IO_URING void *ring_entry; + void *ring_queue; #endif }; From 09488fe618e0d390888f0dca3f4c199ed9793d59 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 5 Mar 2026 16:11:39 -0500 Subject: [PATCH 22/41] fuse: remove unneeded atomic set in uring creation jira SECO-478 RFE: FUSE_IO_URING commit-author Joanne Koong commit 2d066800a4276340a97acc75c148892eb6f8781a When the ring is allocated, it is kzalloc-ed. ring->queue_refs will already be initialized to 0 by default. It does not need to be atomically set to 0. Signed-off-by: Joanne Koong Reviewed-by: Bernd Schubert Signed-off-by: Miklos Szeredi (cherry picked from commit 2d066800a4276340a97acc75c148892eb6f8781a) Signed-off-by: Jonathan Maple --- fs/fuse/dev_uring.c | 1 - 1 file changed, 1 deletion(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 5f1566c69ddcc..3dbf2e8c35670 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -211,7 +211,6 @@ static struct fuse_ring *fuse_uring_create(struct fuse_conn *fc) ring->nr_queues = nr_queues; ring->fc = fc; ring->max_payload_sz = max_payload_size; - atomic_set(&ring->queue_refs, 0); smp_store_release(&fc->ring, ring); spin_unlock(&fc->lock); From 608d474fdd1e28ffc5ad52587c09630c9bfa55bd Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Tue, 21 Oct 2025 22:46:42 +0200 Subject: [PATCH 23/41] fuse: missing copy_finish in fuse-over-io-uring argument copies jira SECO-478 cve CVE-2025-68791 RFE: FUSE_IO_URING commit-author Cheng Ding commit 6e0d7f7f4a43ac8868e98c87ecf48805aa8c24dd upstream-diff | Context diffs from missing upstream type changes (bool write, is_uring = true) and surrounding declarations Fix a possible reference count leak of payload pages during fuse argument copies. [Joanne: simplified error cleanup] Fixes: c090c8abae4b ("fuse: Add io-uring sqe commit and fetch support") Cc: stable@vger.kernel.org # v6.14 Signed-off-by: Cheng Ding Signed-off-by: Bernd Schubert Reviewed-by: Joanne Koong Signed-off-by: Miklos Szeredi (cherry picked from commit 6e0d7f7f4a43ac8868e98c87ecf48805aa8c24dd) Signed-off-by: Brett Mastbergen --- fs/fuse/dev.c | 2 +- fs/fuse/dev_uring.c | 5 ++++- fs/fuse/fuse_dev_i.h | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index d327e93837ea4..2857559d736b0 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -731,7 +731,7 @@ void fuse_copy_init(struct fuse_copy_state *cs, int write, } /* Unmap and put previous page of userspace buffer */ -static void fuse_copy_finish(struct fuse_copy_state *cs) +void fuse_copy_finish(struct fuse_copy_state *cs) { if (cs->currbuf) { struct pipe_buffer *buf = cs->currbuf; diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 3dbf2e8c35670..7e2fd7e19dad1 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -554,7 +554,9 @@ static int fuse_uring_copy_from_ring(struct fuse_ring *ring, cs.is_uring = 1; cs.req = req; - return fuse_copy_out_args(&cs, args, ring_in_out.payload_sz); + err = fuse_copy_out_args(&cs, args, ring_in_out.payload_sz); + fuse_copy_finish(&cs); + return err; } /* @@ -605,6 +607,7 @@ static int fuse_uring_args_to_ring(struct fuse_ring *ring, struct fuse_req *req, /* copy the payload */ err = fuse_copy_args(&cs, num_args, args->in_pages, (struct fuse_arg *)in_args, 0); + fuse_copy_finish(&cs); if (err) { pr_info_ratelimited("%s fuse_copy_args failed\n", __func__); return err; diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h index 2481da3388c5f..f3ce625351858 100644 --- a/fs/fuse/fuse_dev_i.h +++ b/fs/fuse/fuse_dev_i.h @@ -53,6 +53,7 @@ void fuse_dev_end_requests(struct list_head *head); void fuse_copy_init(struct fuse_copy_state *cs, int write, struct iov_iter *iter); +void fuse_copy_finish(struct fuse_copy_state *cs); int fuse_copy_args(struct fuse_copy_state *cs, unsigned int numargs, unsigned int argpages, struct fuse_arg *args, int zeroing); From f3c6cd9b6dbbfcb8245a1b711c19b65747778a00 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Tue, 25 Nov 2025 10:13:47 -0800 Subject: [PATCH 24/41] fuse: fix io-uring list corruption for terminated non-committed requests jira SECO-478 cve CVE-2025-68805 RFE: FUSE_IO_URING commit-author Joanne Koong commit 95c39eef7c2b666026c69ab5b30471da94ea2874 When a request is terminated before it has been committed, the request is not removed from the queue's list. This leaves a dangling list entry that leads to list corruption and use-after-free issues. Remove the request from the queue's list for terminated non-committed requests. Signed-off-by: Joanne Koong Fixes: c090c8abae4b ("fuse: Add io-uring sqe commit and fetch support") Cc: stable@vger.kernel.org Reviewed-by: Bernd Schubert Signed-off-by: Miklos Szeredi (cherry picked from commit 95c39eef7c2b666026c69ab5b30471da94ea2874) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 7e2fd7e19dad1..de706aa363db3 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -85,6 +85,7 @@ static void fuse_uring_req_end(struct fuse_ring_ent *ent, struct fuse_req *req, lockdep_assert_not_held(&queue->lock); spin_lock(&queue->lock); ent->fuse_req = NULL; + list_del_init(&req->list); if (test_bit(FR_BACKGROUND, &req->flags)) { queue->active_background--; spin_lock(&fc->bg_lock); From 00086a77d287d6efaa96c894a40d05240e5bce40 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Fri, 5 Jun 2026 12:27:06 -0700 Subject: [PATCH 25/41] fuse-uring: fix EFAULT clobber in fuse_uring_commit jira SECO-478 cve CVE-2026-64264 RFE: FUSE_IO_URING commit-author Chris Mason commit 3a0a8bc51a13951c5141262bf770eeea3e0b6228 copy_from_user() returns the number of bytes not copied as an unsigned residual on failure (1..sizeof(struct fuse_out_header)). fuse_uring_commit stores that residual in ssize_t err, sets req->out.h.error to -EFAULT, then jumps to out: with err still holding the positive residual. err = copy_from_user(&req->out.h, &ent->headers->in_out, sizeof(req->out.h)); if (err) { req->out.h.error = -EFAULT; goto out; /* err is the positive residual */ } ... out: fuse_uring_req_end(ent, req, err); fuse_uring_req_end() then runs if (error) req->out.h.error = error; which overwrites the just-assigned -EFAULT with the positive residual. FUSE callers such as fuse_simple_request() test err < 0 to detect failure, so the positive value is interpreted as success and the caller proceeds with an uninitialised or partial req->out.args. Fix by assigning err = -EFAULT in the failure branch before jumping to out, so fuse_uring_req_end() receives a negative errno and sets req->out.h.error to -EFAULT. Fixes: c090c8abae4b ("fuse: Add io-uring sqe commit and fetch support") Cc: stable@vger.kernel.org Reviewed-by: Joanne Koong Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason Reviewed-by: Bernd Schubert Signed-off-by: Miklos Szeredi (cherry picked from commit 3a0a8bc51a13951c5141262bf770eeea3e0b6228) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index de706aa363db3..7a1f3f33e157e 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -773,14 +773,11 @@ static void fuse_uring_commit(struct fuse_ring_ent *ent, struct fuse_req *req, { struct fuse_ring *ring = ent->queue->ring; struct fuse_conn *fc = ring->fc; - ssize_t err = 0; + ssize_t err = -EFAULT; - err = copy_from_user(&req->out.h, &ent->headers->in_out, - sizeof(req->out.h)); - if (err) { - req->out.h.error = -EFAULT; + if (copy_from_user(&req->out.h, &ent->headers->in_out, + sizeof(req->out.h))) goto out; - } err = fuse_uring_out_header_has_err(&req->out.h, req, fc); if (err) { From c41990082d768417fc3a0081e2157b39c071809f Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Fri, 5 Jun 2026 12:27:07 -0700 Subject: [PATCH 26/41] fuse-uring: fix data races on ring->ready jira SECO-478 cve CVE-2026-64588 RFE: FUSE_IO_URING commit-author Chris Mason commit 46725a0056c884cf58a6897f222892807327d82d On weakly-ordered architectures, the store to fiq->ops can be reordered past the store to ring->ready, allowing a CPU that sees ring->ready == true via fuse_uring_ready() to dispatch requests through a stale fiq->ops pointer. Upgrade the store to smp_store_release() and the load in fuse_uring_ready() to smp_load_acquire() so that the preceding WRITE_ONCE(fiq->ops, ...) is visible to any CPU that observes ring->ready == true. Additionally, fuse_uring_do_register() publishes ring->ready with WRITE_ONCE() but the fast-path check reads it with a plain load. This is a marked-vs-unmarked access that KCSAN will flag. Wrap it in READ_ONCE() to mark it without adding unnecessary ordering. Also wrap the fc->ring load in fuse_uring_ready() in READ_ONCE() to prevent the compiler from reloading it between the NULL check and the dereference. Fixes: c2c9af9a0b13 ("fuse: Allow to queue fg requests through io-uring") Cc: stable@vger.kernel.org Reviewed-by: Joanne Koong Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason Reviewed-by: Bernd Schubert Signed-off-by: Miklos Szeredi (cherry picked from commit 46725a0056c884cf58a6897f222892807327d82d) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 4 ++-- fs/fuse/dev_uring_i.h | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 7a1f3f33e157e..2da1a4bc9d41e 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -947,12 +947,12 @@ static void fuse_uring_do_register(struct fuse_ring_ent *ent, fuse_uring_ent_avail(ent, queue); spin_unlock(&queue->lock); - if (!ring->ready) { + if (!READ_ONCE(ring->ready)) { bool ready = is_ring_ready(ring, queue->qid); if (ready) { WRITE_ONCE(fiq->ops, &fuse_io_uring_ops); - WRITE_ONCE(ring->ready, true); + smp_store_release(&ring->ready, true); wake_up_all(&fc->blocked_waitq); } } diff --git a/fs/fuse/dev_uring_i.h b/fs/fuse/dev_uring_i.h index d87d0a55cb5f8..3cfdae0bbf08d 100644 --- a/fs/fuse/dev_uring_i.h +++ b/fs/fuse/dev_uring_i.h @@ -168,7 +168,9 @@ static inline void fuse_uring_wait_stopped_queues(struct fuse_conn *fc) static inline bool fuse_uring_ready(struct fuse_conn *fc) { - return fc->ring && fc->ring->ready; + struct fuse_ring *ring = READ_ONCE(fc->ring); + + return ring && smp_load_acquire(&ring->ready); } #else /* CONFIG_FUSE_IO_URING */ From bb833f955423e9f08795e6c702b689c17df3a977 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Mon, 8 Jun 2026 12:21:47 -0700 Subject: [PATCH 27/41] fuse-uring: fix race between registration and connection abortion jira SECO-478 cve CVE-2026-68095 RFE: FUSE_IO_URING commit-author Joanne Koong commit 952b5d36f6a298f57c52a59e72076c69386a8aaf upstream-diff | fch->lock/fch->connected changed to fc->lock/fc->connected (fuse_chan struct not present in this kernel) This fixes this race: - thread a: io_uring_enter -> register sqe -> fuse_uring_create_ring_ent -> allocate ent but doesn't grab queue_ref yet - thread b: fuse_conn_destroy() -> fuse_chan_abort() -> fuse_uring_abort() is a no-op due to queue ref being 0 - thread a: grabs the queue_ref, queue_ref is now 1, rest of fuse_uring_do_register() logic executes - thread b: fuse_chan_abort() returns, fuse_chan_wait_aborted() now runs and calls "wait_event(ring->stop_waitq, atomic_read(&ring->queue_refs) == 0);" The abort/unmount thread will hang indefinitely in unkillable state as nothing will decrement queue_refs or wake stop_waitq, and the ring, queue, and ent are leaked. Fix this by checking fch->connected under fch->lock after the created ent has grabbed a ref count on the queue. This ensures that in the scenario above, it is guaranteed that we either release the queue ref and wake up stop_waitq (in case fuse_chan_wait_aborted() is already waiting) in fuse_uring_do_register() when we detect !fch->connected, or if the connection is aborted after the check, it is guaranteed that the async teardown worker will be running in the background cleaning up ents and decrementing the ent's ref on the queue, which will unblock the eventual queue and ring teardown. Fixes: 24fe962c86f5 ("fuse: {io-uring} Handle SQEs - register commands") Cc: stable@vger.kernel.org Reviewed-by: Bernd Schubert Signed-off-by: Joanne Koong Signed-off-by: Miklos Szeredi (cherry picked from commit 952b5d36f6a298f57c52a59e72076c69386a8aaf) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 2da1a4bc9d41e..99a53dee6b139 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -931,15 +931,26 @@ static bool is_ring_ready(struct fuse_ring *ring, int current_qid) /* * fuse_uring_req_fetch command handling */ -static void fuse_uring_do_register(struct fuse_ring_ent *ent, - struct io_uring_cmd *cmd, - unsigned int issue_flags) +static int fuse_uring_do_register(struct fuse_ring_ent *ent, + struct io_uring_cmd *cmd, + unsigned int issue_flags) { struct fuse_ring_queue *queue = ent->queue; struct fuse_ring *ring = queue->ring; struct fuse_conn *fc = ring->fc; struct fuse_iqueue *fiq = &fc->iq; + spin_lock(&fc->lock); + /* abort teardown path is running or has run */ + if (!fc->connected) { + spin_unlock(&fc->lock); + if (atomic_dec_and_test(&ring->queue_refs)) + wake_up_all(&ring->stop_waitq); + kfree(ent); + return -ECONNABORTED; + } + spin_unlock(&fc->lock); + fuse_uring_prepare_cancel(cmd, issue_flags, ent); spin_lock(&queue->lock); @@ -956,6 +967,7 @@ static void fuse_uring_do_register(struct fuse_ring_ent *ent, wake_up_all(&fc->blocked_waitq); } } + return 0; } /* @@ -1071,9 +1083,7 @@ static int fuse_uring_register(struct io_uring_cmd *cmd, if (IS_ERR(ent)) return PTR_ERR(ent); - fuse_uring_do_register(ent, cmd, issue_flags); - - return 0; + return fuse_uring_do_register(ent, cmd, issue_flags); } /* From 3e530bf4c74af9635658157944d957fb3fd76559 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Thu, 13 Aug 2026 12:06:35 -0400 Subject: [PATCH 28/41] fuse-uring: check connection abort during ring creation jira SECO-478 RFE: FUSE_IO_URING commit-author Joanne Koong commit c146284c4355e96550e50e8f6e694223d107b621 upstream-diff | fch->lock/fch->connected changed to fc->lock/fc->connected in fuse_uring_do_register() already applied in 703224b. Only the fuse_uring_create() hunk is new. Check fc->connected under fc->lock in fuse_uring_create() before attaching a new ring. Without this, a race between fuse_uring_create() and fuse_conn_destroy() can result in the ring, queue, and fpq.processing table being created after fuse_uring_abort() has already run, leading to unnecessary allocation and teardown. These are eventually cleaned up by fuse_uring_destruct() but will linger until the process exits, even with the connection aborted. Reviewed-by: Bernd Schubert Signed-off-by: Joanne Koong Signed-off-by: Miklos Szeredi (cherry picked from commit c146284c4355e96550e50e8f6e694223d107b621) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 99a53dee6b139..d804e3d5ece5d 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -200,6 +200,10 @@ static struct fuse_ring *fuse_uring_create(struct fuse_conn *fc) max_payload_size = max(max_payload_size, fc->max_pages * PAGE_SIZE); spin_lock(&fc->lock); + if (!fc->connected) { + spin_unlock(&fc->lock); + goto out_err; + } if (fc->ring) { /* race, another thread created the ring in the meantime */ spin_unlock(&fc->lock); From cb8f748a407ba1a855a431e757917bca3659ee4f Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Thu, 13 Aug 2026 12:07:57 -0400 Subject: [PATCH 29/41] fuse-uring: fix moving cancelled entry to ent_in_userspace list jira SECO-478 cve CVE-2026-64263 RFE: FUSE_IO_URING commit-author Joanne Koong commit 198f45eeb9f78b2a2d6d8be95e4e43468eb2c6bc upstream-diff | io_uring_cmd_done() uses 4 args (res2=0) vs upstream 3 args. list_move used instead of list_move_tail. fuse_uring_cancel() moves entries that are available (these have no reqs attached) to the ent_in_userspace list. ent_list_request_expired() checks the first entry on ent_in_userspace and dereferences ent->fuse_req unconditionally, which will crash on a cancelled entry that was moved to this list. Fix this by freeing the entry and dropping queue_refs directly in fuse_uring_cancel(). This is safe because cancel is the cancel handler itself - after io_uring_cmd_done(), no more cancels will be dispatched for this command, and teardown serializes with cancel via queue->lock. Since cancel now decrements queue_refs, fuse_uring_abort() must no longer gate fuse_uring_abort_end_requests() on queue_refs > 0, as cancelled entries may have already dropped queue_refs while requests are still queued. Remove the gate so abort always flushes requests and stops queues. Reported-by: Heechan Kang Tested-by: Heechan Kang Reviewed-by: Bernd Schubert Fixes: 4fea593e625c ("fuse: optimize over-io-uring request expiration check") Cc: stable@vger.kernel.org Suggested-by: Jian Huang Li Suggested-by: Horst Birthelmer Signed-off-by: Joanne Koong Signed-off-by: Miklos Szeredi (cherry picked from commit 198f45eeb9f78b2a2d6d8be95e4e43468eb2c6bc) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 6 ++++-- fs/fuse/dev_uring_i.h | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index d804e3d5ece5d..d9ea6aa58bc6d 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -471,8 +471,7 @@ static void fuse_uring_cancel(struct io_uring_cmd *cmd, queue = ent->queue; spin_lock(&queue->lock); if (ent->state == FRRS_AVAILABLE) { - ent->state = FRRS_USERSPACE; - list_move(&ent->list, &queue->ent_in_userspace); + list_del_init(&ent->list); need_cmd_done = true; ent->cmd = NULL; } @@ -481,6 +480,9 @@ static void fuse_uring_cancel(struct io_uring_cmd *cmd, if (need_cmd_done) { /* no queue lock to avoid lock order issues */ io_uring_cmd_done(cmd, -ENOTCONN, 0, issue_flags); + kfree(ent); + if (atomic_dec_and_test(&queue->ring->queue_refs)) + wake_up_all(&queue->ring->stop_waitq); } } diff --git a/fs/fuse/dev_uring_i.h b/fs/fuse/dev_uring_i.h index 3cfdae0bbf08d..89d96a06ddd4f 100644 --- a/fs/fuse/dev_uring_i.h +++ b/fs/fuse/dev_uring_i.h @@ -151,10 +151,10 @@ static inline void fuse_uring_abort(struct fuse_conn *fc) if (ring == NULL) return; - if (atomic_read(&ring->queue_refs) > 0) { - fuse_uring_abort_end_requests(ring); + fuse_uring_abort_end_requests(ring); + + if (atomic_read(&ring->queue_refs) > 0) fuse_uring_stop_queues(ring); - } } static inline void fuse_uring_wait_stopped_queues(struct fuse_conn *fc) From 994591e5e66b363fafe01571ba096d92599960a2 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Mon, 8 Jun 2026 17:28:55 -0700 Subject: [PATCH 30/41] fuse-uring: end fuse_req on io-uring cancel task work jira SECO-478 cve CVE-2026-64262 RFE: FUSE_IO_URING commit-author Chris Mason commit bea4fe98204b6ce7eb8e29f7bf867dd7619b3ddd upstream-diff | fuse_uring_send_in_task has different signature (missing io_tw_req/io_tw_token_t API in this kernel) io_uring_cmd_done() call corrected to pass 4 args (res2=0 was missing in upstream adaptation) When io_uring delivers task work with tw.cancel set (PF_EXITING, PF_KTHREAD fallback, or percpu_ref_is_dying on the ring context), fuse_uring_send_in_task() takes the cancel branch, assigns -ECANCELED, and falls through to fuse_uring_send(). That path only flips the entry to FRRS_USERSPACE and completes the io_uring cmd; it never discharges the ring entry's owning reference to the fuse_req that fuse_uring_add_req_to_ring_ent() handed it at dispatch time. fuse_uring_send_in_task() tw.cancel == true err = -ECANCELED fuse_uring_send(ent, cmd, err, issue_flags) ent->state = FRRS_USERSPACE list_move(&ent->list, &queue->ent_in_userspace) ent->cmd = NULL io_uring_cmd_done(-ECANCELED) /* ent->fuse_req still set, req still hashed */ The fuse_req stays linked on fpq->processing[hash] and fuse_request_end() is never invoked. The originating syscall thread blocks in D-state in request_wait_answer() until fuse_abort_conn() runs, which can be the entire connection lifetime. For FR_BACKGROUND requests fc->num_background is never decremented either, so repeated cancels inflate the counter until max_background is hit and all later background ops stall. tw.cancel does not imply a connection abort (e.g. a single io_uring worker thread exits while the fuse connection stays up), so this cannot be left for fuse_abort_conn() to clean up. Ending the req but still routing the entry through fuse_uring_send() is not enough: that leaves a req-less entry on ent_in_userspace, and ent_list_request_expired() dereferences ent->fuse_req unconditionally on the head of that list, which would then NULL-deref. Fix the cancel branch to release the entry directly. Remove it from the queue, complete the io_uring cmd, end the fuse_req, free the entry, and drop its queue_refs (waking the teardown waiter if it was the last). Fixes: c2c9af9a0b13 ("fuse: Allow to queue fg requests through io-uring") Cc: stable@vger.kernel.org Reviewed-by: Joanne Koong Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason Signed-off-by: Miklos Szeredi (cherry picked from commit bea4fe98204b6ce7eb8e29f7bf867dd7619b3ddd) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index d9ea6aa58bc6d..446c01601267e 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -1195,11 +1195,21 @@ static void fuse_uring_send_in_task(struct io_uring_cmd *cmd, fuse_uring_next_fuse_req(ent, queue, issue_flags); return; } + fuse_uring_send(ent, cmd, err, issue_flags); } else { err = -ECANCELED; - } - fuse_uring_send(ent, cmd, err, issue_flags); + spin_lock(&queue->lock); + list_del_init(&ent->list); + spin_unlock(&queue->lock); + + io_uring_cmd_done(cmd, err, 0, issue_flags); + + fuse_uring_req_end(ent, ent->fuse_req, err); + kfree(ent); + if (atomic_dec_and_test(&queue->ring->queue_refs)) + wake_up_all(&queue->ring->stop_waitq); + } } static struct fuse_ring_queue *fuse_uring_task_to_queue(struct fuse_ring *ring) From 01544dcb94e32b6405072b55adde09ad7a9b09eb Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Thu, 13 Aug 2026 14:21:37 -0400 Subject: [PATCH 31/41] fuse-uring: Avoid use-after-free in fuse_uring_async_stop_queues jira SECO-478 cve CVE-2026-64261 RFE: FUSE_IO_URING commit-author Bernd Schubert commit d351da75066955144515cb2f9aa959f24a04287a upstream-diff | ring->fc used instead of ring->chan->conn fuse_uring_async_stop_queues() might run when the last reference on ring->queue_refs was already dropped. In order to avoid an early destruction a reference on struct fuse_conn is now taken before starting fuse_uring_async_stop_queues() and that reference is only released when that delayed work queue terminates. Fixes: 4a9bfb9b6850 ("fuse: {io-uring} Handle teardown of ring entries") Cc: stable@kernel.org # 6.14 Reported-by: Berkant Koc Signed-off-by: Bernd Schubert Reviewed-by: Joanne Koong Signed-off-by: Miklos Szeredi (cherry picked from commit d351da75066955144515cb2f9aa959f24a04287a) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 446c01601267e..56d33ba5b843f 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -421,6 +421,7 @@ static void fuse_uring_async_stop_queues(struct work_struct *work) FUSE_URING_TEARDOWN_INTERVAL); } else { wake_up_all(&ring->stop_waitq); + fuse_conn_put(ring->fc); } } @@ -441,6 +442,7 @@ void fuse_uring_stop_queues(struct fuse_ring *ring) } if (atomic_read(&ring->queue_refs) > 0) { + fuse_conn_get(ring->fc); ring->teardown_time = jiffies; INIT_DELAYED_WORK(&ring->async_teardown_work, fuse_uring_async_stop_queues); From b5851809bd2b3128d4ca3184e57915481eb819be Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Thu, 13 Aug 2026 15:08:13 -0400 Subject: [PATCH 32/41] fuse-uring: Avoid queue->stopped races and set/read that value under lock jira SECO-478 cve CVE-2026-64260 RFE: FUSE_IO_URING commit-author Bernd Schubert commit b70a3aca16934c196f92abb17b01c1647b9bb63c upstream-diff | ring->fc used instead of ring->chan->conn There are several readers of queue->stopped that check the value under lock, but fuse_uring_commit_fetch() did not and actually the value was not set under the lock in fuse_uring_abort_end_requests() either. Especially in fuse_uring_commit_fetch it is important to check under a lock, because due to races 'struct fuse_req' might be freed with fuse_request_end, but another thread/cpu might already do teardown work. Cc: stable@kernel.org # 6.14 Fixes: 4a9bfb9b6850fec ("fuse: {io-uring} Handle teardown of ring entries") Reported-by: Berkant Koc Reported-by: xlabai Signed-off-by: Bernd Schubert Reviewed-by: Joanne Koong Signed-off-by: Miklos Szeredi (cherry picked from commit b70a3aca16934c196f92abb17b01c1647b9bb63c) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 56d33ba5b843f..7f9b77ee3961a 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -129,10 +129,9 @@ void fuse_uring_abort_end_requests(struct fuse_ring *ring) if (!queue) continue; - queue->stopped = true; - WARN_ON_ONCE(ring->fc->max_background != UINT_MAX); spin_lock(&queue->lock); + queue->stopped = true; spin_lock(&fc->bg_lock); fuse_uring_flush_bg(queue); spin_unlock(&fc->bg_lock); @@ -862,10 +861,15 @@ static int fuse_uring_commit_fetch(struct io_uring_cmd *cmd, int issue_flags, return err; fpq = &queue->fpq; - if (!READ_ONCE(fc->connected) || READ_ONCE(queue->stopped)) + if (!READ_ONCE(fc->connected)) return err; spin_lock(&queue->lock); + if (unlikely(queue->stopped)) { + spin_unlock(&queue->lock); + return err; + } + /* Find a request based on the unique ID of the fuse request * This should get revised, as it needs a hash calculation and list * search. And full struct fuse_pqueue is needed (memory overhead). From 92b0214e53ace4cec4285f4941ba57af9bca29e8 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Mon, 8 Jun 2026 23:03:45 +0200 Subject: [PATCH 33/41] fuse-uring: make a fuse_req on SQE commit only findable after memcpy jira SECO-478 cve CVE-2026-64259 RFE: FUSE_IO_URING commit-author Bernd Schubert commit 1efd3d474fc0ba74dfd984249bca78807d739812 upstream-diff | list_move used instead of list_move_tail and io_uring_cmd_done has extra arg (older API in this kernel) Bad userspace might try to trick us and send commit SQEs request unique / commit-id of requests that are not even send to fuse-server (io_uring_cmd_done() not called) yet. fuse_uring_commit_fetch() ends the fuse request when the ring entry has a wrong state, but that could have caused a use-after-free with the memcpy operations in fuse_uring_send_in_task(). In order to avoid such races the call of fuse_uring_add_to_pq() is moved after the copy operations and just before completing the io-uring request - malicious userspace cannot find the request anymore until all prepration work in fuse-client/kernel is completed. This also moves fuse_uring_add_to_pq() a bit up in the code to avoid a forward declaration. Also not with a preparation commit, to make it easier to back port to older kernels. Reported-by: xlabai Reported-by: Berkant Koc Fixes: c090c8abae4b6b ("fuse: Add io-uring sqe commit and fetch support") Cc: stable@kernel.org # 6.14 Signed-off-by: Bernd Schubert Reviewed-by: Joanne Koong Signed-off-by: Miklos Szeredi (cherry picked from commit 1efd3d474fc0ba74dfd984249bca78807d739812) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 7f9b77ee3961a..ce1fea0416856 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -677,6 +677,19 @@ static int fuse_uring_prepare_send(struct fuse_ring_ent *ent, return err; } +/* Used to find the request on SQE commit */ +static void fuse_uring_add_to_pq(struct fuse_ring_ent *ent) +{ + struct fuse_ring_queue *queue = ent->queue; + struct fuse_pqueue *fpq = &queue->fpq; + unsigned int hash; + struct fuse_req *req = ent->fuse_req; + + req->ring_entry = ent; + hash = fuse_req_hash(req->in.h.unique); + list_move_tail(&req->list, &fpq->processing[hash]); +} + /* * Write data to the ring buffer and send the request to userspace, * userspace will read it @@ -699,6 +712,7 @@ static int fuse_uring_send_next_to_ring(struct fuse_ring_ent *ent, ent->cmd = NULL; ent->state = FRRS_USERSPACE; list_move(&ent->list, &queue->ent_in_userspace); + fuse_uring_add_to_pq(ent); spin_unlock(&queue->lock); io_uring_cmd_done(cmd, 0, 0, issue_flags); @@ -716,19 +730,6 @@ static void fuse_uring_ent_avail(struct fuse_ring_ent *ent, ent->state = FRRS_AVAILABLE; } -/* Used to find the request on SQE commit */ -static void fuse_uring_add_to_pq(struct fuse_ring_ent *ent, - struct fuse_req *req) -{ - struct fuse_ring_queue *queue = ent->queue; - struct fuse_pqueue *fpq = &queue->fpq; - unsigned int hash; - - req->ring_entry = ent; - hash = fuse_req_hash(req->in.h.unique); - list_move_tail(&req->list, &fpq->processing[hash]); -} - /* * Assign a fuse queue entry to the given entry */ @@ -746,10 +747,13 @@ static void fuse_uring_add_req_to_ring_ent(struct fuse_ring_ent *ent, } clear_bit(FR_PENDING, &req->flags); + + /* Until fuse_uring_add_to_pq() the req is not attached to any list */ + list_del_init(&req->list); + ent->fuse_req = req; ent->state = FRRS_FUSE_REQ; list_move(&ent->list, &queue->ent_w_req_queue); - fuse_uring_add_to_pq(ent, req); } /* Fetch the next fuse request if available */ @@ -1178,6 +1182,7 @@ static void fuse_uring_send(struct fuse_ring_ent *ent, struct io_uring_cmd *cmd, ent->state = FRRS_USERSPACE; list_move(&ent->list, &queue->ent_in_userspace); ent->cmd = NULL; + fuse_uring_add_to_pq(ent); spin_unlock(&queue->lock); io_uring_cmd_done(cmd, ret, 0, issue_flags); From 589bf8d9376c826ba9f737744fee1fc07cb5dc75 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Thu, 13 Aug 2026 12:13:10 -0400 Subject: [PATCH 34/41] fuse-uring: remove request-less entries from ent_w_req_queue to fix NULL deref jira SECO-478 cve CVE-2026-64258 RFE: FUSE_IO_URING commit-author Joanne Koong commit 1c57a69be962d459c5e705f5cb4355b841b3461c If a copy into the userspace ring buffer fails, a request will be terminated and fuse_uring_req_end() will set ent->fuse_req to NULL but it will leave the entry on ent_w_req_queue in FRRS_FUSE_REQ state. This can lead to a NULL deref if the request expiration logic scans ent_w_req_queue in the window before the entry is moved off it. Fix this by taking the entry off ent_w_req_queue and changing its state from FRRS_FUSE_REQ to FRRS_INVALID before terminating the request. Fixes: 4fea593e625c ("fuse: optimize over-io-uring request expiration check") Cc: stable@kernel.org Signed-off-by: Joanne Koong Signed-off-by: Miklos Szeredi (cherry picked from commit 1c57a69be962d459c5e705f5cb4355b841b3461c) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index ce1fea0416856..80c38e44d1ee8 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -669,10 +669,20 @@ static int fuse_uring_prepare_send(struct fuse_ring_ent *ent, int err; err = fuse_uring_copy_to_ring(ent, req); - if (!err) + if (!err) { set_bit(FR_SENT, &req->flags); - else + } else { + /* + * Copying the request failed. Remove the entry from the + * ent_w_req_queue list and terminate the request + */ + spin_lock(&ent->queue->lock); + list_del_init(&ent->list); + ent->state = FRRS_INVALID; + spin_unlock(&ent->queue->lock); + fuse_uring_req_end(ent, req, err); + } return err; } From 7c3b7f8d56f4c480c3a6fb60780a7dfb89f9aecd Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Wed, 8 Apr 2026 10:25:10 -0700 Subject: [PATCH 35/41] fuse: fix io-uring background queue dispatch on request completion jira SECO-478 RFE: FUSE_IO_URING commit-author Joanne Koong commit 31da059891bd3be9c6e59280b8e1777ead90db34 upstream-diff | fuse_copy_init uses int write vs bool write in this kernel When a background request completes via the io_uring path, the background queue gets flushed to dispatch pending background requests, but this is done before the connection-level background counters (fc->num_background, fc->active_background) are properly accounted, which may reduce effective queue depth to one. The connection-level counters are decremented in fuse_request_end(), but flush_bg_queue() flushes the /dev/fuse path queue (fc->bg_queue), not the io_uring per-queue bg one, which means pending uring background requests on the queue are never dispatched in this path. Fix this by accounting the connection-level background counters first before flushing the queue's background queue. Since fuse_request_bg_finish() clears FR_BACKGROUND, fuse_request_end() will skip the background cleanup branch entirely, which avoids any double-decrements; it will call the wake_up(&req->waitq) branch but this is effectively a no-op as background requests have no waiters on req->waitq. Reviewed-by: Bernd Schubert Fixes: 857b0263f30e ("fuse: Allow to queue bg requests through io-uring") Cc: stable@vger.kernel.org Signed-off-by: Joanne Koong Signed-off-by: Miklos Szeredi (cherry picked from commit 31da059891bd3be9c6e59280b8e1777ead90db34) Signed-off-by: Brett Mastbergen --- fs/fuse/dev.c | 41 ++++++++++++++++++++++++----------------- fs/fuse/dev_uring.c | 1 + fs/fuse/fuse_dev_i.h | 1 + 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index 2857559d736b0..617891b24b59e 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -332,6 +332,29 @@ static void flush_bg_queue(struct fuse_conn *fc) } } +void fuse_request_bg_finish(struct fuse_conn *fc, struct fuse_req *req) +{ + lockdep_assert_held(&fc->bg_lock); + + clear_bit(FR_BACKGROUND, &req->flags); + if (fc->num_background == fc->max_background) { + fc->blocked = 0; + wake_up(&fc->blocked_waitq); + } else if (!fc->blocked) { + /* + * Wake up next waiter, if any. It's okay to use + * waitqueue_active(), as we've already synced up + * fc->blocked with waiters with the wake_up() call + * above. + */ + if (waitqueue_active(&fc->blocked_waitq)) + wake_up(&fc->blocked_waitq); + } + + fc->num_background--; + fc->active_background--; +} + /* * This function is called when a request is finished. Either a reply * has arrived or it was aborted (and not yet sent) or some error @@ -364,23 +387,7 @@ void fuse_request_end(struct fuse_req *req) WARN_ON(test_bit(FR_SENT, &req->flags)); if (test_bit(FR_BACKGROUND, &req->flags)) { spin_lock(&fc->bg_lock); - clear_bit(FR_BACKGROUND, &req->flags); - if (fc->num_background == fc->max_background) { - fc->blocked = 0; - wake_up(&fc->blocked_waitq); - } else if (!fc->blocked) { - /* - * Wake up next waiter, if any. It's okay to use - * waitqueue_active(), as we've already synced up - * fc->blocked with waiters with the wake_up() call - * above. - */ - if (waitqueue_active(&fc->blocked_waitq)) - wake_up(&fc->blocked_waitq); - } - - fc->num_background--; - fc->active_background--; + fuse_request_bg_finish(fc, req); flush_bg_queue(fc); spin_unlock(&fc->bg_lock); } else { diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 80c38e44d1ee8..311d73b48f12c 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -89,6 +89,7 @@ static void fuse_uring_req_end(struct fuse_ring_ent *ent, struct fuse_req *req, if (test_bit(FR_BACKGROUND, &req->flags)) { queue->active_background--; spin_lock(&fc->bg_lock); + fuse_request_bg_finish(fc, req); fuse_uring_flush_bg(queue); spin_unlock(&fc->bg_lock); } diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h index f3ce625351858..734606ade0acb 100644 --- a/fs/fuse/fuse_dev_i.h +++ b/fs/fuse/fuse_dev_i.h @@ -50,6 +50,7 @@ unsigned int fuse_req_hash(u64 unique); struct fuse_req *fuse_request_find(struct fuse_pqueue *fpq, u64 unique); void fuse_dev_end_requests(struct list_head *head); +void fuse_request_bg_finish(struct fuse_conn *fc, struct fuse_req *req); void fuse_copy_init(struct fuse_copy_state *cs, int write, struct iov_iter *iter); From f0bd8a7145c3f22537ff5f6b5934a1036c404158 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Thu, 13 Aug 2026 12:17:06 -0400 Subject: [PATCH 36/41] fuse-uring: clear ent->fuse_req in commit_fetch error path jira SECO-478 RFE: FUSE_IO_URING commit-author Zhenghang Xiao commit 7d87a5a284bb34edb3f4e7e312ef403b3385a7b7 fuse_uring_commit_fetch() error path called fuse_request_end(req) without clearing ent->fuse_req when fuse_ring_ent_set_commit() fails. The still-pending fuse_uring_send_in_task() task-work later dereferences the dangling pointer through fuse_uring_prepare_send(), causing a use-after-free. End the request with fuse_uring_req_end(), which handles all conditions already. Annotation/edition by Bernd: The UAF should be fixed by other means already and actually has to be avoided that way. Just checking for ent->fuse_req == NULL in fuse_uring_send_in_task() would be prone to race conditions, because if malicious userspace would commit requests that have passed the NULL check, but are in doing args copy, it would still trigger a use-after-free. Setting ent->fuse_req = NULL in fuse_uring_commit_fetch() still makes sense, though. Reported-by: Shuvam Pandey Reported-by: Berkant Koc Signed-off-by: Zhenghang Xiao Signed-off-by: Bernd Schubert Reviewed-by: Joanne Koong Signed-off-by: Miklos Szeredi (cherry picked from commit 7d87a5a284bb34edb3f4e7e312ef403b3385a7b7) Signed-off-by: Brett Mastbergen --- fs/fuse/dev_uring.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 311d73b48f12c..d195279f4e6cb 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -907,9 +907,7 @@ static int fuse_uring_commit_fetch(struct io_uring_cmd *cmd, int issue_flags, pr_info_ratelimited("qid=%d commit_id %llu state %d", queue->qid, commit_id, ent->state); spin_unlock(&queue->lock); - req->out.h.error = err; - clear_bit(FR_SENT, &req->flags); - fuse_request_end(req); + fuse_uring_req_end(ent, req, err); return err; } From 03132b2508e8b3355c12d662db41474dfa6d331a Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Mon, 10 Aug 2026 17:47:41 -0400 Subject: [PATCH 37/41] configs: enable CONFIG_FUSE_IO_URING=y on all supported architectures jira SECO-478 RFE: FUSE_IO_URING Enable FUSE io-uring support in all configs that have both prerequisites (CONFIG_FUSE_FS=m and CONFIG_IO_URING=y): x86_64, aarch64, ppc64le, and s390x across all variants (debug, rt, 64k). Skipped configs lacking prerequisites: - kernel-riscv64-*.config (no CONFIG_FUSE_FS or CONFIG_IO_URING) - kernel-s390x-zfcpdump-rhel.config (CONFIG_FUSE_FS is not set) The feature is gated at runtime by the enable_uring module parameter which defaults to disabled. Signed-off-by: Brett Mastbergen --- configs/kernel-aarch64-64k-debug-rhel.config | 1 + configs/kernel-aarch64-64k-rhel.config | 1 + configs/kernel-aarch64-debug-rhel.config | 1 + configs/kernel-aarch64-rhel.config | 1 + configs/kernel-aarch64-rt-64k-debug-rhel.config | 1 + configs/kernel-aarch64-rt-64k-rhel.config | 1 + configs/kernel-aarch64-rt-debug-rhel.config | 1 + configs/kernel-aarch64-rt-rhel.config | 1 + configs/kernel-x86_64-debug-rhel.config | 1 + configs/kernel-x86_64-rhel.config | 1 + configs/kernel-x86_64-rt-debug-rhel.config | 1 + configs/kernel-x86_64-rt-rhel.config | 1 + 12 files changed, 12 insertions(+) diff --git a/configs/kernel-aarch64-64k-debug-rhel.config b/configs/kernel-aarch64-64k-debug-rhel.config index 8c954151d3f05..f3faa3d7dd1b8 100644 --- a/configs/kernel-aarch64-64k-debug-rhel.config +++ b/configs/kernel-aarch64-64k-debug-rhel.config @@ -2300,6 +2300,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set diff --git a/configs/kernel-aarch64-64k-rhel.config b/configs/kernel-aarch64-64k-rhel.config index d7184d179d8a2..5f470f09028db 100644 --- a/configs/kernel-aarch64-64k-rhel.config +++ b/configs/kernel-aarch64-64k-rhel.config @@ -2284,6 +2284,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set diff --git a/configs/kernel-aarch64-debug-rhel.config b/configs/kernel-aarch64-debug-rhel.config index c728fb0f15ace..64f4b0a27a691 100644 --- a/configs/kernel-aarch64-debug-rhel.config +++ b/configs/kernel-aarch64-debug-rhel.config @@ -2297,6 +2297,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set diff --git a/configs/kernel-aarch64-rhel.config b/configs/kernel-aarch64-rhel.config index 7a7f317b61f37..5e99da15a0831 100644 --- a/configs/kernel-aarch64-rhel.config +++ b/configs/kernel-aarch64-rhel.config @@ -2281,6 +2281,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set diff --git a/configs/kernel-aarch64-rt-64k-debug-rhel.config b/configs/kernel-aarch64-rt-64k-debug-rhel.config index fe4a1928f05bd..b9dad62152aed 100644 --- a/configs/kernel-aarch64-rt-64k-debug-rhel.config +++ b/configs/kernel-aarch64-rt-64k-debug-rhel.config @@ -2339,6 +2339,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set diff --git a/configs/kernel-aarch64-rt-64k-rhel.config b/configs/kernel-aarch64-rt-64k-rhel.config index c15f4ee5a01c6..358794debfae8 100644 --- a/configs/kernel-aarch64-rt-64k-rhel.config +++ b/configs/kernel-aarch64-rt-64k-rhel.config @@ -2323,6 +2323,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set diff --git a/configs/kernel-aarch64-rt-debug-rhel.config b/configs/kernel-aarch64-rt-debug-rhel.config index 2bb35d529aafa..7a0d5c21e335b 100644 --- a/configs/kernel-aarch64-rt-debug-rhel.config +++ b/configs/kernel-aarch64-rt-debug-rhel.config @@ -2336,6 +2336,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set diff --git a/configs/kernel-aarch64-rt-rhel.config b/configs/kernel-aarch64-rt-rhel.config index 39be57f9d2ea8..9689925d7f700 100644 --- a/configs/kernel-aarch64-rt-rhel.config +++ b/configs/kernel-aarch64-rt-rhel.config @@ -2320,6 +2320,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set diff --git a/configs/kernel-x86_64-debug-rhel.config b/configs/kernel-x86_64-debug-rhel.config index 2985ba5f55986..9acaed39d2317 100644 --- a/configs/kernel-x86_64-debug-rhel.config +++ b/configs/kernel-x86_64-debug-rhel.config @@ -2167,6 +2167,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set diff --git a/configs/kernel-x86_64-rhel.config b/configs/kernel-x86_64-rhel.config index b390068c4142a..9338ba7f8b268 100644 --- a/configs/kernel-x86_64-rhel.config +++ b/configs/kernel-x86_64-rhel.config @@ -2151,6 +2151,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set diff --git a/configs/kernel-x86_64-rt-debug-rhel.config b/configs/kernel-x86_64-rt-debug-rhel.config index 8b1a48730fb57..b2333db6a99f7 100644 --- a/configs/kernel-x86_64-rt-debug-rhel.config +++ b/configs/kernel-x86_64-rt-debug-rhel.config @@ -2206,6 +2206,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set diff --git a/configs/kernel-x86_64-rt-rhel.config b/configs/kernel-x86_64-rt-rhel.config index 6823de63e269a..ef2e800124764 100644 --- a/configs/kernel-x86_64-rt-rhel.config +++ b/configs/kernel-x86_64-rt-rhel.config @@ -2190,6 +2190,7 @@ CONFIG_FUNCTION_TRACER=y # CONFIG_FUN_ETH is not set CONFIG_FUSE_DAX=y CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y CONFIG_FUSE_PASSTHROUGH=y # CONFIG_FUSION_CTL is not set # CONFIG_FUSION_FC is not set From b1519cc51c4c68dbfe216e2b4142f6042581d188 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Wed, 12 Aug 2026 11:32:53 -0400 Subject: [PATCH 38/41] selftests: filesystems: Add functional test for the abort file in fusectl jira SECO-478 RFE: FUSE_IO_URING commit-author Chen Linxuan commit 1a7b13781b0d48312e0ead2585776b0afd1343cd This patch add a simple functional test for the 'abort' file in fusectlfs (/sys/fs/fuse/connections/ID/abort). A simple fuse daemon is added for testing. Signed-off-by: Chen Linxuan Acked-by: Shuah Khan Reviewed-by: Amir Goldstein Signed-off-by: Miklos Szeredi (cherry picked from commit 1a7b13781b0d48312e0ead2585776b0afd1343cd) Signed-off-by: Brett Mastbergen --- MAINTAINERS | 1 + tools/testing/selftests/Makefile | 1 + .../selftests/filesystems/fuse/.gitignore | 3 + .../selftests/filesystems/fuse/Makefile | 21 +++ .../selftests/filesystems/fuse/fuse_mnt.c | 146 ++++++++++++++++++ .../selftests/filesystems/fuse/fusectl_test.c | 140 +++++++++++++++++ 6 files changed, 312 insertions(+) create mode 100644 tools/testing/selftests/filesystems/fuse/.gitignore create mode 100644 tools/testing/selftests/filesystems/fuse/Makefile create mode 100644 tools/testing/selftests/filesystems/fuse/fuse_mnt.c create mode 100644 tools/testing/selftests/filesystems/fuse/fusectl_test.c diff --git a/MAINTAINERS b/MAINTAINERS index 9523da3c767cb..f297f7cc2936b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9443,6 +9443,7 @@ T: git git://git.kernel.org/pub/scm/linux/kernel/git/mszeredi/fuse.git F: Documentation/filesystems/fuse.rst F: fs/fuse/ F: include/uapi/linux/fuse.h +F: tools/testing/selftests/filesystems/fuse/ FUTEX SUBSYSTEM M: Thomas Gleixner diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index 2207e51e4d079..d48ab0b95dec9 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -32,6 +32,7 @@ TARGETS += filesystems TARGETS += filesystems/binderfs TARGETS += filesystems/epoll TARGETS += filesystems/fat +TARGETS += filesystems/fuse TARGETS += filesystems/overlayfs TARGETS += filesystems/statmount TARGETS += firmware diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore new file mode 100644 index 0000000000000..3e72e742d08e8 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/.gitignore @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0-only +fuse_mnt +fusectl_test diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile new file mode 100644 index 0000000000000..612aad69a93aa --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/Makefile @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) + +TEST_GEN_PROGS := fusectl_test +TEST_GEN_FILES := fuse_mnt + +include ../../lib.mk + +VAR_CFLAGS := $(shell pkg-config fuse --cflags 2>/dev/null) +ifeq ($(VAR_CFLAGS),) +VAR_CFLAGS := -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse +endif + +VAR_LDLIBS := $(shell pkg-config fuse --libs 2>/dev/null) +ifeq ($(VAR_LDLIBS),) +VAR_LDLIBS := -lfuse -pthread +endif + +$(OUTPUT)/fuse_mnt: CFLAGS += $(VAR_CFLAGS) +$(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_LDLIBS) diff --git a/tools/testing/selftests/filesystems/fuse/fuse_mnt.c b/tools/testing/selftests/filesystems/fuse/fuse_mnt.c new file mode 100644 index 0000000000000..d12b17f30fadc --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fuse_mnt.c @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * fusectl test file-system + * Creates a simple FUSE filesystem with a single read-write file (/test) + */ + +#define FUSE_USE_VERSION 26 + +#include +#include +#include +#include +#include +#include +#include + +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +static char *content; +static size_t content_size = 0; +static const char test_path[] = "/test"; + +static int test_getattr(const char *path, struct stat *st) +{ + memset(st, 0, sizeof(*st)); + + if (!strcmp(path, "/")) { + st->st_mode = S_IFDIR | 0755; + st->st_nlink = 2; + return 0; + } + + if (!strcmp(path, test_path)) { + st->st_mode = S_IFREG | 0664; + st->st_nlink = 1; + st->st_size = content_size; + return 0; + } + + return -ENOENT; +} + +static int test_readdir(const char *path, void *buf, fuse_fill_dir_t filler, + off_t offset, struct fuse_file_info *fi) +{ + if (strcmp(path, "/")) + return -ENOENT; + + filler(buf, ".", NULL, 0); + filler(buf, "..", NULL, 0); + filler(buf, test_path + 1, NULL, 0); + + return 0; +} + +static int test_open(const char *path, struct fuse_file_info *fi) +{ + if (strcmp(path, test_path)) + return -ENOENT; + + return 0; +} + +static int test_read(const char *path, char *buf, size_t size, off_t offset, + struct fuse_file_info *fi) +{ + if (strcmp(path, test_path) != 0) + return -ENOENT; + + if (!content || content_size == 0) + return 0; + + if (offset >= content_size) + return 0; + + if (offset + size > content_size) + size = content_size - offset; + + memcpy(buf, content + offset, size); + + return size; +} + +static int test_write(const char *path, const char *buf, size_t size, + off_t offset, struct fuse_file_info *fi) +{ + size_t new_size; + + if (strcmp(path, test_path) != 0) + return -ENOENT; + + if(offset > content_size) + return -EINVAL; + + new_size = MAX(offset + size, content_size); + + if (new_size > content_size) + content = realloc(content, new_size); + + content_size = new_size; + + if (!content) + return -ENOMEM; + + memcpy(content + offset, buf, size); + + return size; +} + +static int test_truncate(const char *path, off_t size) +{ + if (strcmp(path, test_path) != 0) + return -ENOENT; + + if (size == 0) { + free(content); + content = NULL; + content_size = 0; + return 0; + } + + content = realloc(content, size); + + if (!content) + return -ENOMEM; + + if (size > content_size) + memset(content + content_size, 0, size - content_size); + + content_size = size; + return 0; +} + +static struct fuse_operations memfd_ops = { + .getattr = test_getattr, + .readdir = test_readdir, + .open = test_open, + .read = test_read, + .write = test_write, + .truncate = test_truncate, +}; + +int main(int argc, char *argv[]) +{ + return fuse_main(argc, argv, &memfd_ops, NULL); +} diff --git a/tools/testing/selftests/filesystems/fuse/fusectl_test.c b/tools/testing/selftests/filesystems/fuse/fusectl_test.c new file mode 100644 index 0000000000000..8d124d1cacb26 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fusectl_test.c @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2025 Chen Linxuan + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../kselftest_harness.h" + +#define FUSECTL_MOUNTPOINT "/sys/fs/fuse/connections" +#define FUSE_MOUNTPOINT "/tmp/fuse_mnt_XXXXXX" +#define FUSE_DEVICE "/dev/fuse" +#define FUSECTL_TEST_VALUE "1" + +static void write_file(struct __test_metadata *const _metadata, + const char *path, const char *val) +{ + int fd = open(path, O_WRONLY); + size_t len = strlen(val); + + ASSERT_GE(fd, 0); + ASSERT_EQ(write(fd, val, len), len); + ASSERT_EQ(close(fd), 0); +} + +FIXTURE(fusectl){ + char fuse_mountpoint[sizeof(FUSE_MOUNTPOINT)]; + int connection; +}; + +FIXTURE_SETUP(fusectl) +{ + const char *fuse_mnt_prog = "./fuse_mnt"; + int status, pid; + struct stat statbuf; + uid_t uid = getuid(); + gid_t gid = getgid(); + char buf[32]; + + /* Setup userns */ + ASSERT_EQ(unshare(CLONE_NEWNS|CLONE_NEWUSER), 0); + sprintf(buf, "0 %d 1", uid); + write_file(_metadata, "/proc/self/uid_map", buf); + write_file(_metadata, "/proc/self/setgroups", "deny"); + sprintf(buf, "0 %d 1", gid); + write_file(_metadata, "/proc/self/gid_map", buf); + ASSERT_EQ(mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL), 0); + + strcpy(self->fuse_mountpoint, FUSE_MOUNTPOINT); + + if (!mkdtemp(self->fuse_mountpoint)) + SKIP(return, + "Failed to create FUSE mountpoint %s", + strerror(errno)); + + if (access(FUSECTL_MOUNTPOINT, F_OK)) + SKIP(return, + "FUSE control filesystem not mounted"); + + pid = fork(); + if (pid < 0) + SKIP(return, + "Failed to fork FUSE daemon process: %s", + strerror(errno)); + + if (pid == 0) { + execlp(fuse_mnt_prog, fuse_mnt_prog, self->fuse_mountpoint, NULL); + exit(errno); + } + + waitpid(pid, &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + SKIP(return, + "Failed to start FUSE daemon %s", + strerror(WEXITSTATUS(status))); + } + + if (stat(self->fuse_mountpoint, &statbuf)) + SKIP(return, + "Failed to stat FUSE mountpoint %s", + strerror(errno)); + + self->connection = statbuf.st_dev; +} + +FIXTURE_TEARDOWN(fusectl) +{ + umount2(self->fuse_mountpoint, MNT_DETACH); + rmdir(self->fuse_mountpoint); +} + +TEST_F(fusectl, abort) +{ + char path_buf[PATH_MAX]; + int abort_fd, test_fd, ret; + + sprintf(path_buf, "/sys/fs/fuse/connections/%d/abort", self->connection); + + ASSERT_EQ(0, access(path_buf, F_OK)); + + abort_fd = open(path_buf, O_WRONLY); + ASSERT_GE(abort_fd, 0); + + sprintf(path_buf, "%s/test", self->fuse_mountpoint); + + test_fd = open(path_buf, O_RDWR); + ASSERT_GE(test_fd, 0); + + ret = read(test_fd, path_buf, sizeof(path_buf)); + ASSERT_EQ(ret, 0); + + ret = write(test_fd, "test", sizeof("test")); + ASSERT_EQ(ret, sizeof("test")); + + ret = lseek(test_fd, 0, SEEK_SET); + ASSERT_GE(ret, 0); + + ret = write(abort_fd, FUSECTL_TEST_VALUE, sizeof(FUSECTL_TEST_VALUE)); + ASSERT_GT(ret, 0); + + close(abort_fd); + + ret = read(test_fd, path_buf, sizeof(path_buf)); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENOTCONN); +} + +TEST_HARNESS_MAIN From fae98a6b412fd2d263e61aa4be09bbc98a20897c Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Wed, 12 Aug 2026 13:26:22 -0400 Subject: [PATCH 39/41] selftests/fuse: adapt fuse_mnt to libfuse3 API On systems with only libfuse3-devel installed (no libfuse2-devel), fuse_mnt.c fails to compile because fuse3 requires API version 30+. Update fuse_mnt.c from FUSE API version 26 to 31: - getattr, truncate: add struct fuse_file_info * parameter - readdir: add enum fuse_readdir_flags parameter - filler calls: add flags argument Also update the Makefile to try pkg-config fuse3 as a fallback when pkg-config fuse is not available. Signed-off-by: Brett Mastbergen --- .../selftests/filesystems/fuse/Makefile | 7 +++++++ .../selftests/filesystems/fuse/fuse_mnt.c | 21 ++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile index 612aad69a93aa..b63f50f656a66 100644 --- a/tools/testing/selftests/filesystems/fuse/Makefile +++ b/tools/testing/selftests/filesystems/fuse/Makefile @@ -9,13 +9,20 @@ include ../../lib.mk VAR_CFLAGS := $(shell pkg-config fuse --cflags 2>/dev/null) ifeq ($(VAR_CFLAGS),) +# libfuse2-devel not found; try fuse3 which provides v2 compat headers +VAR_CFLAGS := $(shell pkg-config fuse3 --cflags 2>/dev/null) +ifeq ($(VAR_CFLAGS),) VAR_CFLAGS := -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse endif +endif VAR_LDLIBS := $(shell pkg-config fuse --libs 2>/dev/null) ifeq ($(VAR_LDLIBS),) +VAR_LDLIBS := $(shell pkg-config fuse3 --libs 2>/dev/null) +ifeq ($(VAR_LDLIBS),) VAR_LDLIBS := -lfuse -pthread endif +endif $(OUTPUT)/fuse_mnt: CFLAGS += $(VAR_CFLAGS) $(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_LDLIBS) diff --git a/tools/testing/selftests/filesystems/fuse/fuse_mnt.c b/tools/testing/selftests/filesystems/fuse/fuse_mnt.c index d12b17f30fadc..44a2b5e0fd1dd 100644 --- a/tools/testing/selftests/filesystems/fuse/fuse_mnt.c +++ b/tools/testing/selftests/filesystems/fuse/fuse_mnt.c @@ -4,7 +4,7 @@ * Creates a simple FUSE filesystem with a single read-write file (/test) */ -#define FUSE_USE_VERSION 26 +#define FUSE_USE_VERSION 31 #include #include @@ -20,8 +20,10 @@ static char *content; static size_t content_size = 0; static const char test_path[] = "/test"; -static int test_getattr(const char *path, struct stat *st) +static int test_getattr(const char *path, struct stat *st, + struct fuse_file_info *fi) { + (void)fi; memset(st, 0, sizeof(*st)); if (!strcmp(path, "/")) { @@ -41,14 +43,17 @@ static int test_getattr(const char *path, struct stat *st) } static int test_readdir(const char *path, void *buf, fuse_fill_dir_t filler, - off_t offset, struct fuse_file_info *fi) + off_t offset, struct fuse_file_info *fi, + enum fuse_readdir_flags flags) { + (void)flags; + if (strcmp(path, "/")) return -ENOENT; - filler(buf, ".", NULL, 0); - filler(buf, "..", NULL, 0); - filler(buf, test_path + 1, NULL, 0); + filler(buf, ".", NULL, 0, 0); + filler(buf, "..", NULL, 0, 0); + filler(buf, test_path + 1, NULL, 0, 0); return 0; } @@ -107,8 +112,10 @@ static int test_write(const char *path, const char *buf, size_t size, return size; } -static int test_truncate(const char *path, off_t size) +static int test_truncate(const char *path, off_t size, + struct fuse_file_info *fi) { + (void)fi; if (strcmp(path, test_path) != 0) return -ENOENT; From 470d78933dfaec08b7adfb570ea2f1f21250d2a6 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Wed, 12 Aug 2026 11:44:20 -0400 Subject: [PATCH 40/41] selftests/fuse: add ACL_DONT_CACHE regression test jira SECO-478 RFE: FUSE_IO_URING commit-author Amir Goldstein commit 9acb102522b92f24fba6b238b3668a4d9dfcb592 upstream-diff | kselftest_harness.h include uses relative path (../../kselftest_harness.h) because commit e6fbd1759c9e ("selftests: complete kselftest include centralization") is not present in this tree. A FUSE mount that does not negotiate FUSE_POSIX_ACL initialises every inode with i_acl = i_default_acl = ACL_DONT_CACHE. When a fresh stat is needed (e.g. AT_STATX_FORCE_SYNC), fuse_update_get_attr() calls forget_all_cached_acls() before issuing FUSE_GETATTR. On an unfixed kernel, __forget_cached_acl() replaces ACL_DONT_CACHE with ACL_NOT_CACHED, inadvertently enabling the kernel ACL cache for that inode. This test validates the fix that preserves ACL_DONT_CACHE state in forget_cached_acl(). Signed-off-by: Amir Goldstein Signed-off-by: Christian Brauner (cherry picked from commit 9acb102522b92f24fba6b238b3668a4d9dfcb592) Signed-off-by: Brett Mastbergen --- .../selftests/filesystems/fuse/Makefile | 10 + .../filesystems/fuse/fuse_acl_cache_test.c | 347 ++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile index b63f50f656a66..0d0af19e2d059 100644 --- a/tools/testing/selftests/filesystems/fuse/Makefile +++ b/tools/testing/selftests/filesystems/fuse/Makefile @@ -5,6 +5,13 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) TEST_GEN_PROGS := fusectl_test TEST_GEN_FILES := fuse_mnt +# fuse_acl_cache_test requires libfuse3; add it only when the library is present. +ACL_CFLAGS := $(shell pkg-config fuse3 --cflags 2>/dev/null) +ACL_LDLIBS := $(shell pkg-config fuse3 --libs 2>/dev/null) +ifneq ($(ACL_CFLAGS),) +TEST_GEN_PROGS += fuse_acl_cache_test +endif + include ../../lib.mk VAR_CFLAGS := $(shell pkg-config fuse --cflags 2>/dev/null) @@ -26,3 +33,6 @@ endif $(OUTPUT)/fuse_mnt: CFLAGS += $(VAR_CFLAGS) $(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_LDLIBS) + +$(OUTPUT)/fuse_acl_cache_test: CFLAGS += $(ACL_CFLAGS) +$(OUTPUT)/fuse_acl_cache_test: LDLIBS += $(ACL_LDLIBS) diff --git a/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c b/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c new file mode 100644 index 0000000000000..5d8f28271b1ac --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test: FUSE ACL caching bug triggered by AT_STATX_FORCE_SYNC + * + * A FUSE mount that does not negotiate FUSE_POSIX_ACL initialises every inode + * with i_acl = i_default_acl = ACL_DONT_CACHE. When a fresh stat is needed + * (e.g. AT_STATX_FORCE_SYNC), fuse_update_get_attr() calls + * forget_all_cached_acls() before issuing FUSE_GETATTR. On an unfixed kernel, + * __forget_cached_acl() replaces ACL_DONT_CACHE with ACL_NOT_CACHED, + * inadvertently enabling the kernel ACL cache for that inode. The next + * getxattr populates the cache. Because fuse_set_acl() skips + * forget_all_cached_acls() for !fc->posix_acl mounts, any subsequent change to + * the ACL leaves the stale kernel entry in place, and the next getxattr returns + * wrong data without ever reaching the FUSE daemon. + * + * Fix (fs/posix_acl.c): __forget_cached_acl() returns early when *p is + * ACL_DONT_CACHE, preserving the "never cache" invariant for the inode's + * lifetime. + * + * Test outline: + * 1. Mount a minimal FUSE fs (no FUSE_POSIX_ACL negotiated). + * 2. lgetxattr -> daemon called, ACL_A returned, NOT cached (ACL_DONT_CACHE). + * 3. statx(AT_STATX_FORCE_SYNC) -> forget_all_cached_acls() called. + * Buggy: ACL_DONT_CACHE -> ACL_NOT_CACHED (cache enabled). + * Fixed: ACL_DONT_CACHE preserved. + * 4. lgetxattr -> daemon called, ACL_A returned. + * Buggy: result now cached (ACL_NOT_CACHED -> cached). + * Fixed: result still not cached. + * 5. Daemon switches to ACL_B internally (different size). + * 6. lgetxattr -> should return ACL_B (44 bytes). + * Buggy: cache hit, returns stale ACL_A (28 bytes). FAIL. + * Fixed: no cache, daemon called, returns ACL_B (44 bytes). PASS. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FUSE_USE_VERSION 31 +#include + +#include "../../kselftest_harness.h" + +/* ---- ACL binary encoding ------------------------------------------------ */ +/* + * POSIX ACL v2 xattr format (little-endian): + * header: u32 version (= 0x00000002) + * entry: u16 tag | u16 perm | u32 id + * + * Entries must appear in tag-ascending order; named USER/GROUP entries + * require a MASK entry. Both ACLs pass posix_acl_from_xattr() validation. + */ + +/* ACL_A: 3 entries (USER_OBJ:rwx, GROUP_OBJ:r-x, OTHER:r-x) = 28 bytes */ +static const uint8_t acl_a[] = { + 0x02, 0x00, 0x00, 0x00, /* v2 header */ + 0x01, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff, /* USER_OBJ rwx */ + 0x04, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* GROUP_OBJ r-x */ + 0x20, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* OTHER r-x */ +}; + +/* + * ACL_B: 5 entries — adds USER uid=1 and MASK = 44 bytes. + * A named USER entry requires a MASK; all tags in ascending order. + */ +static const uint8_t acl_b[] = { + 0x02, 0x00, 0x00, 0x00, /* v2 header */ + 0x01, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff, /* USER_OBJ rwx */ + 0x02, 0x00, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, /* USER uid=1 rwx */ + 0x04, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* GROUP_OBJ r-x */ + 0x10, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff, /* MASK rwx */ + 0x20, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* OTHER r-x */ +}; + +/* ---- Shared state (daemon thread <-> test thread) ----------------------- */ + +#define FILE_INO 2 +#define FILE_NAME "testfile" + +struct daemon_state { + pthread_mutex_t lock; + const uint8_t *acl; + size_t acl_size; + int getxattr_count; +}; + +/* + * Global: callbacks are stateless fns so we use a single global. + * Safe because only one test instance runs at a time. + */ +static struct daemon_state g_ds = { + .lock = PTHREAD_MUTEX_INITIALIZER, +}; + +/* ---- FUSE lowlevel callbacks -------------------------------------------- */ + +static void fs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + if (parent != FUSE_ROOT_ID || strcmp(name, FILE_NAME)) { + fuse_reply_err(req, ENOENT); + return; + } + struct fuse_entry_param e = {}; + + /* + * Long attr/entry timeouts so that normal stat() calls do not + * expire and trigger forget_all_cached_acls() on their own; + * only the explicit AT_STATX_FORCE_SYNC should trigger it. + */ + e.ino = FILE_INO; + e.generation = 1; + e.attr_timeout = 10.0; + e.entry_timeout = 10.0; + e.attr.st_ino = FILE_INO; + e.attr.st_mode = S_IFREG | 0644; + e.attr.st_nlink = 1; + fuse_reply_entry(req, &e); +} + +static void fs_getattr(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) +{ + struct stat st = {}; + + (void)fi; + if (ino == FUSE_ROOT_ID) { + st.st_ino = FUSE_ROOT_ID; + st.st_mode = S_IFDIR | 0755; + st.st_nlink = 2; + } else if (ino == FILE_INO) { + st.st_ino = FILE_INO; + st.st_mode = S_IFREG | 0644; + st.st_nlink = 1; + } else { + fuse_reply_err(req, ENOENT); + return; + } + fuse_reply_attr(req, &st, 10); +} + +static void fs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name, + size_t size) +{ + if (ino != FILE_INO || + strcmp(name, "system.posix_acl_access") != 0) { + fuse_reply_err(req, ENODATA); + return; + } + + pthread_mutex_lock(&g_ds.lock); + const uint8_t *acl = g_ds.acl; + size_t acl_size = g_ds.acl_size; + g_ds.getxattr_count++; + pthread_mutex_unlock(&g_ds.lock); + + if (size == 0) + fuse_reply_xattr(req, acl_size); + else if (size < acl_size) + fuse_reply_err(req, ERANGE); + else + fuse_reply_buf(req, (const char *)acl, acl_size); +} + +static const struct fuse_lowlevel_ops fs_ops = { + .lookup = fs_lookup, + .getattr = fs_getattr, + .getxattr = fs_getxattr, +}; + +/* ---- Daemon thread ------------------------------------------------------- */ + +static void *run_daemon(void *arg) +{ + fuse_session_loop((struct fuse_session *)arg); + return NULL; +} + +/* ---- kselftest harness --------------------------------------------------- */ + +FIXTURE(acl_cache) { + struct fuse_session *se; + char mountpoint[PATH_MAX]; + char file_path[PATH_MAX]; + pthread_t thread; +}; + +FIXTURE_SETUP(acl_cache) +{ + char *fuse_argv[] = { "fuse_acl_cache_test", NULL }; + struct fuse_args args = FUSE_ARGS_INIT(1, fuse_argv); + + g_ds.acl = acl_a; + g_ds.acl_size = sizeof(acl_a); + g_ds.getxattr_count = 0; + + strcpy(self->mountpoint, "/tmp/acl_cache_test_XXXXXX"); + if (!mkdtemp(self->mountpoint)) + SKIP(return, "mkdtemp: %s", strerror(errno)); + + snprintf(self->file_path, sizeof(self->file_path), + "%s/" FILE_NAME, self->mountpoint); + + self->se = fuse_session_new(&args, &fs_ops, sizeof(fs_ops), NULL); + if (!self->se) { + rmdir(self->mountpoint); + SKIP(return, "fuse_session_new failed"); + } + + if (fuse_session_mount(self->se, self->mountpoint)) { + fuse_session_destroy(self->se); + rmdir(self->mountpoint); + SKIP(return, "fuse_session_mount failed " + "(missing fusermount3 or insufficient privileges)"); + } + + if (pthread_create(&self->thread, NULL, run_daemon, self->se)) { + fuse_session_unmount(self->se); + fuse_session_destroy(self->se); + rmdir(self->mountpoint); + SKIP(return, "pthread_create: %s", strerror(errno)); + } + + fuse_opt_free_args(&args); +} + +FIXTURE_TEARDOWN(acl_cache) +{ + fuse_session_exit(self->se); + fuse_session_unmount(self->se); + pthread_join(self->thread, NULL); + fuse_session_destroy(self->se); + rmdir(self->mountpoint); +} + +static int do_force_statx(const char *path) +{ + struct statx stx; + + return statx(AT_FDCWD, path, AT_STATX_FORCE_SYNC, STATX_BASIC_STATS, + &stx); +} + +TEST_F(acl_cache, stale_after_force_sync) +{ + char buf[512]; + ssize_t sz; + int count; + + /* + * Step 1: two getxattr calls before any statx(FORCE_SYNC). + * i_acl == ACL_DONT_CACHE. __get_acl's cmpxchg(p, ACL_NOT_CACHED, + * sentinel) finds *p != ACL_NOT_CACHED on every call, so the sentinel + * is never placed and the result is never cached. Both calls must + * reach the daemon, proving ACL_DONT_CACHE suppresses caching. + */ + sz = lgetxattr(self->file_path, "system.posix_acl_access", + buf, sizeof(buf)); + ASSERT_EQ(sz, (ssize_t)sizeof(acl_a)); + + sz = lgetxattr(self->file_path, "system.posix_acl_access", + buf, sizeof(buf)); + ASSERT_EQ(sz, (ssize_t)sizeof(acl_a)); + + pthread_mutex_lock(&g_ds.lock); + count = g_ds.getxattr_count; + pthread_mutex_unlock(&g_ds.lock); + + ASSERT_EQ(count, 2); + TH_LOG("step 1 OK: both pre-trigger getxattrs reached daemon (count=%d), " + "ACL_DONT_CACHE is working", count); + + /* + * Step 2: statx(AT_STATX_FORCE_SYNC). + * fuse_update_get_attr() calls forget_all_cached_acls() before sending + * FUSE_GETATTR. + * Buggy kernel: ACL_DONT_CACHE -> ACL_NOT_CACHED (cache enabled) + * Fixed kernel: ACL_DONT_CACHE preserved (no effect) + */ + ASSERT_EQ(do_force_statx(self->file_path), 0); + TH_LOG("step 2 OK: statx(AT_STATX_FORCE_SYNC) succeeded"); + + /* + * Step 3: getxattr — cache population attempt after the trigger. + * Buggy: *p == ACL_NOT_CACHED -> sentinel placed -> fuse_get_inode_acl + * called -> ACL_A parsed and stored in the kernel cache. + * Fixed: *p == ACL_DONT_CACHE -> sentinel placement skipped -> + * fuse_get_inode_acl called but result not cached. + * Either way the correct ACL_A is returned here. + */ + sz = lgetxattr(self->file_path, "system.posix_acl_access", + buf, sizeof(buf)); + ASSERT_EQ(sz, (ssize_t)sizeof(acl_a)); + + pthread_mutex_lock(&g_ds.lock); + count = g_ds.getxattr_count; + pthread_mutex_unlock(&g_ds.lock); + + ASSERT_EQ(count, 3); + TH_LOG("step 3 OK: post-trigger getxattr reached daemon (count=%d), " + "returned correct ACL_A (%zd bytes)", count, sz); + + /* + * Step 4: switch daemon to ACL_B (different size: 44 vs 28 bytes). + * Simulates an ACL change that fuse_set_acl() would NOT invalidate for + * !fc->posix_acl mounts (it skips forget_all_cached_acls in that case). + * On a fixed kernel the ACL was never cached, so this is moot. + */ + pthread_mutex_lock(&g_ds.lock); + g_ds.acl = acl_b; + g_ds.acl_size = sizeof(acl_b); + pthread_mutex_unlock(&g_ds.lock); + TH_LOG("step 4: daemon switched to ACL_B (%zu bytes)", sizeof(acl_b)); + + /* + * Step 5: getxattr — the decisive check. + * Buggy kernel: cache hit -> stale ACL_A (28 bytes), count stays 3. + * Fixed kernel: no cache -> daemon called -> ACL_B (44 bytes), count 4. + */ + sz = lgetxattr(self->file_path, "system.posix_acl_access", + buf, sizeof(buf)); + + pthread_mutex_lock(&g_ds.lock); + count = g_ds.getxattr_count; + pthread_mutex_unlock(&g_ds.lock); + + if (sz == (ssize_t)sizeof(acl_a)) + TH_LOG("step 5 BUG: stale ACL_A (%zd bytes) from kernel cache " + "(count=%d); ACL_DONT_CACHE corrupted by " + "forget_all_cached_acls()", sz, count); + else + TH_LOG("step 5 OK: daemon reached (count=%d), " + "fresh ACL_B (%zd bytes)", count, sz); + + EXPECT_EQ(sz, (ssize_t)sizeof(acl_b)); + EXPECT_EQ(count, 4); +} + +TEST_HARNESS_MAIN From 778cdf394c6270dd8b481810be1b3295e96e09d4 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Wed, 12 Aug 2026 11:46:53 -0400 Subject: [PATCH 41/41] selftests/fuse: add FUSE io-uring integration test Add a kselftest for the FUSE io-uring request dispatch path. A minimal in-memory FUSE daemon runs in a thread, negotiates FUSE_OVER_IO_URING during FUSE_INIT, and handles requests through FUSE_IO_URING_CMD_REGISTER / FUSE_IO_URING_CMD_COMMIT_AND_FETCH. An atomic counter verifies requests are served through io_uring. Test cases: - read_file: read and verify file content - write_and_readback: write data, read it back, compare - readdir: list directory, verify entries - stat_files: stat root and file, verify attributes - concurrent_io: 4 threads doing interleaved reads and writes - requests_via_uring: per-operation verification that read, write, readdir, and stat each produce io_uring requests - sustained_io: write 256KB in 4KB chunks, read back, verify pattern - crash_recovery: fork daemon, SIGKILL with active I/O, verify kernel health - abort_before_ring_ready: negotiate io_uring but never register entries, abort connection, verify no deadlock or leak Tests SKIP when prerequisites are not met (no root, io_uring disabled, enable_uring != Y). Signed-off-by: Brett Mastbergen --- .../selftests/filesystems/fuse/.gitignore | 1 + .../selftests/filesystems/fuse/Makefile | 5 +- .../testing/selftests/filesystems/fuse/config | 3 + .../filesystems/fuse/fuse_uring_test.c | 1249 +++++++++++++++++ 4 files changed, 1257 insertions(+), 1 deletion(-) create mode 100644 tools/testing/selftests/filesystems/fuse/config create mode 100644 tools/testing/selftests/filesystems/fuse/fuse_uring_test.c diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore index 3e72e742d08e8..323aa31624df3 100644 --- a/tools/testing/selftests/filesystems/fuse/.gitignore +++ b/tools/testing/selftests/filesystems/fuse/.gitignore @@ -1,3 +1,4 @@ # SPDX-License-Identifier: GPL-2.0-only fuse_mnt +fuse_uring_test fusectl_test diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile index 0d0af19e2d059..13b031fe77895 100644 --- a/tools/testing/selftests/filesystems/fuse/Makefile +++ b/tools/testing/selftests/filesystems/fuse/Makefile @@ -2,7 +2,7 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) -TEST_GEN_PROGS := fusectl_test +TEST_GEN_PROGS := fusectl_test fuse_uring_test TEST_GEN_FILES := fuse_mnt # fuse_acl_cache_test requires libfuse3; add it only when the library is present. @@ -36,3 +36,6 @@ $(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_LDLIBS) $(OUTPUT)/fuse_acl_cache_test: CFLAGS += $(ACL_CFLAGS) $(OUTPUT)/fuse_acl_cache_test: LDLIBS += $(ACL_LDLIBS) + +$(OUTPUT)/fuse_uring_test: CFLAGS += -Wextra -Wno-unused-parameter -pthread +$(OUTPUT)/fuse_uring_test: LDLIBS += -luring -lpthread diff --git a/tools/testing/selftests/filesystems/fuse/config b/tools/testing/selftests/filesystems/fuse/config new file mode 100644 index 0000000000000..c4b20b2c2d037 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/config @@ -0,0 +1,3 @@ +CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y +CONFIG_IO_URING=y diff --git a/tools/testing/selftests/filesystems/fuse/fuse_uring_test.c b/tools/testing/selftests/filesystems/fuse/fuse_uring_test.c new file mode 100644 index 0000000000000..13dd157bb55ce --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fuse_uring_test.c @@ -0,0 +1,1249 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * FUSE io-uring selftest + * + * Validates FUSE request dispatch over io_uring by running a minimal + * in-memory filesystem daemon in a thread and exercising it from the + * test thread. The daemon negotiates FUSE_OVER_IO_URING during + * FUSE_INIT, registers ring entries via FUSE_IO_URING_CMD_REGISTER, + * and handles requests through FUSE_IO_URING_CMD_COMMIT_AND_FETCH. + * + * An atomic counter tracks requests served through io_uring so tests + * can verify the kernel is actually using the io_uring path. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +/* linux/fuse.h and liburing.h after glibc headers to avoid redefinition */ +#include +#include + +#include "../../kselftest_harness.h" + +/* + * FUSE io-uring UAPI definitions. Remove this block once linux/fuse.h + * includes the io-uring structures (added upstream in v6.14). + */ + +#ifndef FUSE_OVER_IO_URING +#define FUSE_OVER_IO_URING (1ULL << 41) +#endif + +#ifndef FUSE_URING_IN_OUT_HEADER_SZ +#define FUSE_URING_IN_OUT_HEADER_SZ 128 +#define FUSE_URING_OP_IN_OUT_SZ 128 + +struct fuse_uring_ent_in_out { + uint64_t flags; + uint64_t commit_id; + uint32_t payload_sz; + uint32_t padding; + uint64_t reserved; +}; + +struct fuse_uring_req_header { + char in_out[FUSE_URING_IN_OUT_HEADER_SZ]; + char op_in[FUSE_URING_OP_IN_OUT_SZ]; + struct fuse_uring_ent_in_out ring_ent_in_out; +}; + +enum fuse_uring_cmd { + FUSE_IO_URING_CMD_REGISTER = 1, + FUSE_IO_URING_CMD_COMMIT_AND_FETCH = 2, +}; + +struct fuse_uring_cmd_req { + uint64_t flags; + uint64_t commit_id; + uint16_t qid; + uint8_t padding[6]; +}; +#endif /* FUSE_URING_IN_OUT_HEADER_SZ */ + +/* ---- constants --------------------------------------------------------- */ + +#define HELLO_CONTENT "Hello from FUSE io-uring!\n" +#define HELLO_LEN (sizeof(HELLO_CONTENT) - 1) +#define TESTFILE_MAX (256 * 1024) +#define INODE_ROOT 1 +#define INODE_HELLO 2 +#define INODE_TESTFILE 3 +#define DIRENT_NAME_OFF 24 /* offsetof(struct fuse_dirent, name) */ +#define DIRENT_ALIGN(x) (((x) + 7) & ~(size_t)7) +#define MP_LEN 128 /* max mountpoint path length */ +#define PATH_LEN (MP_LEN + 32) + +/* ---- daemon state (shared between daemon thread and test thread) ------- */ + +struct daemon_state { + int fuse_fd; + struct io_uring ring; + int ring_ready; + int nr_queues; + size_t max_payload_sz; + + /* per-queue entry */ + struct { + struct fuse_uring_req_header *hdr; + void *payload; + struct iovec iov[2]; + } *entries; + + /* synchronisation */ + pthread_mutex_t lock; + pthread_cond_t cond; + volatile int ready; + volatile int exiting; + + /* io-uring path verification */ + atomic_int uring_reqs; + + /* in-memory filesystem */ + char *testfile; + size_t testfile_sz; +}; + +/* ---- FUSE request handling --------------------------------------------- */ + +static size_t add_dirent(void *buf, size_t bufsz, size_t off, + uint64_t ino, unsigned type, + const char *name, uint64_t next_off) +{ + size_t nlen = strlen(name); + size_t elen = DIRENT_ALIGN(DIRENT_NAME_OFF + nlen); + struct fuse_dirent *d; + + if (off + elen > bufsz) + return 0; + d = (struct fuse_dirent *)((char *)buf + off); + d->ino = ino; + d->off = next_off; + d->namelen = nlen; + d->type = type; + memcpy(d->name, name, nlen); + if (elen > DIRENT_NAME_OFF + nlen) + memset(d->name + nlen, 0, elen - DIRENT_NAME_OFF - nlen); + return elen; +} + +static void fill_attr(struct fuse_attr *a, uint64_t ino, size_t tf_sz) +{ + memset(a, 0, sizeof(*a)); + a->ino = ino; + a->blksize = 4096; + switch (ino) { + case INODE_ROOT: + a->mode = S_IFDIR | 0755; + a->nlink = 2; + break; + case INODE_HELLO: + a->mode = S_IFREG | 0444; + a->nlink = 1; + a->size = HELLO_LEN; + break; + case INODE_TESTFILE: + a->mode = S_IFREG | 0644; + a->nlink = 1; + a->size = tf_sz; + break; + } +} + +/* + * Handle one FUSE request. Returns payload bytes (>=0) or -errno. + */ +static int handle_request(struct daemon_state *ds, + struct fuse_uring_req_header *hdr, void *payload, + uint32_t payload_sz) +{ + struct fuse_in_header *ih = (void *)hdr->in_out; + + switch (ih->opcode) { + case FUSE_GETATTR: { + struct fuse_attr_out *ao = (void *)payload; + + if (ih->nodeid < INODE_ROOT || ih->nodeid > INODE_TESTFILE) + return -ENOENT; + memset(ao, 0, sizeof(*ao)); + ao->attr_valid = 1; + fill_attr(&ao->attr, ih->nodeid, ds->testfile_sz); + return sizeof(*ao); + } + case FUSE_LOOKUP: { + const char *name = (const char *)payload; + struct fuse_entry_out *eo; + uint64_t ino = 0; + + if (ih->nodeid != INODE_ROOT) + return -ENOENT; + if (payload_sz >= 5 && !memcmp(name, "hello", 5) && + (payload_sz == 5 || name[5] == '\0')) + ino = INODE_HELLO; + else if (payload_sz >= 8 && !memcmp(name, "testfile", 8) && + (payload_sz == 8 || name[8] == '\0')) + ino = INODE_TESTFILE; + else + return -ENOENT; + eo = (void *)payload; + memset(eo, 0, sizeof(*eo)); + eo->nodeid = ino; + eo->generation = 1; + eo->entry_valid = 1; + eo->attr_valid = 1; + fill_attr(&eo->attr, ino, ds->testfile_sz); + return sizeof(*eo); + } + case FUSE_OPEN: + case FUSE_OPENDIR: { + struct fuse_open_out *oo = (void *)payload; + + memset(oo, 0, sizeof(*oo)); + return sizeof(*oo); + } + case FUSE_READ: { + struct fuse_read_in *ri = (void *)hdr->op_in; + const char *src; + size_t src_sz, sz; + + if (ih->nodeid == INODE_HELLO) { + src = HELLO_CONTENT; + src_sz = HELLO_LEN; + } else if (ih->nodeid == INODE_TESTFILE) { + src = ds->testfile; + src_sz = ds->testfile_sz; + } else { + return -ENOENT; + } + if (ri->offset >= src_sz) + return 0; + sz = src_sz - ri->offset; + if (sz > ri->size) + sz = ri->size; + memcpy(payload, src + ri->offset, sz); + return (int)sz; + } + case FUSE_WRITE: { + struct fuse_write_in *wi = (void *)hdr->op_in; + struct fuse_write_out *wo; + size_t sz = wi->size, end; + + if (ih->nodeid != INODE_TESTFILE) + return -EACCES; + if (sz > payload_sz) + sz = payload_sz; + end = wi->offset + sz; + if (end > TESTFILE_MAX) + return -ENOSPC; + memcpy(ds->testfile + wi->offset, payload, sz); + if (end > ds->testfile_sz) + ds->testfile_sz = end; + wo = (void *)payload; + memset(wo, 0, sizeof(*wo)); + wo->size = sz; + return sizeof(*wo); + } + case FUSE_READDIR: { + struct fuse_read_in *ri = (void *)hdr->op_in; + size_t bufsz = ri->size, w = 0, n; + + if (ih->nodeid != INODE_ROOT) + return -ENOENT; + if (bufsz > ds->max_payload_sz) + bufsz = ds->max_payload_sz; + if (ri->offset < 1) { + n = add_dirent(payload, bufsz, w, INODE_ROOT, DT_DIR, ".", 1); + if (n) w += n; + } + if (ri->offset < 2) { + n = add_dirent(payload, bufsz, w, INODE_ROOT, DT_DIR, "..", 2); + if (n) w += n; + } + if (ri->offset < 3) { + n = add_dirent(payload, bufsz, w, INODE_HELLO, DT_REG, "hello", 3); + if (n) w += n; + } + if (ri->offset < 4) { + n = add_dirent(payload, bufsz, w, INODE_TESTFILE, DT_REG, "testfile", 4); + if (n) w += n; + } + return (int)w; + } + case FUSE_STATFS: { + struct fuse_statfs_out *so = (void *)payload; + + memset(so, 0, sizeof(*so)); + so->st.bsize = 4096; + so->st.frsize = 4096; + so->st.namelen = 255; + so->st.blocks = 1024; + so->st.bfree = 512; + so->st.bavail = 512; + return sizeof(*so); + } + case FUSE_RELEASE: + case FUSE_RELEASEDIR: + case FUSE_FLUSH: + case FUSE_FSYNC: + case FUSE_FSYNCDIR: + return 0; + case FUSE_DESTROY: + ds->exiting = 1; + return 0; + default: + return -ENOSYS; + } +} + +/* ---- io_uring SQE helpers ---------------------------------------------- */ + +static int sqe_register(struct io_uring *ring, int fd, + struct iovec *iov, int qid, void *user_data) +{ + struct io_uring_sqe *sqe = io_uring_get_sqe(ring); + struct fuse_uring_cmd_req *cr; + + if (!sqe) + return -ENOSPC; + io_uring_prep_rw(IORING_OP_URING_CMD, sqe, fd, NULL, 0, 0); + sqe->cmd_op = FUSE_IO_URING_CMD_REGISTER; + sqe->addr = (unsigned long)iov; + sqe->len = 2; + sqe->user_data = (uint64_t)(uintptr_t)user_data; + cr = (void *)sqe->cmd; + memset(cr, 0, sizeof(*cr)); + cr->qid = qid; + return 0; +} + +static int sqe_commit_fetch(struct io_uring *ring, int fd, + struct fuse_uring_req_header *hdr, + uint64_t commit_id, int qid, + int32_t error, uint32_t payload_sz, + void *user_data) +{ + struct io_uring_sqe *sqe = io_uring_get_sqe(ring); + struct fuse_uring_cmd_req *cr; + struct fuse_out_header *oh = (void *)hdr->in_out; + + if (!sqe) + return -ENOSPC; + + oh->len = sizeof(*oh); + oh->error = error; + oh->unique = commit_id; + hdr->ring_ent_in_out.commit_id = commit_id; + hdr->ring_ent_in_out.payload_sz = payload_sz; + + io_uring_prep_rw(IORING_OP_URING_CMD, sqe, fd, NULL, 0, 0); + sqe->cmd_op = FUSE_IO_URING_CMD_COMMIT_AND_FETCH; + sqe->user_data = (uint64_t)(uintptr_t)user_data; + cr = (void *)sqe->cmd; + memset(cr, 0, sizeof(*cr)); + cr->commit_id = commit_id; + cr->qid = qid; + return 0; +} + +/* ---- daemon thread ----------------------------------------------------- */ + +static int daemon_init(struct daemon_state *ds) +{ + char *buf; + char reply[sizeof(struct fuse_out_header) + sizeof(struct fuse_init_out)]; + struct fuse_in_header *ih; + struct fuse_init_in *ii; + struct fuse_out_header *oh; + struct fuse_init_out *io; + ssize_t n; + + buf = malloc(FUSE_MIN_READ_BUFFER); + if (!buf) + return -1; + + n = read(ds->fuse_fd, buf, FUSE_MIN_READ_BUFFER); + if (n < (ssize_t)sizeof(struct fuse_in_header)) { + free(buf); + return -1; + } + ih = (void *)buf; + if (ih->opcode != FUSE_INIT) { + free(buf); + return -1; + } + ii = (void *)(buf + sizeof(*ih)); + + if (!(((uint64_t)ii->flags | ((uint64_t)ii->flags2 << 32)) & FUSE_OVER_IO_URING)) { + free(buf); + return -1; + } + + memset(reply, 0, sizeof(reply)); + oh = (void *)reply; + io = (void *)(reply + sizeof(*oh)); + oh->len = sizeof(reply); + oh->unique = ih->unique; + io->major = FUSE_KERNEL_VERSION; + io->minor = FUSE_KERNEL_MINOR_VERSION; + io->max_readahead = ii->max_readahead; + free(buf); + + io->max_write = TESTFILE_MAX; + io->max_pages = TESTFILE_MAX / 4096; + io->max_background = 16; + io->congestion_threshold = 12; + io->time_gran = 1; + io->flags = FUSE_BIG_WRITES; + io->flags2 = (uint32_t)(FUSE_OVER_IO_URING >> 32); + + n = write(ds->fuse_fd, reply, sizeof(reply)); + if (n != sizeof(reply)) + return -1; + + ds->max_payload_sz = io->max_write; + if (ds->max_payload_sz < FUSE_MIN_READ_BUFFER) + ds->max_payload_sz = FUSE_MIN_READ_BUFFER; + return 0; +} + +static int daemon_setup_ring(struct daemon_state *ds) +{ + struct io_uring_params params = { .flags = IORING_SETUP_SQE128 }; + int i, ret; + + ret = io_uring_queue_init_params(ds->nr_queues * 2 + 4, + &ds->ring, ¶ms); + if (ret < 0) + return ret; + ds->ring_ready = 1; + + ds->testfile = calloc(1, TESTFILE_MAX); + if (!ds->testfile) + return -ENOMEM; + + ds->entries = calloc(ds->nr_queues, sizeof(*ds->entries)); + if (!ds->entries) + return -ENOMEM; + + for (i = 0; i < ds->nr_queues; i++) { + if (posix_memalign((void **)&ds->entries[i].hdr, 4096, + sizeof(*ds->entries[i].hdr))) + return -ENOMEM; + if (posix_memalign(&ds->entries[i].payload, 4096, + ds->max_payload_sz)) + return -ENOMEM; + memset(ds->entries[i].hdr, 0, sizeof(*ds->entries[i].hdr)); + memset(ds->entries[i].payload, 0, ds->max_payload_sz); + ds->entries[i].iov[0].iov_base = ds->entries[i].hdr; + ds->entries[i].iov[0].iov_len = sizeof(*ds->entries[i].hdr); + ds->entries[i].iov[1].iov_base = ds->entries[i].payload; + ds->entries[i].iov[1].iov_len = ds->max_payload_sz; + + ret = sqe_register(&ds->ring, ds->fuse_fd, + ds->entries[i].iov, i, + (void *)(uintptr_t)i); + if (ret) + return ret; + } + return io_uring_submit(&ds->ring); +} + +static void *daemon_loop(void *arg) +{ + struct daemon_state *ds = arg; + struct io_uring_cqe *cqe; + + if (daemon_init(ds)) + goto out; + if (daemon_setup_ring(ds) < 0) + goto out; + + /* signal test thread that we're ready */ + pthread_mutex_lock(&ds->lock); + ds->ready = 1; + pthread_cond_signal(&ds->cond); + pthread_mutex_unlock(&ds->lock); + + while (!ds->exiting) { + if (io_uring_wait_cqe(&ds->ring, &cqe)) + break; + do { + int qid = (int)(uintptr_t)cqe->user_data; + int ret; + + if (cqe->res < 0) { + io_uring_cqe_seen(&ds->ring, cqe); + if (cqe->res == -ENOTCONN || + cqe->res == -ECONNABORTED) + goto out; + continue; + } + + atomic_fetch_add(&ds->uring_reqs, 1); + + ret = handle_request(ds, ds->entries[qid].hdr, + ds->entries[qid].payload, + ds->entries[qid].hdr->ring_ent_in_out.payload_sz); + + if (ret < 0) { + if (sqe_commit_fetch(&ds->ring, ds->fuse_fd, + ds->entries[qid].hdr, + ds->entries[qid].hdr->ring_ent_in_out.commit_id, + qid, ret, 0, + (void *)(uintptr_t)qid)) + goto out; + } else { + if (sqe_commit_fetch(&ds->ring, ds->fuse_fd, + ds->entries[qid].hdr, + ds->entries[qid].hdr->ring_ent_in_out.commit_id, + qid, 0, ret, + (void *)(uintptr_t)qid)) + goto out; + } + + io_uring_cqe_seen(&ds->ring, cqe); + } while (io_uring_peek_cqe(&ds->ring, &cqe) == 0); + + io_uring_submit(&ds->ring); + } +out: + return NULL; +} + +/* ---- daemon lifecycle (used by fixtures) ------------------------------- */ + +static int daemon_start(struct daemon_state *ds, const char *mountpoint) +{ + char opts[256]; + + memset(ds, 0, sizeof(*ds)); + pthread_mutex_init(&ds->lock, NULL); + pthread_cond_init(&ds->cond, NULL); + ds->nr_queues = sysconf(_SC_NPROCESSORS_CONF); + if (ds->nr_queues > 64) + ds->nr_queues = 64; + + ds->fuse_fd = open("/dev/fuse", O_RDWR | O_CLOEXEC); + if (ds->fuse_fd < 0) + return -errno; + + snprintf(opts, sizeof(opts), + "fd=%d,rootmode=40000,user_id=%u,group_id=%u", + ds->fuse_fd, getuid(), getgid()); + if (mount("fuse_uring_test", mountpoint, "fuse", + MS_NOSUID | MS_NODEV, opts)) { + close(ds->fuse_fd); + ds->fuse_fd = -1; + return -errno; + } + + return 0; +} + +static int daemon_wait_ready(struct daemon_state *ds, const char *mountpoint, + pthread_t *thread) +{ + struct timespec ts; + int err; + + if (pthread_create(thread, NULL, daemon_loop, ds)) + return -errno; + + pthread_mutex_lock(&ds->lock); + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += 10; + err = 0; + while (!ds->ready && !err) + err = pthread_cond_timedwait(&ds->cond, &ds->lock, &ts); + pthread_mutex_unlock(&ds->lock); + + if (!ds->ready) { + /* Thread was created but daemon failed; clean up */ + ds->exiting = 1; + umount2(mountpoint, MNT_DETACH); + pthread_join(*thread, NULL); + close(ds->fuse_fd); + ds->fuse_fd = -1; + return -ETIMEDOUT; + } + return 0; +} + +static void daemon_stop(struct daemon_state *ds, const char *mountpoint, + pthread_t thread) +{ + int i; + + umount2(mountpoint, MNT_DETACH); + pthread_join(thread, NULL); + if (ds->ring_ready) + io_uring_queue_exit(&ds->ring); + if (ds->entries) { + for (i = 0; i < ds->nr_queues; i++) { + free(ds->entries[i].hdr); + free(ds->entries[i].payload); + } + free(ds->entries); + } + free(ds->testfile); + if (ds->fuse_fd >= 0) + close(ds->fuse_fd); + pthread_mutex_destroy(&ds->lock); + pthread_cond_destroy(&ds->cond); +} + +/* ---- prerequisites and kernel parameter management --------------------- */ + +static int orig_uring_disabled = -1; /* -1 = not saved */ +static int orig_enable_uring = -1; /* -1 = not saved */ + +static int read_sysctl_int(const char *path) +{ + FILE *f; + int val = -1; + + f = fopen(path, "r"); + if (f) { + if (fscanf(f, "%d", &val) != 1) + val = -1; + fclose(f); + } + return val; +} + +static void write_sysctl(const char *path, const char *val) +{ + FILE *f; + + f = fopen(path, "w"); + if (f) { + fputs(val, f); + fclose(f); + } +} + +static int ensure_io_uring_enabled(void) +{ + int val; + + /* enable io_uring if kernel restricts it */ + val = read_sysctl_int("/proc/sys/kernel/io_uring_disabled"); + if (val > 0) { + if (orig_uring_disabled < 0) + orig_uring_disabled = val; + write_sysctl("/proc/sys/kernel/io_uring_disabled", "0"); + if (read_sysctl_int("/proc/sys/kernel/io_uring_disabled") != 0) + return -1; + } + + return 0; +} + +static int ensure_fuse_uring_enabled(void) +{ + FILE *f; + char val; + + if (access("/dev/fuse", R_OK | W_OK)) + return -1; + + f = fopen("/sys/module/fuse/parameters/enable_uring", "r"); + if (!f) + return -1; + val = fgetc(f); + fclose(f); + + if (val != 'Y') { + if (orig_enable_uring < 0) + orig_enable_uring = 0; + write_sysctl("/sys/module/fuse/parameters/enable_uring", "1"); + /* re-check */ + f = fopen("/sys/module/fuse/parameters/enable_uring", "r"); + if (!f) + return -1; + val = fgetc(f); + fclose(f); + if (val != 'Y') + return -1; + } + + return 0; +} + +static void restore_kernel_params(void) +{ + if (orig_enable_uring >= 0) { + write_sysctl("/sys/module/fuse/parameters/enable_uring", + orig_enable_uring ? "1" : "0"); + orig_enable_uring = -1; + } + if (orig_uring_disabled >= 0) { + char buf[16]; + + snprintf(buf, sizeof(buf), "%d", orig_uring_disabled); + write_sysctl("/proc/sys/kernel/io_uring_disabled", buf); + orig_uring_disabled = -1; + } +} + +/* ---- kselftest fixtures ------------------------------------------------ */ + +FIXTURE(fuse_uring) { + struct daemon_state ds; + char mountpoint[MP_LEN]; + pthread_t thread; + int thread_started; +}; + +FIXTURE_SETUP(fuse_uring) +{ + static int atexit_registered; + + if (getuid() != 0) + SKIP(return, "must run as root"); + if (!atexit_registered) { + atexit(restore_kernel_params); + atexit_registered = 1; + } + if (ensure_io_uring_enabled()) + SKIP(return, "cannot enable io_uring"); + if (ensure_fuse_uring_enabled()) + SKIP(return, "FUSE io-uring not available"); + + strcpy(self->mountpoint, "/tmp/fuse_uring_XXXXXX"); + if (!mkdtemp(self->mountpoint)) + SKIP(return, "mkdtemp: %s", strerror(errno)); + + if (daemon_start(&self->ds, self->mountpoint)) { + rmdir(self->mountpoint); + SKIP(return, "daemon_start: %s", strerror(errno)); + } + if (daemon_wait_ready(&self->ds, self->mountpoint, &self->thread)) { + rmdir(self->mountpoint); + SKIP(return, "daemon did not become ready"); + } + self->thread_started = 1; +} + +FIXTURE_TEARDOWN(fuse_uring) +{ + if (!self->thread_started) + return; + daemon_stop(&self->ds, self->mountpoint, self->thread); + rmdir(self->mountpoint); +} + +/* ---- tests ------------------------------------------------------------- */ + +TEST_F(fuse_uring, read_file) +{ + char buf[256]; + char path[PATH_LEN]; + int fd, n; + + snprintf(path, sizeof(path), "%s/hello", self->mountpoint); + fd = open(path, O_RDONLY); + ASSERT_GE(fd, 0); + n = read(fd, buf, sizeof(buf)); + close(fd); + ASSERT_EQ(n, (int)HELLO_LEN); + buf[n] = '\0'; + ASSERT_STREQ(buf, HELLO_CONTENT); +} + +TEST_F(fuse_uring, write_and_readback) +{ + char path[PATH_LEN]; + const char *msg = "io-uring write test"; + char buf[256]; + int fd, n; + + snprintf(path, sizeof(path), "%s/testfile", self->mountpoint); + + fd = open(path, O_WRONLY); + ASSERT_GE(fd, 0); + n = write(fd, msg, strlen(msg)); + close(fd); + ASSERT_EQ(n, (int)strlen(msg)); + + fd = open(path, O_RDONLY); + ASSERT_GE(fd, 0); + n = read(fd, buf, sizeof(buf)); + close(fd); + ASSERT_EQ(n, (int)strlen(msg)); + buf[n] = '\0'; + ASSERT_STREQ(buf, msg); +} + +TEST_F(fuse_uring, readdir) +{ + DIR *d; + struct dirent *de; + int found_hello = 0, found_testfile = 0; + + d = opendir(self->mountpoint); + ASSERT_NE(d, NULL); + while ((de = readdir(d)) != NULL) { + if (!strcmp(de->d_name, "hello")) + found_hello = 1; + if (!strcmp(de->d_name, "testfile")) + found_testfile = 1; + } + closedir(d); + ASSERT_EQ(found_hello, 1); + ASSERT_EQ(found_testfile, 1); +} + +TEST_F(fuse_uring, stat_files) +{ + struct stat st; + char path[PATH_LEN]; + + ASSERT_EQ(stat(self->mountpoint, &st), 0); + ASSERT_TRUE(S_ISDIR(st.st_mode)); + + snprintf(path, sizeof(path), "%s/hello", self->mountpoint); + ASSERT_EQ(stat(path, &st), 0); + ASSERT_TRUE(S_ISREG(st.st_mode)); + ASSERT_EQ(st.st_size, (off_t)HELLO_LEN); +} + +struct conc_ctx { + const char *mountpoint; + int id; +}; + +static void *concurrent_worker(void *arg) +{ + struct conc_ctx *ctx = arg; + char path[PATH_LEN]; + int i; + + for (i = 0; i < 10; i++) { + char buf[128]; + int fd; + + /* readers hit /hello */ + snprintf(path, sizeof(path), "%s/hello", ctx->mountpoint); + fd = open(path, O_RDONLY); + if (fd >= 0) { + read(fd, buf, sizeof(buf)); + close(fd); + } + + /* writers hit /testfile */ + snprintf(path, sizeof(path), "%s/testfile", ctx->mountpoint); + fd = open(path, O_WRONLY); + if (fd >= 0) { + snprintf(buf, sizeof(buf), "thread%d iter%d", ctx->id, i); + write(fd, buf, strlen(buf)); + close(fd); + } + } + return NULL; +} + +#define CONC_THREADS 4 + +TEST_F(fuse_uring, concurrent_io) +{ + pthread_t threads[CONC_THREADS]; + struct conc_ctx ctxs[CONC_THREADS]; + int i; + + for (i = 0; i < CONC_THREADS; i++) { + ctxs[i].mountpoint = self->mountpoint; + ctxs[i].id = i; + pthread_create(&threads[i], NULL, concurrent_worker, &ctxs[i]); + } + for (i = 0; i < CONC_THREADS; i++) + pthread_join(threads[i], NULL); + + /* verify filesystem still works after concurrent access */ + { + char path[PATH_LEN]; + char buf[128]; + int fd, n; + + snprintf(path, sizeof(path), "%s/hello", self->mountpoint); + fd = open(path, O_RDONLY); + ASSERT_GE(fd, 0); + n = read(fd, buf, sizeof(buf)); + close(fd); + ASSERT_EQ(n, (int)HELLO_LEN); + } +} + +/* + * Per-operation io_uring path verification. Reset the daemon's + * request counter before each distinct operation type and verify it + * incremented afterwards. If the kernel silently falls back to + * /dev/fuse for any opcode, the counter stays at zero for that op. + */ +TEST_F(fuse_uring, requests_via_uring) +{ + char path[PATH_LEN]; + char buf[256]; + int fd, n, before, after; + DIR *d; + struct dirent *de; + struct stat st; + + /* LOOKUP + OPEN + READ + RELEASE (via open+read+close) */ + atomic_store(&self->ds.uring_reqs, 0); + snprintf(path, sizeof(path), "%s/hello", self->mountpoint); + fd = open(path, O_RDONLY); + ASSERT_GE(fd, 0); + n = read(fd, buf, sizeof(buf)); + close(fd); + ASSERT_EQ(n, (int)HELLO_LEN); + after = atomic_load(&self->ds.uring_reqs); + ASSERT_GT(after, 0); + TH_LOG("read path: %d io_uring requests (LOOKUP+OPEN+READ+RELEASE)", + after); + + /* WRITE (via open+write+close on testfile) */ + before = atomic_load(&self->ds.uring_reqs); + snprintf(path, sizeof(path), "%s/testfile", self->mountpoint); + fd = open(path, O_WRONLY); + ASSERT_GE(fd, 0); + n = write(fd, "verify", 6); + close(fd); + ASSERT_EQ(n, 6); + after = atomic_load(&self->ds.uring_reqs); + ASSERT_GT(after, before); + TH_LOG("write path: %d new io_uring requests", after - before); + + /* READDIR (via opendir+readdir+closedir) */ + before = atomic_load(&self->ds.uring_reqs); + d = opendir(self->mountpoint); + ASSERT_NE(d, NULL); + while ((de = readdir(d)) != NULL) + ; + closedir(d); + after = atomic_load(&self->ds.uring_reqs); + ASSERT_GT(after, before); + TH_LOG("readdir path: %d new io_uring requests", after - before); + + /* GETATTR (via stat) */ + before = atomic_load(&self->ds.uring_reqs); + snprintf(path, sizeof(path), "%s/hello", self->mountpoint); + ASSERT_EQ(stat(path, &st), 0); + after = atomic_load(&self->ds.uring_reqs); + ASSERT_GT(after, before); + TH_LOG("stat path: %d new io_uring requests", after - before); + + TH_LOG("total io_uring requests: %d", after); +} + +/* + * Sustained I/O: write 256KB in 4KB chunks with a known pattern, then + * read the entire file back and verify every byte. Exercises payload + * buffer reuse across many commit/fetch cycles and catches corruption + * in the io_uring request pipeline. + */ +TEST_F(fuse_uring, sustained_io) +{ + char path[PATH_LEN]; + char wbuf[4096], rbuf[4096]; + int fd, total, off, n, i; + int write_sz = TESTFILE_MAX; + + snprintf(path, sizeof(path), "%s/testfile", self->mountpoint); + + /* write phase: fill file with pattern */ + fd = open(path, O_WRONLY); + ASSERT_GE(fd, 0); + for (off = 0; off < write_sz; off += sizeof(wbuf)) { + /* fill with offset-dependent pattern */ + for (i = 0; i < (int)sizeof(wbuf); i++) + wbuf[i] = (char)((off + i) & 0xff); + n = write(fd, wbuf, sizeof(wbuf)); + ASSERT_EQ(n, (int)sizeof(wbuf)); + } + close(fd); + + /* read phase: verify every byte */ + fd = open(path, O_RDONLY); + ASSERT_GE(fd, 0); + total = 0; + while (total < write_sz) { + n = read(fd, rbuf, sizeof(rbuf)); + ASSERT_GT(n, 0); + for (i = 0; i < n; i++) { + if (rbuf[i] != (char)((total + i) & 0xff)) { + TH_LOG("data mismatch at offset %d: " + "expected 0x%02x got 0x%02x", + total + i, + (total + i) & 0xff, + (unsigned char)rbuf[i]); + ASSERT_EQ(rbuf[i], + (char)((total + i) & 0xff)); + } + } + total += n; + } + close(fd); + ASSERT_EQ(total, write_sz); + TH_LOG("sustained I/O: wrote and verified %d bytes in 4KB chunks", + write_sz); +} + +/* + * Background requests: fill a file, then read it sequentially in small + * chunks to trigger kernel readahead. Readahead generates background + * FUSE requests that flow through fuse_uring_queue_bq_req() and + * fuse_uring_flush_bg() — the code path fixed by dfba028 (bg dispatch + * ordering). Verify that all data arrives correctly and that the + * io_uring request count exceeds the minimum expected from foreground + * requests alone, indicating background requests were dispatched. + */ +TEST_F(fuse_uring, background_requests) +{ + char path[PATH_LEN]; + char wbuf[4096], rbuf[512]; + int fd, n, i, off, total; + int before, after, read_calls; + int write_sz = TESTFILE_MAX; + + snprintf(path, sizeof(path), "%s/testfile", self->mountpoint); + + /* fill the file with a known pattern */ + fd = open(path, O_WRONLY); + ASSERT_GE(fd, 0); + for (off = 0; off < write_sz; off += sizeof(wbuf)) { + for (i = 0; i < (int)sizeof(wbuf); i++) + wbuf[i] = (char)((off + i) % 251); + n = write(fd, wbuf, sizeof(wbuf)); + ASSERT_EQ(n, (int)sizeof(wbuf)); + } + close(fd); + + /* + * Sequential read in small chunks (512B) to maximise the + * kernel's readahead window. The kernel will issue background + * FUSE_READ requests ahead of what we ask for. + */ + before = atomic_load(&self->ds.uring_reqs); + fd = open(path, O_RDONLY); + ASSERT_GE(fd, 0); + + total = 0; + read_calls = 0; + while (total < write_sz) { + n = read(fd, rbuf, sizeof(rbuf)); + ASSERT_GT(n, 0); + /* verify pattern */ + for (i = 0; i < n; i++) { + if (rbuf[i] != (char)((total + i) % 251)) { + TH_LOG("bg read mismatch at offset %d", + total + i); + ASSERT_EQ(rbuf[i], + (char)((total + i) % 251)); + } + } + total += n; + read_calls++; + } + close(fd); + after = atomic_load(&self->ds.uring_reqs); + + ASSERT_EQ(total, write_sz); + + /* + * The io_uring request count should reflect both the foreground + * reads and any background readahead the kernel issued. At + * minimum we expect LOOKUP + OPEN + reads + RELEASE. + */ + TH_LOG("background requests: %d read() calls for %d bytes, " + "%d io_uring requests (delta from before)", + read_calls, write_sz, after - before); + ASSERT_GT(after - before, 0); +} + +/* + * Crash recovery: fork a daemon process, drive active I/O against it + * from multiple threads, then SIGKILL the daemon while requests are + * in flight. This exercises the teardown paths that race with + * io_uring task work (the bug class fixed by d282e44, 703224b, 109b21e). + */ + +static volatile int crash_io_running; + +static void *crash_io_worker(void *arg) +{ + const char *mp = arg; + char path[PATH_LEN]; + char buf[128]; + + snprintf(path, sizeof(path), "%s/hello", mp); + while (crash_io_running) { + int fd = open(path, O_RDONLY); + + if (fd >= 0) { + read(fd, buf, sizeof(buf)); + close(fd); + } + snprintf(path, sizeof(path), "%s/testfile", mp); + fd = open(path, O_WRONLY); + if (fd >= 0) { + write(fd, "crash", 5); + close(fd); + } + snprintf(path, sizeof(path), "%s/hello", mp); + } + return NULL; +} + +#define CRASH_IO_THREADS 4 + +TEST_F(fuse_uring, crash_recovery) +{ + char crash_mp[MP_LEN]; + pid_t pid; + int status; + + strcpy(crash_mp, "/tmp/fuse_crash_XXXXXX"); + if (!mkdtemp(crash_mp)) + SKIP(return, "mkdtemp: %s", strerror(errno)); + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + /* child: run a daemon, handle requests until killed */ + struct daemon_state cds; + pthread_t ct; + + if (daemon_start(&cds, crash_mp)) + _exit(1); + if (daemon_wait_ready(&cds, crash_mp, &ct)) + _exit(1); + pthread_join(ct, NULL); + _exit(0); + } + + /* parent: wait for child's filesystem to appear */ + { + char path[PATH_LEN]; + int fd, i; + + snprintf(path, sizeof(path), "%s/hello", crash_mp); + for (i = 0; i < 50; i++) { + fd = open(path, O_RDONLY); + if (fd >= 0) { + close(fd); + break; + } + usleep(100000); + } + } + + /* start I/O threads hammering the filesystem */ + { + pthread_t io_threads[CRASH_IO_THREADS]; + int i; + + crash_io_running = 1; + for (i = 0; i < CRASH_IO_THREADS; i++) + pthread_create(&io_threads[i], NULL, + crash_io_worker, crash_mp); + + /* let I/O build up, then kill daemon mid-flight */ + usleep(200000); + kill(pid, SIGKILL); + waitpid(pid, &status, 0); + + /* stop I/O threads (they'll get errors now) */ + crash_io_running = 0; + for (i = 0; i < CRASH_IO_THREADS; i++) + pthread_join(io_threads[i], NULL); + } + + umount2(crash_mp, MNT_DETACH); + rmdir(crash_mp); + + /* + * The kernel should be healthy. Verify by doing an operation + * on the main fixture's filesystem. + */ + { + struct stat st; + + ASSERT_EQ(stat(self->mountpoint, &st), 0); + TH_LOG("kernel healthy after daemon crash with active I/O"); + } +} + +/* + * Abort before ring ready: negotiate FUSE_OVER_IO_URING in FUSE_INIT + * but never register any io_uring entries, then abort the connection. + * Exercises the deadlock fix (b2ed269) where blocked allocators would + * hang forever waiting for a ring that never becomes ready, and the + * abort-during-creation fix (c146284c) where rings created after abort + * would leak. + */ +TEST_F(fuse_uring, abort_before_ring_ready) +{ + char abort_mp[MP_LEN]; + pid_t pid; + int status; + struct stat st; + + strcpy(abort_mp, "/tmp/fuse_abort_XXXXXX"); + if (!mkdtemp(abort_mp)) + SKIP(return, "mkdtemp: %s", strerror(errno)); + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + /* + * Child: open /dev/fuse, mount, do FUSE_INIT with + * FUSE_OVER_IO_URING, but never register io_uring + * entries. The ring never becomes ready. Sleep + * until killed — the kernel must handle teardown of + * an io_uring-negotiated connection where the ring + * was never initialised without deadlocking. + */ + struct daemon_state ds; + + memset(&ds, 0, sizeof(ds)); + if (daemon_start(&ds, abort_mp)) + _exit(1); + if (daemon_init(&ds)) + _exit(1); + pause(); + _exit(0); + } + + /* + * Give the child time to complete FUSE_INIT, then kill it. + * This tears down the FUSE connection while fc->io_uring is + * set but the ring was never readied. On a buggy kernel this + * deadlocks in fuse_block_alloc() or leaks resources. + */ + usleep(500000); + kill(pid, SIGKILL); + waitpid(pid, &status, 0); + umount2(abort_mp, MNT_DETACH); + rmdir(abort_mp); + + /* verify kernel is still healthy */ + ASSERT_EQ(stat(self->mountpoint, &st), 0); + TH_LOG("kernel healthy after teardown of uninitialized io_uring ring"); +} + +TEST_HARNESS_MAIN