Re: [PATCH v7 1/4] fuse: add compound command to combine multiple requests
From: Amir Goldstein
Date: Fri Jun 05 2026 - 04:04:54 EST
On Thu, Jun 4, 2026 at 11:51 AM Horst Birthelmer <horst@xxxxxxxxxxxxxx> wrote:
>
> From: Horst Birthelmer <hbirthelmer@xxxxxxx>
>
> 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@xxxxxxx>
> ---
> 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.