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/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 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/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 a9de694d48fcd..617891b24b59e 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -6,7 +6,9 @@ See the file COPYING. */ +#include "dev_uring_i.h" #include "fuse_i.h" +#include "fuse_dev_i.h" #include #include @@ -29,23 +31,8 @@ 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 end_requests(struct list_head *head); - -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); @@ -90,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 && fc->connected && !fuse_uring_ready(fc)); } static void fuse_drop_waiting(struct fuse_conn *fc) @@ -235,7 +223,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); } @@ -251,7 +239,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) { @@ -264,7 +253,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)) { @@ -343,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 @@ -375,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 { @@ -419,6 +415,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; @@ -440,22 +454,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); } /* @@ -581,7 +593,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; @@ -593,6 +623,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++; @@ -693,22 +729,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; @@ -716,7 +738,7 @@ static 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; @@ -815,6 +837,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; } @@ -1059,9 +1084,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; @@ -1750,7 +1775,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; @@ -1777,9 +1802,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) @@ -1874,7 +1900,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 */ @@ -1939,7 +1965,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; @@ -1951,10 +1977,17 @@ 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); + + 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); @@ -2016,7 +2049,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) { @@ -2054,7 +2087,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); @@ -2209,7 +2242,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 +2345,13 @@ 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); + + /* + * fc->lock must not be taken to avoid conflicts with io-uring + * locks + */ + fuse_uring_abort(fc); } else { spin_unlock(&fc->lock); } @@ -2324,6 +2363,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) @@ -2342,7 +2383,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)) { @@ -2493,6 +2534,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 new file mode 100644 index 0000000000000..d195279f4e6cb --- /dev/null +++ b/fs/fuse/dev_uring.c @@ -0,0 +1,1372 @@ +// 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; +} + +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_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; + list_del_init(&req->list); + 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); + } + + spin_unlock(&queue->lock); + + if (error) + req->out.h.error = error; + + clear_bit(FR_SENT, &req->flags); + 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; + struct fuse_conn *fc = ring->fc; + + for (qid = 0; qid < ring->nr_queues; qid++) { + queue = READ_ONCE(ring->queues[qid]); + if (!queue) + continue; + + 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); + spin_unlock(&queue->lock); + fuse_uring_abort_end_queue_requests(queue); + } +} + +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]; + struct fuse_ring_ent *ent, *next; + + if (!queue) + 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)); + + 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; + } + + 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->connected) { + spin_unlock(&fc->lock); + goto out_err; + } + if (fc->ring) { + /* race, another thread created the ring in the meantime */ + spin_unlock(&fc->lock); + res = fc->ring; + goto out_err; + } + + init_waitqueue_head(&ring->stop_waitq); + + ring->nr_queues = nr_queues; + ring->fc = fc; + ring->max_payload_sz = max_payload_size; + smp_store_release(&fc->ring, ring); + + 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; + 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); + INIT_LIST_HEAD(&queue->fuse_req_bg_queue); + INIT_LIST_HEAD(&queue->ent_released); + + 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]; + } + + /* + * 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; +} + +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); + } + + /* + * 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) + io_uring_cmd_done(cmd, -ENOTCONN, 0, IO_URING_F_UNLOCKED); + + if (req) + fuse_uring_stop_fuse_req_end(req); +} + +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; + } + + ent->state = FRRS_TEARDOWN; + 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); + fuse_conn_put(ring->fc); + } +} + +/* + * 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) { + fuse_conn_get(ring->fc); + 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); + } +} + +/* + * 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) { + list_del_init(&ent->list); + 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); + kfree(ent); + if (atomic_dec_and_test(&queue->ring->queue_refs)) + wake_up_all(&queue->ring->stop_waitq); + } +} + +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 + */ +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; + + err = fuse_copy_out_args(&cs, args, ring_in_out.payload_sz); + fuse_copy_finish(&cs); + return err; +} + + /* + * 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); + fuse_copy_finish(&cs); + 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 { + /* + * 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; +} + +/* 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 + * 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); + fuse_uring_add_to_pq(ent); + spin_unlock(&queue->lock); + + io_uring_cmd_done(cmd, 0, 0, issue_flags); + return 0; +} + +/* + * 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; +} + +/* + * 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; + + 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); + } + + 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); +} + +/* 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 = -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) { + /* 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; + + 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). + * 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); + fuse_uring_req_end(ent, req, err); + return err; + } + + ent->cmd = cmd; + 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); + + /* + * 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; +} + +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 + */ +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); + ent->cmd = cmd; + fuse_uring_ent_avail(ent, queue); + spin_unlock(&queue->lock); + + if (!READ_ONCE(ring->ready)) { + bool ready = is_ring_ready(ring, queue->qid); + + if (ready) { + WRITE_ONCE(fiq->ops, &fuse_io_uring_ops); + smp_store_release(&ring->ready, true); + wake_up_all(&fc->blocked_waitq); + } + } + return 0; +} + +/* + * 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; + + atomic_inc(&ring->queue_refs); + 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 = smp_load_acquire(&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); + + return fuse_uring_do_register(ent, cmd, issue_flags); +} + +/* + * Entry function from io_uring to handle the given passthrough command + * (op code IORING_OP_URING_CMD) + */ +int 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 ((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; + + fud = fuse_get_dev(cmd->file); + if (!fud) { + pr_info_ratelimited("No fuse device found\n"); + return -ENOTCONN; + } + 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) + 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); + fc->io_uring = 0; + wake_up_all(&fc->blocked_waitq); + 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; + } + + 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; + fuse_uring_add_to_pq(ent); + 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; + } + fuse_uring_send(ent, cmd, err, issue_flags); + } else { + err = -ECANCELED; + + 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) +{ + 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; + + 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) + 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); +} + +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; + } + + 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, + 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; +} + +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, + + /* + * 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 new file mode 100644 index 0000000000000..89d96a06ddd4f --- /dev/null +++ b/fs/fuse/dev_uring_i.h @@ -0,0 +1,207 @@ +/* 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 + +#define FUSE_URING_TEARDOWN_TIMEOUT (5 * HZ) +#define FUSE_URING_TEARDOWN_INTERVAL (HZ/20) + +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 got assigned a fuse req */ + FRRS_FUSE_REQ, + + /* 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 */ +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_w_req_queue; + struct list_head ent_commit_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; + + /* background fuse requests */ + struct list_head fuse_req_bg_queue; + + struct fuse_pqueue fpq; + + unsigned int active_background; + + bool stopped; +}; + +/** + * 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; + + /* + * 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 ready; +}; + +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); +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) +{ + struct fuse_ring *ring = fc->ring; + + if (ring == NULL) + return; + + 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) +{ + struct fuse_ring *ring = fc->ring; + + if (ring) + wait_event(ring->stop_waitq, + atomic_read(&ring->queue_refs) == 0); +} + +static inline bool fuse_uring_ready(struct fuse_conn *fc) +{ + struct fuse_ring *ring = READ_ONCE(fc->ring); + + return ring && smp_load_acquire(&ring->ready); +} + +#else /* CONFIG_FUSE_IO_URING */ + +static inline void fuse_uring_destruct(struct fuse_conn *fc) +{ +} + +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) +{ +} + +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/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_dev_i.h b/fs/fuse/fuse_dev_i.h new file mode 100644 index 0000000000000..734606ade0acb --- /dev/null +++ b/fs/fuse/fuse_dev_i.h @@ -0,0 +1,69 @@ +/* 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 + +/* Ordinary requests have even IDs, while interrupts IDs are odd */ +#define FUSE_INT_REQ_BIT (1ULL << 0) +#define FUSE_REQ_ID_STEP (1ULL << 1) + +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; + 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; + 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) +{ + /* + * 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); +} + +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); +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); +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); +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 6873c5f401a27..7cd634f497411 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); }; @@ -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, }; /** @@ -435,6 +437,11 @@ struct fuse_req { /** fuse_mount this request belongs to */ struct fuse_mount *fm; + +#ifdef CONFIG_FUSE_IO_URING + void *ring_entry; + void *ring_queue; +#endif }; struct fuse_iqueue; @@ -860,6 +867,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; @@ -917,6 +927,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 }; /* @@ -941,6 +956,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; @@ -1206,6 +1234,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 8802111e0b478..9a8b3bc214fd1 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 @@ -905,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; @@ -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); } @@ -1355,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; @@ -1414,6 +1419,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/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; 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 */ 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..323aa31624df3 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/.gitignore @@ -0,0 +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 new file mode 100644 index 0000000000000..13b031fe77895 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/Makefile @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) + +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. +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) +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) + +$(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_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 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..44a2b5e0fd1dd --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fuse_mnt.c @@ -0,0 +1,153 @@ +// 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 31 + +#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, + struct fuse_file_info *fi) +{ + (void)fi; + 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, + enum fuse_readdir_flags flags) +{ + (void)flags; + + if (strcmp(path, "/")) + return -ENOENT; + + filler(buf, ".", NULL, 0, 0); + filler(buf, "..", NULL, 0, 0); + filler(buf, test_path + 1, NULL, 0, 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, + struct fuse_file_info *fi) +{ + (void)fi; + 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/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 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