* [PATCH v7 0/4] fuse: compound commands
@ 2026-06-04 9:45 Horst Birthelmer
2026-06-04 9:45 ` [PATCH v7 1/4] fuse: add compound command to combine multiple requests Horst Birthelmer
` (4 more replies)
0 siblings, 5 replies; 18+ messages in thread
From: Horst Birthelmer @ 2026-06-04 9:45 UTC (permalink / raw)
To: Miklos Szeredi, Bernd Schubert, Joanne Koong, Luis Henriques
Cc: linux-kernel, fuse-devel, Horst Birthelmer
This series adds a single new opcode, FUSE_COMPOUND, that bundles a
sequence of subrequests into one round trip. The wire format is
fuse_in_header (opcode FUSE_COMPOUND)
fuse_compound_in
fuse_compound_req_in
fuse_in_header
payload
... (repeated per subop)
Compound is opt-in per connection and discovered by trial: the kernel
assumes support and clears its flag on the first -ENOSYS reply.
-EOPNOTSUPP declines a specific combination without disabling the
feature. In both cases the kernel replays the subops individually
via fuse_simple_request(), so callers never need a separate
non-compound code path.
The series ships two consumers:
- open + getattr, used when fuse_file_open() needs both ff->fh and
fresh attrs (O_APPEND, or cached attrs already stale). This
closes the open-then-stat race described in [1].
- dentry revalidate, fusing LOOKUP + GETATTR when both the entry
and the attribute caches are stale.
The matching libfuse pull request is here [2]. It contains a helper
that decodes a compound and runs each subop in turn, plus a
passthrough_hp patch demonstrating both that path and the
alternative of handling the compound entirely server-side.
Compounds were reccently discussed more of a thing for fusex
but I think we should add them to the 'classic' FUSE as well,
and as long as fusex hasn't landed and the full fuse channels
are not here, we could add these here.
[1] https://lore.kernel.org/all/20240813212149.1909627-1-joannelkoong@gmail.com/
[2] https://github.com/libfuse/libfuse/pull/1418
Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
---
Changes in v7:
- simplify fuse_open_args_fill() handling
- share result handling between the open+getattr compound and the old code
- send open+getattr only when the attributes we have are expired
or we have open flag O_APPEND
- remove fuse_compound_alloc()/fuse_compound_free()
- add small header for every request in a compound to send compound flags
to fuse server
- Link to v6: https://lore.kernel.org/r/20260226-fuse-compounds-upstream-v6-0-8585c5fcd2fc@ddn.com
Changes in v6:
- got rid of the count in the compound header
- added the automatic calling of the combined request if the fuse
server doesn't process the compound and the implementation allows it
- due to the variable max operations in the compounds request struct
fuse_compound_free() had be brought back
- Link to v5: https://lore.kernel.org/r/20260210-fuse-compounds-upstream-v5-0-ea0585f62daa@ddn.com
Changes in v5:
- introduced the flag FUSE_COMPOUND_SEPARABLE as discussed here
- simplify result parsing and streamline the code
- simplify the result and error handling for open+getattr
- fixed a couple of issues pointed out by Joanne
- Link to v4: https://lore.kernel.org/r/20260109-fuse-compounds-upstream-v4-0-0d3b82a4666f@ddn.com
Changes in v4:
- removed RFC
- removed the unnecessary 'parsed' variable in fuse_compound_req, since
we parse the result only once
- reordered the patches about the helper functions to fill in the fuse
args for open and getattr calls
- Link to v3: https://lore.kernel.org/r/20260108-fuse-compounds-upstream-v3-0-8dc91ebf3740@ddn.com
Changes in v3:
- simplified the data handling for compound commands
- remove the validating functionality, since it was only a helper for
development
- remove fuse_compound_request() and use fuse_simple_request()
- add helper functions for creating args for open and attr
- use the newly createn helper functions for arg creation for open and
getattr
- Link to v2: https://lore.kernel.org/r/20251223-fuse-compounds-upstream-v2-0-0f7b4451c85e@ddn.com
Changes in v2:
- fixed issues with error handling in the compounds as well as in the
open+getattr
- Link to v1: https://lore.kernel.org/r/20251223-fuse-compounds-upstream-v1-0-7bade663947b@ddn.com
---
Horst Birthelmer (4):
fuse: add compound command to combine multiple requests
fuse: create helper functions for filling in fuse args for open and getattr
fuse: add an implementation of open+getattr
fuse: add compound command for dentry revalidation
fs/fuse/Makefile | 2 +-
fs/fuse/compound.c | 126 ++++++++++++++++++++++++++++
fs/fuse/dev.c | 206 +++++++++++++++++++++++++++++++++++++++++++---
fs/fuse/dev_uring.c | 31 ++++++-
fs/fuse/dir.c | 139 ++++++++++++++++++++++++++++---
fs/fuse/file.c | 115 +++++++++++++++++++++-----
fs/fuse/fuse_dev_i.h | 5 ++
fs/fuse/fuse_i.h | 39 +++++++++
fs/fuse/inode.c | 9 ++
fs/fuse/ioctl.c | 2 +-
include/uapi/linux/fuse.h | 56 +++++++++++++
11 files changed, 685 insertions(+), 45 deletions(-)
---
base-commit: e43ffb69e0438cddd72aaa30898b4dc446f664f8
change-id: 20251223-fuse-compounds-upstream-c85b4e39b3d3
Best regards,
--
Horst Birthelmer <hbirthelmer@ddn.com>
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH v7 1/4] fuse: add compound command to combine multiple requests
2026-06-04 9:45 [PATCH v7 0/4] fuse: compound commands Horst Birthelmer
@ 2026-06-04 9:45 ` Horst Birthelmer
2026-06-05 7:41 ` Amir Goldstein
2026-06-04 9:45 ` [PATCH v7 2/4] fuse: create helper functions for filling in fuse args for open and getattr Horst Birthelmer
` (3 subsequent siblings)
4 siblings, 1 reply; 18+ messages in thread
From: Horst Birthelmer @ 2026-06-04 9:45 UTC (permalink / raw)
To: Miklos Szeredi, Bernd Schubert, Joanne Koong, Luis Henriques
Cc: linux-kernel, fuse-devel, Horst Birthelmer
From: Horst Birthelmer <hbirthelmer@ddn.com>
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 <hbirthelmer@ddn.com>
---
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) += cuse.o
obj-$(CONFIG_VIRTIO_FS) += virtiofs.o
fuse-y := trace.o # put trace.o first so we see ftrace errors sooner
-fuse-y += dev.o dir.o file.o inode.o control.o xattr.o acl.o readdir.o ioctl.o
+fuse-y += dev.o dir.o file.o inode.o control.o xattr.o acl.o readdir.o ioctl.o compound.o
fuse-y += iomode.o
fuse-$(CONFIG_FUSE_DAX) += dax.o
fuse-$(CONFIG_FUSE_PASSTHROUGH) += 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 == 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 = src->out_args[0].value;
+ if (entry_out)
+ dep->nodeid = 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 = 0; i < count; i++) {
+ struct fuse_compound_op *cur = &ops[i];
+
+ if (cur->dep_index != 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 >= i)) {
+ *cur->error = -EINVAL;
+ continue;
+ }
+ dep = &ops[cur->dep_index];
+
+ if (*dep->error) {
+ *cur->error = *dep->error;
+ continue;
+ }
+ fuse_compound_propagate_nodeid(cur->arg, dep->arg);
+ }
+ *cur->error = 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 = fm->fc;
+ struct fuse_compound_args compound = {
+ .args = { .opcode = FUSE_COMPOUND, },
+ .ops = ops,
+ .count = count,
+ };
+ int ret;
+
+ if (WARN_ON_ONCE(count == 0))
+ return -EINVAL;
+
+ if (!fc->compound_ops) {
+ fuse_compound_send_legacy(fm, ops, count);
+ return 0;
+ }
+
+ ret = fuse_simple_request(fm, &compound.args);
+ if (ret == -ENOSYS)
+ fc->compound_ops = 0;
+ if (ret == -ENOSYS || ret == -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 = {
};
EXPORT_SYMBOL_GPL(fuse_dev_fiq_ops);
+static inline struct fuse_compound_args *
+fuse_get_compound_args(struct fuse_args *args)
+{
+ if (args->opcode == 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 = sizeof(struct fuse_in_header) +
+ sizeof(struct fuse_compound_in);
+ unsigned int i, j;
+
+ for (i = 0; i < compound->count; i++) {
+ struct fuse_args *op_args = compound->ops[i].arg;
+
+ total += sizeof(struct fuse_compound_req_in) +
+ sizeof(struct fuse_in_header);
+ for (j = 0; j < op_args->in_numargs; j++)
+ total += op_args->in_args[j].size;
+ }
+
+ return total;
+}
+
+void fuse_set_req_len(struct fuse_req *req)
+{
+ struct fuse_compound_args *compound = fuse_get_compound_args(req->args);
+
+ if (compound)
+ req->in.h.len = fuse_compound_req_size(compound);
+ else
+ req->in.h.len = 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 = 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);
}
@@ -713,9 +749,7 @@ static bool fuse_request_queue_background_uring(struct fuse_conn *fc,
{
struct fuse_iqueue *fiq = &fc->iq;
- req->in.h.len = 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);
return fuse_uring_queue_bq_req(req);
@@ -1204,7 +1238,7 @@ static int fuse_copy_folios(struct fuse_copy_state *cs, unsigned nbytes,
}
/* Copy a single argument in the request to/from userspace buffer */
-static int fuse_copy_one(struct fuse_copy_state *cs, void *val, unsigned size)
+static int fuse_copy_one(struct fuse_copy_state *cs, void *val, unsigned int size)
{
while (size) {
if (!cs->len) {
@@ -1399,6 +1433,66 @@ __releases(fiq->lock)
return fuse_read_batch_forget(fiq, cs, nbytes);
}
+/*
+ * 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 = fuse_copy_one(cs, &compound->in_header,
+ sizeof(compound->in_header));
+ if (err)
+ return err;
+ }
+
+ for (i = 0; i < compound->count; i++) {
+ struct fuse_compound_op *op = &compound->ops[i];
+ struct fuse_args *op_args = op->arg;
+ struct fuse_compound_req_in sub = {
+ .dep_index = 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 = {
+ .unique = i,
+ .opcode = op_args->opcode,
+ .nodeid = op_args->nodeid,
+ .uid = cs->req->in.h.uid,
+ .gid = cs->req->in.h.gid,
+ .pid = cs->req->in.h.pid,
+ .len = sizeof(hdr),
+ };
+
+ for (j = 0; j < op_args->in_numargs; j++)
+ hdr.len += op_args->in_args[j].size;
+
+ err = fuse_copy_one(cs, &sub, sizeof(sub));
+ if (!err)
+ err = fuse_copy_one(cs, &hdr, sizeof(hdr));
+ if (!err)
+ err = 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 = req;
err = fuse_copy_one(cs, &req->in.h, sizeof(req->in.h));
- if (!err)
- err = fuse_copy_args(cs, args->in_numargs, args->in_pages,
- (struct fuse_arg *) args->in_args, 0);
+ if (!err) {
+ struct fuse_compound_args *compound = fuse_get_compound_args(args);
+
+ if (compound)
+ err = fuse_copy_compound_in_args(cs, compound);
+ else
+ err = 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_pqueue *fpq, u64 unique)
return NULL;
}
+int fuse_copy_compound_out_args(struct fuse_copy_state *cs,
+ struct fuse_compound_args *compound)
+{
+ unsigned int i;
+ int err;
+
+ err = fuse_copy_one(cs, &compound->out_header,
+ sizeof(compound->out_header));
+ if (err)
+ return err;
+
+ for (i = 0; i < compound->count; i++) {
+ struct fuse_compound_op *op = &compound->ops[i];
+ struct fuse_out_header op_hdr;
+ size_t expected;
+
+ err = 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 = 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 != i)
+ return -EIO;
+
+ *op->error = op_hdr.error;
+ if (op_hdr.error) {
+ /* Errored replies carry only the fuse_out_header. */
+ if (op_hdr.len != 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 = 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 =
+ &op->arg->out_args[op->arg->out_numargs - 1];
+ size_t diff = expected - op_hdr.len;
+
+ if (diff > lastarg->size)
+ return -EIO;
+ lastarg->size -= diff;
+ }
+
+ err = 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 *fud,
if (!req->args->page_replace)
cs->move_folios = false;
- if (oh.error)
+ if (oh.error) {
err = nbytes != sizeof(oh) ? -EINVAL : 0;
- else
- err = fuse_copy_out_args(cs, req->args, nbytes);
+ } else {
+ struct fuse_compound_args *compound = fuse_get_compound_args(req->args);
+
+ if (compound)
+ err = fuse_copy_compound_out_args(cs, compound);
+ else
+ err = fuse_copy_out_args(cs, req->args, nbytes);
+ }
fuse_copy_finish(cs);
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 = true;
cs.req = req;
- err = fuse_copy_out_args(&cs, args, ring_in_out.payload_sz);
+ if (args->opcode == FUSE_COMPOUND) {
+ /* Stream compound response directly into operation buffers */
+ struct fuse_compound_args *compound =
+ container_of(args, struct fuse_compound_args, args);
+ err = fuse_copy_compound_out_args(&cs, compound);
+ } else {
+ err = 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 *ring, struct fuse_req *req,
cs.is_uring = true;
cs.req = req;
+ if (args->opcode == 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 =
+ container_of(args, struct fuse_compound_args, args);
+
+ err = 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 = 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 *ring, struct fuse_req *req,
/* copy the payload */
err = 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 *req);
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;
+ /* 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;
@@ -1272,6 +1275,35 @@ static inline ssize_t fuse_simple_idmap_request(struct mnt_idmap *idmap,
int fuse_simple_background(struct fuse_mount *fm, struct fuse_args *args,
gfp_t gfp_flags);
+/*
+ * 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 fuse_mount *fm,
fc->name_max = FUSE_NAME_LOW_MAX;
fc->timeout.req_timeout = 0;
+ /*
+ * 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 = 1;
+
if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH))
fuse_backing_files_init(fc);
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 = 52,
FUSE_COPY_FILE_RANGE_64 = 53,
+ /* 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 = 54,
+
/* CUSE specific operations */
CUSE_INIT = 4096,
@@ -1245,6 +1252,55 @@ struct fuse_supp_groups {
uint32_t groups[];
};
+/*
+ * 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
*/
--
2.54.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH v7 2/4] fuse: create helper functions for filling in fuse args for open and getattr
2026-06-04 9:45 [PATCH v7 0/4] fuse: compound commands Horst Birthelmer
2026-06-04 9:45 ` [PATCH v7 1/4] fuse: add compound command to combine multiple requests Horst Birthelmer
@ 2026-06-04 9:45 ` Horst Birthelmer
2026-06-05 7:42 ` Amir Goldstein
2026-06-04 9:45 ` [PATCH v7 3/4] fuse: add an implementation of open+getattr Horst Birthelmer
` (2 subsequent siblings)
4 siblings, 1 reply; 18+ messages in thread
From: Horst Birthelmer @ 2026-06-04 9:45 UTC (permalink / raw)
To: Miklos Szeredi, Bernd Schubert, Joanne Koong, Luis Henriques
Cc: linux-kernel, fuse-devel, Horst Birthelmer
From: Horst Birthelmer <hbirthelmer@ddn.com>
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 <joannelkoong@gmail.com>
Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
---
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, struct inode *inode,
return 0;
}
+/*
+ * 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 = FUSE_GETATTR;
+ args->nodeid = nodeid;
+ args->in_numargs = 1;
+ args->in_args[0].size = sizeof(*inarg);
+ args->in_args[0].value = inarg;
+ args->out_numargs = 1;
+ args->out_args[0].size = sizeof(*outarg);
+ args->out_args[0].value = 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 |= FUSE_GETATTR_FH;
inarg.fh = ff->fh;
}
- args.opcode = FUSE_GETATTR;
- args.nodeid = get_node_id(inode);
- args.in_numargs = 1;
- args.in_args[0].size = sizeof(inarg);
- args.in_args[0].value = &inarg;
- args.out_numargs = 1;
- args.out_args[0].size = sizeof(outarg);
- args.out_args[0].value = &outarg;
+ fuse_getattr_args_fill(&args, get_node_id(inode), &inarg, &outarg);
err = 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 <linux/task_io_accounting_ops.h>
#include <linux/iomap.h>
+/*
+ * Helper function to initialize fuse_args for OPEN/OPENDIR operations
+ */
+static void fuse_open_args_fill(struct fuse_mount *fm, struct fuse_args *args,
+ u64 nodeid, int opcode, unsigned int open_flags,
+ struct fuse_open_in *inarg,
+ struct fuse_open_out *outarg)
+{
+ inarg->flags = open_flags & ~(O_CREAT | O_EXCL | O_NOCTTY);
+
+ if (!fm->fc->atomic_o_trunc)
+ inarg->flags &= ~O_TRUNC;
+
+ if (fm->fc->handle_killpriv_v2 &&
+ (inarg->flags & O_TRUNC) && !capable(CAP_FSETID))
+ inarg->open_flags |= FUSE_OPEN_KILL_SUIDGID;
+
+ args->opcode = opcode;
+ args->nodeid = nodeid;
+ args->in_numargs = 1;
+ args->in_args[0].size = sizeof(*inarg);
+ args->in_args[0].value = inarg;
+ args->out_numargs = 1;
+ args->out_args[0].size = sizeof(*outarg);
+ args->out_args[0].value = 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 nodeid,
FUSE_ARGS(args);
memset(&inarg, 0, sizeof(inarg));
- inarg.flags = open_flags & ~(O_CREAT | O_EXCL | O_NOCTTY);
- if (!fm->fc->atomic_o_trunc)
- inarg.flags &= ~O_TRUNC;
-
- if (fm->fc->handle_killpriv_v2 &&
- (inarg.flags & O_TRUNC) && !capable(CAP_FSETID)) {
- inarg.open_flags |= FUSE_OPEN_KILL_SUIDGID;
- }
- args.opcode = opcode;
- args.nodeid = nodeid;
- args.in_numargs = 1;
- args.in_args[0].size = sizeof(inarg);
- args.in_args[0].value = &inarg;
- args.out_numargs = 1;
- args.out_args[0].size = sizeof(*outargp);
- args.out_args[0].value = outargp;
+ fuse_open_args_fill(fm, &args, nodeid, opcode, open_flags, &inarg, outargp);
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);
+/*
+ * 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);
struct fuse_file *fuse_file_alloc(struct fuse_mount *fm, bool release);
void fuse_file_free(struct fuse_file *ff);
--
2.54.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH v7 3/4] fuse: add an implementation of open+getattr
2026-06-04 9:45 [PATCH v7 0/4] fuse: compound commands Horst Birthelmer
2026-06-04 9:45 ` [PATCH v7 1/4] fuse: add compound command to combine multiple requests Horst Birthelmer
2026-06-04 9:45 ` [PATCH v7 2/4] fuse: create helper functions for filling in fuse args for open and getattr Horst Birthelmer
@ 2026-06-04 9:45 ` Horst Birthelmer
2026-06-05 7:50 ` Amir Goldstein
2026-06-04 9:45 ` [PATCH v7 4/4] fuse: add compound command for dentry revalidation Horst Birthelmer
2026-06-05 8:12 ` [PATCH v7 0/4] fuse: compound commands Amir Goldstein
4 siblings, 1 reply; 18+ messages in thread
From: Horst Birthelmer @ 2026-06-04 9:45 UTC (permalink / raw)
To: Miklos Szeredi, Bernd Schubert, Joanne Koong, Luis Henriques
Cc: linux-kernel, fuse-devel, Horst Birthelmer
From: Horst Birthelmer <hbirthelmer@ddn.com>
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 <hbirthelmer@ddn.com>
---
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 sync)
}
}
+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 = {};
+ struct fuse_args open_args = {};
+ struct fuse_args getattr_args = {};
+ struct fuse_open_in open_in = {};
+ struct fuse_getattr_in getattr_in = {};
+ int open_error = 0, getattr_error = 0;
+ struct fuse_compound_op ops[2] = {
+ { .arg = &open_args, .error = &open_error,
+ .dep_index = FUSE_COMPOUND_NO_DEP },
+ { .arg = &getattr_args, .error = &getattr_error,
+ .dep_index = 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 = fuse_compound_send(fm, ops, 2);
+ if (err)
+ return err;
+
+ /*
+ * Open succeeded if open_error == 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 = 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 = &ff->args->open_outarg;
+ bool try_compound = false;
int err;
- err = fuse_send_open(fm, nodeid, open_flags, opcode, outargp);
+ if (inode) {
+ struct fuse_inode *fi = get_fuse_inode(inode);
+
+ try_compound = (open_flags & O_APPEND) ||
+ time_before64(fi->i_time, get_jiffies_64()) ||
+ (fi->inval_mask & STATX_BASIC_STATS);
+ }
+
+ if (try_compound)
+ err = fuse_compound_open_getattr(fm, nodeid, inode,
+ open_flags, opcode,
+ outargp);
+ else
+ err = fuse_send_open(fm, nodeid, open_flags, opcode,
+ outargp);
+
if (!err) {
ff->fh = outargp->fh;
ff->open_flags = 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 = fuse_file_open(fm, nodeid, file->f_flags, isdir);
+ struct fuse_file *ff = fuse_file_open(fm, nodeid, file_inode(file),
+ file->f_flags, isdir);
if (!IS_ERR(ff))
file->private_data = 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, struct inode *inode);
/* 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);
- 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);
}
static void fuse_priv_ioctl_cleanup(struct inode *inode, struct fuse_file *ff)
--
2.54.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH v7 4/4] fuse: add compound command for dentry revalidation
2026-06-04 9:45 [PATCH v7 0/4] fuse: compound commands Horst Birthelmer
` (2 preceding siblings ...)
2026-06-04 9:45 ` [PATCH v7 3/4] fuse: add an implementation of open+getattr Horst Birthelmer
@ 2026-06-04 9:45 ` Horst Birthelmer
2026-06-05 8:06 ` Amir Goldstein
2026-06-05 8:12 ` [PATCH v7 0/4] fuse: compound commands Amir Goldstein
4 siblings, 1 reply; 18+ messages in thread
From: Horst Birthelmer @ 2026-06-04 9:45 UTC (permalink / raw)
To: Miklos Szeredi, Bernd Schubert, Joanne Koong, Luis Henriques
Cc: linux-kernel, fuse-devel, Horst Birthelmer
From: Horst Birthelmer <hbirthelmer@ddn.com>
During dentry revalidation the compound LOOKUP+GETATTR
will save a round trip to user space.
Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
---
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 = outarg;
}
+/*
+ * 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 = {};
+ struct fuse_attr_out getattr_out = {};
+ struct fuse_getattr_in getattr_in = {};
+ struct fuse_args lookup_args = {};
+ struct fuse_args getattr_args = {};
+ struct fuse_forget_link *forget;
+ int lookup_err = 0, getattr_err = 0;
+ struct fuse_compound_op ops[2] = {
+ { .arg = &lookup_args, .error = &lookup_err,
+ .dep_index = FUSE_COMPOUND_NO_DEP },
+ { .arg = &getattr_args, .error = &getattr_err,
+ .dep_index = 0 /* nodeid comes from lookup */ },
+ };
+ struct fuse_inode *fi;
+ int ret;
+
+ forget = 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 = fuse_compound_send(fm, ops, 2);
+ if (ret == -ENOMEM || ret == -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 == -ENOMEM || lookup_err == -EINTR) {
+ ret = lookup_err;
+ goto out;
+ }
+ if (ret < 0 || lookup_err || !lookup_out.nodeid) {
+ ret = 0;
+ goto out;
+ }
+
+ fi = get_fuse_inode(inode);
+ if (lookup_out.nodeid != get_node_id(inode) ||
+ (bool)IS_AUTOMOUNT(inode) != (bool)(lookup_out.attr.flags & FUSE_ATTR_SUBMOUNT)) {
+ fuse_queue_forget(fm->fc, forget, lookup_out.nodeid, 1);
+ forget = NULL;
+ ret = 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 = 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 = 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;
fm = get_fuse_mount(inode);
+ attr_version = 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 = fuse_dentry_revalidate_compound(dir, name, entry,
+ inode, fm,
+ attr_version);
+ if (ret < 0)
+ goto out;
+ if (ret == 0)
+ goto invalid;
+ goto out;
+ }
forget = fuse_alloc_forget();
ret = -ENOMEM;
if (!forget)
goto out;
- attr_version = fuse_get_attr_version(fm->fc);
-
fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
ret = fuse_simple_request(fm, &args);
/* Zero nodeid is same as -ENOENT */
--
2.54.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* Re: [PATCH v7 1/4] fuse: add compound command to combine multiple requests
2026-06-04 9:45 ` [PATCH v7 1/4] fuse: add compound command to combine multiple requests Horst Birthelmer
@ 2026-06-05 7:41 ` Amir Goldstein
2026-06-05 8:03 ` Horst Birthelmer
2026-06-06 17:30 ` Horst Birthelmer
0 siblings, 2 replies; 18+ messages in thread
From: Amir Goldstein @ 2026-06-05 7:41 UTC (permalink / raw)
To: Horst Birthelmer
Cc: Miklos Szeredi, Bernd Schubert, Joanne Koong, Luis Henriques,
linux-kernel, fuse-devel, Horst Birthelmer
On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@birthelmer.com> wrote:
>
> From: Horst Birthelmer <hbirthelmer@ddn.com>
>
> 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 <hbirthelmer@ddn.com>
> ---
> 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) += cuse.o
> obj-$(CONFIG_VIRTIO_FS) += virtiofs.o
>
> fuse-y := trace.o # put trace.o first so we see ftrace errors sooner
> -fuse-y += dev.o dir.o file.o inode.o control.o xattr.o acl.o readdir.o ioctl.o
> +fuse-y += dev.o dir.o file.o inode.o control.o xattr.o acl.o readdir.o ioctl.o compound.o
> fuse-y += iomode.o
> fuse-$(CONFIG_FUSE_DAX) += dax.o
> fuse-$(CONFIG_FUSE_PASSTHROUGH) += 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
Copyright to /dev/null?
> + *
> + * 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 == 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 = src->out_args[0].value;
> + if (entry_out)
> + dep->nodeid = 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 = 0; i < count; i++) {
> + struct fuse_compound_op *cur = &ops[i];
> +
> + if (cur->dep_index != 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 >= i)) {
> + *cur->error = -EINVAL;
> + continue;
> + }
> + dep = &ops[cur->dep_index];
> +
> + if (*dep->error) {
> + *cur->error = *dep->error;
> + continue;
> + }
> + fuse_compound_propagate_nodeid(cur->arg, dep->arg);
> + }
> + *cur->error = 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 = fm->fc;
> + struct fuse_compound_args compound = {
> + .args = { .opcode = FUSE_COMPOUND, },
> + .ops = ops,
> + .count = count,
> + };
> + int ret;
> +
> + if (WARN_ON_ONCE(count == 0))
> + return -EINVAL;
> +
> + if (!fc->compound_ops) {
> + fuse_compound_send_legacy(fm, ops, count);
> + return 0;
> + }
> +
> + ret = fuse_simple_request(fm, &compound.args);
> + if (ret == -ENOSYS)
> + fc->compound_ops = 0;
> + if (ret == -ENOSYS || ret == -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 = {
> };
> EXPORT_SYMBOL_GPL(fuse_dev_fiq_ops);
>
> +static inline struct fuse_compound_args *
> +fuse_get_compound_args(struct fuse_args *args)
> +{
> + if (args->opcode == 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 = sizeof(struct fuse_in_header) +
> + sizeof(struct fuse_compound_in);
> + unsigned int i, j;
> +
> + for (i = 0; i < compound->count; i++) {
> + struct fuse_args *op_args = compound->ops[i].arg;
> +
> + total += sizeof(struct fuse_compound_req_in) +
> + sizeof(struct fuse_in_header);
> + for (j = 0; j < op_args->in_numargs; j++)
> + total += op_args->in_args[j].size;
> + }
> +
> + return total;
> +}
> +
> +void fuse_set_req_len(struct fuse_req *req)
> +{
> + struct fuse_compound_args *compound = fuse_get_compound_args(req->args);
> +
> + if (compound)
> + req->in.h.len = fuse_compound_req_size(compound);
> + else
> + req->in.h.len = 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 = 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);
> }
>
> @@ -713,9 +749,7 @@ static bool fuse_request_queue_background_uring(struct fuse_conn *fc,
> {
> struct fuse_iqueue *fiq = &fc->iq;
>
> - req->in.h.len = 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);
>
> return fuse_uring_queue_bq_req(req);
> @@ -1204,7 +1238,7 @@ static int fuse_copy_folios(struct fuse_copy_state *cs, unsigned nbytes,
> }
>
> /* Copy a single argument in the request to/from userspace buffer */
> -static int fuse_copy_one(struct fuse_copy_state *cs, void *val, unsigned size)
> +static int fuse_copy_one(struct fuse_copy_state *cs, void *val, unsigned int size)
> {
> while (size) {
> if (!cs->len) {
> @@ -1399,6 +1433,66 @@ __releases(fiq->lock)
> return fuse_read_batch_forget(fiq, cs, nbytes);
> }
>
> +/*
> + * 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 = fuse_copy_one(cs, &compound->in_header,
> + sizeof(compound->in_header));
> + if (err)
> + return err;
> + }
> +
> + for (i = 0; i < compound->count; i++) {
> + struct fuse_compound_op *op = &compound->ops[i];
> + struct fuse_args *op_args = op->arg;
> + struct fuse_compound_req_in sub = {
> + .dep_index = 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 = {
> + .unique = i,
> + .opcode = op_args->opcode,
> + .nodeid = op_args->nodeid,
> + .uid = cs->req->in.h.uid,
> + .gid = cs->req->in.h.gid,
> + .pid = cs->req->in.h.pid,
> + .len = sizeof(hdr),
> + };
> +
> + for (j = 0; j < op_args->in_numargs; j++)
> + hdr.len += op_args->in_args[j].size;
> +
> + err = fuse_copy_one(cs, &sub, sizeof(sub));
> + if (!err)
> + err = fuse_copy_one(cs, &hdr, sizeof(hdr));
> + if (!err)
> + err = 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 = req;
> err = fuse_copy_one(cs, &req->in.h, sizeof(req->in.h));
> - if (!err)
> - err = fuse_copy_args(cs, args->in_numargs, args->in_pages,
> - (struct fuse_arg *) args->in_args, 0);
> + if (!err) {
> + struct fuse_compound_args *compound = fuse_get_compound_args(args);
> +
> + if (compound)
> + err = fuse_copy_compound_in_args(cs, compound);
> + else
> + err = 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_pqueue *fpq, u64 unique)
> return NULL;
> }
>
> +int fuse_copy_compound_out_args(struct fuse_copy_state *cs,
> + struct fuse_compound_args *compound)
> +{
> + unsigned int i;
> + int err;
> +
> + err = fuse_copy_one(cs, &compound->out_header,
> + sizeof(compound->out_header));
> + if (err)
> + return err;
> +
> + for (i = 0; i < compound->count; i++) {
> + struct fuse_compound_op *op = &compound->ops[i];
> + struct fuse_out_header op_hdr;
> + size_t expected;
> +
> + err = 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 = 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 != i)
> + return -EIO;
> +
> + *op->error = op_hdr.error;
> + if (op_hdr.error) {
> + /* Errored replies carry only the fuse_out_header. */
> + if (op_hdr.len != 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 = 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 =
> + &op->arg->out_args[op->arg->out_numargs - 1];
> + size_t diff = expected - op_hdr.len;
> +
> + if (diff > lastarg->size)
> + return -EIO;
> + lastarg->size -= diff;
> + }
> +
> + err = 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;
> +}
> +
Move more of those compound helpers to compound.c instead of bloating dev.c
> 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 *fud,
> if (!req->args->page_replace)
> cs->move_folios = false;
>
> - if (oh.error)
> + if (oh.error) {
> err = nbytes != sizeof(oh) ? -EINVAL : 0;
> - else
> - err = fuse_copy_out_args(cs, req->args, nbytes);
> + } else {
> + struct fuse_compound_args *compound = fuse_get_compound_args(req->args);
> +
> + if (compound)
> + err = fuse_copy_compound_out_args(cs, compound);
> + else
> + err = fuse_copy_out_args(cs, req->args, nbytes);
> + }
> fuse_copy_finish(cs);
>
> 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 = true;
> cs.req = req;
>
> - err = fuse_copy_out_args(&cs, args, ring_in_out.payload_sz);
> + if (args->opcode == FUSE_COMPOUND) {
> + /* Stream compound response directly into operation buffers */
> + struct fuse_compound_args *compound =
> + container_of(args, struct fuse_compound_args, args);
> + err = fuse_copy_compound_out_args(&cs, compound);
> + } else {
> + err = 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 *ring, struct fuse_req *req,
> cs.is_uring = true;
> cs.req = req;
>
> + if (args->opcode == 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 =
> + container_of(args, struct fuse_compound_args, args);
> +
> + err = 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 = 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 *ring, struct fuse_req *req,
> /* copy the payload */
> err = 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 *req);
> 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;
>
> + /* 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;
>
> @@ -1272,6 +1275,35 @@ static inline ssize_t fuse_simple_idmap_request(struct mnt_idmap *idmap,
> int fuse_simple_background(struct fuse_mount *fm, struct fuse_args *args,
> gfp_t gfp_flags);
>
> +/*
> + * 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 fuse_mount *fm,
> fc->name_max = FUSE_NAME_LOW_MAX;
> fc->timeout.req_timeout = 0;
>
> + /*
> + * 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 = 1;
> +
> if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH))
> fuse_backing_files_init(fc);
>
> 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 = 52,
> FUSE_COPY_FILE_RANGE_64 = 53,
>
> + /* 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 = 54,
> +
> /* CUSE specific operations */
> CUSE_INIT = 4096,
>
> @@ -1245,6 +1252,55 @@ struct fuse_supp_groups {
> uint32_t groups[];
> };
>
> +/*
> + * 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];
> +};
> +
It's ok that currently only nodeid (outarg[0]) is propagated,
but I think that the UAPI should describe that this is the case.
Something like this?
uint8_t dep_index;
uint8_t dep_arg_idx;
uint8_t dep_arg_type;
};
We probably don't need to waste uint8_t on dep_arg_idx IDK
TBH, I did not look at NFS/SMB compound protocols, not io_uring
command chains, so I would appreciate it if you include a survey of the
state of the art in other protocols practices for compound commands.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v7 2/4] fuse: create helper functions for filling in fuse args for open and getattr
2026-06-04 9:45 ` [PATCH v7 2/4] fuse: create helper functions for filling in fuse args for open and getattr Horst Birthelmer
@ 2026-06-05 7:42 ` Amir Goldstein
0 siblings, 0 replies; 18+ messages in thread
From: Amir Goldstein @ 2026-06-05 7:42 UTC (permalink / raw)
To: Horst Birthelmer
Cc: Miklos Szeredi, Bernd Schubert, Joanne Koong, Luis Henriques,
linux-kernel, fuse-devel, Horst Birthelmer
On Thu, Jun 4, 2026 at 11:57 AM Horst Birthelmer <horst@birthelmer.com> wrote:
>
> From: Horst Birthelmer <hbirthelmer@ddn.com>
>
> 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 <joannelkoong@gmail.com>
> Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
> ---
> 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, struct inode *inode,
> return 0;
> }
>
> +/*
> + * 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 = FUSE_GETATTR;
> + args->nodeid = nodeid;
> + args->in_numargs = 1;
> + args->in_args[0].size = sizeof(*inarg);
> + args->in_args[0].value = inarg;
> + args->out_numargs = 1;
> + args->out_args[0].size = sizeof(*outarg);
> + args->out_args[0].value = 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 |= FUSE_GETATTR_FH;
> inarg.fh = ff->fh;
> }
> - args.opcode = FUSE_GETATTR;
> - args.nodeid = get_node_id(inode);
> - args.in_numargs = 1;
> - args.in_args[0].size = sizeof(inarg);
> - args.in_args[0].value = &inarg;
> - args.out_numargs = 1;
> - args.out_args[0].size = sizeof(outarg);
> - args.out_args[0].value = &outarg;
> + fuse_getattr_args_fill(&args, get_node_id(inode), &inarg, &outarg);
> err = 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 <linux/task_io_accounting_ops.h>
> #include <linux/iomap.h>
>
> +/*
> + * Helper function to initialize fuse_args for OPEN/OPENDIR operations
> + */
> +static void fuse_open_args_fill(struct fuse_mount *fm, struct fuse_args *args,
> + u64 nodeid, int opcode, unsigned int open_flags,
> + struct fuse_open_in *inarg,
> + struct fuse_open_out *outarg)
> +{
> + inarg->flags = open_flags & ~(O_CREAT | O_EXCL | O_NOCTTY);
> +
> + if (!fm->fc->atomic_o_trunc)
> + inarg->flags &= ~O_TRUNC;
> +
> + if (fm->fc->handle_killpriv_v2 &&
> + (inarg->flags & O_TRUNC) && !capable(CAP_FSETID))
> + inarg->open_flags |= FUSE_OPEN_KILL_SUIDGID;
> +
> + args->opcode = opcode;
> + args->nodeid = nodeid;
> + args->in_numargs = 1;
> + args->in_args[0].size = sizeof(*inarg);
> + args->in_args[0].value = inarg;
> + args->out_numargs = 1;
> + args->out_args[0].size = sizeof(*outarg);
> + args->out_args[0].value = 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 nodeid,
> FUSE_ARGS(args);
>
> memset(&inarg, 0, sizeof(inarg));
> - inarg.flags = open_flags & ~(O_CREAT | O_EXCL | O_NOCTTY);
> - if (!fm->fc->atomic_o_trunc)
> - inarg.flags &= ~O_TRUNC;
> -
> - if (fm->fc->handle_killpriv_v2 &&
> - (inarg.flags & O_TRUNC) && !capable(CAP_FSETID)) {
> - inarg.open_flags |= FUSE_OPEN_KILL_SUIDGID;
> - }
>
> - args.opcode = opcode;
> - args.nodeid = nodeid;
> - args.in_numargs = 1;
> - args.in_args[0].size = sizeof(inarg);
> - args.in_args[0].value = &inarg;
> - args.out_numargs = 1;
> - args.out_args[0].size = sizeof(*outargp);
> - args.out_args[0].value = outargp;
> + fuse_open_args_fill(fm, &args, nodeid, opcode, open_flags, &inarg, outargp);
>
> 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);
>
> +/*
> + * 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);
>
> struct fuse_file *fuse_file_alloc(struct fuse_mount *fm, bool release);
> void fuse_file_free(struct fuse_file *ff);
>
> --
> 2.54.0
>
>
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v7 3/4] fuse: add an implementation of open+getattr
2026-06-04 9:45 ` [PATCH v7 3/4] fuse: add an implementation of open+getattr Horst Birthelmer
@ 2026-06-05 7:50 ` Amir Goldstein
0 siblings, 0 replies; 18+ messages in thread
From: Amir Goldstein @ 2026-06-05 7:50 UTC (permalink / raw)
To: Horst Birthelmer
Cc: Miklos Szeredi, Bernd Schubert, Joanne Koong, Luis Henriques,
linux-kernel, fuse-devel, Horst Birthelmer
On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@birthelmer.com> wrote:
>
> From: Horst Birthelmer <hbirthelmer@ddn.com>
>
> 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 <hbirthelmer@ddn.com>
> ---
> 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 sync)
> }
> }
>
> +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 = {};
> + struct fuse_args open_args = {};
> + struct fuse_args getattr_args = {};
> + struct fuse_open_in open_in = {};
> + struct fuse_getattr_in getattr_in = {};
> + int open_error = 0, getattr_error = 0;
> + struct fuse_compound_op ops[2] = {
> + { .arg = &open_args, .error = &open_error,
> + .dep_index = FUSE_COMPOUND_NO_DEP },
> + { .arg = &getattr_args, .error = &getattr_error,
> + .dep_index = 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 = fuse_compound_send(fm, ops, 2);
> + if (err)
> + return err;
> +
> + /*
> + * Open succeeded if open_error == 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 = 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 = &ff->args->open_outarg;
> + bool try_compound = false;
> int err;
>
> - err = fuse_send_open(fm, nodeid, open_flags, opcode, outargp);
> + if (inode) {
> + struct fuse_inode *fi = get_fuse_inode(inode);
> +
> + try_compound = (open_flags & O_APPEND) ||
> + time_before64(fi->i_time, get_jiffies_64()) ||
> + (fi->inval_mask & STATX_BASIC_STATS);
> + }
> +
> + if (try_compound)
> + err = fuse_compound_open_getattr(fm, nodeid, inode,
> + open_flags, opcode,
> + outargp);
> + else
> + err = fuse_send_open(fm, nodeid, open_flags, opcode,
> + outargp);
> +
Pure semantic complaint -
The name "try_compound" confuses me because it sounds like the caller
would need to do the fallback.
In fact when the try_compound condition happens, code is going to call
fuse_compound_send(), which takes responsibility of the compound
request one way or the other.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: Re: [PATCH v7 1/4] fuse: add compound command to combine multiple requests
2026-06-05 7:41 ` Amir Goldstein
@ 2026-06-05 8:03 ` Horst Birthelmer
2026-06-06 17:30 ` Horst Birthelmer
1 sibling, 0 replies; 18+ messages in thread
From: Horst Birthelmer @ 2026-06-05 8:03 UTC (permalink / raw)
To: Amir Goldstein
Cc: Horst Birthelmer, Miklos Szeredi, Bernd Schubert, Joanne Koong,
Luis Henriques, linux-kernel, fuse-devel, Horst Birthelmer
Thanks a lot, Amir, for looking at this!
On Fri, Jun 05, 2026 at 09:41:00AM +0200, Amir Goldstein wrote:
> On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@birthelmer.com> wrote:
> > +/*
> > + * FUSE: Filesystem in Userspace
> > + * Copyright (C) 2025-2026
>
> Copyright to /dev/null?
>
LOL, that's a copy paste error of the header ...
> > 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 = {
> > };
...
>
> Move more of those compound helpers to compound.c instead of bloating dev.c
>
I completely get your point and I will change that. I just thought that it would be
easier to 'review' if it is clearly visible where the change is done to the normal flow,
since this was one of the main things for this version
> > + */
> > +struct fuse_compound_req_in {
> > + uint8_t dep_index;
> > + uint8_t reserved[3];
> > +};
> > +
>
>
> It's ok that currently only nodeid (outarg[0]) is propagated,
> but I think that the UAPI should describe that this is the case.
> Something like this?
>
> uint8_t dep_index;
> uint8_t dep_arg_idx;
> uint8_t dep_arg_type;
> };
>
Good idea, I will add this and let 0 be the nodeid type(?), so that the code
does not change ;-)
> We probably don't need to waste uint8_t on dep_arg_idx IDK
>
> TBH, I did not look at NFS/SMB compound protocols, not io_uring
> command chains, so I would appreciate it if you include a survey of the
> state of the art in other protocols practices for compound commands.
OK, I will do that. I don't remember how NFS does it.
>
> Thanks,
> Amir.
Thanks,
Horst
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v7 4/4] fuse: add compound command for dentry revalidation
2026-06-04 9:45 ` [PATCH v7 4/4] fuse: add compound command for dentry revalidation Horst Birthelmer
@ 2026-06-05 8:06 ` Amir Goldstein
2026-06-05 8:09 ` Horst Birthelmer
0 siblings, 1 reply; 18+ messages in thread
From: Amir Goldstein @ 2026-06-05 8:06 UTC (permalink / raw)
To: Horst Birthelmer
Cc: Miklos Szeredi, Bernd Schubert, Joanne Koong, Luis Henriques,
linux-kernel, fuse-devel, Horst Birthelmer
On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@birthelmer.com> wrote:
>
> From: Horst Birthelmer <hbirthelmer@ddn.com>
>
> During dentry revalidation the compound LOOKUP+GETATTR
> will save a round trip to user space.
>
> Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
> ---
> 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 = outarg;
> }
>
> +/*
> + * 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 = {};
> + struct fuse_attr_out getattr_out = {};
> + struct fuse_getattr_in getattr_in = {};
> + struct fuse_args lookup_args = {};
> + struct fuse_args getattr_args = {};
> + struct fuse_forget_link *forget;
> + int lookup_err = 0, getattr_err = 0;
> + struct fuse_compound_op ops[2] = {
> + { .arg = &lookup_args, .error = &lookup_err,
> + .dep_index = FUSE_COMPOUND_NO_DEP },
> + { .arg = &getattr_args, .error = &getattr_err,
> + .dep_index = 0 /* nodeid comes from lookup */ },
> + };
> + struct fuse_inode *fi;
> + int ret;
> +
> + forget = 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 = fuse_compound_send(fm, ops, 2);
> + if (ret == -ENOMEM || ret == -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 == -ENOMEM || lookup_err == -EINTR) {
> + ret = lookup_err;
> + goto out;
> + }
> + if (ret < 0 || lookup_err || !lookup_out.nodeid) {
> + ret = 0;
> + goto out;
> + }
> +
> + fi = get_fuse_inode(inode);
> + if (lookup_out.nodeid != get_node_id(inode) ||
> + (bool)IS_AUTOMOUNT(inode) != (bool)(lookup_out.attr.flags & FUSE_ATTR_SUBMOUNT)) {
> + fuse_queue_forget(fm->fc, forget, lookup_out.nodeid, 1);
> + forget = NULL;
> + ret = 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 = 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 = 1;
> +
> +out:
> + kfree(forget);
> + return ret;
> +}
> +
That's duplicating a lot of subtle code.
I think this calls for some helpers.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: Re: [PATCH v7 4/4] fuse: add compound command for dentry revalidation
2026-06-05 8:06 ` Amir Goldstein
@ 2026-06-05 8:09 ` Horst Birthelmer
0 siblings, 0 replies; 18+ messages in thread
From: Horst Birthelmer @ 2026-06-05 8:09 UTC (permalink / raw)
To: Amir Goldstein
Cc: Horst Birthelmer, Miklos Szeredi, Bernd Schubert, Joanne Koong,
Luis Henriques, linux-kernel, fuse-devel, Horst Birthelmer
On Fri, Jun 05, 2026 at 10:06:55AM +0200, Amir Goldstein wrote:
> On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@birthelmer.com> wrote:
> >
> > From: Horst Birthelmer <hbirthelmer@ddn.com>
> >
> > During dentry revalidation the compound LOOKUP+GETATTR
> > will save a round trip to user space.
> >
> > Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
> > ---
> > 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 = outarg;
> > }
> >
> > +/*
> > + * 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 = {};
> > + struct fuse_attr_out getattr_out = {};
> > + struct fuse_getattr_in getattr_in = {};
> > + struct fuse_args lookup_args = {};
> > + struct fuse_args getattr_args = {};
> > + struct fuse_forget_link *forget;
> > + int lookup_err = 0, getattr_err = 0;
> > + struct fuse_compound_op ops[2] = {
> > + { .arg = &lookup_args, .error = &lookup_err,
> > + .dep_index = FUSE_COMPOUND_NO_DEP },
> > + { .arg = &getattr_args, .error = &getattr_err,
> > + .dep_index = 0 /* nodeid comes from lookup */ },
> > + };
> > + struct fuse_inode *fi;
> > + int ret;
> > +
> > + forget = 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 = fuse_compound_send(fm, ops, 2);
> > + if (ret == -ENOMEM || ret == -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 == -ENOMEM || lookup_err == -EINTR) {
> > + ret = lookup_err;
> > + goto out;
> > + }
> > + if (ret < 0 || lookup_err || !lookup_out.nodeid) {
> > + ret = 0;
> > + goto out;
> > + }
> > +
> > + fi = get_fuse_inode(inode);
> > + if (lookup_out.nodeid != get_node_id(inode) ||
> > + (bool)IS_AUTOMOUNT(inode) != (bool)(lookup_out.attr.flags & FUSE_ATTR_SUBMOUNT)) {
> > + fuse_queue_forget(fm->fc, forget, lookup_out.nodeid, 1);
> > + forget = NULL;
> > + ret = 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 = 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 = 1;
> > +
> > +out:
> > + kfree(forget);
> > + return ret;
> > +}
> > +
>
> That's duplicating a lot of subtle code.
> I think this calls for some helpers.
OK ... this was a new one (compound I mean), and I valued the compactness of the patch more
than the possible code duplication. You're probably right ...
>
> Thanks,
> Amir.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v7 0/4] fuse: compound commands
2026-06-04 9:45 [PATCH v7 0/4] fuse: compound commands Horst Birthelmer
` (3 preceding siblings ...)
2026-06-04 9:45 ` [PATCH v7 4/4] fuse: add compound command for dentry revalidation Horst Birthelmer
@ 2026-06-05 8:12 ` Amir Goldstein
2026-06-05 8:49 ` Horst Birthelmer
4 siblings, 1 reply; 18+ messages in thread
From: Amir Goldstein @ 2026-06-05 8:12 UTC (permalink / raw)
To: Horst Birthelmer
Cc: Miklos Szeredi, Bernd Schubert, Joanne Koong, Luis Henriques,
linux-kernel, fuse-devel, Horst Birthelmer
On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@birthelmer.com> wrote:
>
> This series adds a single new opcode, FUSE_COMPOUND, that bundles a
> sequence of subrequests into one round trip. The wire format is
>
> fuse_in_header (opcode FUSE_COMPOUND)
> fuse_compound_in
> fuse_compound_req_in
> fuse_in_header
> payload
> ... (repeated per subop)
>
> Compound is opt-in per connection and discovered by trial: the kernel
> assumes support and clears its flag on the first -ENOSYS reply.
> -EOPNOTSUPP declines a specific combination without disabling the
> feature. In both cases the kernel replays the subops individually
> via fuse_simple_request(), so callers never need a separate
> non-compound code path.
>
> The series ships two consumers:
>
> - open + getattr, used when fuse_file_open() needs both ff->fh and
> fresh attrs (O_APPEND, or cached attrs already stale). This
> closes the open-then-stat race described in [1].
> - dentry revalidate, fusing LOOKUP + GETATTR when both the entry
> and the attribute caches are stale.
I am not sure if the intention for fusex is to carry over or phase out GETATTR
in favor of STATX, but apart of the strategic question whether FUSE_COMPOUND
should or should not be added to current FUSE protocol, we need to answer the
more concrete question:
Is FUSE_COMPOUND intended to improve existing unmodified servers
which link with newer libfuse and run on a newer kernel?
If not, then maybe we should start with OPEN/LOOKUP + STATX
from the start.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: Re: [PATCH v7 0/4] fuse: compound commands
2026-06-05 8:12 ` [PATCH v7 0/4] fuse: compound commands Amir Goldstein
@ 2026-06-05 8:49 ` Horst Birthelmer
2026-06-05 9:15 ` Amir Goldstein
2026-06-05 10:49 ` Bernd Schubert
0 siblings, 2 replies; 18+ messages in thread
From: Horst Birthelmer @ 2026-06-05 8:49 UTC (permalink / raw)
To: Amir Goldstein
Cc: Horst Birthelmer, Miklos Szeredi, Bernd Schubert, Joanne Koong,
Luis Henriques, linux-kernel, fuse-devel, Horst Birthelmer
On Fri, Jun 05, 2026 at 10:12:59AM +0200, Amir Goldstein wrote:
> On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@birthelmer.com> wrote:
> >
> > This series adds a single new opcode, FUSE_COMPOUND, that bundles a
> > sequence of subrequests into one round trip. The wire format is
> >
> > fuse_in_header (opcode FUSE_COMPOUND)
> > fuse_compound_in
> > fuse_compound_req_in
> > fuse_in_header
> > payload
> > ... (repeated per subop)
> >
> > Compound is opt-in per connection and discovered by trial: the kernel
> > assumes support and clears its flag on the first -ENOSYS reply.
> > -EOPNOTSUPP declines a specific combination without disabling the
> > feature. In both cases the kernel replays the subops individually
> > via fuse_simple_request(), so callers never need a separate
> > non-compound code path.
> >
> > The series ships two consumers:
> >
> > - open + getattr, used when fuse_file_open() needs both ff->fh and
> > fresh attrs (O_APPEND, or cached attrs already stale). This
> > closes the open-then-stat race described in [1].
> > - dentry revalidate, fusing LOOKUP + GETATTR when both the entry
> > and the attribute caches are stale.
>
> I am not sure if the intention for fusex is to carry over or phase out GETATTR
> in favor of STATX, but apart of the strategic question whether FUSE_COMPOUND
> should or should not be added to current FUSE protocol, we need to answer the
> more concrete question:
>
> Is FUSE_COMPOUND intended to improve existing unmodified servers
> which link with newer libfuse and run on a newer kernel?
>
> If not, then maybe we should start with OPEN/LOOKUP + STATX
> from the start.
To your first question about phase out of GETATTR, I don't think so,
since fusex will use the same opcodes, so it will be there and we will
have to fall back IMHO.
I have told this to a couple of people I have talked to about fusex
I would actually favor to negotiate supported opcodes and features in fusex
and adjust and overwrite the write operations accordingly. This of course is
miles away from the current state.
I don't think compounds will do anything for fuse servers that do not support it
and that don't have special cases that could be made faster when basically knowing
on a semantical level what the kernel actually wants (this is like some sort of
lookahead in fuse requests. If you are in fuse_atomic_open() the LOOKUP you are
sending is most likely followed by the CREATE right down below ... but the fuse
server cannot know that unless the kernel tells it)
It could have been when the compound handling of not supported operations would
have been in libfuse (which theoretically it still is), then you will save
user/kernel space switches, but when the kernel has to step in to do the 'legacy'
calls you actually will lose that intial try, where the fuse server tells you
ENOSYS or EOPNOTSUP.
So when linked with a not yet existing new libfuse, we could get faster due to the
lesser switches to user space. Do you think that answers your initial question?
I actually have an implementation of the atomic open (this is counter productive
for upstream, but I'm using it here as a concrete example to calrify the more general
point) and since our fuse server can do the atomic open way more efficiently
(everybody knows by now that distributed locks cost you performance)
I get 15%-20% more performance on metadataa tests.
The definitve answer here is probably somewhere around 'your milage may vary'.
I'm really interested in further discussion about this ... and your opinion here.
Would you want to use compounds for some case?
BTW, OPEN+GETATTR is a special case of OPEN+STATX, isn't it?
>
> Thanks,
> Amir.
Thanks,
Horst
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: Re: [PATCH v7 0/4] fuse: compound commands
2026-06-05 8:49 ` Horst Birthelmer
@ 2026-06-05 9:15 ` Amir Goldstein
2026-06-05 9:28 ` Horst Birthelmer
2026-06-05 10:49 ` Bernd Schubert
1 sibling, 1 reply; 18+ messages in thread
From: Amir Goldstein @ 2026-06-05 9:15 UTC (permalink / raw)
To: Horst Birthelmer
Cc: Horst Birthelmer, Miklos Szeredi, Bernd Schubert, Joanne Koong,
Luis Henriques, linux-kernel, fuse-devel, Horst Birthelmer
On Fri, Jun 5, 2026 at 10:49 AM Horst Birthelmer <horst@birthelmer.de> wrote:
>
> On Fri, Jun 05, 2026 at 10:12:59AM +0200, Amir Goldstein wrote:
> > On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@birthelmer.com> wrote:
> > >
> > > This series adds a single new opcode, FUSE_COMPOUND, that bundles a
> > > sequence of subrequests into one round trip. The wire format is
> > >
> > > fuse_in_header (opcode FUSE_COMPOUND)
> > > fuse_compound_in
> > > fuse_compound_req_in
> > > fuse_in_header
> > > payload
> > > ... (repeated per subop)
> > >
> > > Compound is opt-in per connection and discovered by trial: the kernel
> > > assumes support and clears its flag on the first -ENOSYS reply.
> > > -EOPNOTSUPP declines a specific combination without disabling the
> > > feature. In both cases the kernel replays the subops individually
> > > via fuse_simple_request(), so callers never need a separate
> > > non-compound code path.
> > >
> > > The series ships two consumers:
> > >
> > > - open + getattr, used when fuse_file_open() needs both ff->fh and
> > > fresh attrs (O_APPEND, or cached attrs already stale). This
> > > closes the open-then-stat race described in [1].
> > > - dentry revalidate, fusing LOOKUP + GETATTR when both the entry
> > > and the attribute caches are stale.
> >
> > I am not sure if the intention for fusex is to carry over or phase out GETATTR
> > in favor of STATX, but apart of the strategic question whether FUSE_COMPOUND
> > should or should not be added to current FUSE protocol, we need to answer the
> > more concrete question:
> >
> > Is FUSE_COMPOUND intended to improve existing unmodified servers
> > which link with newer libfuse and run on a newer kernel?
> >
> > If not, then maybe we should start with OPEN/LOOKUP + STATX
> > from the start.
>
> To your first question about phase out of GETATTR, I don't think so,
> since fusex will use the same opcodes, so it will be there and we will
> have to fall back IMHO.
>
> I have told this to a couple of people I have talked to about fusex
> I would actually favor to negotiate supported opcodes and features in fusex
> and adjust and overwrite the write operations accordingly. This of course is
> miles away from the current state.
>
> I don't think compounds will do anything for fuse servers that do not support it
> and that don't have special cases that could be made faster when basically knowing
> on a semantical level what the kernel actually wants (this is like some sort of
> lookahead in fuse requests. If you are in fuse_atomic_open() the LOOKUP you are
> sending is most likely followed by the CREATE right down below ... but the fuse
> server cannot know that unless the kernel tells it)
>
> It could have been when the compound handling of not supported operations would
> have been in libfuse (which theoretically it still is), then you will save
> user/kernel space switches, but when the kernel has to step in to do the 'legacy'
> calls you actually will lose that intial try, where the fuse server tells you
> ENOSYS or EOPNOTSUP.
>
> So when linked with a not yet existing new libfuse, we could get faster due to the
> lesser switches to user space. Do you think that answers your initial question?
>
> I actually have an implementation of the atomic open (this is counter productive
> for upstream, but I'm using it here as a concrete example to calrify the more general
> point) and since our fuse server can do the atomic open way more efficiently
> (everybody knows by now that distributed locks cost you performance)
> I get 15%-20% more performance on metadataa tests.
>
> The definitve answer here is probably somewhere around 'your milage may vary'.
> I'm really interested in further discussion about this ... and your opinion here.
> Would you want to use compounds for some case?
No, I do not have any use cases for compounds that I am aware of.
Compounds of READDIR+STATX was discussed as an technical alternative
to READDIRPLUS2 which would need to return backing_ids, but I am still
if this direction is worth pursuing.
>
> BTW, OPEN+GETATTR is a special case of OPEN+STATX, isn't it?
>
Yes, I was asking whether FUSE_STATX is expected to supersede
FUSE_GETATTR in fusex. I don't know that answer.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: Re: Re: [PATCH v7 0/4] fuse: compound commands
2026-06-05 9:15 ` Amir Goldstein
@ 2026-06-05 9:28 ` Horst Birthelmer
0 siblings, 0 replies; 18+ messages in thread
From: Horst Birthelmer @ 2026-06-05 9:28 UTC (permalink / raw)
To: Amir Goldstein
Cc: Horst Birthelmer, Miklos Szeredi, Bernd Schubert, Joanne Koong,
Luis Henriques, linux-kernel, fuse-devel, Horst Birthelmer
On Fri, Jun 05, 2026 at 11:15:03AM +0200, Amir Goldstein wrote:
> On Fri, Jun 5, 2026 at 10:49 AM Horst Birthelmer <horst@birthelmer.de> wrote:
> >
> > On Fri, Jun 05, 2026 at 10:12:59AM +0200, Amir Goldstein wrote:
> > > On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@birthelmer.com> wrote:
> > > >
> > > > This series adds a single new opcode, FUSE_COMPOUND, that bundles a
> > > > sequence of subrequests into one round trip. The wire format is
> > > >
> > > > fuse_in_header (opcode FUSE_COMPOUND)
> > > > fuse_compound_in
> > > > fuse_compound_req_in
> > > > fuse_in_header
> > > > payload
> > > > ... (repeated per subop)
> > > >
> > > > Compound is opt-in per connection and discovered by trial: the kernel
> > > > assumes support and clears its flag on the first -ENOSYS reply.
> > > > -EOPNOTSUPP declines a specific combination without disabling the
> > > > feature. In both cases the kernel replays the subops individually
> > > > via fuse_simple_request(), so callers never need a separate
> > > > non-compound code path.
> > > >
> > > > The series ships two consumers:
> > > >
> > > > - open + getattr, used when fuse_file_open() needs both ff->fh and
> > > > fresh attrs (O_APPEND, or cached attrs already stale). This
> > > > closes the open-then-stat race described in [1].
> > > > - dentry revalidate, fusing LOOKUP + GETATTR when both the entry
> > > > and the attribute caches are stale.
> > >
> > > I am not sure if the intention for fusex is to carry over or phase out GETATTR
> > > in favor of STATX, but apart of the strategic question whether FUSE_COMPOUND
> > > should or should not be added to current FUSE protocol, we need to answer the
> > > more concrete question:
> > >
> > > Is FUSE_COMPOUND intended to improve existing unmodified servers
> > > which link with newer libfuse and run on a newer kernel?
> > >
> > > If not, then maybe we should start with OPEN/LOOKUP + STATX
> > > from the start.
> >
> > To your first question about phase out of GETATTR, I don't think so,
> > since fusex will use the same opcodes, so it will be there and we will
> > have to fall back IMHO.
> >
> > I have told this to a couple of people I have talked to about fusex
> > I would actually favor to negotiate supported opcodes and features in fusex
> > and adjust and overwrite the write operations accordingly. This of course is
> > miles away from the current state.
> >
> > I don't think compounds will do anything for fuse servers that do not support it
> > and that don't have special cases that could be made faster when basically knowing
> > on a semantical level what the kernel actually wants (this is like some sort of
> > lookahead in fuse requests. If you are in fuse_atomic_open() the LOOKUP you are
> > sending is most likely followed by the CREATE right down below ... but the fuse
> > server cannot know that unless the kernel tells it)
> >
> > It could have been when the compound handling of not supported operations would
> > have been in libfuse (which theoretically it still is), then you will save
> > user/kernel space switches, but when the kernel has to step in to do the 'legacy'
> > calls you actually will lose that intial try, where the fuse server tells you
> > ENOSYS or EOPNOTSUP.
> >
> > So when linked with a not yet existing new libfuse, we could get faster due to the
> > lesser switches to user space. Do you think that answers your initial question?
> >
> > I actually have an implementation of the atomic open (this is counter productive
> > for upstream, but I'm using it here as a concrete example to calrify the more general
> > point) and since our fuse server can do the atomic open way more efficiently
> > (everybody knows by now that distributed locks cost you performance)
> > I get 15%-20% more performance on metadataa tests.
> >
> > The definitve answer here is probably somewhere around 'your milage may vary'.
> > I'm really interested in further discussion about this ... and your opinion here.
> > Would you want to use compounds for some case?
>
> No, I do not have any use cases for compounds that I am aware of.
>
> Compounds of READDIR+STATX was discussed as an technical alternative
> to READDIRPLUS2 which would need to return backing_ids, but I am still
> if this direction is worth pursuing.
>
> >
> > BTW, OPEN+GETATTR is a special case of OPEN+STATX, isn't it?
> >
>
> Yes, I was asking whether FUSE_STATX is expected to supersede
> FUSE_GETATTR in fusex. I don't know that answer.
I would support that idea, if it was up to me.
Fortunately it's not ;-)
>
> Thanks,
> Amir.
Thanks,
Horst
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v7 0/4] fuse: compound commands
2026-06-05 8:49 ` Horst Birthelmer
2026-06-05 9:15 ` Amir Goldstein
@ 2026-06-05 10:49 ` Bernd Schubert
2026-06-05 11:26 ` Horst Birthelmer
1 sibling, 1 reply; 18+ messages in thread
From: Bernd Schubert @ 2026-06-05 10:49 UTC (permalink / raw)
To: Horst Birthelmer, Amir Goldstein
Cc: Horst Birthelmer, Miklos Szeredi, Bernd Schubert, Joanne Koong,
Luis Henriques, linux-kernel, fuse-devel, Horst Birthelmer
On 6/5/26 10:49, Horst Birthelmer wrote:
> On Fri, Jun 05, 2026 at 10:12:59AM +0200, Amir Goldstein wrote:
>> On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@birthelmer.com> wrote:
>>>
>>> This series adds a single new opcode, FUSE_COMPOUND, that bundles a
>>> sequence of subrequests into one round trip. The wire format is
>>>
>>> fuse_in_header (opcode FUSE_COMPOUND)
>>> fuse_compound_in
>>> fuse_compound_req_in
>>> fuse_in_header
>>> payload
>>> ... (repeated per subop)
>>>
>>> Compound is opt-in per connection and discovered by trial: the kernel
>>> assumes support and clears its flag on the first -ENOSYS reply.
>>> -EOPNOTSUPP declines a specific combination without disabling the
>>> feature. In both cases the kernel replays the subops individually
>>> via fuse_simple_request(), so callers never need a separate
>>> non-compound code path.
>>>
>>> The series ships two consumers:
>>>
>>> - open + getattr, used when fuse_file_open() needs both ff->fh and
>>> fresh attrs (O_APPEND, or cached attrs already stale). This
>>> closes the open-then-stat race described in [1].
>>> - dentry revalidate, fusing LOOKUP + GETATTR when both the entry
>>> and the attribute caches are stale.
>>
>> I am not sure if the intention for fusex is to carry over or phase out GETATTR
>> in favor of STATX, but apart of the strategic question whether FUSE_COMPOUND
>> should or should not be added to current FUSE protocol, we need to answer the
>> more concrete question:
>>
>> Is FUSE_COMPOUND intended to improve existing unmodified servers
>> which link with newer libfuse and run on a newer kernel?
>>
>> If not, then maybe we should start with OPEN/LOOKUP + STATX
>> from the start.
>
> To your first question about phase out of GETATTR, I don't think so,
> since fusex will use the same opcodes, so it will be there and we will
> have to fall back IMHO.
I agree with Amir and also with recent DDN requirements for DLM - there
is no good reason to keep getattr. Basically for open we need to know
the updated file size. Depending on the backend implementation, getting
additionally the time stamps and other attributes _might_ be expensive.
And that exactly there the statx mask helps.
And I don't think it is related to fusex vs fuse. If libfuse or fuse
server do not support statx with the mask, well, then open+getattr will
just not supported for open+getattr - existing behavior?
>
> I have told this to a couple of people I have talked to about fusex
> I would actually favor to negotiate supported opcodes and features in fusex
> and adjust and overwrite the write operations accordingly. This of course is
> miles away from the current state.
>
> I don't think compounds will do anything for fuse servers that do not support it
> and that don't have special cases that could be made faster when basically knowing
> on a semantical level what the kernel actually wants (this is like some sort of
> lookahead in fuse requests. If you are in fuse_atomic_open() the LOOKUP you are
> sending is most likely followed by the CREATE right down below ... but the fuse
> server cannot know that unless the kernel tells it)
>
> It could have been when the compound handling of not supported operations would
> have been in libfuse (which theoretically it still is), then you will save
> user/kernel space switches, but when the kernel has to step in to do the 'legacy'
> calls you actually will lose that intial try, where the fuse server tells you
> ENOSYS or EOPNOTSUP.
>
> So when linked with a not yet existing new libfuse, we could get faster due to the
> lesser switches to user space. Do you think that answers your initial question?
>
> I actually have an implementation of the atomic open (this is counter productive
> for upstream, but I'm using it here as a concrete example to calrify the more general
> point) and since our fuse server can do the atomic open way more efficiently
> (everybody knows by now that distributed locks cost you performance)
> I get 15%-20% more performance on metadataa tests.
>
> The definitve answer here is probably somewhere around 'your milage may vary'.
> I'm really interested in further discussion about this ... and your opinion here.
> Would you want to use compounds for some case?
>
> BTW, OPEN+GETATTR is a special case of OPEN+STATX, isn't it?
Exactly, except that statx has a mask built in of what it needs.
Thanks,
Bernd
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: Re: [PATCH v7 0/4] fuse: compound commands
2026-06-05 10:49 ` Bernd Schubert
@ 2026-06-05 11:26 ` Horst Birthelmer
0 siblings, 0 replies; 18+ messages in thread
From: Horst Birthelmer @ 2026-06-05 11:26 UTC (permalink / raw)
To: Bernd Schubert
Cc: Amir Goldstein, Horst Birthelmer, Miklos Szeredi, Bernd Schubert,
Joanne Koong, Luis Henriques, linux-kernel, fuse-devel,
Horst Birthelmer
On Fri, Jun 05, 2026 at 12:49:55PM +0200, Bernd Schubert wrote:
> On 6/5/26 10:49, Horst Birthelmer wrote:
> > On Fri, Jun 05, 2026 at 10:12:59AM +0200, Amir Goldstein wrote:
> >> On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@birthelmer.com> wrote:
> >>>
> >>> This series adds a single new opcode, FUSE_COMPOUND, that bundles a
> >>> sequence of subrequests into one round trip. The wire format is
> >>>
> >>> fuse_in_header (opcode FUSE_COMPOUND)
> >>> fuse_compound_in
> >>> fuse_compound_req_in
> >>> fuse_in_header
> >>> payload
> >>> ... (repeated per subop)
> >>>
> >>> Compound is opt-in per connection and discovered by trial: the kernel
> >>> assumes support and clears its flag on the first -ENOSYS reply.
> >>> -EOPNOTSUPP declines a specific combination without disabling the
> >>> feature. In both cases the kernel replays the subops individually
> >>> via fuse_simple_request(), so callers never need a separate
> >>> non-compound code path.
> >>>
> >>> The series ships two consumers:
> >>>
> >>> - open + getattr, used when fuse_file_open() needs both ff->fh and
> >>> fresh attrs (O_APPEND, or cached attrs already stale). This
> >>> closes the open-then-stat race described in [1].
> >>> - dentry revalidate, fusing LOOKUP + GETATTR when both the entry
> >>> and the attribute caches are stale.
> >>
> >> I am not sure if the intention for fusex is to carry over or phase out GETATTR
> >> in favor of STATX, but apart of the strategic question whether FUSE_COMPOUND
> >> should or should not be added to current FUSE protocol, we need to answer the
> >> more concrete question:
> >>
> >> Is FUSE_COMPOUND intended to improve existing unmodified servers
> >> which link with newer libfuse and run on a newer kernel?
> >>
> >> If not, then maybe we should start with OPEN/LOOKUP + STATX
> >> from the start.
> >
> > To your first question about phase out of GETATTR, I don't think so,
> > since fusex will use the same opcodes, so it will be there and we will
> > have to fall back IMHO.
>
> I agree with Amir and also with recent DDN requirements for DLM - there
> is no good reason to keep getattr. Basically for open we need to know
> the updated file size. Depending on the backend implementation, getting
> additionally the time stamps and other attributes _might_ be expensive.
> And that exactly there the statx mask helps.
>
> And I don't think it is related to fusex vs fuse. If libfuse or fuse
> server do not support statx with the mask, well, then open+getattr will
> just not supported for open+getattr - existing behavior?
>
> >
> > I have told this to a couple of people I have talked to about fusex
> > I would actually favor to negotiate supported opcodes and features in fusex
> > and adjust and overwrite the write operations accordingly. This of course is
> > miles away from the current state.
> >
> > I don't think compounds will do anything for fuse servers that do not support it
> > and that don't have special cases that could be made faster when basically knowing
> > on a semantical level what the kernel actually wants (this is like some sort of
> > lookahead in fuse requests. If you are in fuse_atomic_open() the LOOKUP you are
> > sending is most likely followed by the CREATE right down below ... but the fuse
> > server cannot know that unless the kernel tells it)
> >
> > It could have been when the compound handling of not supported operations would
> > have been in libfuse (which theoretically it still is), then you will save
> > user/kernel space switches, but when the kernel has to step in to do the 'legacy'
> > calls you actually will lose that intial try, where the fuse server tells you
> > ENOSYS or EOPNOTSUP.
> >
> > So when linked with a not yet existing new libfuse, we could get faster due to the
> > lesser switches to user space. Do you think that answers your initial question?
> >
> > I actually have an implementation of the atomic open (this is counter productive
> > for upstream, but I'm using it here as a concrete example to calrify the more general
> > point) and since our fuse server can do the atomic open way more efficiently
> > (everybody knows by now that distributed locks cost you performance)
> > I get 15%-20% more performance on metadataa tests.
> >
> > The definitve answer here is probably somewhere around 'your milage may vary'.
> > I'm really interested in further discussion about this ... and your opinion here.
> > Would you want to use compounds for some case?
> >
> > BTW, OPEN+GETATTR is a special case of OPEN+STATX, isn't it?
>
> Exactly, except that statx has a mask built in of what it needs.
In this regard. I think we can probably make it OPEN+STATX and set the appropriate mask.
> Thanks,
> Bernd
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: Re: [PATCH v7 1/4] fuse: add compound command to combine multiple requests
2026-06-05 7:41 ` Amir Goldstein
2026-06-05 8:03 ` Horst Birthelmer
@ 2026-06-06 17:30 ` Horst Birthelmer
1 sibling, 0 replies; 18+ messages in thread
From: Horst Birthelmer @ 2026-06-06 17:30 UTC (permalink / raw)
To: Amir Goldstein
Cc: Horst Birthelmer, Miklos Szeredi, Bernd Schubert, Joanne Koong,
Luis Henriques, linux-kernel, fuse-devel, Horst Birthelmer
On Fri, Jun 05, 2026 at 09:41:00AM +0200, Amir Goldstein wrote:
>
> TBH, I did not look at NFS/SMB compound protocols, not io_uring
> command chains, so I would appreciate it if you include a survey of the
> state of the art in other protocols practices for compound commands.
I think the NFS compounds and FUSE compounds have very little in common.
Except for the the name ;-)
NFS has a tiny stack machine in the compound engine you could write stuff like this:
SEQUENCE(sessionid, slotid, seqid, ...)
PUTFH(parent_fh) # CFH = parent dir
SAVEFH # SFH = parent
OPEN(name, claim, ...) # CFH = opened file, returns stateid
GETFH # return CFH so client learns the fh
GETATTR(bitmap) # attrs of the opened file
RESTOREFH # CFH = parent again
GETATTR(bitmap) # attrs of parent (change attr for cache)
With the latest changes, I had to get a liitle bit in that direction, but I
would hesitate to even compare them. I don't think we need somthing like that
for FUSE, even though it could be useful, since they can be atomic or even
transactional AFAIK.
I don't think there could be conditions, though.
My first approach was to bunch the FUSE requests together and send them together
to userspace via fuse_simple_request(), nothing more. What made this more
complicated was the later requirement, that we have to do call the requests separately
if the fuse server does not support compounds or does not support this particular
compound.
In NFS if you get a minor version mismatch the server will fall back to a smaller
version. This is a little bit like what we do here.
Further, NFS has a map of supported operations. ATM I don't think we need that.
I would rather want to have a map of combinations we support, since my whole
motivation was, to be able to implement optimized combinations in the fuse server.
FUSE IMHO wasn't designed for this.
It was designed to send the requests to the server and wait for an answer for every
request, but with io-uring you actually could have a bunch of them in parallel.
---
IO-uring command chains I rejected, because I did not want to tie this to uring.
But with fusex, this is a whole new ball game.
AFAICT, the uring chains (or lately there were groups introduced) are just commands
that are linked together as a sequence. IOSQE_IO_LINK means that the next sqe should
start after this one finishes successful. Our compounds are probably more related
to uring groups, but there I don't know enough about, exept that groups can share
resources, but I have no idea how that works.
My compounds were more of a semantic grouping, where the fuse server could provide
optimized implementation of the whole group request. At least that is how I use them
at the moment.
>
> Thanks,
> Amir.
Thanks for challenging me to actually compare these. I haven't done this before since
it was something completely different in my mind ... and to some degree it still is.
Thanks,
Horst
^ permalink raw reply [flat|nested] 18+ messages in thread
end of thread, other threads:[~2026-06-06 17:31 UTC | newest]
Thread overview: 18+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-04 9:45 [PATCH v7 0/4] fuse: compound commands Horst Birthelmer
2026-06-04 9:45 ` [PATCH v7 1/4] fuse: add compound command to combine multiple requests Horst Birthelmer
2026-06-05 7:41 ` Amir Goldstein
2026-06-05 8:03 ` Horst Birthelmer
2026-06-06 17:30 ` Horst Birthelmer
2026-06-04 9:45 ` [PATCH v7 2/4] fuse: create helper functions for filling in fuse args for open and getattr Horst Birthelmer
2026-06-05 7:42 ` Amir Goldstein
2026-06-04 9:45 ` [PATCH v7 3/4] fuse: add an implementation of open+getattr Horst Birthelmer
2026-06-05 7:50 ` Amir Goldstein
2026-06-04 9:45 ` [PATCH v7 4/4] fuse: add compound command for dentry revalidation Horst Birthelmer
2026-06-05 8:06 ` Amir Goldstein
2026-06-05 8:09 ` Horst Birthelmer
2026-06-05 8:12 ` [PATCH v7 0/4] fuse: compound commands Amir Goldstein
2026-06-05 8:49 ` Horst Birthelmer
2026-06-05 9:15 ` Amir Goldstein
2026-06-05 9:28 ` Horst Birthelmer
2026-06-05 10:49 ` Bernd Schubert
2026-06-05 11:26 ` Horst Birthelmer
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.