From nobody Mon Jun 8 09:49:28 2026 Received: from smtp02-ext3.udag.de (smtp02-ext3.udag.de [62.146.106.33]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9D2743A9D8E for ; Thu, 4 Jun 2026 09:46:17 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=62.146.106.33 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780566383; cv=none; b=mmRDBEq/JVFjZkwado+00h5baUfpt8LqHLSy6v1H+6iCntOYe//sH0QCMUSQ7GK5Kj7vsPUF8w9PbIMF75zg5XRT3EdBxJ4+Ftx8iX/nUDkfABGAms4UVzPpdh42/OuUZ6amN8sCbFnRywL3KMsVU05lxdgWsVHM8yXd5HfKo/Q= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780566383; c=relaxed/simple; bh=3qy1TSa/3OBLwOMgwqdx7kcEEP2+b2bJNQJyPivjeR0=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=LSWMMcxXaJxQEiTFo8VZ4TapofWR89qIzqWZJbrV3IxYjk91M4Zq72xw8luvb9vw4ia9DrVnsOr4WE5UeEd5jUwJfim4c3dzda6iqBCc33U8f0w45Iz2KUBezg+1HNIZRgYUbXCp1sY42jMD2Mn1kjOescfyXtEnmgZuufpO6L4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=birthelmer.com; spf=pass smtp.mailfrom=birthelmer.com; dkim=pass (2048-bit key) header.d=birthelmer.com header.i=@birthelmer.com header.b=VmFSAf3U; arc=none smtp.client-ip=62.146.106.33 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=birthelmer.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=birthelmer.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=birthelmer.com header.i=@birthelmer.com header.b="VmFSAf3U" Received: from fedora.fritz.box (065-142-067-156.ip-addr.inexio.net [156.67.142.65]) by smtp02-ext3.udag.de (Postfix) with ESMTPA id 12008E05A4; Thu, 4 Jun 2026 11:46:10 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=birthelmer.com; s=uddkim-202310; t=1780566370; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=JmUkGassz6a2ba4561xprEeQfCc9tIACqIF0rHcQMP0=; b=VmFSAf3U+ga4BjWSG3yJpwlGYsBmHyx4e03L6kbYM8zdLrqnbHWzJCQAQ0OL6r5UIOaLUa 1B1hLn3tddUx1qk7ZXcSOH4VhEyguHQoA71TaEvteW+cdGopqnkKF8js2us1ks2inPE3Bv 3OjS2J9vN1TUq9cgFOUoRZtgCxsBbuEtoA6P84cq/7b+5Gri7DM5VvwAT52O6zzyP6Br2m gmGWhpcD2TeJQYV0cKJN1dILcSRNVgpdqpj2q76EZFxykA06xSGtoNCYAOjbePux1BSkGg jPK0+vw3V/3jcb26rueMRCwa79jtV4NlRrxaEkdqVNLzhxZ+mMKlGYh+5vaGYQ== Authentication-Results: smtp02-ext3.udag.de; auth=pass smtp.auth=birthelmercom-0001 smtp.mailfrom=horst@birthelmer.com From: Horst Birthelmer Date: Thu, 04 Jun 2026 11:45:42 +0200 Subject: [PATCH v7 1/4] fuse: add compound command to combine multiple requests Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260604-fuse-compounds-upstream-v7-1-27331d085c2a@ddn.com> References: <20260604-fuse-compounds-upstream-v7-0-27331d085c2a@ddn.com> In-Reply-To: <20260604-fuse-compounds-upstream-v7-0-27331d085c2a@ddn.com> To: Miklos Szeredi , Bernd Schubert , Joanne Koong , Luis Henriques Cc: linux-kernel@vger.kernel.org, fuse-devel@lists.linux.dev, Horst Birthelmer X-Mailer: b4 0.14.3 X-Developer-Signature: v=1; a=ed25519-sha256; t=1780566367; l=21860; i=hbirthelmer@ddn.com; s=20251006; h=from:subject:message-id; bh=+4eAUxUEVvaDUdQ21O3P2PJbWVwxSJMmO1LJgm0JwBI=; b=9rwYVfaowtAdGhOlFCGw8vn6tB1BHTVbPxL+iWd9wQvSoi5IodeB3Lzfl/OIidWdHKd/ZTxwP ZA/qJBIIuYmB7YdEt6EU74lGH1M+woYBRC19EAexN93tkYrfLW6IUN4 X-Developer-Key: i=hbirthelmer@ddn.com; a=ed25519; pk=v3BVDFoy16EzgHZ23ObqW+kbpURtjrwxgKu8YNDKjGg= From: Horst Birthelmer Introduce FUSE_COMPOUND, a meta-opcode that bundles several FUSE operations into a single request/response round trip. The wire format is: fuse_in_header (opcode FUSE_COMPOUND) fuse_compound_in (reserved metadata) fuse_compound_req_in (per-subop header) fuse_in_header (per-subop opcode/nodeid/credentials) payload ... (repeated per subop) Each call site supplies an array of struct fuse_compound_op { struct fuse_args *arg; int *error; u8 dep_index; }; where @dep_index is FUSE_COMPOUND_NO_DEP or the index of an earlier subop whose output should be threaded into this subop's input (currently only the producing op's nodeid is propagated). Per-subop status is reported via *error. The reply mirrors the request: fuse_out_header (compound status) fuse_compound_out (reserved metadata) fuse_out_header (per-subop status) payload ... (repeated per subop) If the server returns -ENOSYS, FUSE_COMPOUND is disabled for the connection and each subop is dispatched individually via fuse_simple_request(). -EOPNOTSUPP signals that this specific combination is unsupported and triggers per-request legacy dispatch without disabling the feature. The legacy path validates that dep_index refers to a strictly earlier subop and warns otherwise. Signed-off-by: Horst Birthelmer --- fs/fuse/Makefile | 2 +- fs/fuse/compound.c | 126 ++++++++++++++++++++++++++++ fs/fuse/dev.c | 206 ++++++++++++++++++++++++++++++++++++++++++= +--- fs/fuse/dev_uring.c | 31 ++++++- fs/fuse/fuse_dev_i.h | 5 ++ fs/fuse/fuse_i.h | 32 +++++++ fs/fuse/inode.c | 9 ++ include/uapi/linux/fuse.h | 56 +++++++++++++ 8 files changed, 452 insertions(+), 15 deletions(-) diff --git a/fs/fuse/Makefile b/fs/fuse/Makefile index 22ad9538dfc4..4c09038ef995 100644 --- a/fs/fuse/Makefile +++ b/fs/fuse/Makefile @@ -11,7 +11,7 @@ obj-$(CONFIG_CUSE) +=3D cuse.o obj-$(CONFIG_VIRTIO_FS) +=3D virtiofs.o =20 fuse-y :=3D trace.o # put trace.o first so we see ftrace errors sooner -fuse-y +=3D dev.o dir.o file.o inode.o control.o xattr.o acl.o readdir.o i= octl.o +fuse-y +=3D dev.o dir.o file.o inode.o control.o xattr.o acl.o readdir.o i= octl.o compound.o fuse-y +=3D iomode.o fuse-$(CONFIG_FUSE_DAX) +=3D dax.o fuse-$(CONFIG_FUSE_PASSTHROUGH) +=3D passthrough.o backing.o diff --git a/fs/fuse/compound.c b/fs/fuse/compound.c new file mode 100644 index 000000000000..debf2a19846d --- /dev/null +++ b/fs/fuse/compound.c @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * FUSE: Filesystem in Userspace + * Copyright (C) 2025-2026 + * + * Compound operations for FUSE - batch multiple operations into a single + * request to reduce round trips between kernel and userspace. + */ + +#include "fuse_i.h" + +/* + * Copy the nodeid from a producing subop's output into a dependent + * subop's input. Only entry-producing opcodes are recognised; depending + * on a non-entry op is a caller bug and triggers a warning so the bad + * dispatch is visible instead of silently sending nodeid 0. + */ +static void fuse_compound_propagate_nodeid(struct fuse_args *dep, + const struct fuse_args *src) +{ + const struct fuse_entry_out *entry_out; + + if (src->out_numargs =3D=3D 0) + return; + + switch (src->opcode) { + case FUSE_LOOKUP: + case FUSE_MKNOD: + case FUSE_MKDIR: + case FUSE_SYMLINK: + case FUSE_LINK: + case FUSE_CREATE: + case FUSE_TMPFILE: + entry_out =3D src->out_args[0].value; + if (entry_out) + dep->nodeid =3D entry_out->nodeid; + break; + default: + WARN_ONCE(1, "fuse: compound dep on non-entry opcode %u\n", + src->opcode); + break; + } +} + +/* Fallback: dispatch each subop individually as a normal FUSE request. */ +static void fuse_compound_send_legacy(struct fuse_mount *fm, + struct fuse_compound_op *ops, + unsigned int count) +{ + unsigned int i; + + for (i =3D 0; i < count; i++) { + struct fuse_compound_op *cur =3D &ops[i]; + + if (cur->dep_index !=3D FUSE_COMPOUND_NO_DEP) { + struct fuse_compound_op *dep; + + /* + * dep_index must refer to an earlier subop in the + * same compound so its result is already available. + * A forward or self reference is a caller bug; fail + * the subop loudly instead of reading uninitialised + * memory. + */ + if (WARN_ON_ONCE(cur->dep_index >=3D i)) { + *cur->error =3D -EINVAL; + continue; + } + dep =3D &ops[cur->dep_index]; + + if (*dep->error) { + *cur->error =3D *dep->error; + continue; + } + fuse_compound_propagate_nodeid(cur->arg, dep->arg); + } + *cur->error =3D fuse_simple_request(fm, cur->arg); + } +} + +/* + * Send a compound request. Per-subop status is reported via the @error + * pointer of each fuse_compound_op; the return value is 0 if the + * compound was dispatched (whether server-side or via the legacy + * fallback) and a negative errno only if dispatch itself failed. + * + * Server-side decline signaling: + * -ENOSYS Compound is not implemented at all. Disable the + * feature for this connection and fall back to legacy + * dispatch for this and every subsequent request. + * -EOPNOTSUPP This specific compound combination is not supported, + * but the feature remains usable. Fall back to legacy + * dispatch for this request only; leave fc->compound_ops + * set so future requests may still go through compound. + * + * (ENOTSUPP is a Linux-internal errno > 511 and is rejected by + * fuse_dev_do_write(), so a userspace server cannot signal it.) + */ +int fuse_compound_send(struct fuse_mount *fm, + struct fuse_compound_op *ops, unsigned int count) +{ + struct fuse_conn *fc =3D fm->fc; + struct fuse_compound_args compound =3D { + .args =3D { .opcode =3D FUSE_COMPOUND, }, + .ops =3D ops, + .count =3D count, + }; + int ret; + + if (WARN_ON_ONCE(count =3D=3D 0)) + return -EINVAL; + + if (!fc->compound_ops) { + fuse_compound_send_legacy(fm, ops, count); + return 0; + } + + ret =3D fuse_simple_request(fm, &compound.args); + if (ret =3D=3D -ENOSYS) + fc->compound_ops =3D 0; + if (ret =3D=3D -ENOSYS || ret =3D=3D -EOPNOTSUPP) { + fuse_compound_send_legacy(fm, ops, count); + return 0; + } + return ret; +} diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index 5dda7080f4a9..7b20e4e9971a 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -413,11 +413,47 @@ const struct fuse_iqueue_ops fuse_dev_fiq_ops =3D { }; EXPORT_SYMBOL_GPL(fuse_dev_fiq_ops); =20 +static inline struct fuse_compound_args * +fuse_get_compound_args(struct fuse_args *args) +{ + if (args->opcode =3D=3D FUSE_COMPOUND) + return container_of(args, struct fuse_compound_args, args); + return NULL; +} + +static size_t fuse_compound_req_size(struct fuse_compound_args *compound) +{ + size_t total =3D sizeof(struct fuse_in_header) + + sizeof(struct fuse_compound_in); + unsigned int i, j; + + for (i =3D 0; i < compound->count; i++) { + struct fuse_args *op_args =3D compound->ops[i].arg; + + total +=3D sizeof(struct fuse_compound_req_in) + + sizeof(struct fuse_in_header); + for (j =3D 0; j < op_args->in_numargs; j++) + total +=3D op_args->in_args[j].size; + } + + return total; +} + +void fuse_set_req_len(struct fuse_req *req) +{ + struct fuse_compound_args *compound =3D fuse_get_compound_args(req->args); + + if (compound) + req->in.h.len =3D fuse_compound_req_size(compound); + else + req->in.h.len =3D sizeof(struct fuse_in_header) + + fuse_len_args(req->args->in_numargs, + (struct fuse_arg *)req->args->in_args); +} + static void fuse_send_one(struct fuse_iqueue *fiq, struct fuse_req *req) { - req->in.h.len =3D sizeof(struct fuse_in_header) + - fuse_len_args(req->args->in_numargs, - (struct fuse_arg *) req->args->in_args); + fuse_set_req_len(req); fiq->ops->send_req(fiq, req); } =20 @@ -713,9 +749,7 @@ static bool fuse_request_queue_background_uring(struct = fuse_conn *fc, { struct fuse_iqueue *fiq =3D &fc->iq; =20 - req->in.h.len =3D sizeof(struct fuse_in_header) + - fuse_len_args(req->args->in_numargs, - (struct fuse_arg *) req->args->in_args); + fuse_set_req_len(req); fuse_request_assign_unique(fiq, req); =20 return fuse_uring_queue_bq_req(req); @@ -1204,7 +1238,7 @@ static int fuse_copy_folios(struct fuse_copy_state *c= s, unsigned nbytes, } =20 /* Copy a single argument in the request to/from userspace buffer */ -static int fuse_copy_one(struct fuse_copy_state *cs, void *val, unsigned s= ize) +static int fuse_copy_one(struct fuse_copy_state *cs, void *val, unsigned i= nt size) { while (size) { if (!cs->len) { @@ -1399,6 +1433,66 @@ __releases(fiq->lock) return fuse_read_batch_forget(fiq, cs, nbytes); } =20 +/* + * Stream the body of a compound request directly from the per-subop + * argument buffers, avoiding a pre-built linear copy. + * + * The fuse_compound_in header is inlined in the request stream for + * /dev/fuse but for io-uring it is carried in a separate fixed slot + * (ent->headers->op_in) written before this function runs. + */ +int fuse_copy_compound_in_args(struct fuse_copy_state *cs, + struct fuse_compound_args *compound) +{ + unsigned int i, j; + int err; + + if (!cs->is_uring) { + err =3D fuse_copy_one(cs, &compound->in_header, + sizeof(compound->in_header)); + if (err) + return err; + } + + for (i =3D 0; i < compound->count; i++) { + struct fuse_compound_op *op =3D &compound->ops[i]; + struct fuse_args *op_args =3D op->arg; + struct fuse_compound_req_in sub =3D { + .dep_index =3D op->dep_index, + }; + /* + * Inherit the outer request's credentials so each subop's + * fuse_in_header carries valid uid/gid/pid instead of + * zeros that would mislead the server. + */ + struct fuse_in_header hdr =3D { + .unique =3D i, + .opcode =3D op_args->opcode, + .nodeid =3D op_args->nodeid, + .uid =3D cs->req->in.h.uid, + .gid =3D cs->req->in.h.gid, + .pid =3D cs->req->in.h.pid, + .len =3D sizeof(hdr), + }; + + for (j =3D 0; j < op_args->in_numargs; j++) + hdr.len +=3D op_args->in_args[j].size; + + err =3D fuse_copy_one(cs, &sub, sizeof(sub)); + if (!err) + err =3D fuse_copy_one(cs, &hdr, sizeof(hdr)); + if (!err) + err =3D fuse_copy_args(cs, op_args->in_numargs, + op_args->in_pages, + (struct fuse_arg *)op_args->in_args, + 0); + if (err) + return err; + } + + return 0; +} + /* * Read a single request into the userspace filesystem's buffer. This * function waits until a request is available, then removes it from @@ -1503,9 +1597,15 @@ static ssize_t fuse_dev_do_read(struct fuse_dev *fud= , struct file *file, spin_unlock(&fpq->lock); cs->req =3D req; err =3D fuse_copy_one(cs, &req->in.h, sizeof(req->in.h)); - if (!err) - err =3D fuse_copy_args(cs, args->in_numargs, args->in_pages, - (struct fuse_arg *) args->in_args, 0); + if (!err) { + struct fuse_compound_args *compound =3D fuse_get_compound_args(args); + + if (compound) + err =3D fuse_copy_compound_in_args(cs, compound); + else + err =3D fuse_copy_args(cs, args->in_numargs, args->in_pages, + (struct fuse_arg *)args->in_args, 0); + } fuse_copy_finish(cs); spin_lock(&fpq->lock); clear_bit(FR_LOCKED, &req->flags); @@ -2146,6 +2246,80 @@ struct fuse_req *fuse_request_find(struct fuse_pqueu= e *fpq, u64 unique) return NULL; } =20 +int fuse_copy_compound_out_args(struct fuse_copy_state *cs, + struct fuse_compound_args *compound) +{ + unsigned int i; + int err; + + err =3D fuse_copy_one(cs, &compound->out_header, + sizeof(compound->out_header)); + if (err) + return err; + + for (i =3D 0; i < compound->count; i++) { + struct fuse_compound_op *op =3D &compound->ops[i]; + struct fuse_out_header op_hdr; + size_t expected; + + err =3D fuse_copy_one(cs, &op_hdr, sizeof(op_hdr)); + if (err) + return err; + if (op_hdr.len < sizeof(op_hdr)) + return -EIO; + /* + * Subop replies must echo the request's per-subop unique + * back to the kernel; we wrote unique =3D i in + * fuse_copy_compound_in_args() so the server is expected + * to mirror it here. Reject otherwise: a mismatch means + * the server reordered or duplicated subop replies. + */ + if (op_hdr.unique !=3D i) + return -EIO; + + *op->error =3D op_hdr.error; + if (op_hdr.error) { + /* Errored replies carry only the fuse_out_header. */ + if (op_hdr.len !=3D sizeof(op_hdr)) + return -EIO; + continue; + } + + /* + * Validate the wire length against what the kernel can + * accept. expected is the maximum: sum of all declared + * out_args plus the per-subop header. Fixed-size subops + * must match exactly; out_argvar subops may report any + * length in [expected - lastarg->size, expected] and the + * last arg shrinks to fit. Mirrors fuse_copy_out_args(). + */ + expected =3D sizeof(op_hdr) + + fuse_len_args(op->arg->out_numargs, + op->arg->out_args); + if (op_hdr.len > expected || + (op_hdr.len < expected && !op->arg->out_argvar)) + return -EIO; + if (op_hdr.len < expected) { + struct fuse_arg *lastarg =3D + &op->arg->out_args[op->arg->out_numargs - 1]; + size_t diff =3D expected - op_hdr.len; + + if (diff > lastarg->size) + return -EIO; + lastarg->size -=3D diff; + } + + err =3D fuse_copy_args(cs, op->arg->out_numargs, + op->arg->out_pages, + (struct fuse_arg *)op->arg->out_args, + op->arg->page_zeroing); + if (err) + return err; + } + + return 0; +} + int fuse_copy_out_args(struct fuse_copy_state *cs, struct fuse_args *args, unsigned nbytes) { @@ -2253,10 +2427,16 @@ static ssize_t fuse_dev_do_write(struct fuse_dev *f= ud, if (!req->args->page_replace) cs->move_folios =3D false; =20 - if (oh.error) + if (oh.error) { err =3D nbytes !=3D sizeof(oh) ? -EINVAL : 0; - else - err =3D fuse_copy_out_args(cs, req->args, nbytes); + } else { + struct fuse_compound_args *compound =3D fuse_get_compound_args(req->args= ); + + if (compound) + err =3D fuse_copy_compound_out_args(cs, compound); + else + err =3D fuse_copy_out_args(cs, req->args, nbytes); + } fuse_copy_finish(cs); =20 spin_lock(&fpq->lock); diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c index 7b9822e8837b..f6eee4cff136 100644 --- a/fs/fuse/dev_uring.c +++ b/fs/fuse/dev_uring.c @@ -595,7 +595,14 @@ static int fuse_uring_copy_from_ring(struct fuse_ring = *ring, cs.is_uring =3D true; cs.req =3D req; =20 - err =3D fuse_copy_out_args(&cs, args, ring_in_out.payload_sz); + if (args->opcode =3D=3D FUSE_COMPOUND) { + /* Stream compound response directly into operation buffers */ + struct fuse_compound_args *compound =3D + container_of(args, struct fuse_compound_args, args); + err =3D fuse_copy_compound_out_args(&cs, compound); + } else { + err =3D fuse_copy_out_args(&cs, args, ring_in_out.payload_sz); + } fuse_copy_finish(&cs); return err; } @@ -627,6 +634,27 @@ static int fuse_uring_args_to_ring(struct fuse_ring *r= ing, struct fuse_req *req, cs.is_uring =3D true; cs.req =3D req; =20 + if (args->opcode =3D=3D FUSE_COMPOUND) { + /* + * Treat fuse_compound_in as the per-op header: it goes + * into ent->headers->op_in (matching the placement of any + * other op-specific header on the io-uring transport), + * while the per-subop stream flows through ent->payload. + */ + struct fuse_compound_args *compound =3D + container_of(args, struct fuse_compound_args, args); + + err =3D copy_to_user(&ent->headers->op_in, &compound->in_header, + sizeof(compound->in_header)); + if (err) { + pr_info_ratelimited("Copying the compound header failed.\n"); + return -EFAULT; + } + + err =3D fuse_copy_compound_in_args(&cs, compound); + goto out_finish; + } + if (num_args > 0) { /* * Expectation is that the first argument is the per op header. @@ -648,6 +676,7 @@ static int fuse_uring_args_to_ring(struct fuse_ring *ri= ng, struct fuse_req *req, /* copy the payload */ err =3D fuse_copy_args(&cs, num_args, args->in_pages, (struct fuse_arg *)in_args, 0); +out_finish: fuse_copy_finish(&cs); if (err) { pr_info_ratelimited("%s fuse_copy_args failed\n", __func__); diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h index 910f883cd090..5114b376fd34 100644 --- a/fs/fuse/fuse_dev_i.h +++ b/fs/fuse/fuse_dev_i.h @@ -86,6 +86,11 @@ int fuse_copy_args(struct fuse_copy_state *cs, unsigned = int numargs, int zeroing); int fuse_copy_out_args(struct fuse_copy_state *cs, struct fuse_args *args, unsigned int nbytes); +int fuse_copy_compound_in_args(struct fuse_copy_state *cs, + struct fuse_compound_args *compound); +int fuse_copy_compound_out_args(struct fuse_copy_state *cs, + struct fuse_compound_args *compound); +void fuse_set_req_len(struct fuse_req *req); 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 *re= q); diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 17423d4e3cfa..af4ea2af19d1 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -911,6 +911,9 @@ struct fuse_conn { /** Passthrough support for read/write IO */ unsigned int passthrough:1; =20 + /* does fuse server support compound operations? */ + unsigned int compound_ops:1; + /* Use pages instead of pointer for kernel I/O */ unsigned int use_pages_for_kvec_io:1; =20 @@ -1272,6 +1275,35 @@ static inline ssize_t fuse_simple_idmap_request(stru= ct mnt_idmap *idmap, int fuse_simple_background(struct fuse_mount *fm, struct fuse_args *args, gfp_t gfp_flags); =20 +/* + * One subrequest in a compound. @dep_index is FUSE_COMPOUND_NO_DEP, or + * the index of an earlier op in the array whose output should be used to + * fill in this op's nodeid before dispatch. @error receives the per-op + * status after fuse_compound_send() returns. + */ +struct fuse_compound_op { + struct fuse_args *arg; + int *error; + u8 dep_index; +}; + +/* + * Compound wrapper. Embeds fuse_args as the first member so the device + * layer can container_of() back to the operation array. The in_header + * and out_header fields are reserved-only today but reach the wire so + * future extensions can attach compound-level metadata. + */ +struct fuse_compound_args { + struct fuse_args args; + struct fuse_compound_op *ops; + unsigned int count; + struct fuse_compound_in in_header; + struct fuse_compound_out out_header; +}; + +int fuse_compound_send(struct fuse_mount *fm, + struct fuse_compound_op *ops, unsigned int count); + /** * Assign a unique id to a fuse request */ diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index deddfffb037f..c275864710b6 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -1030,6 +1030,15 @@ void fuse_conn_init(struct fuse_conn *fc, struct fus= e_mount *fm, fc->name_max =3D FUSE_NAME_LOW_MAX; fc->timeout.req_timeout =3D 0; =20 + /* + * Compound support is discovered by trial: assume the server + * implements it and clear the flag on the first -ENOSYS reply. + * Unlike most connection features there is no FUSE_INIT flag, so + * default-on is correct here even though other capability bits + * default to zero. + */ + fc->compound_ops =3D 1; + if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH)) fuse_backing_files_init(fc); =20 diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h index c13e1f9a2f12..58dd07a6c53a 100644 --- a/include/uapi/linux/fuse.h +++ b/include/uapi/linux/fuse.h @@ -664,6 +664,13 @@ enum fuse_opcode { FUSE_STATX =3D 52, FUSE_COPY_FILE_RANGE_64 =3D 53, =20 + /* A compound request is handled like a single request, + * but contains multiple requests as input. + * This can be used to signal to the fuse server that + * the requests can be combined atomically. + */ + FUSE_COMPOUND =3D 54, + /* CUSE specific operations */ CUSE_INIT =3D 4096, =20 @@ -1245,6 +1252,55 @@ struct fuse_supp_groups { uint32_t groups[]; }; =20 +/* + * Sentinel value for fuse_compound_req_in.dep_index meaning the + * subrequest does not depend on any other subrequest. The dep_index + * field is a uint8_t so the largest dispatchable compound is bounded + * by FUSE_COMPOUND_MAX_OPS subrequests. + */ +#define FUSE_COMPOUND_NO_DEP 0xff + +/* + * Compound request layout: + * + * fuse_in_header (opcode FUSE_COMPOUND) + * fuse_compound_in + * fuse_compound_req_in + * fuse_in_header + * payload + * ... (repeated per subrequest) + * + * The compound reply layout mirrors it: + * + * fuse_out_header + * fuse_compound_out + * fuse_out_header + * payload + * ... (repeated per subrequest) + * + * fuse_compound_in / fuse_compound_out currently only carry reserved + * fields; they exist so future extensions can attach compound-level + * metadata without another wire-format change. + */ +struct fuse_compound_in { + uint64_t reserved[2]; +}; + +struct fuse_compound_out { + uint64_t reserved[2]; +}; + +/* + * Per-subrequest header. dep_index identifies an earlier subrequest in + * the same compound whose output should be threaded into this one's + * input (currently only the producing op's nodeid is propagated), or + * FUSE_COMPOUND_NO_DEP if the subrequest is independent. + */ +struct fuse_compound_req_in { + uint8_t dep_index; + uint8_t reserved[3]; +}; + /** * Size of the ring buffer header */ --=20 2.54.0 From nobody Mon Jun 8 09:49:28 2026 Received: from smtp02-ext3.udag.de (smtp02-ext3.udag.de [62.146.106.33]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 829243CB902 for ; Thu, 4 Jun 2026 09:46:21 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=62.146.106.33 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780566383; cv=none; b=OxstJQRm5zcSzx8UB4zuY2T92HogPG/RCmajUAFiCLCJ1mxaiwwUtPhl//X0ROPWV9e60c/e105kaZrGCz3Yngg7CA5bxtmiwh0bIC7WyQuan9F9xyZANMUjAE/cN6AgwHgyg9nqdOh1J5k5jmO4DV21lCtHUh0gzGlNeMJKdEo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780566383; c=relaxed/simple; bh=/jOUuE8A827fdgh0IwjfkmQUwDq72aMWzDur6/M5Nxo=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=hGafHYPXyKX+ZaZ/75b75CzOLr6+YUpF91ihK51EYbgjx2zJcMzMmdsI7yi1jeNxZHz5pQSxby/7iclkoVfq7THuNehV0gLmJP+rZZ1UT6cHW5yb0EjksU+fhlEuxwE8jBCrmFFSKlnmNIOGIwv/RKAThEJgisvf4iMUXMX6tsU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=birthelmer.com; spf=pass smtp.mailfrom=birthelmer.com; dkim=pass (2048-bit key) header.d=birthelmer.com header.i=@birthelmer.com header.b=KPQ9mBYD; arc=none smtp.client-ip=62.146.106.33 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=birthelmer.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=birthelmer.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=birthelmer.com header.i=@birthelmer.com header.b="KPQ9mBYD" Received: from fedora.fritz.box (065-142-067-156.ip-addr.inexio.net [156.67.142.65]) by smtp02-ext3.udag.de (Postfix) with ESMTPA id B744CE05AC; Thu, 4 Jun 2026 11:46:10 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=birthelmer.com; s=uddkim-202310; t=1780566371; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=JjIvc08CQzM3YhafcvZd048VbZJEJMd2z8bQM4fbVk0=; b=KPQ9mBYDw90Cwaiq/eO3PwgtbhcSJEToC7zsy+Vpg2t4xTRzMDqiXaJWpoGqOZi/CRGL6A tQWcKFh+e60DQ80wK+TRmS15Des1mRY2TrmkKPQ7LP5iAIVdcI1xBL7H6kOIf8n4bhSQlf EGtUnq7/VDAypZ4Dm3GAbmiLAa5FSW6oQnEu4txEhuoTF5DbGAW//AOtZgWgrO/n7NJbWE HCHBRpyhsuB+pIzMsqTIsy4IF1VC3tF0xrTX8D/EurLqQW8q/vTsG72JwA56j82XRyNeUG E3oGV+6QJTegND1/UfEC+5MN8An8DEBVn2XgYjkHaFw1UHCnUq5YxEb9/bPJvA== Authentication-Results: smtp02-ext3.udag.de; auth=pass smtp.auth=birthelmercom-0001 smtp.mailfrom=horst@birthelmer.com From: Horst Birthelmer Date: Thu, 04 Jun 2026 11:45:43 +0200 Subject: [PATCH v7 2/4] fuse: create helper functions for filling in fuse args for open and getattr Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260604-fuse-compounds-upstream-v7-2-27331d085c2a@ddn.com> References: <20260604-fuse-compounds-upstream-v7-0-27331d085c2a@ddn.com> In-Reply-To: <20260604-fuse-compounds-upstream-v7-0-27331d085c2a@ddn.com> To: Miklos Szeredi , Bernd Schubert , Joanne Koong , Luis Henriques Cc: linux-kernel@vger.kernel.org, fuse-devel@lists.linux.dev, Horst Birthelmer X-Mailer: b4 0.14.3 X-Developer-Signature: v=1; a=ed25519-sha256; t=1780566367; l=4707; i=hbirthelmer@ddn.com; s=20251006; h=from:subject:message-id; bh=dTYbeJXdiN8bAxaQBlu5xp2dPQ272w4b4WKdf5qCz/A=; b=KY8wds4adfF1yBeLx7TW7cjYATy54TI7dL2IYp3Lunh9rhd8ymwRDsBGdI1aPzJfVk8wJF/mn Geq2Rp8jrFWCNBgp7WxQuLSH9p2ncT/UclHHBS5XYtrUr2vqWevGNN2 X-Developer-Key: i=hbirthelmer@ddn.com; a=ed25519; pk=v3BVDFoy16EzgHZ23ObqW+kbpURtjrwxgKu8YNDKjGg= From: Horst Birthelmer create fuse_getattr_args_fill() and fuse_open_args_fill() to fill in the parameters for the open and getattr calls. This is in preparation for implementing open+getattr and does not represent any functional change. Suggested-by: Joanne Koong Signed-off-by: Horst Birthelmer Reviewed-by: Amir Goldstein --- fs/fuse/dir.c | 26 ++++++++++++++++++-------- fs/fuse/file.c | 44 ++++++++++++++++++++++++++++---------------- fs/fuse/fuse_i.h | 6 ++++++ 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index b658b6baf72f..b3406c33abd2 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -1475,6 +1475,23 @@ static int fuse_do_statx(struct mnt_idmap *idmap, st= ruct inode *inode, return 0; } =20 +/* + * Helper function to initialize fuse_args for GETATTR operations + */ +void fuse_getattr_args_fill(struct fuse_args *args, u64 nodeid, + struct fuse_getattr_in *inarg, + struct fuse_attr_out *outarg) +{ + args->opcode =3D FUSE_GETATTR; + args->nodeid =3D nodeid; + args->in_numargs =3D 1; + args->in_args[0].size =3D sizeof(*inarg); + args->in_args[0].value =3D inarg; + args->out_numargs =3D 1; + args->out_args[0].size =3D sizeof(*outarg); + args->out_args[0].value =3D outarg; +} + static int fuse_do_getattr(struct mnt_idmap *idmap, struct inode *inode, struct kstat *stat, struct file *file) { @@ -1496,14 +1513,7 @@ static int fuse_do_getattr(struct mnt_idmap *idmap, = struct inode *inode, inarg.getattr_flags |=3D FUSE_GETATTR_FH; inarg.fh =3D ff->fh; } - args.opcode =3D FUSE_GETATTR; - args.nodeid =3D get_node_id(inode); - args.in_numargs =3D 1; - args.in_args[0].size =3D sizeof(inarg); - args.in_args[0].value =3D &inarg; - args.out_numargs =3D 1; - args.out_args[0].size =3D sizeof(outarg); - args.out_args[0].value =3D &outarg; + fuse_getattr_args_fill(&args, get_node_id(inode), &inarg, &outarg); err =3D fuse_simple_request(fm, &args); if (!err) { if (fuse_invalid_attr(&outarg.attr) || diff --git a/fs/fuse/file.c b/fs/fuse/file.c index f94f3dc082c6..a7d602225f45 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -23,6 +23,33 @@ #include #include =20 +/* + * Helper function to initialize fuse_args for OPEN/OPENDIR operations + */ +static void fuse_open_args_fill(struct fuse_mount *fm, struct fuse_args *a= rgs, + u64 nodeid, int opcode, unsigned int open_flags, + struct fuse_open_in *inarg, + struct fuse_open_out *outarg) +{ + inarg->flags =3D open_flags & ~(O_CREAT | O_EXCL | O_NOCTTY); + + if (!fm->fc->atomic_o_trunc) + inarg->flags &=3D ~O_TRUNC; + + if (fm->fc->handle_killpriv_v2 && + (inarg->flags & O_TRUNC) && !capable(CAP_FSETID)) + inarg->open_flags |=3D FUSE_OPEN_KILL_SUIDGID; + + args->opcode =3D opcode; + args->nodeid =3D nodeid; + args->in_numargs =3D 1; + args->in_args[0].size =3D sizeof(*inarg); + args->in_args[0].value =3D inarg; + args->out_numargs =3D 1; + args->out_args[0].size =3D sizeof(*outarg); + args->out_args[0].value =3D outarg; +} + static int fuse_send_open(struct fuse_mount *fm, u64 nodeid, unsigned int open_flags, int opcode, struct fuse_open_out *outargp) @@ -31,23 +58,8 @@ static int fuse_send_open(struct fuse_mount *fm, u64 nod= eid, FUSE_ARGS(args); =20 memset(&inarg, 0, sizeof(inarg)); - inarg.flags =3D open_flags & ~(O_CREAT | O_EXCL | O_NOCTTY); - if (!fm->fc->atomic_o_trunc) - inarg.flags &=3D ~O_TRUNC; - - if (fm->fc->handle_killpriv_v2 && - (inarg.flags & O_TRUNC) && !capable(CAP_FSETID)) { - inarg.open_flags |=3D FUSE_OPEN_KILL_SUIDGID; - } =20 - args.opcode =3D opcode; - args.nodeid =3D nodeid; - args.in_numargs =3D 1; - args.in_args[0].size =3D sizeof(inarg); - args.in_args[0].value =3D &inarg; - args.out_numargs =3D 1; - args.out_args[0].size =3D sizeof(*outargp); - args.out_args[0].value =3D outargp; + fuse_open_args_fill(fm, &args, nodeid, opcode, open_flags, &inarg, outarg= p); =20 return fuse_simple_request(fm, &args); } diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index af4ea2af19d1..219312d5f21e 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -1181,6 +1181,12 @@ struct fuse_io_args { void fuse_read_args_fill(struct fuse_io_args *ia, struct file *file, loff_= t pos, size_t count, int opcode); =20 +/* + * Helper functions to initialize fuse_args for common operations + */ +void fuse_getattr_args_fill(struct fuse_args *args, u64 nodeid, + struct fuse_getattr_in *inarg, + struct fuse_attr_out *outarg); =20 struct fuse_file *fuse_file_alloc(struct fuse_mount *fm, bool release); void fuse_file_free(struct fuse_file *ff); --=20 2.54.0 From nobody Mon Jun 8 09:49:28 2026 Received: from smtp02-ext3.udag.de (smtp02-ext3.udag.de [62.146.106.33]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 882113CC308 for ; Thu, 4 Jun 2026 09:46:21 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=62.146.106.33 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780566383; cv=none; b=BLuM21+nmIWIQqQTvpX5SRwFRR1fa1mB96RMeO0bA4NSrtSfneQFYEpEuTZEVzigpA8L+nx2MuwTKQ4cwQH33QsbkKqmQxBQOU9N+TYuyA94P5ClVt5CaBkJJk+vf1FplrZaaSwBUYkHKGQ/ahYqc9lzWqooXqxzXxE+R6bpVq4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780566383; c=relaxed/simple; bh=AqyUfgZZlsS6/eTHdH//5yUL1hczxMwgCooZdfyolQQ=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=TOY0+syvGgWqgpRU08h+x+GB/1qMlpPjBLffw0E7/h/q0HpC/w02rRP+uudoS2BAY98ADFzgy5XXQ2YOPQdcKO1BG6+IX2UiR0jr2roxWc+eB5coexZGHrGwlahM8XNkgbaXffevjBwBNbI8NI/kv9OPy43mNBO53iX9bOjsS7E= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=birthelmer.com; spf=pass smtp.mailfrom=birthelmer.com; dkim=pass (2048-bit key) header.d=birthelmer.com header.i=@birthelmer.com header.b=Db9HJdTu; arc=none smtp.client-ip=62.146.106.33 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=birthelmer.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=birthelmer.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=birthelmer.com header.i=@birthelmer.com header.b="Db9HJdTu" Received: from fedora.fritz.box (065-142-067-156.ip-addr.inexio.net [156.67.142.65]) by smtp02-ext3.udag.de (Postfix) with ESMTPA id 888D8E0403; Thu, 4 Jun 2026 11:46:11 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=birthelmer.com; s=uddkim-202310; t=1780566371; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=lMuTmFW5r599WNvlY21ci1uqO+sTbZsvcCQXc9MkK/c=; b=Db9HJdTu1b5wHFSYQuOcPLaW2byTF4PPfPgEONZXkP6seO0cPM21yJj2BNgePYe4KTzH+1 ICF84bhR0RkN2Fak/tv9+GP2RxiOtWkWvuaYPv+fv7VJh9rnwYMgv2NHosQy42ZaqMQIAx uyWEYWxjbf8OvMYE3Rff4wiKsM5hc1XDf7IKemUv4+og9iiHLiJSplmNnbOyxwEVpdp0/p gWBx2Ox/5rb3L1s6D4XhYs96V6mYQZg1GWII85q+DeP04Do3DvDymbCyBr1OX/1cT1RM1w zRYORaqySPziHo9nzA4U6ZxmiAEYKIkXcTea8Jsy3w5h4tdb55Nf4BXlVs/CTA== Authentication-Results: smtp02-ext3.udag.de; auth=pass smtp.auth=birthelmercom-0001 smtp.mailfrom=horst@birthelmer.com From: Horst Birthelmer Date: Thu, 04 Jun 2026 11:45:44 +0200 Subject: [PATCH v7 3/4] fuse: add an implementation of open+getattr Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260604-fuse-compounds-upstream-v7-3-27331d085c2a@ddn.com> References: <20260604-fuse-compounds-upstream-v7-0-27331d085c2a@ddn.com> In-Reply-To: <20260604-fuse-compounds-upstream-v7-0-27331d085c2a@ddn.com> To: Miklos Szeredi , Bernd Schubert , Joanne Koong , Luis Henriques Cc: linux-kernel@vger.kernel.org, fuse-devel@lists.linux.dev, Horst Birthelmer X-Mailer: b4 0.14.3 X-Developer-Signature: v=1; a=ed25519-sha256; t=1780566367; l=4926; i=hbirthelmer@ddn.com; s=20251006; h=from:subject:message-id; bh=6af5hNwMWG0GIEHjaQTxHaE+gWa2QypMDLzCwtERvU0=; b=lDHZY+dYkMFPfpyGT3E5v3DIJjCD8geSvqnRgv18I0bLakRTKwD7j/YEbxMVSQ8TZQ2tn1TMH B16xFECVJ1mBv7ZYdWRl3MxhtvJAl/ckDwK7u+jpiHwG5J2WCCBPCsw X-Developer-Key: i=hbirthelmer@ddn.com; a=ed25519; pk=v3BVDFoy16EzgHZ23ObqW+kbpURtjrwxgKu8YNDKjGg= From: Horst Birthelmer Fix the issue described here: https://lore.kernel.org/all/20240813212149.1909627-1-joannelkoong@gmail.com/ When appending to a file or having stale attributes we can use a compound to open the file and retrieve the attributes. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++= +--- fs/fuse/fuse_i.h | 1 + fs/fuse/ioctl.c | 2 +- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index a7d602225f45..0a47d50c293b 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -144,8 +144,56 @@ static void fuse_file_put(struct fuse_file *ff, bool s= ync) } } =20 +static int fuse_compound_open_getattr(struct fuse_mount *fm, u64 nodeid, + struct inode *inode, + unsigned int open_flags, int opcode, + struct fuse_open_out *outopenp) +{ + struct fuse_attr_out attr_outarg =3D {}; + struct fuse_args open_args =3D {}; + struct fuse_args getattr_args =3D {}; + struct fuse_open_in open_in =3D {}; + struct fuse_getattr_in getattr_in =3D {}; + int open_error =3D 0, getattr_error =3D 0; + struct fuse_compound_op ops[2] =3D { + { .arg =3D &open_args, .error =3D &open_error, + .dep_index =3D FUSE_COMPOUND_NO_DEP }, + { .arg =3D &getattr_args, .error =3D &getattr_error, + .dep_index =3D FUSE_COMPOUND_NO_DEP }, + }; + int err; + + fuse_open_args_fill(fm, &open_args, nodeid, opcode, open_flags, + &open_in, outopenp); + fuse_getattr_args_fill(&getattr_args, nodeid, &getattr_in, &attr_outarg); + + err =3D fuse_compound_send(fm, ops, 2); + if (err) + return err; + + /* + * Open succeeded if open_error =3D=3D 0; the getattr part is best + * effort. If the server returned invalid or wrong-type attrs as + * part of the compound, mark the inode bad (matching fuse_do_getattr) + * but do not fail the open -- otherwise we would leak the just- + * acquired file handle on the server side. + */ + if (!getattr_error) { + if (fuse_invalid_attr(&attr_outarg.attr) || + inode_wrong_type(inode, attr_outarg.attr.mode)) + fuse_make_bad(inode); + else + fuse_change_attributes(inode, &attr_outarg.attr, NULL, + ATTR_TIMEOUT(&attr_outarg), + fuse_get_attr_version(fm->fc)); + } + + return open_error; +} + struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid, - unsigned int open_flags, bool isdir) + struct inode *inode, + unsigned int open_flags, bool isdir) { struct fuse_conn *fc =3D fm->fc; struct fuse_file *ff; @@ -171,9 +219,25 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm= , u64 nodeid, if (open) { /* Store outarg for fuse_finish_open() */ struct fuse_open_out *outargp =3D &ff->args->open_outarg; + bool try_compound =3D false; int err; =20 - err =3D fuse_send_open(fm, nodeid, open_flags, opcode, outargp); + if (inode) { + struct fuse_inode *fi =3D get_fuse_inode(inode); + + try_compound =3D (open_flags & O_APPEND) || + time_before64(fi->i_time, get_jiffies_64()) || + (fi->inval_mask & STATX_BASIC_STATS); + } + + if (try_compound) + err =3D fuse_compound_open_getattr(fm, nodeid, inode, + open_flags, opcode, + outargp); + else + err =3D fuse_send_open(fm, nodeid, open_flags, opcode, + outargp); + if (!err) { ff->fh =3D outargp->fh; ff->open_flags =3D outargp->open_flags; @@ -203,7 +267,8 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm,= u64 nodeid, int fuse_do_open(struct fuse_mount *fm, u64 nodeid, struct file *file, bool isdir) { - struct fuse_file *ff =3D fuse_file_open(fm, nodeid, file->f_flags, isdir); + struct fuse_file *ff =3D fuse_file_open(fm, nodeid, file_inode(file), + file->f_flags, isdir); =20 if (!IS_ERR(ff)) file->private_data =3D ff; diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 219312d5f21e..2017908de35e 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -1580,6 +1580,7 @@ void fuse_file_io_release(struct fuse_file *ff, struc= t inode *inode); =20 /* file.c */ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid, + struct inode *inode, unsigned int open_flags, bool isdir); void fuse_file_release(struct inode *inode, struct fuse_file *ff, unsigned int open_flags, fl_owner_t id, bool isdir); diff --git a/fs/fuse/ioctl.c b/fs/fuse/ioctl.c index fdc175e93f74..07a02e47b2c3 100644 --- a/fs/fuse/ioctl.c +++ b/fs/fuse/ioctl.c @@ -494,7 +494,7 @@ static struct fuse_file *fuse_priv_ioctl_prepare(struct= inode *inode) if (!S_ISREG(inode->i_mode) && !isdir) return ERR_PTR(-ENOTTY); =20 - return fuse_file_open(fm, get_node_id(inode), O_RDONLY, isdir); + return fuse_file_open(fm, get_node_id(inode), NULL, O_RDONLY, isdir); } =20 static void fuse_priv_ioctl_cleanup(struct inode *inode, struct fuse_file = *ff) --=20 2.54.0 From nobody Mon Jun 8 09:49:28 2026 Received: from smtp02-ext3.udag.de (smtp02-ext3.udag.de [62.146.106.33]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 5996C3DB632 for ; Thu, 4 Jun 2026 09:46:24 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=62.146.106.33 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780566385; cv=none; b=hxJ5J1dVu6DEXBav3GT/Dj+y6rU6xvLo0q0ym3akXDwWEfvELkh1FtK2YfpZ8vyxE5uq3AvmKRWo5sFxnVZcioXIBzdtAWdzoROR7HQkAYen2IIAmFRas5hAcza0uzsO8xKgdUJo34PG8SWZ3J80hkz/d9fjKFSPDGz76q1d20k= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780566385; c=relaxed/simple; bh=kIHUKYth3RzqAa0vIDE4cMpVXhNM2Sjv0ky32TSHgFI=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=CTvyrWFsRuXYtpmcZ+x8qCRsYbADVbV7DuY2ueP2Atj1kzSRNS2VdaTe2/3WeupTPKMjF0OkuZxYvfA0F9E4tfz0Mpv5VqmOXbh7Blqtv+jLaNCovZ0I6zUZu8Uc26CosQoAJzzrjd4FZYzsld+oxvyuZcet7rDP7Nu/G4wS6OA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=birthelmer.com; spf=pass smtp.mailfrom=birthelmer.com; dkim=pass (2048-bit key) header.d=birthelmer.com header.i=@birthelmer.com header.b=AQlCaDMq; arc=none smtp.client-ip=62.146.106.33 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=birthelmer.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=birthelmer.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=birthelmer.com header.i=@birthelmer.com header.b="AQlCaDMq" Received: from fedora.fritz.box (065-142-067-156.ip-addr.inexio.net [156.67.142.65]) by smtp02-ext3.udag.de (Postfix) with ESMTPA id 47B29E05B9; Thu, 4 Jun 2026 11:46:12 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=birthelmer.com; s=uddkim-202310; t=1780566372; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=E0wsDmLNEKGtXZ2Fte1h1PnQTp+arVSPfrZX5pkUEJo=; b=AQlCaDMq4+v7SlOHGUFYQ0sCgBiiZaaPr6aJmjQzWkUujzbjYIC8zGfajmpoPsedoxxwmI grxqRxAfVwQ6r/amb91enCjYiuWFHSuaeAhywlPJ9NERae9hPXqI/hG/JcxVwUpDYr6pFf jwklRfaq8w1ZfBwAoHMsS1LCTGrvFjNgCNV1oO+hcQtmf+g23bGLlLFaTadnkLg+SB8i4K RsYN2+T6FULVO58UVtrx+A6xCk+XbBT7GzQsABwHUtnqOg0nRFwQQ4IMpBKR4NpV2UaV1G FnY31Q87haMiLAlD1l7NLCUSHhSFzaLUdcrLB0xjIQIG74Y4I+8ld+LG7gwGdw== Authentication-Results: smtp02-ext3.udag.de; auth=pass smtp.auth=birthelmercom-0001 smtp.mailfrom=horst@birthelmer.com From: Horst Birthelmer Date: Thu, 04 Jun 2026 11:45:45 +0200 Subject: [PATCH v7 4/4] fuse: add compound command for dentry revalidation Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260604-fuse-compounds-upstream-v7-4-27331d085c2a@ddn.com> References: <20260604-fuse-compounds-upstream-v7-0-27331d085c2a@ddn.com> In-Reply-To: <20260604-fuse-compounds-upstream-v7-0-27331d085c2a@ddn.com> To: Miklos Szeredi , Bernd Schubert , Joanne Koong , Luis Henriques Cc: linux-kernel@vger.kernel.org, fuse-devel@lists.linux.dev, Horst Birthelmer X-Mailer: b4 0.14.3 X-Developer-Signature: v=1; a=ed25519-sha256; t=1780566367; l=4463; i=hbirthelmer@ddn.com; s=20251006; h=from:subject:message-id; bh=ubG6DBB8TVKI3rbqvPq0gS0kwZvdAusJXUsJwNp0rbY=; b=gaGjkQG8XtB7uWGo69938xL1O/syxl17L+yPeyvdaZ/Uv8Z9CmKOi3gPrvzZaISHF9r31LxTL EmN9oYhhotXC9Qd4PlcUjB5SOCi3FLWAJDVkdwMRCDIFttjClNk+QmD X-Developer-Key: i=hbirthelmer@ddn.com; a=ed25519; pk=v3BVDFoy16EzgHZ23ObqW+kbpURtjrwxgKu8YNDKjGg= From: Horst Birthelmer During dentry revalidation the compound LOOKUP+GETATTR will save a round trip to user space. Signed-off-by: Horst Birthelmer --- fs/fuse/dir.c | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++= ++-- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index b3406c33abd2..99800e8ca895 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -372,6 +372,101 @@ static void fuse_lookup_init(struct fuse_args *args, = u64 nodeid, args->out_args[0].value =3D outarg; } =20 +/* + * Revalidate a dentry using a compound LOOKUP+GETATTR. Saves a round + * trip when both the entry and the attributes need refreshing. + * + * Returns 1 if valid, 0 if the dentry should be invalidated, or a + * negative errno that the caller should propagate (only -ENOMEM / + * -EINTR; other errors are mapped to invalidate). + */ +static int fuse_dentry_revalidate_compound(struct inode *dir, + const struct qstr *name, + struct dentry *entry, + struct inode *inode, + struct fuse_mount *fm, + u64 attr_version) +{ + struct fuse_entry_out lookup_out =3D {}; + struct fuse_attr_out getattr_out =3D {}; + struct fuse_getattr_in getattr_in =3D {}; + struct fuse_args lookup_args =3D {}; + struct fuse_args getattr_args =3D {}; + struct fuse_forget_link *forget; + int lookup_err =3D 0, getattr_err =3D 0; + struct fuse_compound_op ops[2] =3D { + { .arg =3D &lookup_args, .error =3D &lookup_err, + .dep_index =3D FUSE_COMPOUND_NO_DEP }, + { .arg =3D &getattr_args, .error =3D &getattr_err, + .dep_index =3D 0 /* nodeid comes from lookup */ }, + }; + struct fuse_inode *fi; + int ret; + + forget =3D fuse_alloc_forget(); + if (!forget) + return -ENOMEM; + + fuse_lookup_init(&lookup_args, get_node_id(dir), name, &lookup_out); + /* nodeid is filled from the lookup result before getattr is sent */ + fuse_getattr_args_fill(&getattr_args, 0, &getattr_in, &getattr_out); + + ret =3D fuse_compound_send(fm, ops, 2); + if (ret =3D=3D -ENOMEM || ret =3D=3D -EINTR) + goto out; + /* + * The non-compound revalidate path propagates -ENOMEM / -EINTR + * from the lookup to VFS instead of treating them as "invalidate + * this dentry". Keep that behaviour when the lookup ran inside + * a compound: surface the per-subop error to the caller. + */ + if (lookup_err =3D=3D -ENOMEM || lookup_err =3D=3D -EINTR) { + ret =3D lookup_err; + goto out; + } + if (ret < 0 || lookup_err || !lookup_out.nodeid) { + ret =3D 0; + goto out; + } + + fi =3D get_fuse_inode(inode); + if (lookup_out.nodeid !=3D get_node_id(inode) || + (bool)IS_AUTOMOUNT(inode) !=3D (bool)(lookup_out.attr.flags & FUSE_AT= TR_SUBMOUNT)) { + fuse_queue_forget(fm->fc, forget, lookup_out.nodeid, 1); + forget =3D NULL; + ret =3D 0; + goto out; + } + + spin_lock(&fi->lock); + fi->nlookup++; + spin_unlock(&fi->lock); + + if (fuse_invalid_attr(&lookup_out.attr) || + fuse_stale_inode(inode, lookup_out.generation, &lookup_out.attr)) { + ret =3D 0; + goto out; + } + + forget_all_cached_acls(inode); + + if (!getattr_err && !fuse_invalid_attr(&getattr_out.attr)) + fuse_change_attributes(inode, &getattr_out.attr, NULL, + ATTR_TIMEOUT(&getattr_out), + attr_version); + else + fuse_change_attributes(inode, &lookup_out.attr, NULL, + ATTR_TIMEOUT(&lookup_out), + attr_version); + + fuse_change_entry_timeout(entry, &lookup_out); + ret =3D 1; + +out: + kfree(forget); + return ret; +} + /* * Check whether the dentry is still valid * @@ -413,14 +508,28 @@ static int fuse_dentry_revalidate(struct inode *dir, = const struct qstr *name, goto out; =20 fm =3D get_fuse_mount(inode); + attr_version =3D fuse_get_attr_version(fm->fc); + + /* + * Use compound LOOKUP+GETATTR when available to fold the + * attribute refresh into the same round trip. + */ + if (fm->fc->compound_ops) { + ret =3D fuse_dentry_revalidate_compound(dir, name, entry, + inode, fm, + attr_version); + if (ret < 0) + goto out; + if (ret =3D=3D 0) + goto invalid; + goto out; + } =20 forget =3D fuse_alloc_forget(); ret =3D -ENOMEM; if (!forget) goto out; =20 - attr_version =3D fuse_get_attr_version(fm->fc); - fuse_lookup_init(&args, get_node_id(dir), name, &outarg); ret =3D fuse_simple_request(fm, &args); /* Zero nodeid is same as -ENOENT */ --=20 2.54.0