* [PATCH v2 01/21] fuse: introduce FUSE_PASSTHROUGH_INO mode
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 02/21] fuse: prepare for passthrough of inode operations Joanne Koong
` (19 subsequent siblings)
20 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
This is a more strict variant of FUSE_PASSTHROUGH mode, in which the
backing file inode number must match the fuse inode number.
This mode will allow the kernel to instantiate fuse inodes by
passthrough lookup and passthrough readdirplus and notify about those
inodes to the server, using the backing file inode number as a unique
identifier for fuse inodes across kernel and server.
This mode limits the possibility to map multiple fuse inodes to the same
backing file, unless they are all hardlinks.
This mode is only supported on 64bit arch, where ino_t is u64.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/file.c | 3 +--
fs/fuse/fuse_i.h | 6 ++++--
fs/fuse/inode.c | 8 +++++++-
fs/fuse/iomode.c | 13 ++++++++++---
include/uapi/linux/fuse.h | 6 +++++-
5 files changed, 27 insertions(+), 9 deletions(-)
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 7a35ac3e0023..06dc8cfe3c7b 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1429,7 +1429,6 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
bool *exclusive)
{
struct inode *inode = file_inode(iocb->ki_filp);
- struct fuse_inode *fi = get_fuse_inode(inode);
*exclusive = fuse_dio_wr_exclusive_lock(iocb, from);
if (*exclusive) {
@@ -1444,7 +1443,7 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
* have raced, so check it again.
*/
if (fuse_io_past_eof(iocb, from) ||
- fuse_inode_uncached_io_start(fi, NULL) != 0) {
+ fuse_inode_uncached_io_start(inode, NULL) != 0) {
inode_unlock_shared(inode);
inode_lock(inode);
*exclusive = true;
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 3a7ac74a23ed..0b925ac3e195 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -698,6 +698,9 @@ struct fuse_conn {
/** @passthrough: Passthrough support for read/write IO */
unsigned int passthrough:1;
+ /** @passthrough_ino: One-to-one mapping between fuse ino to backing ino */
+ unsigned int passthrough_ino:1;
+
/** @use_pages_for_kvec_io: Use pages instead of pointer for kernel I/O */
unsigned int use_pages_for_kvec_io:1;
@@ -1237,8 +1240,7 @@ int fuse_fileattr_set(struct mnt_idmap *idmap,
/* iomode.c */
int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff);
-int fuse_inode_uncached_io_start(struct fuse_inode *fi,
- struct fuse_backing *fb);
+int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb);
void fuse_inode_uncached_io_end(struct fuse_inode *fi);
int fuse_file_io_open(struct file *file, struct inode *inode);
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index 33009227e91d..b2a5892a4dc3 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -1397,6 +1397,8 @@ static void process_init_reply(struct fuse_args *args, int error)
fc->passthrough = 1;
fc->max_stack_depth = arg->max_stack_depth;
fm->sb->s_stack_depth = arg->max_stack_depth;
+ if (flags & FUSE_PASSTHROUGH_INO)
+ fc->passthrough_ino = 1;
}
if (flags & FUSE_NO_EXPORT_SUPPORT)
fm->sb->s_export_op = &fuse_export_fid_operations;
@@ -1476,8 +1478,12 @@ static struct fuse_init_args *fuse_new_init(struct fuse_mount *fm)
#endif
if (fm->fc->auto_submounts)
flags |= FUSE_SUBMOUNTS;
- if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH))
+ if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH)) {
flags |= FUSE_PASSTHROUGH;
+ /* one-to-one ino mapping requires 64bit ino */
+ if (sizeof(ino_t) == sizeof(u64))
+ flags |= FUSE_PASSTHROUGH_INO;
+ }
/*
* This is just an information flag for fuse server. No need to check
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index 3728933188f3..ca3b28597722 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -82,8 +82,10 @@ static void fuse_file_cached_io_release(struct fuse_file *ff,
}
/* Start strictly uncached io mode where cache access is not allowed */
-int fuse_inode_uncached_io_start(struct fuse_inode *fi, struct fuse_backing *fb)
+int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb)
{
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_conn *fc = get_fuse_conn(inode);
struct fuse_backing *oldfb;
int err = 0;
@@ -94,6 +96,12 @@ int fuse_inode_uncached_io_start(struct fuse_inode *fi, struct fuse_backing *fb)
err = -EBUSY;
goto unlock;
}
+ /* With FUSE_PASSTHROUGH_INO, fuse and backing ino must match */
+ if (fb && fc->passthrough_ino &&
+ fb->file->f_inode->i_ino != inode->i_ino) {
+ err = -EIO;
+ goto unlock;
+ }
if (fi->iocachectr > 0) {
err = -ETXTBSY;
goto unlock;
@@ -117,10 +125,9 @@ static int fuse_file_uncached_io_open(struct inode *inode,
struct fuse_file *ff,
struct fuse_backing *fb)
{
- struct fuse_inode *fi = get_fuse_inode(inode);
int err;
- err = fuse_inode_uncached_io_start(fi, fb);
+ err = fuse_inode_uncached_io_start(inode, fb);
if (err)
return err;
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index c13e1f9a2f12..4be9ccc5b3ff 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -240,6 +240,9 @@
* - add FUSE_COPY_FILE_RANGE_64
* - add struct fuse_copy_file_range_out
* - add FUSE_NOTIFY_PRUNE
+ *
+ * 7.46
+ * - add FUSE_PASSTHROUGH_INO
*/
#ifndef _LINUX_FUSE_H
@@ -275,7 +278,7 @@
#define FUSE_KERNEL_VERSION 7
/** Minor version number of this interface */
-#define FUSE_KERNEL_MINOR_VERSION 45
+#define FUSE_KERNEL_MINOR_VERSION 46
/** The node ID of the root inode */
#define FUSE_ROOT_ID 1
@@ -495,6 +498,7 @@ struct fuse_file_lock {
#define FUSE_ALLOW_IDMAP (1ULL << 40)
#define FUSE_OVER_IO_URING (1ULL << 41)
#define FUSE_REQUEST_TIMEOUT (1ULL << 42)
+#define FUSE_PASSTHROUGH_INO (1ULL << 43)
/**
* CUSE INIT request/reply flags
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* [PATCH v2 02/21] fuse: prepare for passthrough of inode operations
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
2026-05-16 0:39 ` [PATCH v2 01/21] fuse: introduce FUSE_PASSTHROUGH_INO mode Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 1:34 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 03/21] fuse: prepare for readdir passthrough on directories Joanne Koong
` (18 subsequent siblings)
20 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
So far, fuse passthrough was implemented for read/write/splice/mmap
operations for regular files opened with FOPEN_PASSTHROUGH.
A backing file is attached to a fuse inode, but only for as long as
there are FOPEN_PASSTHROUGH files opened on this inode.
We would like to attach a backing file to fuse inode also without an
open file to allow passthrough of some inode operations.
Add field ops_mask to the input argument of FUSE_DEV_IOC_BACKING_OPEN
ioctl to declare the operations that would passthrough to the backing
file once it has been attached to the fuse inode on lookup.
Setting the FUSE_READ/FUSE_WRITE operations in the ops_mask is not
required because those operations are implied by FOPEN_PASSTHROUGH.
When setting operations other than FUSE_READ/FUSE_WRITE in ops_mask,
non-regular backing files are allowed, so we need to verify when
attaching a backing file to a fuse inode, that their file types match.
For simplification of inode attribute caching, for now, require a
filesystem with FUSE_PASSTHROUGH_INO (one-to-one mapping from fuse inode
to backing inode) for setting up passthrough of any inode operations.
We may consider relaxing this requirement for some inode operations
in the future.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/backing.c | 13 ++++++++++---
fs/fuse/fuse_i.h | 30 ++++++++++++++++++++++++++++++
fs/fuse/iomode.c | 16 ++++++++++++++++
include/uapi/linux/fuse.h | 9 ++++++++-
4 files changed, 64 insertions(+), 4 deletions(-)
diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index 3d3f49c2dd42..1fbb4d876eec 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -87,7 +87,8 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
struct fuse_backing *fb = NULL;
int res;
- pr_debug("%s: fd=%d flags=0x%x\n", __func__, map->fd, map->flags);
+ pr_debug("%s: fd=%d flags=0x%x ops_mask=0x%llx\n", __func__,
+ map->fd, map->flags, map->ops_mask);
/* TODO: relax CAP_SYS_ADMIN once backing files are visible to lsof */
res = -EPERM;
@@ -95,7 +96,11 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
goto out;
res = -EINVAL;
- if (map->flags || map->padding)
+ if (map->flags || map->ops_mask & ~FUSE_BACKING_MAP_VALID_OPS)
+ goto out;
+
+ /* For now passthrough inode operations requires FUSE_PASSTHROUGH_INO */
+ if (!fc->passthrough_ino && map->ops_mask & FUSE_PASSTHROUGH_INODE_OPS)
goto out;
file = fget_raw(map->fd);
@@ -105,7 +110,8 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
/* read/write/splice/mmap passthrough only relevant for regular files */
res = d_is_dir(file->f_path.dentry) ? -EISDIR : -EINVAL;
- if (!d_is_reg(file->f_path.dentry))
+ if (!(map->ops_mask & ~FUSE_PASSTHROUGH_RW_OPS) &&
+ !d_is_reg(file->f_path.dentry))
goto out_fput;
backing_sb = file_inode(file)->i_sb;
@@ -120,6 +126,7 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
fb->file = file;
fb->cred = prepare_creds();
+ fb->ops_mask = map->ops_mask;
refcount_set(&fb->count, 1);
res = fuse_backing_id_alloc(fc, fb);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 0b925ac3e195..8e54dc7d56af 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -92,6 +92,7 @@ struct fuse_submount_lookup {
struct fuse_backing {
struct file *file;
struct cred *cred;
+ u64 ops_mask;
/* refcount */
refcount_t count;
@@ -254,6 +255,8 @@ enum {
* or the fuse server has an exclusive "lease" on distributed fs
*/
FUSE_I_EXCLUSIVE,
+ /* Has backing file for inode ops passthrough */
+ FUSE_I_PASSTHROUGH,
};
struct fuse_conn;
@@ -1252,6 +1255,25 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
void fuse_file_release(struct inode *inode, struct fuse_file *ff,
unsigned int open_flags, fl_owner_t id, bool isdir);
+/* passthrough.c */
+
+/* READ/WRITE are implied by FOPEN_PASSTHROUGH, but defined for completeness */
+#define FUSE_PASSTHROUGH_RW_OPS \
+ (FUSE_PASSTHROUGH_OP_READ | FUSE_PASSTHROUGH_OP_WRITE)
+
+/* File passthrough operations require a file opened with FOPEN_PASSTHROUGH */
+#define FUSE_PASSTHROUGH_FILE_OPS \
+ (FUSE_PASSTHROUGH_RW_OPS)
+
+/* Inode passthrough operations for backing file attached to inode */
+#define FUSE_PASSTHROUGH_INODE_OPS (0)
+
+#define FUSE_BACKING_MAP_OP(map, op) \
+ ((map)->ops_mask & FUSE_PASSTHROUGH_OP(op))
+
+#define FUSE_BACKING_MAP_VALID_OPS \
+ (FUSE_PASSTHROUGH_FILE_OPS | FUSE_PASSTHROUGH_INODE_OPS)
+
/* backing.c */
#ifdef CONFIG_FUSE_PASSTHROUGH
struct fuse_backing *fuse_backing_get(struct fuse_backing *fb);
@@ -1319,6 +1341,14 @@ ssize_t fuse_passthrough_splice_write(struct pipe_inode_info *pipe,
size_t len, unsigned int flags);
ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma);
+static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
+{
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_backing *fb = fuse_inode_backing(fi);
+
+ return fb && fb->ops_mask & FUSE_PASSTHROUGH_OP(op);
+}
+
#ifdef CONFIG_SYSCTL
extern int fuse_sysctl_register(void);
extern void fuse_sysctl_unregister(void);
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index ca3b28597722..5517711a3eca 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -96,6 +96,11 @@ int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb)
err = -EBUSY;
goto unlock;
}
+ /* fuse and backing file types must match */
+ if (fb && ((fb->file->f_inode->i_mode ^ inode->i_mode) & S_IFMT)) {
+ err = -EIO;
+ goto unlock;
+ }
/* With FUSE_PASSTHROUGH_INO, fuse and backing ino must match */
if (fb && fc->passthrough_ino &&
fb->file->f_inode->i_ino != inode->i_ino) {
@@ -176,6 +181,7 @@ static int fuse_file_passthrough_open(struct inode *inode, struct file *file)
{
struct fuse_file *ff = file->private_data;
struct fuse_conn *fc = get_fuse_conn(inode);
+ struct fuse_inode *fi = get_fuse_inode(inode);
struct fuse_backing *fb;
int err;
@@ -188,11 +194,21 @@ static int fuse_file_passthrough_open(struct inode *inode, struct file *file)
if (IS_ERR(fb))
return PTR_ERR(fb);
+ /*
+ * Inode ops passthrough requires backing file setup at
+ * creation/lookup time.
+ */
+ err = -EOPNOTSUPP;
+ if (!fuse_inode_backing(fi) &&
+ (fb->ops_mask & FUSE_PASSTHROUGH_INODE_OPS))
+ goto fail;
+
/* First passthrough file open denies caching inode io mode */
err = fuse_file_uncached_io_open(inode, ff, fb);
if (!err)
return 0;
+fail:
fuse_passthrough_release(ff, fb);
fuse_backing_put(fb);
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 4be9ccc5b3ff..0f1e1c1ec367 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -243,6 +243,7 @@
*
* 7.46
* - add FUSE_PASSTHROUGH_INO
+ * - add ops_mask field to struct fuse_backing_map
*/
#ifndef _LINUX_FUSE_H
@@ -1133,9 +1134,15 @@ struct fuse_notify_prune_out {
struct fuse_backing_map {
int32_t fd;
uint32_t flags;
- uint64_t padding;
+ uint64_t ops_mask;
};
+#define FUSE_PASSTHROUGH_OP(op) (1ULL << ((op) - 1))
+
+/* op bits for fuse_backing_map ops_mask */
+#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
+#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
+
/* Device ioctls: */
#define FUSE_DEV_IOC_MAGIC 229
#define FUSE_DEV_IOC_CLONE _IOR(FUSE_DEV_IOC_MAGIC, 0, uint32_t)
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 02/21] fuse: prepare for passthrough of inode operations
2026-05-16 0:39 ` [PATCH v2 02/21] fuse: prepare for passthrough of inode operations Joanne Koong
@ 2026-05-16 1:34 ` Joanne Koong
2026-05-16 16:11 ` Amir Goldstein
0 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 1:34 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
On Fri, May 15, 2026 at 5:52 PM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> From: Amir Goldstein <amir73il@gmail.com>
>
> So far, fuse passthrough was implemented for read/write/splice/mmap
> operations for regular files opened with FOPEN_PASSTHROUGH.
>
> A backing file is attached to a fuse inode, but only for as long as
> there are FOPEN_PASSTHROUGH files opened on this inode.
>
> We would like to attach a backing file to fuse inode also without an
> open file to allow passthrough of some inode operations.
>
> Add field ops_mask to the input argument of FUSE_DEV_IOC_BACKING_OPEN
> ioctl to declare the operations that would passthrough to the backing
> file once it has been attached to the fuse inode on lookup.
>
> Setting the FUSE_READ/FUSE_WRITE operations in the ops_mask is not
> required because those operations are implied by FOPEN_PASSTHROUGH.
>
> When setting operations other than FUSE_READ/FUSE_WRITE in ops_mask,
> non-regular backing files are allowed, so we need to verify when
> attaching a backing file to a fuse inode, that their file types match.
>
> For simplification of inode attribute caching, for now, require a
> filesystem with FUSE_PASSTHROUGH_INO (one-to-one mapping from fuse inode
> to backing inode) for setting up passthrough of any inode operations.
> We may consider relaxing this requirement for some inode operations
> in the future.
>
> Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
> Signed-off-by: Amir Goldstein <amir73il@gmail.com>
> ---
> fs/fuse/backing.c | 13 ++++++++++---
> fs/fuse/fuse_i.h | 30 ++++++++++++++++++++++++++++++
> fs/fuse/iomode.c | 16 ++++++++++++++++
> include/uapi/linux/fuse.h | 9 ++++++++-
> 4 files changed, 64 insertions(+), 4 deletions(-)
>
> diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
> index 4be9ccc5b3ff..0f1e1c1ec367 100644
> --- a/include/uapi/linux/fuse.h
> +++ b/include/uapi/linux/fuse.h
> @@ -243,6 +243,7 @@
>
> +#define FUSE_PASSTHROUGH_OP(op) (1ULL << ((op) - 1))
> +
> +/* op bits for fuse_backing_map ops_mask */
> +#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
> +#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
Do you think we should couple the passthrough op code to the fuse
opcode so closely, instead of defining the passthrough ops separately,
eg doing something like this?:
#define FUSE_PASSTHROUGH_READ (1 << 0)
#define FUSE_PASSTHROUGH_WRITE (1 << 1)
In the (far) future when some more advanced passthrough features get
added (eg full subtree passthrough), it seems like we'd want to add a
passthrough op for that, but that wouldn't map to a fuse op. I think
there are also some fuse ops we might skip defining as passthrough ops
but are implicitly passed through (eg FUSE_STATX which is covered by
FUSE_GETATTR), so it seems more intuitive to define the passthrough
ops as describing capabilities rather than implying that it describes
what specific opcodes get passed through?
Thanks,
Joanne
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 02/21] fuse: prepare for passthrough of inode operations
2026-05-16 1:34 ` Joanne Koong
@ 2026-05-16 16:11 ` Amir Goldstein
2026-05-18 18:04 ` Joanne Koong
0 siblings, 1 reply; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 16:11 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 3:34 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Fri, May 15, 2026 at 5:52 PM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > From: Amir Goldstein <amir73il@gmail.com>
> >
> > So far, fuse passthrough was implemented for read/write/splice/mmap
> > operations for regular files opened with FOPEN_PASSTHROUGH.
> >
> > A backing file is attached to a fuse inode, but only for as long as
> > there are FOPEN_PASSTHROUGH files opened on this inode.
> >
> > We would like to attach a backing file to fuse inode also without an
> > open file to allow passthrough of some inode operations.
> >
> > Add field ops_mask to the input argument of FUSE_DEV_IOC_BACKING_OPEN
> > ioctl to declare the operations that would passthrough to the backing
> > file once it has been attached to the fuse inode on lookup.
> >
> > Setting the FUSE_READ/FUSE_WRITE operations in the ops_mask is not
> > required because those operations are implied by FOPEN_PASSTHROUGH.
> >
> > When setting operations other than FUSE_READ/FUSE_WRITE in ops_mask,
> > non-regular backing files are allowed, so we need to verify when
> > attaching a backing file to a fuse inode, that their file types match.
> >
> > For simplification of inode attribute caching, for now, require a
> > filesystem with FUSE_PASSTHROUGH_INO (one-to-one mapping from fuse inode
> > to backing inode) for setting up passthrough of any inode operations.
> > We may consider relaxing this requirement for some inode operations
> > in the future.
> >
> > Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
> > Signed-off-by: Amir Goldstein <amir73il@gmail.com>
> > ---
> > fs/fuse/backing.c | 13 ++++++++++---
> > fs/fuse/fuse_i.h | 30 ++++++++++++++++++++++++++++++
> > fs/fuse/iomode.c | 16 ++++++++++++++++
> > include/uapi/linux/fuse.h | 9 ++++++++-
> > 4 files changed, 64 insertions(+), 4 deletions(-)
> >
> > diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
> > index 4be9ccc5b3ff..0f1e1c1ec367 100644
> > --- a/include/uapi/linux/fuse.h
> > +++ b/include/uapi/linux/fuse.h
> > @@ -243,6 +243,7 @@
> >
> > +#define FUSE_PASSTHROUGH_OP(op) (1ULL << ((op) - 1))
> > +
> > +/* op bits for fuse_backing_map ops_mask */
> > +#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
> > +#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
>
> Do you think we should couple the passthrough op code to the fuse
> opcode so closely, instead of defining the passthrough ops separately,
> eg doing something like this?:
>
> #define FUSE_PASSTHROUGH_READ (1 << 0)
> #define FUSE_PASSTHROUGH_WRITE (1 << 1)
Don't drop the _OP_ please
Need it to distinguish from FUSE_PASSTHROUGH{,_INO}
and we need to think if we want to format this as 32bit and extend later
or start with a u64 ops_mask format from the start.
>
> In the (far) future when some more advanced passthrough features get
> added (eg full subtree passthrough), it seems like we'd want to add a
> passthrough op for that,
I don't know. fuse_backing_map has flags, why would we want to put that
in ops_mask.
> but that wouldn't map to a fuse op. I think
> there are also some fuse ops we might skip defining as passthrough ops
> but are implicitly passed through (eg FUSE_STATX which is covered by
> FUSE_GETATTR),
This could also become the case with FUSE_CREATE{,_HANDLE}
or {FUSE,FUSEX}_CREAT.
TBH, the fact that some ops will never be in the mask and
that some ops have a canonical bit in itself does not justify creating
a different mapping. Do we also want to squeeze the mask into 32bit
and leave reserved space? my intuition is to leave it u64.
> so it seems more intuitive to define the passthrough
> ops as describing capabilities rather than implying that it describes
> what specific opcodes get passed through?
hmm. I guess you have a point that it is a bit limiting to commit
to this arithmetic in UAPI.
My thinking behind this was ease of use with these helpers:
if (!fuse_passthrough_op(file_inode(in), FUSE_READ))
but those could just as well be macros:
static inline bool fuse_inode_passthrough_op(struct inode *inode,
u64 opbit)...
#define FUSE_PASSTHROUGH_OP(inode, opname) \
fuse_passthrough_op(inode, FUSE_PASSTHROUGH_OP_ ## opname)
#define FUSE_BACKING_MAP_OP(map, opname) \
((map)->ops_mask & FUSE_PASSTHROUGH_OP ## opname))
...
if (!FUSE_PASSTHROUGH_OP(file_inode(in), READ))
wdyt?
Thanks,
Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread* Re: [PATCH v2 02/21] fuse: prepare for passthrough of inode operations
2026-05-16 16:11 ` Amir Goldstein
@ 2026-05-18 18:04 ` Joanne Koong
2026-05-18 18:39 ` Amir Goldstein
0 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-18 18:04 UTC (permalink / raw)
To: Amir Goldstein; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 9:11 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Sat, May 16, 2026 at 3:34 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > On Fri, May 15, 2026 at 5:52 PM Joanne Koong <joannelkoong@gmail.com> wrote:
> > >
> > > +/* op bits for fuse_backing_map ops_mask */
> > > +#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
> > > +#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
> >
> > Do you think we should couple the passthrough op code to the fuse
> > opcode so closely, instead of defining the passthrough ops separately,
> > eg doing something like this?:
> >
> > #define FUSE_PASSTHROUGH_READ (1 << 0)
> > #define FUSE_PASSTHROUGH_WRITE (1 << 1)
>
> Don't drop the _OP_ please
> Need it to distinguish from FUSE_PASSTHROUGH{,_INO}
> and we need to think if we want to format this as 32bit and extend later
> or start with a u64 ops_mask format from the start.
>
> >
> > In the (far) future when some more advanced passthrough features get
> > added (eg full subtree passthrough), it seems like we'd want to add a
> > passthrough op for that,
>
> I don't know. fuse_backing_map has flags, why would we want to put that
> in ops_mask.
That's a good point, I like your idea of using flags for that.
>
> > but that wouldn't map to a fuse op. I think
> > there are also some fuse ops we might skip defining as passthrough ops
> > but are implicitly passed through (eg FUSE_STATX which is covered by
> > FUSE_GETATTR),
>
> This could also become the case with FUSE_CREATE{,_HANDLE}
> or {FUSE,FUSEX}_CREAT.
>
> TBH, the fact that some ops will never be in the mask and
> that some ops have a canonical bit in itself does not justify creating
> a different mapping. Do we also want to squeeze the mask into 32bit
> and leave reserved space? my intuition is to leave it u64.
>
> > so it seems more intuitive to define the passthrough
> > ops as describing capabilities rather than implying that it describes
> > what specific opcodes get passed through?
>
> hmm. I guess you have a point that it is a bit limiting to commit
> to this arithmetic in UAPI.
>
> My thinking behind this was ease of use with these helpers:
>
> if (!fuse_passthrough_op(file_inode(in), FUSE_READ))
>
> but those could just as well be macros:
>
> static inline bool fuse_inode_passthrough_op(struct inode *inode,
> u64 opbit)...
>
> #define FUSE_PASSTHROUGH_OP(inode, opname) \
> fuse_passthrough_op(inode, FUSE_PASSTHROUGH_OP_ ## opname)
>
> #define FUSE_BACKING_MAP_OP(map, opname) \
> ((map)->ops_mask & FUSE_PASSTHROUGH_OP ## opname))
>
> ...
> if (!FUSE_PASSTHROUGH_OP(file_inode(in), READ))
>
> wdyt?
Nice, the macro idea is neat!
I feel like it gives more flexibility to define it separately and I
think with that, u32 would be more than enough bits as well. But I'm
happy to go with what you think would be better here.
Thanks,
Joanne
>
> Thanks,
> Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread* Re: [PATCH v2 02/21] fuse: prepare for passthrough of inode operations
2026-05-18 18:04 ` Joanne Koong
@ 2026-05-18 18:39 ` Amir Goldstein
2026-05-18 20:56 ` Joanne Koong
0 siblings, 1 reply; 54+ messages in thread
From: Amir Goldstein @ 2026-05-18 18:39 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Mon, May 18, 2026 at 8:04 PM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Sat, May 16, 2026 at 9:11 AM Amir Goldstein <amir73il@gmail.com> wrote:
> >
> > On Sat, May 16, 2026 at 3:34 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> > >
> > > On Fri, May 15, 2026 at 5:52 PM Joanne Koong <joannelkoong@gmail.com> wrote:
> > > >
> > > > +/* op bits for fuse_backing_map ops_mask */
> > > > +#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
> > > > +#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
> > >
> > > Do you think we should couple the passthrough op code to the fuse
> > > opcode so closely, instead of defining the passthrough ops separately,
> > > eg doing something like this?:
> > >
> > > #define FUSE_PASSTHROUGH_READ (1 << 0)
> > > #define FUSE_PASSTHROUGH_WRITE (1 << 1)
> >
> > Don't drop the _OP_ please
> > Need it to distinguish from FUSE_PASSTHROUGH{,_INO}
> > and we need to think if we want to format this as 32bit and extend later
> > or start with a u64 ops_mask format from the start.
> >
> > >
> > > In the (far) future when some more advanced passthrough features get
> > > added (eg full subtree passthrough), it seems like we'd want to add a
> > > passthrough op for that,
> >
> > I don't know. fuse_backing_map has flags, why would we want to put that
> > in ops_mask.
>
> That's a good point, I like your idea of using flags for that.
>
> >
> > > but that wouldn't map to a fuse op. I think
> > > there are also some fuse ops we might skip defining as passthrough ops
> > > but are implicitly passed through (eg FUSE_STATX which is covered by
> > > FUSE_GETATTR),
> >
> > This could also become the case with FUSE_CREATE{,_HANDLE}
> > or {FUSE,FUSEX}_CREAT.
> >
> > TBH, the fact that some ops will never be in the mask and
> > that some ops have a canonical bit in itself does not justify creating
> > a different mapping. Do we also want to squeeze the mask into 32bit
> > and leave reserved space? my intuition is to leave it u64.
> >
> > > so it seems more intuitive to define the passthrough
> > > ops as describing capabilities rather than implying that it describes
> > > what specific opcodes get passed through?
> >
> > hmm. I guess you have a point that it is a bit limiting to commit
> > to this arithmetic in UAPI.
> >
> > My thinking behind this was ease of use with these helpers:
> >
> > if (!fuse_passthrough_op(file_inode(in), FUSE_READ))
> >
> > but those could just as well be macros:
> >
> > static inline bool fuse_inode_passthrough_op(struct inode *inode,
> > u64 opbit)...
> >
> > #define FUSE_PASSTHROUGH_OP(inode, opname) \
> > fuse_passthrough_op(inode, FUSE_PASSTHROUGH_OP_ ## opname)
> >
> > #define FUSE_BACKING_MAP_OP(map, opname) \
> > ((map)->ops_mask & FUSE_PASSTHROUGH_OP ## opname))
> >
> > ...
> > if (!FUSE_PASSTHROUGH_OP(file_inode(in), READ))
> >
> > wdyt?
>
> Nice, the macro idea is neat!
>
> I feel like it gives more flexibility to define it separately and I
> think with that, u32 would be more than enough bits as well. But I'm
> happy to go with what you think would be better here.
I am not sure. Let's keep it u64 for now.
Since the struct is in an ioctl, it does not really matter much.
If we ever consider moving backing_map responses inline
in lookup/readdirplus response (maybe over io uring) we could reconsider.
In case you missed the exciting conversations about this API
original fuse passthough patches from Android had the backing fd
in the open response but it was deemed unsafe writing fds to
/dev/fuse, so ioctl was chosen instead [1].
With io_uring I don't think the security concern holds, so I see no reason
why backing_map would not be possible in-place for readdirplus
responses if we wanted to do that, but we can also ignore all this for now.
Thanks,
Amir.
[1] https://lore.kernel.org/linux-fsdevel/CAG48ez3ZX8R9kRAQhung2_e3wjowu5cPh7WL3U866mkga-kftQ@mail.gmail.com/
^ permalink raw reply [flat|nested] 54+ messages in thread* Re: [PATCH v2 02/21] fuse: prepare for passthrough of inode operations
2026-05-18 18:39 ` Amir Goldstein
@ 2026-05-18 20:56 ` Joanne Koong
0 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-18 20:56 UTC (permalink / raw)
To: Amir Goldstein; +Cc: miklos, fuse-devel, linux-unionfs
On Mon, May 18, 2026 at 11:39 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Mon, May 18, 2026 at 8:04 PM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > On Sat, May 16, 2026 at 9:11 AM Amir Goldstein <amir73il@gmail.com> wrote:
> > >
> > > On Sat, May 16, 2026 at 3:34 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> > > >
> > > > On Fri, May 15, 2026 at 5:52 PM Joanne Koong <joannelkoong@gmail.com> wrote:
> > > > >
> > > > > +/* op bits for fuse_backing_map ops_mask */
> > > > > +#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
> > > > > +#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
> > > >
> > > > Do you think we should couple the passthrough op code to the fuse
> > > > opcode so closely, instead of defining the passthrough ops separately,
> > > > eg doing something like this?:
> > > >
> > > > #define FUSE_PASSTHROUGH_READ (1 << 0)
> > > > #define FUSE_PASSTHROUGH_WRITE (1 << 1)
> > >
> > > Don't drop the _OP_ please
> > > Need it to distinguish from FUSE_PASSTHROUGH{,_INO}
> > > and we need to think if we want to format this as 32bit and extend later
> > > or start with a u64 ops_mask format from the start.
> > >
> > > >
> > > > In the (far) future when some more advanced passthrough features get
> > > > added (eg full subtree passthrough), it seems like we'd want to add a
> > > > passthrough op for that,
> > >
> > > I don't know. fuse_backing_map has flags, why would we want to put that
> > > in ops_mask.
> >
> > That's a good point, I like your idea of using flags for that.
> >
> > >
> > > > but that wouldn't map to a fuse op. I think
> > > > there are also some fuse ops we might skip defining as passthrough ops
> > > > but are implicitly passed through (eg FUSE_STATX which is covered by
> > > > FUSE_GETATTR),
> > >
> > > This could also become the case with FUSE_CREATE{,_HANDLE}
> > > or {FUSE,FUSEX}_CREAT.
> > >
> > > TBH, the fact that some ops will never be in the mask and
> > > that some ops have a canonical bit in itself does not justify creating
> > > a different mapping. Do we also want to squeeze the mask into 32bit
> > > and leave reserved space? my intuition is to leave it u64.
> > >
> > > > so it seems more intuitive to define the passthrough
> > > > ops as describing capabilities rather than implying that it describes
> > > > what specific opcodes get passed through?
> > >
> > > hmm. I guess you have a point that it is a bit limiting to commit
> > > to this arithmetic in UAPI.
> > >
> > > My thinking behind this was ease of use with these helpers:
> > >
> > > if (!fuse_passthrough_op(file_inode(in), FUSE_READ))
> > >
> > > but those could just as well be macros:
> > >
> > > static inline bool fuse_inode_passthrough_op(struct inode *inode,
> > > u64 opbit)...
> > >
> > > #define FUSE_PASSTHROUGH_OP(inode, opname) \
> > > fuse_passthrough_op(inode, FUSE_PASSTHROUGH_OP_ ## opname)
> > >
> > > #define FUSE_BACKING_MAP_OP(map, opname) \
> > > ((map)->ops_mask & FUSE_PASSTHROUGH_OP ## opname))
> > >
> > > ...
> > > if (!FUSE_PASSTHROUGH_OP(file_inode(in), READ))
> > >
> > > wdyt?
> >
> > Nice, the macro idea is neat!
> >
> > I feel like it gives more flexibility to define it separately and I
> > think with that, u32 would be more than enough bits as well. But I'm
> > happy to go with what you think would be better here.
>
> I am not sure. Let's keep it u64 for now.
> Since the struct is in an ioctl, it does not really matter much.
> If we ever consider moving backing_map responses inline
> in lookup/readdirplus response (maybe over io uring) we could reconsider.
Sounds good. For v3 I'll keep it a u64 but define them separately and
use your macro helper for mapping from opcode to passthrough op.
>
> In case you missed the exciting conversations about this API
> original fuse passthough patches from Android had the backing fd
> in the open response but it was deemed unsafe writing fds to
> /dev/fuse, so ioctl was chosen instead [1].
Wow, that's a clever attack. Thanks for sharing the link.
Thanks,
Joanne
>
> With io_uring I don't think the security concern holds, so I see no reason
> why backing_map would not be possible in-place for readdirplus
> responses if we wanted to do that, but we can also ignore all this for now.
>
> Thanks,
> Amir.
>
> [1] https://lore.kernel.org/linux-fsdevel/CAG48ez3ZX8R9kRAQhung2_e3wjowu5cPh7WL3U866mkga-kftQ@mail.gmail.com/
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 03/21] fuse: prepare for readdir passthrough on directories
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
2026-05-16 0:39 ` [PATCH v2 01/21] fuse: introduce FUSE_PASSTHROUGH_INO mode Joanne Koong
2026-05-16 0:39 ` [PATCH v2 02/21] fuse: prepare for passthrough of inode operations Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 04/21] fuse: implement passthrough for readdir Joanne Koong
` (17 subsequent siblings)
20 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
In preparation for readdir passthrough, allow the inode iomode state
to be applicable to directory inodes and prepare the helper
fuse_sync_release() for directories.
Directory inodes will support cached mode, "direct" uncached readdir
mode and readdir passthrough mode, but will not need to wait for
parallel dio like regular files.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/cuse.c | 2 +-
fs/fuse/dir.c | 4 ++--
fs/fuse/file.c | 11 ++++++-----
fs/fuse/fuse_i.h | 15 +++++++++------
fs/fuse/inode.c | 2 +-
fs/fuse/iomode.c | 31 ++++++++++++++++++-------------
6 files changed, 37 insertions(+), 28 deletions(-)
diff --git a/fs/fuse/cuse.c b/fs/fuse/cuse.c
index bac327cfc7f1..9c4d7a1993e8 100644
--- a/fs/fuse/cuse.c
+++ b/fs/fuse/cuse.c
@@ -148,7 +148,7 @@ static int cuse_release(struct inode *inode, struct file *file)
struct fuse_file *ff = file->private_data;
struct fuse_mount *fm = ff->fm;
- fuse_sync_release(NULL, ff, file->f_flags);
+ fuse_sync_release(NULL, ff, file->f_flags, false);
fuse_conn_put(fm->fc);
return 0;
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index be41c14ef329..937165788e79 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -893,7 +893,7 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
&outentry.attr, ATTR_TIMEOUT(&outentry), 0, 0);
if (!inode) {
flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
- fuse_sync_release(NULL, ff, flags);
+ fuse_sync_release(NULL, ff, flags, false);
fuse_chan_queue_forget(fm->fc->chan, forget, outentry.nodeid, 1);
err = -ENOMEM;
goto out_err;
@@ -910,7 +910,7 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
}
if (err) {
fi = get_fuse_inode(inode);
- fuse_sync_release(fi, ff, flags);
+ fuse_sync_release(fi, ff, flags, false);
} else {
if (fm->fc->atomic_o_trunc && trunc)
truncate_pagecache(inode, 0);
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 06dc8cfe3c7b..332032cd5622 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -285,7 +285,7 @@ static int fuse_open(struct inode *inode, struct file *file)
ff = file->private_data;
err = fuse_finish_open(inode, file);
if (err)
- fuse_sync_release(fi, ff, file->f_flags);
+ fuse_sync_release(fi, ff, file->f_flags, false);
else if (is_truncate)
fuse_truncate_update_attr(inode, file);
}
@@ -408,10 +408,12 @@ static int fuse_release(struct inode *inode, struct file *file)
}
void fuse_sync_release(struct fuse_inode *fi, struct fuse_file *ff,
- unsigned int flags)
+ unsigned int flags, bool isdir)
{
+ int opcode = isdir ? FUSE_RELEASEDIR : FUSE_RELEASE;
+
WARN_ON(refcount_read(&ff->count) > 1);
- fuse_prepare_release(fi, ff, flags, FUSE_RELEASE, true);
+ fuse_prepare_release(fi, ff, flags, opcode, true);
fuse_file_put(ff, true);
}
EXPORT_SYMBOL_GPL(fuse_sync_release);
@@ -1454,13 +1456,12 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
static void fuse_dio_unlock(struct kiocb *iocb, bool exclusive)
{
struct inode *inode = file_inode(iocb->ki_filp);
- struct fuse_inode *fi = get_fuse_inode(inode);
if (exclusive) {
inode_unlock(inode);
} else {
/* Allow opens in caching mode after last parallel dio end */
- fuse_inode_uncached_io_end(fi);
+ fuse_inode_uncached_io_end(inode);
inode_unlock_shared(inode);
}
}
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 8e54dc7d56af..2436219338b3 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -160,9 +160,6 @@ struct fuse_inode {
*/
int writectr;
- /** @iocachectr: Number of files/maps using page cache */
- int iocachectr;
-
/** @page_waitq: Waitq for writepage completion */
wait_queue_head_t page_waitq;
@@ -216,6 +213,12 @@ struct fuse_inode {
/** @lock: Lock to protect write-related fields */
spinlock_t lock;
+ /**
+ * @iocachectr: Number of files/maps using page cache (negative for
+ * passthrough)
+ */
+ int iocachectr;
+
#ifdef CONFIG_FUSE_DAX
/**
* @dax: Dax specific inode data
@@ -248,7 +251,7 @@ enum {
FUSE_I_BAD,
/* Has btime */
FUSE_I_BTIME,
- /* Wants or already has page cache IO */
+ /* Regular file wants or already has page cache IO */
FUSE_I_CACHE_IO_MODE,
/*
* Client has exclusive access to the inode, either because fs is local
@@ -946,7 +949,7 @@ void fuse_file_free(struct fuse_file *ff);
int fuse_finish_open(struct inode *inode, struct file *file);
void fuse_sync_release(struct fuse_inode *fi, struct fuse_file *ff,
- unsigned int flags);
+ unsigned int flags, bool isdir);
/*
* Send RELEASE or RELEASEDIR request
@@ -1244,7 +1247,7 @@ int fuse_fileattr_set(struct mnt_idmap *idmap,
/* iomode.c */
int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff);
int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb);
-void fuse_inode_uncached_io_end(struct fuse_inode *fi);
+void fuse_inode_uncached_io_end(struct inode *inode);
int fuse_file_io_open(struct file *file, struct inode *inode);
void fuse_file_io_release(struct fuse_file *ff, struct inode *inode);
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index b2a5892a4dc3..cef8b853b3d8 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -180,10 +180,10 @@ static void fuse_evict_inode(struct inode *inode)
atomic64_inc(&fc->evict_ctr);
}
if (S_ISREG(inode->i_mode) && !fuse_is_bad(inode)) {
- WARN_ON(fi->iocachectr != 0);
WARN_ON(!list_empty(&fi->write_files));
WARN_ON(!list_empty(&fi->queued_writes));
}
+ WARN_ON(fi->iocachectr != 0);
}
static int fuse_reconfigure(struct fs_context *fsc)
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index 5517711a3eca..e2e7fbaac9af 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -15,9 +15,12 @@
/*
* Return true if need to wait for new opens in caching mode.
*/
-static inline bool fuse_is_io_cache_wait(struct fuse_inode *fi)
+static inline bool fuse_is_io_cache_wait(struct inode *inode)
{
- return READ_ONCE(fi->iocachectr) < 0 && !fuse_inode_backing(fi);
+ struct fuse_inode *fi = get_fuse_inode(inode);
+
+ return S_ISREG(inode->i_mode) &&
+ READ_ONCE(fi->iocachectr) < 0 && !fuse_inode_backing(fi);
}
/*
@@ -40,10 +43,10 @@ int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff)
* Setting the bit advises new direct-io writes to use an exclusive
* lock - without it the wait below might be forever.
*/
- while (fuse_is_io_cache_wait(fi)) {
+ while (fuse_is_io_cache_wait(inode)) {
set_bit(FUSE_I_CACHE_IO_MODE, &fi->state);
spin_unlock(&fi->lock);
- wait_event(fi->direct_io_waitq, !fuse_is_io_cache_wait(fi));
+ wait_event(fi->direct_io_waitq, !fuse_is_io_cache_wait(inode));
spin_lock(&fi->lock);
}
@@ -69,8 +72,10 @@ int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff)
}
static void fuse_file_cached_io_release(struct fuse_file *ff,
- struct fuse_inode *fi)
+ struct inode *inode)
{
+ struct fuse_inode *fi = get_fuse_inode(inode);
+
spin_lock(&fi->lock);
WARN_ON(fi->iocachectr <= 0);
WARN_ON(ff->iomode != IOM_CACHED);
@@ -141,15 +146,17 @@ static int fuse_file_uncached_io_open(struct inode *inode,
return 0;
}
-void fuse_inode_uncached_io_end(struct fuse_inode *fi)
+void fuse_inode_uncached_io_end(struct inode *inode)
{
+ struct fuse_inode *fi = get_fuse_inode(inode);
struct fuse_backing *oldfb = NULL;
spin_lock(&fi->lock);
WARN_ON(fi->iocachectr >= 0);
fi->iocachectr++;
if (!fi->iocachectr) {
- wake_up(&fi->direct_io_waitq);
+ if (S_ISREG(inode->i_mode))
+ wake_up(&fi->direct_io_waitq);
oldfb = fuse_inode_backing_set(fi, NULL);
}
spin_unlock(&fi->lock);
@@ -159,11 +166,11 @@ void fuse_inode_uncached_io_end(struct fuse_inode *fi)
/* Drop uncached_io reference from passthrough open */
static void fuse_file_uncached_io_release(struct fuse_file *ff,
- struct fuse_inode *fi)
+ struct inode *inode)
{
WARN_ON(ff->iomode != IOM_UNCACHED);
ff->iomode = IOM_NONE;
- fuse_inode_uncached_io_end(fi);
+ fuse_inode_uncached_io_end(inode);
}
/*
@@ -278,8 +285,6 @@ int fuse_file_io_open(struct file *file, struct inode *inode)
/* No more pending io and no new io possible to inode via open/mmapped file */
void fuse_file_io_release(struct fuse_file *ff, struct inode *inode)
{
- struct fuse_inode *fi = get_fuse_inode(inode);
-
/*
* Last passthrough file close allows caching inode io mode.
* Last caching file close exits caching inode io mode.
@@ -289,10 +294,10 @@ void fuse_file_io_release(struct fuse_file *ff, struct inode *inode)
/* Nothing to do */
break;
case IOM_UNCACHED:
- fuse_file_uncached_io_release(ff, fi);
+ fuse_file_uncached_io_release(ff, inode);
break;
case IOM_CACHED:
- fuse_file_cached_io_release(ff, fi);
+ fuse_file_cached_io_release(ff, inode);
break;
}
}
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* [PATCH v2 04/21] fuse: implement passthrough for readdir
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (2 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 03/21] fuse: prepare for readdir passthrough on directories Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 05/21] fuse: prepare for long lived reference on backing file Joanne Koong
` (16 subsequent siblings)
20 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
Requires both requesting the passthrough inode op READDIR when setting
up the backing file and opening the dir with FOPEN_PASSTHROUGH.
The dir opened with FOPEN_PASSTHROUGH must not request cached readdir
with FOPEN_CACHE_DIR flag.
Like regular files, a directory inode cannot be opened at the same time
for cached readdir and readdir passthrough.
A directory opened without both FOPEN_CACHE_DIR and FOPEN_PASSTHROUGH
is marked as FOPEN_DIRECT_IO and does not affect io mode.
Note that opt-in for passthrough of READDIR operation means that there
is no READDIRPLUS call to server.
For the ls -l use case, the cost of FUSE_LOOKUP to server will be
paid for every entry, so passthrough of READDIR is not always a
performance win.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/backing.c | 5 +++++
fs/fuse/dir.c | 7 +++++++
fs/fuse/file.c | 15 +++++++++++++--
fs/fuse/fuse_i.h | 7 ++++++-
fs/fuse/iomode.c | 9 ++++++++-
fs/fuse/passthrough.c | 24 ++++++++++++++++++++++++
fs/fuse/readdir.c | 3 +++
include/uapi/linux/fuse.h | 1 +
8 files changed, 67 insertions(+), 4 deletions(-)
diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index 1fbb4d876eec..52fccb9ff283 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -114,6 +114,11 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
!d_is_reg(file->f_path.dentry))
goto out_fput;
+ res = -ENOTDIR;
+ if (map->ops_mask & FUSE_PASSTHROUGH_DIR_OPS &&
+ !d_is_dir(file->f_path.dentry))
+ goto out_fput;
+
backing_sb = file_inode(file)->i_sb;
res = -ELOOP;
if (backing_sb->s_stack_depth >= fc->max_stack_depth)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 937165788e79..05f69b8fa4c4 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -1891,6 +1891,7 @@ static const char *fuse_get_link(struct dentry *dentry, struct inode *inode,
static int fuse_dir_open(struct inode *inode, struct file *file)
{
struct fuse_mount *fm = get_fuse_mount(inode);
+ struct fuse_inode *fi = get_fuse_inode(inode);
int err;
if (fuse_is_bad(inode))
@@ -1904,6 +1905,12 @@ static int fuse_dir_open(struct inode *inode, struct file *file)
if (!err) {
struct fuse_file *ff = file->private_data;
+ err = fuse_file_io_open(file, inode);
+ if (err) {
+ fuse_sync_release(fi, ff, file->f_flags, true);
+ return err;
+ }
+
/*
* Keep handling FOPEN_STREAM and FOPEN_NONSEEKABLE for
* directories for backward compatibility, though it's unlikely
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 332032cd5622..828e3fc0ec31 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -180,8 +180,19 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
}
}
- if (isdir)
- ff->open_flags &= ~FOPEN_DIRECT_IO;
+ /*
+ * Mark an uncached dir open as FOPEN_DIRECT_IO for fuse_file_io_open().
+ * The explicit combination of FOPEN_PASSTHROUGH | FOPEN_DIRECT_IO is
+ * allowed and it means (similar to regular files) a request from the
+ * server for uncached readdir open for an inode that already has files
+ * open in passthrough readdir mode.
+ */
+ if (isdir) {
+ if (ff->open_flags & FOPEN_CACHE_DIR)
+ ff->open_flags &= ~FOPEN_DIRECT_IO;
+ else if (!(ff->open_flags & FOPEN_PASSTHROUGH))
+ ff->open_flags |= FOPEN_DIRECT_IO;
+ }
ff->nodeid = nodeid;
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 2436219338b3..23d2510f8e93 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1264,9 +1264,13 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff,
#define FUSE_PASSTHROUGH_RW_OPS \
(FUSE_PASSTHROUGH_OP_READ | FUSE_PASSTHROUGH_OP_WRITE)
+/* Passthrough operations for directories */
+#define FUSE_PASSTHROUGH_DIR_OPS \
+ (FUSE_PASSTHROUGH_OP_READDIR)
+
/* File passthrough operations require a file opened with FOPEN_PASSTHROUGH */
#define FUSE_PASSTHROUGH_FILE_OPS \
- (FUSE_PASSTHROUGH_RW_OPS)
+ (FUSE_PASSTHROUGH_RW_OPS | FUSE_PASSTHROUGH_OP_READDIR)
/* Inode passthrough operations for backing file attached to inode */
#define FUSE_PASSTHROUGH_INODE_OPS (0)
@@ -1343,6 +1347,7 @@ ssize_t fuse_passthrough_splice_write(struct pipe_inode_info *pipe,
struct file *out, loff_t *ppos,
size_t len, unsigned int flags);
ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma);
+int fuse_passthrough_readdir(struct file *file, struct dir_context *ctx);
static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
{
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index e2e7fbaac9af..c83a070cd834 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -201,15 +201,20 @@ static int fuse_file_passthrough_open(struct inode *inode, struct file *file)
if (IS_ERR(fb))
return PTR_ERR(fb);
+ err = -EOPNOTSUPP;
/*
* Inode ops passthrough requires backing file setup at
* creation/lookup time.
*/
- err = -EOPNOTSUPP;
if (!fuse_inode_backing(fi) &&
(fb->ops_mask & FUSE_PASSTHROUGH_INODE_OPS))
goto fail;
+ /* Readdir passthrough requires opt-in on backing file setup */
+ if (S_ISDIR(inode->i_mode) &&
+ !(fb->ops_mask & FUSE_PASSTHROUGH_OP_READDIR))
+ goto fail;
+
/* First passthrough file open denies caching inode io mode */
err = fuse_file_uncached_io_open(inode, ff, fb);
if (!err)
@@ -253,6 +258,8 @@ int fuse_file_io_open(struct file *file, struct inode *inode)
/*
* First passthrough file open denies caching inode io mode.
* First caching file open enters caching inode io mode.
+ * A directory opened without FOPEN_CACHE_DIR is marked with
+ * FOPEN_DIRECT_IO and like regular file dio, does not affect io mode.
*
* Note that if user opens a file open with O_DIRECT, but server did
* not specify FOPEN_DIRECT_IO, a later fcntl() could remove O_DIRECT,
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index 72de97c03d0e..a1d87ed51a94 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -144,6 +144,30 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
return backing_file_mmap(backing_file, vma, &ctx);
}
+int fuse_passthrough_readdir(struct file *file, struct dir_context *ctx)
+{
+ int ret;
+ const struct cred *old_cred;
+ struct inode *inode = file_inode(file);
+ struct fuse_file *ff = file->private_data;
+ struct file *backing_file = fuse_file_passthrough(ff);
+ bool locked;
+
+ pr_debug("%s: backing_file=0x%p, pos=%lld\n", __func__,
+ backing_file, ctx->pos);
+
+ old_cred = override_creds(ff->cred);
+ locked = fuse_lock_inode(inode);
+ /* Respect seekdir() on fuse dir */
+ vfs_llseek(backing_file, ctx->pos, SEEK_SET);
+ ret = iterate_dir(backing_file, ctx);
+ fuse_invalidate_atime(inode);
+ fuse_unlock_inode(inode, locked);
+ revert_creds(old_cred);
+
+ return ret;
+}
+
/*
* Setup passthrough to a backing file.
*
diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
index c88194e52d18..49226f022339 100644
--- a/fs/fuse/readdir.c
+++ b/fs/fuse/readdir.c
@@ -593,6 +593,9 @@ int fuse_readdir(struct file *file, struct dir_context *ctx)
if (fuse_is_bad(inode))
return -EIO;
+ if (fuse_file_passthrough(ff))
+ return fuse_passthrough_readdir(file, ctx);
+
err = UNCACHED;
if (ff->open_flags & FOPEN_CACHE_DIR)
err = fuse_readdir_cached(file, ctx);
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 0f1e1c1ec367..df366e390f0c 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -1142,6 +1142,7 @@ struct fuse_backing_map {
/* op bits for fuse_backing_map ops_mask */
#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
+#define FUSE_PASSTHROUGH_OP_READDIR FUSE_PASSTHROUGH_OP(FUSE_READDIR)
/* Device ioctls: */
#define FUSE_DEV_IOC_MAGIC 229
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* [PATCH v2 05/21] fuse: prepare for long lived reference on backing file
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (3 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 04/21] fuse: implement passthrough for readdir Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 06/21] fuse: implement passthrough for getattr/statx Joanne Koong
` (15 subsequent siblings)
20 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
Currently backing file is attached to fuse inode on the first
passthrough open of the inode and detached on last passthrough close.
In preparation for attaching a backing file to inode with no open file,
allow attaching a single long lived reference on fuse inode backing file
that will be used for passthrough of inode operations and detached on
fuse inode evict.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/file.c | 3 ++-
fs/fuse/fuse_i.h | 5 +++--
fs/fuse/inode.c | 5 +++++
fs/fuse/iomode.c | 16 ++++++++++++----
4 files changed, 22 insertions(+), 7 deletions(-)
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 828e3fc0ec31..1173811b2ea7 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1442,6 +1442,7 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
bool *exclusive)
{
struct inode *inode = file_inode(iocb->ki_filp);
+ struct fuse_file *ff = iocb->ki_filp->private_data;
*exclusive = fuse_dio_wr_exclusive_lock(iocb, from);
if (*exclusive) {
@@ -1456,7 +1457,7 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
* have raced, so check it again.
*/
if (fuse_io_past_eof(iocb, from) ||
- fuse_inode_uncached_io_start(inode, NULL) != 0) {
+ fuse_inode_uncached_io_start(inode, ff, NULL) != 0) {
inode_unlock_shared(inode);
inode_lock(inode);
*exclusive = true;
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 23d2510f8e93..502b3b3f7e89 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -258,7 +258,7 @@ enum {
* or the fuse server has an exclusive "lease" on distributed fs
*/
FUSE_I_EXCLUSIVE,
- /* Has backing file for inode ops passthrough */
+ /* Has long lived backing file for inode ops passthrough */
FUSE_I_PASSTHROUGH,
};
@@ -1246,7 +1246,8 @@ int fuse_fileattr_set(struct mnt_idmap *idmap,
/* iomode.c */
int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff);
-int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb);
+int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_file *ff,
+ struct fuse_backing *fb);
void fuse_inode_uncached_io_end(struct inode *inode);
int fuse_file_io_open(struct file *file, struct inode *inode);
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index cef8b853b3d8..9e9c20e846e7 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -183,6 +183,11 @@ static void fuse_evict_inode(struct inode *inode)
WARN_ON(!list_empty(&fi->write_files));
WARN_ON(!list_empty(&fi->queued_writes));
}
+ /* fuse inode may have a long lived reference to backing file */
+ if (fuse_inode_backing(fi)) {
+ WARN_ON(!test_bit(FUSE_I_PASSTHROUGH, &fi->state));
+ fuse_inode_uncached_io_end(inode);
+ }
WARN_ON(fi->iocachectr != 0);
}
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index c83a070cd834..6e2ddfe384b3 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -87,7 +87,8 @@ static void fuse_file_cached_io_release(struct fuse_file *ff,
}
/* Start strictly uncached io mode where cache access is not allowed */
-int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb)
+int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_file *ff,
+ struct fuse_backing *fb)
{
struct fuse_inode *fi = get_fuse_inode(inode);
struct fuse_conn *fc = get_fuse_conn(inode);
@@ -116,12 +117,19 @@ int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb)
err = -ETXTBSY;
goto unlock;
}
- fi->iocachectr--;
+ /* every open file holds a single refcount of backing file... */
+ if (ff)
+ fi->iocachectr--;
- /* fuse inode holds a single refcount of backing file */
if (fb && !oldfb) {
oldfb = fuse_inode_backing_set(fi, fb);
WARN_ON_ONCE(oldfb != NULL);
+ /* ...and an optional extra refcount for inode ops */
+ if ((fb->ops_mask & FUSE_PASSTHROUGH_INODE_OPS)) {
+ WARN_ON_ONCE(test_bit(FUSE_I_PASSTHROUGH, &fi->state));
+ set_bit(FUSE_I_PASSTHROUGH, &fi->state);
+ fi->iocachectr--;
+ }
} else {
fuse_backing_put(fb);
}
@@ -137,7 +145,7 @@ static int fuse_file_uncached_io_open(struct inode *inode,
{
int err;
- err = fuse_inode_uncached_io_start(inode, fb);
+ err = fuse_inode_uncached_io_start(inode, ff, fb);
if (err)
return err;
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* [PATCH v2 06/21] fuse: implement passthrough for getattr/statx
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (4 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 05/21] fuse: prepare for long lived reference on backing file Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 12:42 ` Amir Goldstein
2026-05-16 0:39 ` [PATCH v2 07/21] fuse: prepare to setup backing inode passthrough on lookup Joanne Koong
` (14 subsequent siblings)
20 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
Call vfs_getattr() on backing inode to respond to a user statx(2)
request and update the fuse inode attributes with the response.
For now, we assume that calling vfs_getattr() on backing inode is cheap,
so we never use cached attributed and always update fuse inode
attributes from backing attributes in fuse_permission() and
fuse_update_attributes().
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/dir.c | 32 ++++++++++++++++++++++++++++++++
fs/fuse/fuse_i.h | 8 +++++++-
fs/fuse/passthrough.c | 36 ++++++++++++++++++++++++++++++++++++
include/uapi/linux/fuse.h | 1 +
4 files changed, 76 insertions(+), 1 deletion(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 05f69b8fa4c4..9a8e525f4d2b 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -1414,6 +1414,28 @@ static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr)
attr->blksize = sx->blksize;
}
+void fuse_kstat_to_attr(struct fuse_conn *fc, const struct kstat *stat,
+ struct fuse_attr *attr)
+{
+ memset(attr, 0, sizeof(*attr));
+
+ attr->ino = stat->ino;
+ attr->size = stat->size;
+ attr->blocks = stat->blocks;
+ attr->atime = stat->atime.tv_sec;
+ attr->mtime = stat->mtime.tv_sec;
+ attr->ctime = stat->ctime.tv_sec;
+ attr->atimensec = stat->atime.tv_nsec;
+ attr->mtimensec = stat->mtime.tv_nsec;
+ attr->ctimensec = stat->ctime.tv_nsec;
+ attr->mode = stat->mode;
+ attr->nlink = stat->nlink;
+ attr->uid = from_kuid(fc->user_ns, stat->uid);
+ attr->gid = from_kgid(fc->user_ns, stat->gid);
+ attr->rdev = new_encode_dev(MKDEV(MAJOR(stat->rdev), MINOR(stat->rdev)));
+ attr->blksize = stat->blksize;
+}
+
static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode,
struct file *file, struct kstat *stat)
{
@@ -1539,6 +1561,12 @@ static int fuse_update_get_attr(struct mnt_idmap *idmap, struct inode *inode,
if (fc->no_statx)
request_mask &= STATX_BASIC_STATS;
+ if (fuse_passthrough_op(inode, FUSE_GETATTR)) {
+ forget_all_cached_acls(inode);
+ return fuse_passthrough_getattr(inode, stat, request_mask,
+ flags);
+ }
+
if (!request_mask)
sync = false;
else if (flags & AT_STATX_FORCE_SYNC)
@@ -1737,6 +1765,10 @@ static int fuse_perm_getattr(struct inode *inode, int mask)
return -ECHILD;
forget_all_cached_acls(inode);
+ if (fuse_passthrough_op(inode, FUSE_GETATTR))
+ return fuse_passthrough_getattr(inode, NULL,
+ STATX_BASIC_STATS, 0);
+
return fuse_do_getattr(&nop_mnt_idmap, inode, NULL, NULL);
}
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 502b3b3f7e89..389e28497dc7 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -991,6 +991,8 @@ void fuse_init_symlink(struct inode *inode);
/*
* Change attributes of an inode
*/
+void fuse_kstat_to_attr(struct fuse_conn *fc, const struct kstat *stat,
+ struct fuse_attr *attr);
void fuse_change_attributes(struct inode *inode, struct fuse_attr *attr,
struct fuse_statx *sx,
u64 attr_valid, u64 attr_version);
@@ -1274,7 +1276,8 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff,
(FUSE_PASSTHROUGH_RW_OPS | FUSE_PASSTHROUGH_OP_READDIR)
/* Inode passthrough operations for backing file attached to inode */
-#define FUSE_PASSTHROUGH_INODE_OPS (0)
+#define FUSE_PASSTHROUGH_INODE_OPS \
+ (FUSE_PASSTHROUGH_OP_GETATTR)
#define FUSE_BACKING_MAP_OP(map, op) \
((map)->ops_mask & FUSE_PASSTHROUGH_OP(op))
@@ -1358,6 +1361,9 @@ static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
return fb && fb->ops_mask & FUSE_PASSTHROUGH_OP(op);
}
+int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
+ u32 request_mask, unsigned int flags);
+
#ifdef CONFIG_SYSCTL
extern int fuse_sysctl_register(void);
extern void fuse_sysctl_unregister(void);
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index a1d87ed51a94..7de038960b2f 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -219,3 +219,39 @@ void fuse_passthrough_release(struct fuse_file *ff, struct fuse_backing *fb)
put_cred(ff->cred);
ff->cred = NULL;
}
+
+/*
+ * Inode passthrough operations for backing file attached on lookup.
+ */
+int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
+ u32 request_mask, unsigned int flags)
+{
+ struct fuse_conn *fc = get_fuse_conn(inode);
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_backing *fb = fuse_inode_backing(fi);
+ u64 attr_version = fuse_get_attr_version(fc);
+ const struct path *fb_path = &fb->file->f_path;
+ const struct cred *old_cred;
+ struct kstat backing_stat;
+ struct fuse_attr attr;
+ int err;
+
+ if (!stat)
+ stat = &backing_stat;
+
+ old_cred = override_creds(fb->cred);
+ err = vfs_getattr(fb_path, stat, request_mask, flags);
+ revert_creds(old_cred);
+ if (err)
+ return err;
+
+ /* Always override st_dev and st_ino with FUSE dev and ino */
+ stat->dev = inode->i_sb->s_dev;
+ stat->ino = inode->i_ino;
+
+ /* Fill fuse inode attrs from backing inode stat */
+ fuse_kstat_to_attr(fc, stat, &attr);
+ fuse_change_attributes(inode, &attr, NULL, 0, attr_version);
+
+ return 0;
+}
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index df366e390f0c..6404ed95c758 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -1143,6 +1143,7 @@ struct fuse_backing_map {
#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
#define FUSE_PASSTHROUGH_OP_READDIR FUSE_PASSTHROUGH_OP(FUSE_READDIR)
+#define FUSE_PASSTHROUGH_OP_GETATTR FUSE_PASSTHROUGH_OP(FUSE_GETATTR)
/* Device ioctls: */
#define FUSE_DEV_IOC_MAGIC 229
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 06/21] fuse: implement passthrough for getattr/statx
2026-05-16 0:39 ` [PATCH v2 06/21] fuse: implement passthrough for getattr/statx Joanne Koong
@ 2026-05-16 12:42 ` Amir Goldstein
0 siblings, 0 replies; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 12:42 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 2:52 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> From: Amir Goldstein <amir73il@gmail.com>
>
> Call vfs_getattr() on backing inode to respond to a user statx(2)
> request and update the fuse inode attributes with the response.
>
> For now, we assume that calling vfs_getattr() on backing inode is cheap,
> so we never use cached attributed and always update fuse inode
> attributes from backing attributes in fuse_permission() and
> fuse_update_attributes().
>
> Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
> Signed-off-by: Amir Goldstein <amir73il@gmail.com>
I think you've corrected enough of my flop in this patch to justify
Signed-of-by, but as you wish :)
Thanks,
Amir.
> ---
> fs/fuse/dir.c | 32 ++++++++++++++++++++++++++++++++
> fs/fuse/fuse_i.h | 8 +++++++-
> fs/fuse/passthrough.c | 36 ++++++++++++++++++++++++++++++++++++
> include/uapi/linux/fuse.h | 1 +
> 4 files changed, 76 insertions(+), 1 deletion(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index 05f69b8fa4c4..9a8e525f4d2b 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -1414,6 +1414,28 @@ static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr)
> attr->blksize = sx->blksize;
> }
>
> +void fuse_kstat_to_attr(struct fuse_conn *fc, const struct kstat *stat,
> + struct fuse_attr *attr)
> +{
> + memset(attr, 0, sizeof(*attr));
> +
> + attr->ino = stat->ino;
> + attr->size = stat->size;
> + attr->blocks = stat->blocks;
> + attr->atime = stat->atime.tv_sec;
> + attr->mtime = stat->mtime.tv_sec;
> + attr->ctime = stat->ctime.tv_sec;
> + attr->atimensec = stat->atime.tv_nsec;
> + attr->mtimensec = stat->mtime.tv_nsec;
> + attr->ctimensec = stat->ctime.tv_nsec;
> + attr->mode = stat->mode;
> + attr->nlink = stat->nlink;
> + attr->uid = from_kuid(fc->user_ns, stat->uid);
> + attr->gid = from_kgid(fc->user_ns, stat->gid);
> + attr->rdev = new_encode_dev(MKDEV(MAJOR(stat->rdev), MINOR(stat->rdev)));
> + attr->blksize = stat->blksize;
> +}
> +
> static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode,
> struct file *file, struct kstat *stat)
> {
> @@ -1539,6 +1561,12 @@ static int fuse_update_get_attr(struct mnt_idmap *idmap, struct inode *inode,
> if (fc->no_statx)
> request_mask &= STATX_BASIC_STATS;
>
> + if (fuse_passthrough_op(inode, FUSE_GETATTR)) {
> + forget_all_cached_acls(inode);
> + return fuse_passthrough_getattr(inode, stat, request_mask,
> + flags);
> + }
> +
> if (!request_mask)
> sync = false;
> else if (flags & AT_STATX_FORCE_SYNC)
> @@ -1737,6 +1765,10 @@ static int fuse_perm_getattr(struct inode *inode, int mask)
> return -ECHILD;
>
> forget_all_cached_acls(inode);
> + if (fuse_passthrough_op(inode, FUSE_GETATTR))
> + return fuse_passthrough_getattr(inode, NULL,
> + STATX_BASIC_STATS, 0);
> +
> return fuse_do_getattr(&nop_mnt_idmap, inode, NULL, NULL);
> }
>
> diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> index 502b3b3f7e89..389e28497dc7 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -991,6 +991,8 @@ void fuse_init_symlink(struct inode *inode);
> /*
> * Change attributes of an inode
> */
> +void fuse_kstat_to_attr(struct fuse_conn *fc, const struct kstat *stat,
> + struct fuse_attr *attr);
> void fuse_change_attributes(struct inode *inode, struct fuse_attr *attr,
> struct fuse_statx *sx,
> u64 attr_valid, u64 attr_version);
> @@ -1274,7 +1276,8 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff,
> (FUSE_PASSTHROUGH_RW_OPS | FUSE_PASSTHROUGH_OP_READDIR)
>
> /* Inode passthrough operations for backing file attached to inode */
> -#define FUSE_PASSTHROUGH_INODE_OPS (0)
> +#define FUSE_PASSTHROUGH_INODE_OPS \
> + (FUSE_PASSTHROUGH_OP_GETATTR)
>
> #define FUSE_BACKING_MAP_OP(map, op) \
> ((map)->ops_mask & FUSE_PASSTHROUGH_OP(op))
> @@ -1358,6 +1361,9 @@ static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
> return fb && fb->ops_mask & FUSE_PASSTHROUGH_OP(op);
> }
>
> +int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
> + u32 request_mask, unsigned int flags);
> +
> #ifdef CONFIG_SYSCTL
> extern int fuse_sysctl_register(void);
> extern void fuse_sysctl_unregister(void);
> diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> index a1d87ed51a94..7de038960b2f 100644
> --- a/fs/fuse/passthrough.c
> +++ b/fs/fuse/passthrough.c
> @@ -219,3 +219,39 @@ void fuse_passthrough_release(struct fuse_file *ff, struct fuse_backing *fb)
> put_cred(ff->cred);
> ff->cred = NULL;
> }
> +
> +/*
> + * Inode passthrough operations for backing file attached on lookup.
> + */
> +int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
> + u32 request_mask, unsigned int flags)
> +{
> + struct fuse_conn *fc = get_fuse_conn(inode);
> + struct fuse_inode *fi = get_fuse_inode(inode);
> + struct fuse_backing *fb = fuse_inode_backing(fi);
> + u64 attr_version = fuse_get_attr_version(fc);
> + const struct path *fb_path = &fb->file->f_path;
> + const struct cred *old_cred;
> + struct kstat backing_stat;
> + struct fuse_attr attr;
> + int err;
> +
> + if (!stat)
> + stat = &backing_stat;
> +
> + old_cred = override_creds(fb->cred);
> + err = vfs_getattr(fb_path, stat, request_mask, flags);
> + revert_creds(old_cred);
> + if (err)
> + return err;
> +
> + /* Always override st_dev and st_ino with FUSE dev and ino */
> + stat->dev = inode->i_sb->s_dev;
> + stat->ino = inode->i_ino;
> +
> + /* Fill fuse inode attrs from backing inode stat */
> + fuse_kstat_to_attr(fc, stat, &attr);
> + fuse_change_attributes(inode, &attr, NULL, 0, attr_version);
> +
> + return 0;
> +}
> diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
> index df366e390f0c..6404ed95c758 100644
> --- a/include/uapi/linux/fuse.h
> +++ b/include/uapi/linux/fuse.h
> @@ -1143,6 +1143,7 @@ struct fuse_backing_map {
> #define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
> #define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
> #define FUSE_PASSTHROUGH_OP_READDIR FUSE_PASSTHROUGH_OP(FUSE_READDIR)
> +#define FUSE_PASSTHROUGH_OP_GETATTR FUSE_PASSTHROUGH_OP(FUSE_GETATTR)
>
> /* Device ioctls: */
> #define FUSE_DEV_IOC_MAGIC 229
> --
> 2.52.0
>
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 07/21] fuse: prepare to setup backing inode passthrough on lookup
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (5 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 06/21] fuse: implement passthrough for getattr/statx Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 08/21] fuse: handle zero ops_mask in FUSE_DEV_IOC_BACKING_OPEN Joanne Koong
` (13 subsequent siblings)
20 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
Add a helper for requesting to associate a backing inode for inode
passthrough operations.
This helper is expected to be used to setup backing inode on lookup
response.
The minimal requirement for the backing inode is to support the
GETATTR passthrough op.
Unlike setting of passthrough from open, this mapping is created for
the entire lifetime of the inode and cannot be changed later.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/backing.c | 32 ++++++++++++++++++++------------
fs/fuse/fuse_i.h | 6 ++++--
fs/fuse/iomode.c | 43 +++++++++++++++++++++++++++++++++++++++++++
fs/fuse/passthrough.c | 12 +++++-------
4 files changed, 72 insertions(+), 21 deletions(-)
diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index 52fccb9ff283..2234ab47406a 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -17,6 +17,26 @@ struct fuse_backing *fuse_backing_get(struct fuse_backing *fb)
return NULL;
}
+/*
+ * Get fuse backing object by backing id.
+ *
+ * Returns an fb object with elevated refcount to be stored in fuse inode.
+ */
+struct fuse_backing *fuse_backing_id_get(struct fuse_conn *fc, int backing_id)
+{
+ struct fuse_backing *fb;
+
+ if (backing_id <= 0)
+ return ERR_PTR(-EINVAL);
+
+ rcu_read_lock();
+ fb = idr_find(&fc->backing_files_map, backing_id);
+ fb = fuse_backing_get(fb);
+ rcu_read_unlock();
+
+ return fb;
+}
+
static void fuse_backing_free(struct fuse_backing *fb)
{
pr_debug("%s: fb=0x%p\n", __func__, fb);
@@ -178,15 +198,3 @@ int fuse_backing_close(struct fuse_conn *fc, int backing_id)
return err;
}
-
-struct fuse_backing *fuse_backing_lookup(struct fuse_conn *fc, int backing_id)
-{
- struct fuse_backing *fb;
-
- rcu_read_lock();
- fb = idr_find(&fc->backing_files_map, backing_id);
- fb = fuse_backing_get(fb);
- rcu_read_unlock();
-
- return fb;
-}
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 389e28497dc7..9e5142a94a09 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1289,7 +1289,7 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff,
#ifdef CONFIG_FUSE_PASSTHROUGH
struct fuse_backing *fuse_backing_get(struct fuse_backing *fb);
void fuse_backing_put(struct fuse_backing *fb);
-struct fuse_backing *fuse_backing_lookup(struct fuse_conn *fc, int backing_id);
+struct fuse_backing *fuse_backing_id_get(struct fuse_conn *fc, int backing_id);
#else
static inline struct fuse_backing *fuse_backing_get(struct fuse_backing *fb)
@@ -1300,7 +1300,7 @@ static inline struct fuse_backing *fuse_backing_get(struct fuse_backing *fb)
static inline void fuse_backing_put(struct fuse_backing *fb)
{
}
-static inline struct fuse_backing *fuse_backing_lookup(struct fuse_conn *fc,
+static inline struct fuse_backing *fuse_backing_id_get(struct fuse_conn *fc,
int backing_id)
{
return NULL;
@@ -1353,6 +1353,8 @@ ssize_t fuse_passthrough_splice_write(struct pipe_inode_info *pipe,
ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma);
int fuse_passthrough_readdir(struct file *file, struct dir_context *ctx);
+int fuse_inode_set_passthrough(struct inode *inode, int backing_id);
+
static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
{
struct fuse_inode *fi = get_fuse_inode(inode);
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index 6e2ddfe384b3..3483aaec698e 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -181,6 +181,49 @@ static void fuse_file_uncached_io_release(struct fuse_file *ff,
fuse_inode_uncached_io_end(inode);
}
+/* Setup passthrough for inode operations without an open file */
+int fuse_inode_set_passthrough(struct inode *inode, int backing_id)
+{
+ struct fuse_conn *fc = get_fuse_conn(inode);
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_backing *current_fb, *fb;
+ int err;
+
+ if (!IS_ENABLED(CONFIG_FUSE_PASSTHROUGH) || !fc->passthrough_ino)
+ return 0;
+
+ fb = fuse_backing_id_get(fc, backing_id);
+ if (IS_ERR_OR_NULL(fb)) {
+ err = fb ? PTR_ERR(fb) : -ENOENT;
+ fb = NULL;
+ goto fail;
+ }
+
+ current_fb = fuse_inode_backing(fi);
+ if (current_fb) {
+ /* Different backing file on same inode is not allowed */
+ err = current_fb == fb ? 0 : -EBUSY;
+ fuse_backing_put(fb);
+ return err;
+ }
+
+ /* Backing inode requires at least GETATTR op passthrough */
+ err = -EOPNOTSUPP;
+ if (!(fb->ops_mask & FUSE_PASSTHROUGH_OP_GETATTR))
+ goto fail;
+
+ err = fuse_inode_uncached_io_start(inode, NULL, fb);
+ if (err)
+ goto fail;
+
+ return 0;
+fail:
+ fuse_backing_put(fb);
+ pr_debug("failed to setup backing inode (ino=%lu, backing_id=%d, err=%i).\n",
+ inode->i_ino, backing_id, err);
+ return err;
+}
+
/*
* Open flags that are allowed in combination with FOPEN_PASSTHROUGH.
* A combination of FOPEN_PASSTHROUGH and FOPEN_DIRECT_IO means that read/write
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index 7de038960b2f..f2025772c9c1 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -181,14 +181,12 @@ struct fuse_backing *fuse_passthrough_open(struct file *file, int backing_id)
struct file *backing_file;
int err;
- err = -EINVAL;
- if (backing_id <= 0)
- goto out;
-
- err = -ENOENT;
- fb = fuse_backing_lookup(fc, backing_id);
- if (!fb)
+ fb = fuse_backing_id_get(fc, backing_id);
+ if (IS_ERR_OR_NULL(fb)) {
+ err = fb ? PTR_ERR(fb) : -ENOENT;
+ fb = NULL;
goto out;
+ }
/* Allocate backing file per fuse file to store fuse path */
backing_file = backing_file_open(&file->f_path, file->f_flags,
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* [PATCH v2 08/21] fuse: handle zero ops_mask in FUSE_DEV_IOC_BACKING_OPEN
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (6 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 07/21] fuse: prepare to setup backing inode passthrough on lookup Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 15:35 ` Amir Goldstein
2026-05-16 0:39 ` [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap Joanne Koong
` (12 subsequent siblings)
20 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Servers that pre-date the ops_mask field will pass a zero ops_mask in
FUSE_DEV_IOC_BACKING_OPEN. Default this to FUSE_PASSTHROUGH_RW_OPS to
maintain backwards compatibility.
For FUSE_PASSTHROUGH_INO servers, an ops_mask must be set.
FUSE_PASSTHROUGH_INO is a new feature with no backwards compatibliity
requirements.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/backing.c | 20 +++++++++++++++++---
1 file changed, 17 insertions(+), 3 deletions(-)
diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index 2234ab47406a..b499860e1185 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -119,9 +119,23 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
if (map->flags || map->ops_mask & ~FUSE_BACKING_MAP_VALID_OPS)
goto out;
- /* For now passthrough inode operations requires FUSE_PASSTHROUGH_INO */
- if (!fc->passthrough_ino && map->ops_mask & FUSE_PASSTHROUGH_INODE_OPS)
- goto out;
+ if (!fc->passthrough_ino) {
+ /*
+ * For now passthrough inode operations requires
+ * FUSE_PASSTHROUGH_INO
+ */
+ if (map->ops_mask & FUSE_PASSTHROUGH_INODE_OPS)
+ goto out;
+ /*
+ * To maintain backwards compatibility with servers that
+ * pre-date the ops_mask field, a zero ops_mask defaults
+ * to passing through both reads and writes
+ */
+ if (!map->ops_mask)
+ map->ops_mask |= FUSE_PASSTHROUGH_RW_OPS;
+ } else if (!map->ops_mask) {
+ return -EINVAL;
+ }
file = fget_raw(map->fd);
res = -EBADF;
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 08/21] fuse: handle zero ops_mask in FUSE_DEV_IOC_BACKING_OPEN
2026-05-16 0:39 ` [PATCH v2 08/21] fuse: handle zero ops_mask in FUSE_DEV_IOC_BACKING_OPEN Joanne Koong
@ 2026-05-16 15:35 ` Amir Goldstein
0 siblings, 0 replies; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 15:35 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 2:52 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Servers that pre-date the ops_mask field will pass a zero ops_mask in
> FUSE_DEV_IOC_BACKING_OPEN. Default this to FUSE_PASSTHROUGH_RW_OPS to
> maintain backwards compatibility.
>
> For FUSE_PASSTHROUGH_INO servers, an ops_mask must be set.
> FUSE_PASSTHROUGH_INO is a new feature with no backwards compatibliity
> requirements.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
> ---
> fs/fuse/backing.c | 20 +++++++++++++++++---
> 1 file changed, 17 insertions(+), 3 deletions(-)
>
> diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
> index 2234ab47406a..b499860e1185 100644
> --- a/fs/fuse/backing.c
> +++ b/fs/fuse/backing.c
> @@ -119,9 +119,23 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
> if (map->flags || map->ops_mask & ~FUSE_BACKING_MAP_VALID_OPS)
> goto out;
>
> - /* For now passthrough inode operations requires FUSE_PASSTHROUGH_INO */
> - if (!fc->passthrough_ino && map->ops_mask & FUSE_PASSTHROUGH_INODE_OPS)
> - goto out;
> + if (!fc->passthrough_ino) {
> + /*
> + * For now passthrough inode operations requires
> + * FUSE_PASSTHROUGH_INO
> + */
> + if (map->ops_mask & FUSE_PASSTHROUGH_INODE_OPS)
> + goto out;
> + /*
> + * To maintain backwards compatibility with servers that
> + * pre-date the ops_mask field, a zero ops_mask defaults
> + * to passing through both reads and writes
> + */
> + if (!map->ops_mask)
> + map->ops_mask |= FUSE_PASSTHROUGH_RW_OPS;
> + } else if (!map->ops_mask) {
> + return -EINVAL;
> + }
>
> file = fget_raw(map->fd);
> res = -EBADF;
> --
> 2.52.0
>
>
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (7 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 08/21] fuse: handle zero ops_mask in FUSE_DEV_IOC_BACKING_OPEN Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 16:57 ` Amir Goldstein
2026-05-16 0:39 ` [PATCH v2 10/21] fuse: prepare to cache statx attributes from entry replies Joanne Koong
` (11 subsequent siblings)
20 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Servers can now pass through only reads or writes instead of being
required to pass through both. When the read or write operation is not
passed through, fall back to direct io for handling it. This avoids
cache coherency issues from mixing the backing file's page cache with
the fuse inode's page cache.
Reject mmap when passthrough is partial or not set for read/writes since
mmap requires both read and write access to the backing page cache.
The FOPEN_DIRECT_IO check in the read/write fallback is not strictly
needed as the caller already handles it, but including it here makes
the fallback logic self-contained.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/file.c | 7 ++++---
fs/fuse/fuse_i.h | 3 +++
fs/fuse/passthrough.c | 18 ++++++++++++++++++
3 files changed, 25 insertions(+), 3 deletions(-)
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 1173811b2ea7..5b70d8272700 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1781,7 +1781,7 @@ static ssize_t __fuse_direct_read(struct fuse_io_priv *io,
static ssize_t fuse_direct_IO(struct kiocb *iocb, struct iov_iter *iter);
-static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
+ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
ssize_t res;
@@ -1796,7 +1796,7 @@ static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
return res;
}
-static ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
+ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct inode *inode = file_inode(iocb->ki_filp);
ssize_t res;
@@ -1884,7 +1884,8 @@ static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out,
struct fuse_file *ff = out->private_data;
/* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
- if (fuse_file_passthrough(ff) && !(ff->open_flags & FOPEN_DIRECT_IO))
+ if (fuse_passthrough_op(file_inode(out), FUSE_WRITE) &&
+ !(ff->open_flags & FOPEN_DIRECT_IO))
return fuse_passthrough_splice_write(pipe, out, ppos, len, flags);
else
return iter_file_splice_write(pipe, out, ppos, len, flags);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 9e5142a94a09..a1034533ce60 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1261,6 +1261,9 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
void fuse_file_release(struct inode *inode, struct fuse_file *ff,
unsigned int open_flags, fl_owner_t id, bool isdir);
+ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to);
+ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from);
+
/* passthrough.c */
/* READ/WRITE are implied by FOPEN_PASSTHROUGH, but defined for completeness */
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index f2025772c9c1..d62a1c751157 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -44,6 +44,10 @@ ssize_t fuse_passthrough_read_iter(struct kiocb *iocb, struct iov_iter *iter)
if (!count)
return 0;
+ if ((ff->open_flags & FOPEN_DIRECT_IO) ||
+ !fuse_passthrough_op(file_inode(file), FUSE_READ))
+ return fuse_direct_read_iter(iocb, iter);
+
ret = backing_file_read_iter(backing_file, iter, iocb, iocb->ki_flags,
&ctx);
@@ -70,6 +74,10 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
if (!count)
return 0;
+ if ((ff->open_flags & FOPEN_DIRECT_IO) ||
+ !fuse_passthrough_op(inode, FUSE_WRITE))
+ return fuse_direct_write_iter(iocb, iter);
+
inode_lock(inode);
ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
&ctx);
@@ -91,6 +99,9 @@ ssize_t fuse_passthrough_splice_read(struct file *in, loff_t *ppos,
struct kiocb iocb;
ssize_t ret;
+ if (!fuse_passthrough_op(file_inode(in), FUSE_READ))
+ return copy_splice_read(in, ppos, pipe, len, flags);
+
pr_debug("%s: backing_file=0x%p, pos=%lld, len=%zu, flags=0x%x\n", __func__,
backing_file, *ppos, len, flags);
@@ -133,6 +144,9 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
{
struct fuse_file *ff = file->private_data;
struct file *backing_file = fuse_file_passthrough(ff);
+ struct inode *inode = file_inode(file);
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_backing *fb = fuse_inode_backing(fi);
struct backing_file_ctx ctx = {
.cred = ff->cred,
.accessed = fuse_file_accessed,
@@ -141,6 +155,10 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
pr_debug("%s: backing_file=0x%p, start=%lu, end=%lu\n", __func__,
backing_file, vma->vm_start, vma->vm_end);
+ /* mmap requires both read and write passthrough */
+ if ((fb->ops_mask & FUSE_PASSTHROUGH_RW_OPS) != FUSE_PASSTHROUGH_RW_OPS)
+ return -ENODEV;
+
return backing_file_mmap(backing_file, vma, &ctx);
}
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap
2026-05-16 0:39 ` [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap Joanne Koong
@ 2026-05-16 16:57 ` Amir Goldstein
2026-05-16 18:34 ` Amir Goldstein
2026-05-18 21:46 ` Joanne Koong
0 siblings, 2 replies; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 16:57 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 2:52 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Servers can now pass through only reads or writes instead of being
> required to pass through both. When the read or write operation is not
> passed through, fall back to direct io for handling it. This avoids
> cache coherency issues from mixing the backing file's page cache with
> the fuse inode's page cache.
>
> Reject mmap when passthrough is partial or not set for read/writes since
> mmap requires both read and write access to the backing page cache.
>
> The FOPEN_DIRECT_IO check in the read/write fallback is not strictly
> needed as the caller already handles it, but including it here makes
> the fallback logic self-contained.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
> fs/fuse/file.c | 7 ++++---
> fs/fuse/fuse_i.h | 3 +++
> fs/fuse/passthrough.c | 18 ++++++++++++++++++
> 3 files changed, 25 insertions(+), 3 deletions(-)
>
> diff --git a/fs/fuse/file.c b/fs/fuse/file.c
> index 1173811b2ea7..5b70d8272700 100644
> --- a/fs/fuse/file.c
> +++ b/fs/fuse/file.c
> @@ -1781,7 +1781,7 @@ static ssize_t __fuse_direct_read(struct fuse_io_priv *io,
>
> static ssize_t fuse_direct_IO(struct kiocb *iocb, struct iov_iter *iter);
>
> -static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
> +ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
> {
> ssize_t res;
>
> @@ -1796,7 +1796,7 @@ static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
> return res;
> }
>
> -static ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
> +ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
> {
> struct inode *inode = file_inode(iocb->ki_filp);
> ssize_t res;
> @@ -1884,7 +1884,8 @@ static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out,
> struct fuse_file *ff = out->private_data;
>
> /* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
> - if (fuse_file_passthrough(ff) && !(ff->open_flags & FOPEN_DIRECT_IO))
> + if (fuse_passthrough_op(file_inode(out), FUSE_WRITE) &&
Removing fuse_file_passthrough(ff) makes me uncomfortable.
It's true that if server sets PASSTHROUGH_OP_WRITE on lookup
the file has to be opened with FOPEN_PASSTHROUGH, but this
not easily understood by passing by reviewers.
It's be nicer if we keep this logic contained in a helper where it can be
properly documented, something like this (following the changes to
FUSE_PASSTHROUGH_OP() discussed earlier):
static inline u64 fuse_passthrough_op(struct inode *inode, u64 op_mask)
{
struct fuse_inode *fi = get_fuse_inode(inode);
struct fuse_backing *fb = fuse_inode_passthrough(fi);
return !fb ? 0 : fb->ops_mask & op_mask;
}
static inline struct file *fuse_file_passthrough_op(struct file *file,
u64 op_mask)
{
struct fuse_file *ff = file->private_data;
/*
* FOPEN_DIRECT_IO can be used to opt-out of passthrough per file and
* ops_mask can be used to opt-out of passthrough per operation type.
*/
return fuse_file_passthrough(ff) &&
!(ff->open_flags & FOPEN_DIRECT_IO) &&
fuse_passthrough_op(file_inode(file), op_mask) == op_mask;
}
...
static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out,
loff_t *ppos, size_t len, unsigned int flags)
{
if (fuse_file_passthrough_op(out, FUSE_PASSTHROUGH_OP_WRITE))
return fuse_passthrough_splice_write(pipe, out, ppos,
len, flags);
else
return iter_file_splice_write(pipe, out, ppos, len, flags);
}
> + !(ff->open_flags & FOPEN_DIRECT_IO))
> return fuse_passthrough_splice_write(pipe, out, ppos, len, flags);
> else
> return iter_file_splice_write(pipe, out, ppos, len, flags);
> diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> index 9e5142a94a09..a1034533ce60 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -1261,6 +1261,9 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
> void fuse_file_release(struct inode *inode, struct fuse_file *ff,
> unsigned int open_flags, fl_owner_t id, bool isdir);
>
> +ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to);
> +ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from);
> +
> /* passthrough.c */
>
> /* READ/WRITE are implied by FOPEN_PASSTHROUGH, but defined for completeness */
> diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> index f2025772c9c1..d62a1c751157 100644
> --- a/fs/fuse/passthrough.c
> +++ b/fs/fuse/passthrough.c
> @@ -44,6 +44,10 @@ ssize_t fuse_passthrough_read_iter(struct kiocb *iocb, struct iov_iter *iter)
> if (!count)
> return 0;
>
> + if ((ff->open_flags & FOPEN_DIRECT_IO) ||
> + !fuse_passthrough_op(file_inode(file), FUSE_READ))
> + return fuse_direct_read_iter(iocb, iter);
> +
> ret = backing_file_read_iter(backing_file, iter, iocb, iocb->ki_flags,
> &ctx);
>
> @@ -70,6 +74,10 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
> if (!count)
> return 0;
>
> + if ((ff->open_flags & FOPEN_DIRECT_IO) ||
> + !fuse_passthrough_op(inode, FUSE_WRITE))
> + return fuse_direct_write_iter(iocb, iter);
> +
> inode_lock(inode);
> ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
> &ctx);
> @@ -91,6 +99,9 @@ ssize_t fuse_passthrough_splice_read(struct file *in, loff_t *ppos,
> struct kiocb iocb;
> ssize_t ret;
>
> + if (!fuse_passthrough_op(file_inode(in), FUSE_READ))
> + return copy_splice_read(in, ppos, pipe, len, flags);
> +
> pr_debug("%s: backing_file=0x%p, pos=%lld, len=%zu, flags=0x%x\n", __func__,
> backing_file, *ppos, len, flags);
>
I understand why you brought this "jump back to direct" into the passthrough
helpers to reduce complexity in the call sites, but I think that if you use
the fuse_file_passthrough_op() helper in the call sites code will look
better and will not need to "jump back to direct io" in passthrough.c.
> @@ -133,6 +144,9 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
> {
> struct fuse_file *ff = file->private_data;
> struct file *backing_file = fuse_file_passthrough(ff);
> + struct inode *inode = file_inode(file);
> + struct fuse_inode *fi = get_fuse_inode(inode);
> + struct fuse_backing *fb = fuse_inode_backing(fi);
> struct backing_file_ctx ctx = {
> .cred = ff->cred,
> .accessed = fuse_file_accessed,
> @@ -141,6 +155,10 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
> pr_debug("%s: backing_file=0x%p, start=%lu, end=%lu\n", __func__,
> backing_file, vma->vm_start, vma->vm_end);
>
> + /* mmap requires both read and write passthrough */
> + if ((fb->ops_mask & FUSE_PASSTHROUGH_RW_OPS) != FUSE_PASSTHROUGH_RW_OPS)
> + return -ENODEV;
> +
Changing the helper in call site in nicer and already hits the
existing ENODEV case:
if (fuse_file_passthrough_op(file, FUSE_PASSTHROUGH_RW_OPS))
return fuse_passthrough_mmap(file, vma);
else if (fuse_inode_backing(get_fuse_inode(inode)))
return -ENODEV;
Thanks,
Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread* Re: [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap
2026-05-16 16:57 ` Amir Goldstein
@ 2026-05-16 18:34 ` Amir Goldstein
2026-05-18 21:46 ` Joanne Koong
1 sibling, 0 replies; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 18:34 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 6:57 PM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Sat, May 16, 2026 at 2:52 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > Servers can now pass through only reads or writes instead of being
> > required to pass through both. When the read or write operation is not
> > passed through, fall back to direct io for handling it. This avoids
> > cache coherency issues from mixing the backing file's page cache with
> > the fuse inode's page cache.
> >
> > Reject mmap when passthrough is partial or not set for read/writes since
> > mmap requires both read and write access to the backing page cache.
> >
> > The FOPEN_DIRECT_IO check in the read/write fallback is not strictly
> > needed as the caller already handles it, but including it here makes
> > the fallback logic self-contained.
> >
> > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > ---
> > fs/fuse/file.c | 7 ++++---
> > fs/fuse/fuse_i.h | 3 +++
> > fs/fuse/passthrough.c | 18 ++++++++++++++++++
> > 3 files changed, 25 insertions(+), 3 deletions(-)
> >
> > diff --git a/fs/fuse/file.c b/fs/fuse/file.c
> > index 1173811b2ea7..5b70d8272700 100644
> > --- a/fs/fuse/file.c
> > +++ b/fs/fuse/file.c
> > @@ -1781,7 +1781,7 @@ static ssize_t __fuse_direct_read(struct fuse_io_priv *io,
> >
> > static ssize_t fuse_direct_IO(struct kiocb *iocb, struct iov_iter *iter);
> >
> > -static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
> > +ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
> > {
> > ssize_t res;
> >
> > @@ -1796,7 +1796,7 @@ static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
> > return res;
> > }
> >
> > -static ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
> > +ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
> > {
> > struct inode *inode = file_inode(iocb->ki_filp);
> > ssize_t res;
> > @@ -1884,7 +1884,8 @@ static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out,
> > struct fuse_file *ff = out->private_data;
> >
> > /* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
> > - if (fuse_file_passthrough(ff) && !(ff->open_flags & FOPEN_DIRECT_IO))
> > + if (fuse_passthrough_op(file_inode(out), FUSE_WRITE) &&
>
> Removing fuse_file_passthrough(ff) makes me uncomfortable.
> It's true that if server sets PASSTHROUGH_OP_WRITE on lookup
> the file has to be opened with FOPEN_PASSTHROUGH, but this
> not easily understood by passing by reviewers.
>
> It's be nicer if we keep this logic contained in a helper where it can be
> properly documented, something like this (following the changes to
> FUSE_PASSTHROUGH_OP() discussed earlier):
>
> static inline u64 fuse_passthrough_op(struct inode *inode, u64 op_mask)
> {
> struct fuse_inode *fi = get_fuse_inode(inode);
> struct fuse_backing *fb = fuse_inode_passthrough(fi);
This needs to be:
struct fuse_backing *fb = fuse_inode_backing(fi);
so the helper will work correctly whether the backing inode was set on
lookup (long lived) or on open.
>
> return !fb ? 0 : fb->ops_mask & op_mask;
> }
>
> static inline struct file *fuse_file_passthrough_op(struct file *file,
> u64 op_mask)
> {
> struct fuse_file *ff = file->private_data;
>
> /*
> * FOPEN_DIRECT_IO can be used to opt-out of passthrough per file and
> * ops_mask can be used to opt-out of passthrough per operation type.
> */
> return fuse_file_passthrough(ff) &&
> !(ff->open_flags & FOPEN_DIRECT_IO) &&
> fuse_passthrough_op(file_inode(file), op_mask) == op_mask;
> }
>
> ...
>
> static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out,
> loff_t *ppos, size_t len, unsigned int flags)
> {
> if (fuse_file_passthrough_op(out, FUSE_PASSTHROUGH_OP_WRITE))
> return fuse_passthrough_splice_write(pipe, out, ppos,
> len, flags);
> else
> return iter_file_splice_write(pipe, out, ppos, len, flags);
> }
>
>
> > + !(ff->open_flags & FOPEN_DIRECT_IO))
> > return fuse_passthrough_splice_write(pipe, out, ppos, len, flags);
> > else
> > return iter_file_splice_write(pipe, out, ppos, len, flags);
> > diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> > index 9e5142a94a09..a1034533ce60 100644
> > --- a/fs/fuse/fuse_i.h
> > +++ b/fs/fuse/fuse_i.h
> > @@ -1261,6 +1261,9 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
> > void fuse_file_release(struct inode *inode, struct fuse_file *ff,
> > unsigned int open_flags, fl_owner_t id, bool isdir);
> >
> > +ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to);
> > +ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from);
> > +
> > /* passthrough.c */
> >
> > /* READ/WRITE are implied by FOPEN_PASSTHROUGH, but defined for completeness */
> > diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> > index f2025772c9c1..d62a1c751157 100644
> > --- a/fs/fuse/passthrough.c
> > +++ b/fs/fuse/passthrough.c
> > @@ -44,6 +44,10 @@ ssize_t fuse_passthrough_read_iter(struct kiocb *iocb, struct iov_iter *iter)
> > if (!count)
> > return 0;
> >
> > + if ((ff->open_flags & FOPEN_DIRECT_IO) ||
> > + !fuse_passthrough_op(file_inode(file), FUSE_READ))
> > + return fuse_direct_read_iter(iocb, iter);
> > +
> > ret = backing_file_read_iter(backing_file, iter, iocb, iocb->ki_flags,
> > &ctx);
> >
> > @@ -70,6 +74,10 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
> > if (!count)
> > return 0;
> >
> > + if ((ff->open_flags & FOPEN_DIRECT_IO) ||
> > + !fuse_passthrough_op(inode, FUSE_WRITE))
> > + return fuse_direct_write_iter(iocb, iter);
> > +
> > inode_lock(inode);
> > ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
> > &ctx);
> > @@ -91,6 +99,9 @@ ssize_t fuse_passthrough_splice_read(struct file *in, loff_t *ppos,
> > struct kiocb iocb;
> > ssize_t ret;
> >
> > + if (!fuse_passthrough_op(file_inode(in), FUSE_READ))
> > + return copy_splice_read(in, ppos, pipe, len, flags);
> > +
> > pr_debug("%s: backing_file=0x%p, pos=%lld, len=%zu, flags=0x%x\n", __func__,
> > backing_file, *ppos, len, flags);
> >
>
> I understand why you brought this "jump back to direct" into the passthrough
> helpers to reduce complexity in the call sites, but I think that if you use
> the fuse_file_passthrough_op() helper in the call sites code will look
> better and will not need to "jump back to direct io" in passthrough.c.
>
> > @@ -133,6 +144,9 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
> > {
> > struct fuse_file *ff = file->private_data;
> > struct file *backing_file = fuse_file_passthrough(ff);
> > + struct inode *inode = file_inode(file);
> > + struct fuse_inode *fi = get_fuse_inode(inode);
> > + struct fuse_backing *fb = fuse_inode_backing(fi);
> > struct backing_file_ctx ctx = {
> > .cred = ff->cred,
> > .accessed = fuse_file_accessed,
> > @@ -141,6 +155,10 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
> > pr_debug("%s: backing_file=0x%p, start=%lu, end=%lu\n", __func__,
> > backing_file, vma->vm_start, vma->vm_end);
> >
> > + /* mmap requires both read and write passthrough */
> > + if ((fb->ops_mask & FUSE_PASSTHROUGH_RW_OPS) != FUSE_PASSTHROUGH_RW_OPS)
> > + return -ENODEV;
> > +
>
> Changing the helper in call site in nicer and already hits the
> existing ENODEV case:
>
> if (fuse_file_passthrough_op(file, FUSE_PASSTHROUGH_RW_OPS))
> return fuse_passthrough_mmap(file, vma);
> else if (fuse_inode_backing(get_fuse_inode(inode)))
> return -ENODEV;
This could technically be relaxed to:
else if (fuse_passthrough_op(inode, FUSE_PASSTHROUGH_RW_OPS))
return -ENODEV;
However, I should note about this sketch patch that the condition for
fuse_passthrough_mmap()
is that both read/write are passthrough and the condition for ENODEV
is that either read/write are passthrough but looking at this code it is
not at all clear to the reader.
Because if there is no read/write passthrough, (only GETATTR passthrough)
there is not really a reason to prevent fuse page cache mmap.
See similar comment I made regarding fuse_file_cached_io_open() on the
documentation patch.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread* Re: [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap
2026-05-16 16:57 ` Amir Goldstein
2026-05-16 18:34 ` Amir Goldstein
@ 2026-05-18 21:46 ` Joanne Koong
1 sibling, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-18 21:46 UTC (permalink / raw)
To: Amir Goldstein; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 9:57 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Sat, May 16, 2026 at 2:52 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > Servers can now pass through only reads or writes instead of being
> > required to pass through both. When the read or write operation is not
> > passed through, fall back to direct io for handling it. This avoids
> > cache coherency issues from mixing the backing file's page cache with
> > the fuse inode's page cache.
> >
> > Reject mmap when passthrough is partial or not set for read/writes since
> > mmap requires both read and write access to the backing page cache.
> >
> > The FOPEN_DIRECT_IO check in the read/write fallback is not strictly
> > needed as the caller already handles it, but including it here makes
> > the fallback logic self-contained.
> >
> > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > ---
> > fs/fuse/file.c | 7 ++++---
> > fs/fuse/fuse_i.h | 3 +++
> > fs/fuse/passthrough.c | 18 ++++++++++++++++++
> > 3 files changed, 25 insertions(+), 3 deletions(-)
> >
> > diff --git a/fs/fuse/file.c b/fs/fuse/file.c
> > index 1173811b2ea7..5b70d8272700 100644
> > --- a/fs/fuse/file.c
> > +++ b/fs/fuse/file.c
> > @@ -1781,7 +1781,7 @@ static ssize_t __fuse_direct_read(struct fuse_io_priv *io,
> >
> > static ssize_t fuse_direct_IO(struct kiocb *iocb, struct iov_iter *iter);
> >
> > -static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
> > +ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
> > {
> > ssize_t res;
> >
> > @@ -1796,7 +1796,7 @@ static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
> > return res;
> > }
> >
> > -static ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
> > +ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
> > {
> > struct inode *inode = file_inode(iocb->ki_filp);
> > ssize_t res;
> > @@ -1884,7 +1884,8 @@ static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out,
> > struct fuse_file *ff = out->private_data;
> >
> > /* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
> > - if (fuse_file_passthrough(ff) && !(ff->open_flags & FOPEN_DIRECT_IO))
> > + if (fuse_passthrough_op(file_inode(out), FUSE_WRITE) &&
>
> Removing fuse_file_passthrough(ff) makes me uncomfortable.
> It's true that if server sets PASSTHROUGH_OP_WRITE on lookup
> the file has to be opened with FOPEN_PASSTHROUGH, but this
> not easily understood by passing by reviewers.
>
> It's be nicer if we keep this logic contained in a helper where it can be
> properly documented, something like this (following the changes to
> FUSE_PASSTHROUGH_OP() discussed earlier):
>
> static inline u64 fuse_passthrough_op(struct inode *inode, u64 op_mask)
> {
> struct fuse_inode *fi = get_fuse_inode(inode);
> struct fuse_backing *fb = fuse_inode_passthrough(fi);
>
> return !fb ? 0 : fb->ops_mask & op_mask;
> }
>
> static inline struct file *fuse_file_passthrough_op(struct file *file,
> u64 op_mask)
> {
> struct fuse_file *ff = file->private_data;
>
> /*
> * FOPEN_DIRECT_IO can be used to opt-out of passthrough per file and
> * ops_mask can be used to opt-out of passthrough per operation type.
> */
> return fuse_file_passthrough(ff) &&
> !(ff->open_flags & FOPEN_DIRECT_IO) &&
> fuse_passthrough_op(file_inode(file), op_mask) == op_mask;
> }
>
> ...
>
> static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out,
> loff_t *ppos, size_t len, unsigned int flags)
> {
> if (fuse_file_passthrough_op(out, FUSE_PASSTHROUGH_OP_WRITE))
> return fuse_passthrough_splice_write(pipe, out, ppos,
> len, flags);
> else
> return iter_file_splice_write(pipe, out, ppos, len, flags);
> }
>
I really like all your suggestions, Amir. I agree that this looks much
nicer. I'll incorporate this into v3.
>
> > + !(ff->open_flags & FOPEN_DIRECT_IO))
> > return fuse_passthrough_splice_write(pipe, out, ppos, len, flags);
> > else
> > return iter_file_splice_write(pipe, out, ppos, len, flags);
> > diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> > index 9e5142a94a09..a1034533ce60 100644
> > --- a/fs/fuse/fuse_i.h
> > +++ b/fs/fuse/fuse_i.h
> > @@ -1261,6 +1261,9 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
> > void fuse_file_release(struct inode *inode, struct fuse_file *ff,
> > unsigned int open_flags, fl_owner_t id, bool isdir);
> >
> > +ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to);
> > +ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from);
> > +
> > /* passthrough.c */
> >
> > /* READ/WRITE are implied by FOPEN_PASSTHROUGH, but defined for completeness */
> > diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> > index f2025772c9c1..d62a1c751157 100644
> > --- a/fs/fuse/passthrough.c
> > +++ b/fs/fuse/passthrough.c
> > @@ -44,6 +44,10 @@ ssize_t fuse_passthrough_read_iter(struct kiocb *iocb, struct iov_iter *iter)
> > if (!count)
> > return 0;
> >
> > + if ((ff->open_flags & FOPEN_DIRECT_IO) ||
> > + !fuse_passthrough_op(file_inode(file), FUSE_READ))
> > + return fuse_direct_read_iter(iocb, iter);
> > +
> > ret = backing_file_read_iter(backing_file, iter, iocb, iocb->ki_flags,
> > &ctx);
> >
> > @@ -70,6 +74,10 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
> > if (!count)
> > return 0;
> >
> > + if ((ff->open_flags & FOPEN_DIRECT_IO) ||
> > + !fuse_passthrough_op(inode, FUSE_WRITE))
> > + return fuse_direct_write_iter(iocb, iter);
> > +
> > inode_lock(inode);
> > ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
> > &ctx);
> > @@ -91,6 +99,9 @@ ssize_t fuse_passthrough_splice_read(struct file *in, loff_t *ppos,
> > struct kiocb iocb;
> > ssize_t ret;
> >
> > + if (!fuse_passthrough_op(file_inode(in), FUSE_READ))
> > + return copy_splice_read(in, ppos, pipe, len, flags);
> > +
> > pr_debug("%s: backing_file=0x%p, pos=%lld, len=%zu, flags=0x%x\n", __func__,
> > backing_file, *ppos, len, flags);
> >
>
> I understand why you brought this "jump back to direct" into the passthrough
> helpers to reduce complexity in the call sites, but I think that if you use
> the fuse_file_passthrough_op() helper in the call sites code will look
> better and will not need to "jump back to direct io" in passthrough.c.
>
> > @@ -133,6 +144,9 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
> > {
> > struct fuse_file *ff = file->private_data;
> > struct file *backing_file = fuse_file_passthrough(ff);
> > + struct inode *inode = file_inode(file);
> > + struct fuse_inode *fi = get_fuse_inode(inode);
> > + struct fuse_backing *fb = fuse_inode_backing(fi);
> > struct backing_file_ctx ctx = {
> > .cred = ff->cred,
> > .accessed = fuse_file_accessed,
> > @@ -141,6 +155,10 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
> > pr_debug("%s: backing_file=0x%p, start=%lu, end=%lu\n", __func__,
> > backing_file, vma->vm_start, vma->vm_end);
> >
> > + /* mmap requires both read and write passthrough */
> > + if ((fb->ops_mask & FUSE_PASSTHROUGH_RW_OPS) != FUSE_PASSTHROUGH_RW_OPS)
> > + return -ENODEV;
> > +
>
> Changing the helper in call site in nicer and already hits the
> existing ENODEV case:
>
> if (fuse_file_passthrough_op(file, FUSE_PASSTHROUGH_RW_OPS))
> return fuse_passthrough_mmap(file, vma);
> else if (fuse_inode_backing(get_fuse_inode(inode)))
> return -ENODEV;
Nice! This looks a lot cleaner.
Thanks,
Joanne
>
> Thanks,
> Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 10/21] fuse: prepare to cache statx attributes from entry replies
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (8 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 11/21] fuse: clean up fuse_dentry_revalidate() Joanne Koong
` (10 subsequent siblings)
20 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Add an arg for a struct fuse_statx pointer to fuse_iget() and pass it
through to fuse_change_attributes_i() so the statx attributes get
persisted in cache.
All existing callers pass in NULL for the arg. This change is in
preparation for a new struct fuse_entry2_out outarg which will contain
filled out statx information from the server returned on
LOOKUP/MKDIR/MKNOD/etc requests.
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 6 +++---
fs/fuse/fuse_i.h | 7 +++----
fs/fuse/inode.c | 12 ++++++------
fs/fuse/readdir.c | 2 +-
4 files changed, 13 insertions(+), 14 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 9a8e525f4d2b..7d3c9878b833 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -590,7 +590,7 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
}
*inode = fuse_iget(sb, outarg->nodeid, outarg->generation,
- &outarg->attr, ATTR_TIMEOUT(outarg),
+ &outarg->attr, NULL, ATTR_TIMEOUT(outarg),
attr_version, evict_ctr);
err = -ENOMEM;
if (!*inode) {
@@ -890,7 +890,7 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
ff->nodeid = outentry.nodeid;
ff->open_flags = outopenp->open_flags;
inode = fuse_iget(dir->i_sb, outentry.nodeid, outentry.generation,
- &outentry.attr, ATTR_TIMEOUT(&outentry), 0, 0);
+ &outentry.attr, NULL, ATTR_TIMEOUT(&outentry), 0, 0);
if (!inode) {
flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
fuse_sync_release(NULL, ff, flags, false);
@@ -1017,7 +1017,7 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
goto out_put_forget_req;
inode = fuse_iget(dir->i_sb, outarg.nodeid, outarg.generation,
- &outarg.attr, ATTR_TIMEOUT(&outarg), 0, 0);
+ &outarg.attr, NULL, ATTR_TIMEOUT(&outarg), 0, 0);
if (!inode) {
fuse_chan_queue_forget(fm->fc->chan, forget, outarg.nodeid, 1);
return ERR_PTR(-ENOMEM);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index a1034533ce60..1f2c849ea4e3 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -912,10 +912,9 @@ extern const struct dentry_operations fuse_dentry_operations;
/*
* Get a filled in inode
*/
-struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
- int generation, struct fuse_attr *attr,
- u64 attr_valid, u64 attr_version,
- u64 evict_ctr);
+struct inode *fuse_iget(struct super_block *sb, u64 nodeid, int generation,
+ struct fuse_attr *attr, struct fuse_statx *sx,
+ u64 attr_valid, u64 attr_version, u64 evict_ctr);
int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name,
struct fuse_entry_out *outarg, struct inode **inode);
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index 9e9c20e846e7..8757c01e3bb2 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -457,8 +457,8 @@ static int fuse_inode_set(struct inode *inode, void *_nodeidp)
struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
int generation, struct fuse_attr *attr,
- u64 attr_valid, u64 attr_version,
- u64 evict_ctr)
+ struct fuse_statx *sx, u64 attr_valid,
+ u64 attr_version, u64 evict_ctr)
{
struct inode *inode;
struct fuse_inode *fi;
@@ -520,7 +520,7 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
fi->nlookup++;
spin_unlock(&fi->lock);
done:
- fuse_change_attributes_i(inode, attr, NULL, attr_valid, attr_version,
+ fuse_change_attributes_i(inode, attr, sx, attr_valid, attr_version,
evict_ctr);
if (is_new_inode)
unlock_new_inode(inode);
@@ -1049,7 +1049,7 @@ static struct inode *fuse_get_root_inode(struct super_block *sb, unsigned int mo
attr.mode = mode;
attr.ino = FUSE_ROOT_ID;
attr.nlink = 1;
- return fuse_iget(sb, FUSE_ROOT_ID, 0, &attr, 0, 0, 0);
+ return fuse_iget(sb, FUSE_ROOT_ID, 0, &attr, NULL, 0, 0, 0);
}
struct fuse_inode_handle {
@@ -1652,8 +1652,8 @@ static int fuse_fill_super_submount(struct super_block *sb,
return -ENOMEM;
fuse_fill_attr_from_inode(&root_attr, parent_fi);
- root = fuse_iget(sb, parent_fi->nodeid, 0, &root_attr, 0, 0,
- fuse_get_evict_ctr(fm->fc));
+ root = fuse_iget(sb, parent_fi->nodeid, 0, &root_attr, NULL, 0,
+ 0, fuse_get_evict_ctr(fm->fc));
/*
* This inode is just a duplicate, so it is not looked up and
* its nlookup should not be incremented. fuse_iget() does
diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
index 49226f022339..0dcb2d75d50f 100644
--- a/fs/fuse/readdir.c
+++ b/fs/fuse/readdir.c
@@ -234,7 +234,7 @@ static int fuse_direntplus_link(struct file *file,
*/
} else {
inode = fuse_iget(dir->i_sb, o->nodeid, o->generation,
- &o->attr, ATTR_TIMEOUT(o),
+ &o->attr, NULL, ATTR_TIMEOUT(o),
attr_version, evict_ctr);
if (!inode)
inode = ERR_PTR(-ENOMEM);
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* [PATCH v2 11/21] fuse: clean up fuse_dentry_revalidate()
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (9 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 10/21] fuse: prepare to cache statx attributes from entry replies Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies Joanne Koong
` (9 subsequent siblings)
20 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Remove gotos with direct returns and flatten nested if/else structure.
No functional changes.
Suggested-by: Amir Goldstein <amir73il@gmail.com>
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 112 +++++++++++++++++++++++---------------------------
1 file changed, 52 insertions(+), 60 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 7d3c9878b833..a338f2a06b50 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -386,72 +386,31 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
struct dentry *entry, unsigned int flags)
{
struct inode *inode;
- struct fuse_mount *fm;
struct fuse_conn *fc;
struct fuse_inode *fi;
+ struct fuse_entry_out outarg;
+ struct fuse_forget_link *forget;
+ FUSE_ARGS(args);
+ u64 attr_version;
+ bool need_reval;
int ret;
fc = get_fuse_conn_super(dir->i_sb);
if (entry->d_time < atomic_read(&fc->epoch))
- goto invalid;
+ return 0;
inode = d_inode_rcu(entry);
if (inode && fuse_is_bad(inode))
- goto invalid;
- else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
- (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {
- struct fuse_entry_out outarg;
- FUSE_ARGS(args);
- struct fuse_forget_link *forget;
- u64 attr_version;
-
- /* For negative dentries, always do a fresh lookup */
- if (!inode)
- goto invalid;
-
- ret = -ECHILD;
- if (flags & LOOKUP_RCU)
- goto out;
-
- fm = get_fuse_mount(inode);
+ return 0;
- forget = fuse_alloc_forget();
- ret = -ENOMEM;
- if (!forget)
- goto out;
+ need_reval = time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
+ (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET));
- 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 */
- if (!ret && !outarg.nodeid)
- ret = -ENOENT;
- if (!ret) {
- fi = get_fuse_inode(inode);
- if (outarg.nodeid != get_node_id(inode) ||
- (bool) IS_AUTOMOUNT(inode) != (bool) (outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
- fuse_chan_queue_forget(fm->fc->chan, forget,
- outarg.nodeid, 1);
- goto invalid;
- }
- spin_lock(&fi->lock);
- fi->nlookup++;
- spin_unlock(&fi->lock);
- }
- kfree(forget);
- if (ret == -ENOMEM || ret == -EINTR)
- goto out;
- if (ret || fuse_invalid_attr(&outarg.attr) ||
- fuse_stale_inode(inode, outarg.generation, &outarg.attr))
- goto invalid;
+ /* For negative dentries that need revalidation, always do a fresh lookup */
+ if (!inode)
+ return need_reval ? 0 : 1;
- forget_all_cached_acls(inode);
- fuse_change_attributes(inode, &outarg.attr, NULL,
- ATTR_TIMEOUT(&outarg),
- attr_version);
- fuse_change_entry_timeout(entry, &outarg);
- } else if (inode) {
+ if (!need_reval) {
fi = get_fuse_inode(inode);
if (flags & LOOKUP_RCU) {
if (test_bit(FUSE_I_INIT_RDPLUS, &fi->state))
@@ -459,14 +418,47 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
} else if (test_and_clear_bit(FUSE_I_INIT_RDPLUS, &fi->state)) {
fuse_advise_use_readdirplus(dir);
}
+
+ return 1;
}
- ret = 1;
-out:
- return ret;
-invalid:
- ret = 0;
- goto out;
+ if (flags & LOOKUP_RCU)
+ return -ECHILD;
+
+ forget = fuse_alloc_forget();
+ if (!forget)
+ return -ENOMEM;
+
+ attr_version = fuse_get_attr_version(fc);
+
+ fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
+ ret = fuse_simple_request(get_fuse_mount(inode), &args);
+ if (ret || !outarg.nodeid) {
+ kfree(forget);
+ return (ret == -ENOMEM || ret == -EINTR) ? ret : 0;
+ }
+
+ if (outarg.nodeid != get_node_id(inode) ||
+ !!IS_AUTOMOUNT(inode) != !!(outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
+ fuse_chan_queue_forget(fc->chan, forget, outarg.nodeid, 1);
+ return 0;
+ }
+
+ kfree(forget);
+ fi = get_fuse_inode(inode);
+ spin_lock(&fi->lock);
+ fi->nlookup++;
+ spin_unlock(&fi->lock);
+
+ if (fuse_invalid_attr(&outarg.attr) ||
+ fuse_stale_inode(inode, outarg.generation, &outarg.attr))
+ return 0;
+
+ forget_all_cached_acls(inode);
+ fuse_change_attributes(inode, &outarg.attr, NULL, ATTR_TIMEOUT(&outarg),
+ attr_version);
+ fuse_change_entry_timeout(entry, &outarg);
+ return 1;
}
static int fuse_dentry_init(struct dentry *dentry)
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (10 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 11/21] fuse: clean up fuse_dentry_revalidate() Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 17:25 ` Amir Goldstein
2026-05-16 0:39 ` [PATCH v2 13/21] fuse: add passthrough lookup Joanne Koong
` (8 subsequent siblings)
20 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Add struct fuse_entry2_out, which is a new extended entry reply struct
that carries a backing_id and statx attributes. This will be necessary
for setting fuse passthrough on inodes.
Add helpers that subsequent commits will use to process fuse_entry2_out
for passthrough support for lookup, revalidate, and create.
fuse_statx_to_attr() is also moved to earlier in the file to avoid
forward declaring.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 116 +++++++++++++++++++++++++++++---------
fs/fuse/fuse_i.h | 5 ++
include/uapi/linux/fuse.h | 14 +++++
3 files changed, 107 insertions(+), 28 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index a338f2a06b50..b7a9d2b0476a 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -355,11 +355,18 @@ static void fuse_invalidate_entry(struct dentry *entry)
fuse_invalidate_entry_cache(entry);
}
-static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
- const struct qstr *name,
- struct fuse_entry_out *outarg)
+static void fuse_lookup_init(struct fuse_conn *fc, struct fuse_args *args,
+ u64 nodeid, const struct qstr *name,
+ struct fuse_entry_out *outarg,
+ struct fuse_entry2_out *outarg2)
{
- memset(outarg, 0, sizeof(struct fuse_entry_out));
+ bool use_entry2 = fuse_use_entry2(fc);
+
+ if (use_entry2)
+ memset(outarg2, 0, sizeof(struct fuse_entry2_out));
+ else
+ memset(outarg, 0, sizeof(struct fuse_entry_out));
+
args->opcode = FUSE_LOOKUP;
args->nodeid = nodeid;
args->in_numargs = 3;
@@ -369,8 +376,79 @@ static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
args->in_args[2].size = 1;
args->in_args[2].value = "";
args->out_numargs = 1;
- args->out_args[0].size = sizeof(struct fuse_entry_out);
- args->out_args[0].value = outarg;
+
+ if (use_entry2) {
+ args->out_args[0].size = sizeof(struct fuse_entry2_out);
+ args->out_args[0].value = outarg2;
+ } else {
+ args->out_args[0].size = sizeof(struct fuse_entry_out);
+ args->out_args[0].value = outarg;
+ }
+}
+
+static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr)
+{
+ memset(attr, 0, sizeof(*attr));
+ attr->ino = sx->ino;
+ attr->size = sx->size;
+ attr->blocks = sx->blocks;
+ attr->atime = sx->atime.tv_sec;
+ attr->mtime = sx->mtime.tv_sec;
+ attr->ctime = sx->ctime.tv_sec;
+ attr->atimensec = sx->atime.tv_nsec;
+ attr->mtimensec = sx->mtime.tv_nsec;
+ attr->ctimensec = sx->ctime.tv_nsec;
+ attr->mode = sx->mode;
+ attr->nlink = sx->nlink;
+ attr->uid = sx->uid;
+ attr->gid = sx->gid;
+ attr->rdev = new_encode_dev(MKDEV(sx->rdev_major, sx->rdev_minor));
+ attr->blksize = sx->blksize;
+}
+
+static void fuse_entry2_to_entry(struct fuse_entry2_out *outarg2,
+ struct fuse_entry_out *outarg)
+{
+ memset(outarg, 0, sizeof(struct fuse_entry_out));
+ outarg->nodeid = outarg2->nodeid;
+ outarg->generation = outarg2->generation;
+ outarg->entry_valid = outarg2->entry_valid;
+ outarg->attr_valid = outarg2->attr_valid;
+ outarg->entry_valid_nsec = outarg2->entry_valid_nsec;
+ outarg->attr_valid_nsec = outarg2->attr_valid_nsec;
+ fuse_statx_to_attr(&outarg2->statx, &outarg->attr);
+ outarg->attr.flags = outarg2->flags;
+}
+
+static __maybe_unused int fuse_process_entry2(struct fuse_conn *fc,
+ struct fuse_entry2_out *outarg2,
+ struct fuse_entry_out *outarg,
+ struct fuse_statx **sxp)
+{
+ if (!fuse_use_entry2(fc))
+ return 0;
+
+ /*
+ * Convert entry2 format to entry format before error checking since the
+ * caller needs outarg->nodeid field even on error
+ */
+ fuse_entry2_to_entry(outarg2, outarg);
+
+ if (outarg2->reserved)
+ return -EINVAL;
+
+ /* error */
+ if (outarg2->backing_id < 0)
+ return outarg2->backing_id;
+
+ /*
+ * If passthrough is enabled (backing_id > 0), statx attributes are not
+ * cached because passthrough getattr fetches them directly from the
+ * backing inode
+ */
+ if (!outarg2->backing_id)
+ *sxp = &outarg2->statx;
+ return outarg2->backing_id;
}
/*
@@ -389,6 +467,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
struct fuse_conn *fc;
struct fuse_inode *fi;
struct fuse_entry_out outarg;
+ struct fuse_entry2_out outarg2;
struct fuse_forget_link *forget;
FUSE_ARGS(args);
u64 attr_version;
@@ -431,7 +510,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
attr_version = fuse_get_attr_version(fc);
- fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
+ fuse_lookup_init(fc, &args, get_node_id(dir), name, &outarg, &outarg2);
ret = fuse_simple_request(get_fuse_mount(inode), &args);
if (ret || !outarg.nodeid) {
kfree(forget);
@@ -551,6 +630,7 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
FUSE_ARGS(args);
struct fuse_forget_link *forget;
u64 attr_version, evict_ctr;
+ struct fuse_entry2_out outarg2;
int err;
*inode = NULL;
@@ -567,7 +647,7 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
attr_version = fuse_get_attr_version(fm->fc);
evict_ctr = fuse_get_evict_ctr(fm->fc);
- fuse_lookup_init(&args, nodeid, name, outarg);
+ fuse_lookup_init(fm->fc, &args, nodeid, name, outarg, &outarg2);
err = fuse_simple_request(fm, &args);
/* Zero nodeid is same as -ENOENT, but with valid timeout */
if (err || !outarg->nodeid)
@@ -1386,26 +1466,6 @@ static void fuse_fillattr(struct mnt_idmap *idmap, struct inode *inode,
stat->blksize = 1 << blkbits;
}
-static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr)
-{
- memset(attr, 0, sizeof(*attr));
- attr->ino = sx->ino;
- attr->size = sx->size;
- attr->blocks = sx->blocks;
- attr->atime = sx->atime.tv_sec;
- attr->mtime = sx->mtime.tv_sec;
- attr->ctime = sx->ctime.tv_sec;
- attr->atimensec = sx->atime.tv_nsec;
- attr->mtimensec = sx->mtime.tv_nsec;
- attr->ctimensec = sx->ctime.tv_nsec;
- attr->mode = sx->mode;
- attr->nlink = sx->nlink;
- attr->uid = sx->uid;
- attr->gid = sx->gid;
- attr->rdev = new_encode_dev(MKDEV(sx->rdev_major, sx->rdev_minor));
- attr->blksize = sx->blksize;
-}
-
void fuse_kstat_to_attr(struct fuse_conn *fc, const struct kstat *stat,
struct fuse_attr *attr)
{
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 1f2c849ea4e3..89c9333e9702 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1368,6 +1368,11 @@ static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
u32 request_mask, unsigned int flags);
+static inline bool fuse_use_entry2(struct fuse_conn *fc)
+{
+ return fc->passthrough_ino;
+}
+
#ifdef CONFIG_SYSCTL
extern int fuse_sysctl_register(void);
extern void fuse_sysctl_unregister(void);
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 6404ed95c758..3963631558f9 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -244,6 +244,7 @@
* 7.46
* - add FUSE_PASSTHROUGH_INO
* - add ops_mask field to struct fuse_backing_map
+ * - add fuse_entry2_out
*/
#ifndef _LINUX_FUSE_H
@@ -705,6 +706,19 @@ struct fuse_entry_out {
struct fuse_attr attr;
};
+struct fuse_entry2_out {
+ uint64_t nodeid;
+ uint64_t generation;
+ uint64_t entry_valid;
+ uint64_t attr_valid;
+ uint32_t entry_valid_nsec;
+ uint32_t attr_valid_nsec;
+ int32_t backing_id;
+ uint32_t flags;
+ uint64_t reserved;
+ struct fuse_statx statx;
+};
+
struct fuse_forget_in {
uint64_t nlookup;
};
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies
2026-05-16 0:39 ` [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies Joanne Koong
@ 2026-05-16 17:25 ` Amir Goldstein
2026-05-16 19:29 ` Horst Birthelmer
0 siblings, 1 reply; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 17:25 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 2:52 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Add struct fuse_entry2_out, which is a new extended entry reply struct
> that carries a backing_id and statx attributes. This will be necessary
> for setting fuse passthrough on inodes.
>
> Add helpers that subsequent commits will use to process fuse_entry2_out
> for passthrough support for lookup, revalidate, and create.
> fuse_statx_to_attr() is also moved to earlier in the file to avoid
> forward declaring.
>
I think you had a discussion with Miklos about whether
it makes sense to do a compound response for LOOKUP+STATX.
I don't remember the conclusion, but if this can save us all the
if (use_entry2) { } else {} all over the code, then I think it is worth it.
Can you give me the bottom line from your discussion or was there
no conclusion reached?
If we do that, where do we return backing_id?
Could make use of the reserved[] fields in fuse_statx_out.
I happen to have a test patch at the tip of my fuse-backing-inode-wip
branch:
6e296625eafd6 fuse: support setting backing inode passthrough on getattr
It's not using the statx response, and it was just done for testing,
but in theory,
if a server wants to change an inode from non-passthrough to
yes-passthrough mid lifetime
doing this on attribute cache expiry may make more sense than on entry
timeout expiry, because backing_id is an attribute of the inode not the dentry.
Not sure if this is making any sense.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread* Re: Re: [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies
2026-05-16 17:25 ` Amir Goldstein
@ 2026-05-16 19:29 ` Horst Birthelmer
2026-05-18 23:12 ` Joanne Koong
0 siblings, 1 reply; 54+ messages in thread
From: Horst Birthelmer @ 2026-05-16 19:29 UTC (permalink / raw)
To: Amir Goldstein; +Cc: Joanne Koong, miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 07:25:05PM +0200, Amir Goldstein wrote:
> On Sat, May 16, 2026 at 2:52 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > Add struct fuse_entry2_out, which is a new extended entry reply struct
> > that carries a backing_id and statx attributes. This will be necessary
> > for setting fuse passthrough on inodes.
> >
> > Add helpers that subsequent commits will use to process fuse_entry2_out
> > for passthrough support for lookup, revalidate, and create.
> > fuse_statx_to_attr() is also moved to earlier in the file to avoid
> > forward declaring.
> >
>
> I think you had a discussion with Miklos about whether
> it makes sense to do a compound response for LOOKUP+STATX.
> I don't remember the conclusion, but if this can save us all the
> if (use_entry2) { } else {} all over the code, then I think it is worth it.
>
> Can you give me the bottom line from your discussion or was there
> no conclusion reached?
>
> If we do that, where do we return backing_id?
> Could make use of the reserved[] fields in fuse_statx_out.
>
> I happen to have a test patch at the tip of my fuse-backing-inode-wip
> branch:
> 6e296625eafd6 fuse: support setting backing inode passthrough on getattr
>
> It's not using the statx response, and it was just done for testing,
> but in theory,
> if a server wants to change an inode from non-passthrough to
> yes-passthrough mid lifetime
> doing this on attribute cache expiry may make more sense than on entry
> timeout expiry, because backing_id is an attribute of the inode not the dentry.
Hi Amir,
I see there is a lot of talk about using the compounds in a couple of different contexts.
I really have to apologize for not having the latest patch ready by now, but I'm
a bit unsure how to do it.
I'm not entirely sure if compounds will only be a thing for fusex or if it will ever
be accepted for the classic FUSE.
I actually have a patch that I have not tested well enough, since I have to maintain
our own version and then have to do the fusex one and probably the new verison for
the classic FUSE version.
So, sorry about that, but I'm not having enough clarity on what to focus on.
>
> Not sure if this is making any sense.
>
> Thanks,
> Amir.
>
Horst
^ permalink raw reply [flat|nested] 54+ messages in thread* Re: Re: [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies
2026-05-16 19:29 ` Horst Birthelmer
@ 2026-05-18 23:12 ` Joanne Koong
2026-05-19 14:17 ` Amir Goldstein
0 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-18 23:12 UTC (permalink / raw)
To: Horst Birthelmer; +Cc: Amir Goldstein, miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 12:29 PM Horst Birthelmer <horst@birthelmer.de> wrote:
>
> On Sat, May 16, 2026 at 07:25:05PM +0200, Amir Goldstein wrote:
> > On Sat, May 16, 2026 at 2:52 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> > >
> > > Add struct fuse_entry2_out, which is a new extended entry reply struct
> > > that carries a backing_id and statx attributes. This will be necessary
> > > for setting fuse passthrough on inodes.
> > >
> > > Add helpers that subsequent commits will use to process fuse_entry2_out
> > > for passthrough support for lookup, revalidate, and create.
> > > fuse_statx_to_attr() is also moved to earlier in the file to avoid
> > > forward declaring.
> > >
> >
> > I think you had a discussion with Miklos about whether
> > it makes sense to do a compound response for LOOKUP+STATX.
> > I don't remember the conclusion, but if this can save us all the
> > if (use_entry2) { } else {} all over the code, then I think it is worth it.
> >
> > Can you give me the bottom line from your discussion or was there
> > no conclusion reached?
I think the conclusion was that for fusex it will be compounded, but
I'm not sure what Miklos is thinking for fuse.
> >
> > If we do that, where do we return backing_id?
> > Could make use of the reserved[] fields in fuse_statx_out.
> >
> > I happen to have a test patch at the tip of my fuse-backing-inode-wip
> > branch:
> > 6e296625eafd6 fuse: support setting backing inode passthrough on getattr
> >
> > It's not using the statx response, and it was just done for testing,
> > but in theory,
> > if a server wants to change an inode from non-passthrough to
> > yes-passthrough mid lifetime
> > doing this on attribute cache expiry may make more sense than on entry
> > timeout expiry, because backing_id is an attribute of the inode not the dentry.
I like this idea but I think for supporting yes passthrough -> no
passthrough, it would have to go through an ioctl mechanism since the
getattr could be passed through. In that case, I was thinking it would
be better to have the interfaces be consistent (eg both yes
passthrough -> no passthrough and no passthrough -> yes passthrough go
through an ioctl interface).
I think even with the backing id as part of statx and lookup+statx
compounded together, there would still have to be the "if (use_entry2)
{} else {}" branching logic to handle the new slimmed-down-lookup
differently from old lookup).
>
> Hi Amir,
>
> I see there is a lot of talk about using the compounds in a couple of different contexts.
> I really have to apologize for not having the latest patch ready by now, but I'm
> a bit unsure how to do it.
>
> I'm not entirely sure if compounds will only be a thing for fusex or if it will ever
> be accepted for the classic FUSE.
I'm not sure what the plan is either. I think this will get clarified
on Thursday hopefully.
Thanks,
Joanne
>
> I actually have a patch that I have not tested well enough, since I have to maintain
> our own version and then have to do the fusex one and probably the new verison for
> the classic FUSE version.
>
> So, sorry about that, but I'm not having enough clarity on what to focus on.
>
> >
> > Not sure if this is making any sense.
> >
> > Thanks,
> > Amir.
> >
>
> Horst
^ permalink raw reply [flat|nested] 54+ messages in thread* Re: Re: [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies
2026-05-18 23:12 ` Joanne Koong
@ 2026-05-19 14:17 ` Amir Goldstein
2026-05-19 15:26 ` Luis Henriques
0 siblings, 1 reply; 54+ messages in thread
From: Amir Goldstein @ 2026-05-19 14:17 UTC (permalink / raw)
To: Joanne Koong; +Cc: Horst Birthelmer, miklos, fuse-devel, linux-unionfs
On Tue, May 19, 2026 at 1:12 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Sat, May 16, 2026 at 12:29 PM Horst Birthelmer <horst@birthelmer.de> wrote:
...
> > Hi Amir,
> >
> > I see there is a lot of talk about using the compounds in a couple of different contexts.
> > I really have to apologize for not having the latest patch ready by now, but I'm
> > a bit unsure how to do it.
> >
> > I'm not entirely sure if compounds will only be a thing for fusex or if it will ever
> > be accepted for the classic FUSE.
>
> I'm not sure what the plan is either. I think this will get clarified
> on Thursday hopefully.
>
Wait, is there a FUSE call? I did not get an invite :-D
Thanks,
Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies
2026-05-19 14:17 ` Amir Goldstein
@ 2026-05-19 15:26 ` Luis Henriques
2026-05-19 22:07 ` Joanne Koong
0 siblings, 1 reply; 54+ messages in thread
From: Luis Henriques @ 2026-05-19 15:26 UTC (permalink / raw)
To: Amir Goldstein
Cc: Joanne Koong, Horst Birthelmer, miklos, fuse-devel, linux-unionfs
(Sorry, I'm slow catching-up with email :-) )
On Tue, May 19 2026, Amir Goldstein wrote:
> On Tue, May 19, 2026 at 1:12 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>>
>> On Sat, May 16, 2026 at 12:29 PM Horst Birthelmer <horst@birthelmer.de> wrote:
> ...
>> > Hi Amir,
>> >
>> > I see there is a lot of talk about using the compounds in a couple of different contexts.
>> > I really have to apologize for not having the latest patch ready by now, but I'm
>> > a bit unsure how to do it.
>> >
>> > I'm not entirely sure if compounds will only be a thing for fusex or if it will ever
>> > be accepted for the classic FUSE.
>>
>> I'm not sure what the plan is either. I think this will get clarified
>> on Thursday hopefully.
From Miklos' email "[post LSFMM summary] where is fuse going?"[1] I
thought the plan was clear: both compound and file handles are going to be
in fusex. Quoting:
> COMPOUND REQS: let's add this to fusex. uAPI is simple, kAPI needs refining.
>
> FILE HANDLES: same: let's add to fusex.
[1] https://lore.kernel.org/all/CAJfpegvXnA1QpmfM9V+3VB470xYDNWH8FV0mBV1R1-HqvjfJ8A@mail.gmail.com
> Wait, is there a FUSE call? I did not get an invite :-D
Don't worry, I think no one has yet received an invite ;-)
Cheers,
--
Luís
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies
2026-05-19 15:26 ` Luis Henriques
@ 2026-05-19 22:07 ` Joanne Koong
0 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-19 22:07 UTC (permalink / raw)
To: Luis Henriques
Cc: Amir Goldstein, Horst Birthelmer, miklos, fuse-devel,
linux-unionfs
On Tue, May 19, 2026 at 8:26 AM Luis Henriques <luis@igalia.com> wrote:
>
>
> (Sorry, I'm slow catching-up with email :-) )
>
> On Tue, May 19 2026, Amir Goldstein wrote:
>
> > On Tue, May 19, 2026 at 1:12 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >>
> >> On Sat, May 16, 2026 at 12:29 PM Horst Birthelmer <horst@birthelmer.de> wrote:
> > ...
> >> > Hi Amir,
> >> >
> >> > I see there is a lot of talk about using the compounds in a couple of different contexts.
> >> > I really have to apologize for not having the latest patch ready by now, but I'm
> >> > a bit unsure how to do it.
> >> >
> >> > I'm not entirely sure if compounds will only be a thing for fusex or if it will ever
> >> > be accepted for the classic FUSE.
> >>
> >> I'm not sure what the plan is either. I think this will get clarified
> >> on Thursday hopefully.
>
> From Miklos' email "[post LSFMM summary] where is fuse going?"[1] I
> thought the plan was clear: both compound and file handles are going to be
> in fusex. Quoting:
>
> > COMPOUND REQS: let's add this to fusex. uAPI is simple, kAPI needs refining.
> >
> > FILE HANDLES: same: let's add to fusex.
>
I think the part we're unclear about is whether the plan is to also
have this in fuse or just fusex.
>
> [1] https://lore.kernel.org/all/CAJfpegvXnA1QpmfM9V+3VB470xYDNWH8FV0mBV1R1-HqvjfJ8A@mail.gmail.com
>
> > Wait, is there a FUSE call? I did not get an invite :-D
>
> Don't worry, I think no one has yet received an invite ;-)
I haven't either. I think the only thing we nailed down was the time
(Thursday at 20:30 CEDT) but I think John is still setting up the
logistics for it.
Thanks,
Joanne
>
> Cheers,
> --
> Luís
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 13/21] fuse: add passthrough lookup
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (11 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 21:11 ` Amir Goldstein
2026-05-16 0:39 ` [PATCH v2 14/21] fuse: add passthrough support for entry creation Joanne Koong
` (7 subsequent siblings)
20 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Use the new/extended struct fuse_entry2_out for lookups. If a backing id
is set, associate the fuse inode with the backing inode that should be
used for passthrough operations.
If no backing id was set, cache the statx attributes from the reply.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 66 ++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 52 insertions(+), 14 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index b7a9d2b0476a..3a6adae530da 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -420,10 +420,10 @@ static void fuse_entry2_to_entry(struct fuse_entry2_out *outarg2,
outarg->attr.flags = outarg2->flags;
}
-static __maybe_unused int fuse_process_entry2(struct fuse_conn *fc,
- struct fuse_entry2_out *outarg2,
- struct fuse_entry_out *outarg,
- struct fuse_statx **sxp)
+static int fuse_process_entry2(struct fuse_conn *fc,
+ struct fuse_entry2_out *outarg2,
+ struct fuse_entry_out *outarg,
+ struct fuse_statx **sxp)
{
if (!fuse_use_entry2(fc))
return 0;
@@ -469,7 +469,9 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
struct fuse_entry_out outarg;
struct fuse_entry2_out outarg2;
struct fuse_forget_link *forget;
+ struct fuse_statx *sx = NULL;
FUSE_ARGS(args);
+ int backing_id = 0;
u64 attr_version;
bool need_reval;
int ret;
@@ -512,15 +514,27 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
fuse_lookup_init(fc, &args, get_node_id(dir), name, &outarg, &outarg2);
ret = fuse_simple_request(get_fuse_mount(inode), &args);
+ if (!ret)
+ backing_id = fuse_process_entry2(fc, &outarg2, &outarg, &sx);
if (ret || !outarg.nodeid) {
kfree(forget);
return (ret == -ENOMEM || ret == -EINTR) ? ret : 0;
}
+ if (backing_id < 0) {
+ ret = backing_id;
+ goto forget;
+ }
+
+ ret = 0;
if (outarg.nodeid != get_node_id(inode) ||
- !!IS_AUTOMOUNT(inode) != !!(outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
- fuse_chan_queue_forget(fc->chan, forget, outarg.nodeid, 1);
- return 0;
+ !!IS_AUTOMOUNT(inode) != !!(outarg.attr.flags & FUSE_ATTR_SUBMOUNT))
+ goto forget;
+
+ if (backing_id) {
+ ret = fuse_inode_set_passthrough(inode, backing_id);
+ if (ret)
+ goto forget;
}
kfree(forget);
@@ -534,10 +548,14 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
return 0;
forget_all_cached_acls(inode);
- fuse_change_attributes(inode, &outarg.attr, NULL, ATTR_TIMEOUT(&outarg),
+ fuse_change_attributes(inode, &outarg.attr, sx, ATTR_TIMEOUT(&outarg),
attr_version);
fuse_change_entry_timeout(entry, &outarg);
return 1;
+
+forget:
+ fuse_chan_queue_forget(fc->chan, forget, outarg.nodeid, 1);
+ return ret;
}
static int fuse_dentry_init(struct dentry *dentry)
@@ -631,6 +649,8 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
struct fuse_forget_link *forget;
u64 attr_version, evict_ctr;
struct fuse_entry2_out outarg2;
+ struct fuse_statx *sx = NULL;
+ int backing_id;
int err;
*inode = NULL;
@@ -649,9 +669,18 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
fuse_lookup_init(fm->fc, &args, nodeid, name, outarg, &outarg2);
err = fuse_simple_request(fm, &args);
+ if (err)
+ goto out_put_forget;
+
+ backing_id = fuse_process_entry2(fm->fc, &outarg2, outarg, &sx);
+
/* Zero nodeid is same as -ENOENT, but with valid timeout */
- if (err || !outarg->nodeid)
+ if (!outarg->nodeid)
goto out_put_forget;
+ if (backing_id < 0) {
+ err = backing_id;
+ goto out_send_forget;
+ }
err = -EIO;
if (fuse_invalid_attr(&outarg->attr))
@@ -662,19 +691,28 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
}
*inode = fuse_iget(sb, outarg->nodeid, outarg->generation,
- &outarg->attr, NULL, ATTR_TIMEOUT(outarg),
+ &outarg->attr, sx, ATTR_TIMEOUT(outarg),
attr_version, evict_ctr);
err = -ENOMEM;
- if (!*inode) {
- fuse_chan_queue_forget(fm->fc->chan, forget, outarg->nodeid, 1);
- goto out;
- }
+ if (!*inode)
+ goto out_send_forget;
+
err = 0;
+ if (backing_id) {
+ err = fuse_inode_set_passthrough(*inode, backing_id);
+ if (err) {
+ iput(*inode);
+ *inode = NULL;
+ }
+ }
out_put_forget:
kfree(forget);
out:
return err;
+ out_send_forget:
+ fuse_chan_queue_forget(fm->fc->chan, forget, outarg->nodeid, 1);
+ return err;
}
static struct dentry *fuse_lookup(struct inode *dir, struct dentry *entry,
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 13/21] fuse: add passthrough lookup
2026-05-16 0:39 ` [PATCH v2 13/21] fuse: add passthrough lookup Joanne Koong
@ 2026-05-16 21:11 ` Amir Goldstein
0 siblings, 0 replies; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 21:11 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 2:52 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Use the new/extended struct fuse_entry2_out for lookups. If a backing id
> is set, associate the fuse inode with the backing inode that should be
> used for passthrough operations.
>
> If no backing id was set, cache the statx attributes from the reply.
The 3 lookup patches look fine in general, but I will review them more
closely after I have a clear understanding on the desired protocol.
Thanks,
Amir.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
> fs/fuse/dir.c | 66 ++++++++++++++++++++++++++++++++++++++++-----------
> 1 file changed, 52 insertions(+), 14 deletions(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index b7a9d2b0476a..3a6adae530da 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -420,10 +420,10 @@ static void fuse_entry2_to_entry(struct fuse_entry2_out *outarg2,
> outarg->attr.flags = outarg2->flags;
> }
>
> -static __maybe_unused int fuse_process_entry2(struct fuse_conn *fc,
> - struct fuse_entry2_out *outarg2,
> - struct fuse_entry_out *outarg,
> - struct fuse_statx **sxp)
> +static int fuse_process_entry2(struct fuse_conn *fc,
> + struct fuse_entry2_out *outarg2,
> + struct fuse_entry_out *outarg,
> + struct fuse_statx **sxp)
> {
> if (!fuse_use_entry2(fc))
> return 0;
> @@ -469,7 +469,9 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
> struct fuse_entry_out outarg;
> struct fuse_entry2_out outarg2;
> struct fuse_forget_link *forget;
> + struct fuse_statx *sx = NULL;
> FUSE_ARGS(args);
> + int backing_id = 0;
> u64 attr_version;
> bool need_reval;
> int ret;
> @@ -512,15 +514,27 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
>
> fuse_lookup_init(fc, &args, get_node_id(dir), name, &outarg, &outarg2);
> ret = fuse_simple_request(get_fuse_mount(inode), &args);
> + if (!ret)
> + backing_id = fuse_process_entry2(fc, &outarg2, &outarg, &sx);
> if (ret || !outarg.nodeid) {
> kfree(forget);
> return (ret == -ENOMEM || ret == -EINTR) ? ret : 0;
> }
>
> + if (backing_id < 0) {
> + ret = backing_id;
> + goto forget;
> + }
> +
> + ret = 0;
> if (outarg.nodeid != get_node_id(inode) ||
> - !!IS_AUTOMOUNT(inode) != !!(outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
> - fuse_chan_queue_forget(fc->chan, forget, outarg.nodeid, 1);
> - return 0;
> + !!IS_AUTOMOUNT(inode) != !!(outarg.attr.flags & FUSE_ATTR_SUBMOUNT))
> + goto forget;
> +
> + if (backing_id) {
> + ret = fuse_inode_set_passthrough(inode, backing_id);
> + if (ret)
> + goto forget;
> }
>
> kfree(forget);
> @@ -534,10 +548,14 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
> return 0;
>
> forget_all_cached_acls(inode);
> - fuse_change_attributes(inode, &outarg.attr, NULL, ATTR_TIMEOUT(&outarg),
> + fuse_change_attributes(inode, &outarg.attr, sx, ATTR_TIMEOUT(&outarg),
> attr_version);
> fuse_change_entry_timeout(entry, &outarg);
> return 1;
> +
> +forget:
> + fuse_chan_queue_forget(fc->chan, forget, outarg.nodeid, 1);
> + return ret;
> }
>
> static int fuse_dentry_init(struct dentry *dentry)
> @@ -631,6 +649,8 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
> struct fuse_forget_link *forget;
> u64 attr_version, evict_ctr;
> struct fuse_entry2_out outarg2;
> + struct fuse_statx *sx = NULL;
> + int backing_id;
> int err;
>
> *inode = NULL;
> @@ -649,9 +669,18 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
>
> fuse_lookup_init(fm->fc, &args, nodeid, name, outarg, &outarg2);
> err = fuse_simple_request(fm, &args);
> + if (err)
> + goto out_put_forget;
> +
> + backing_id = fuse_process_entry2(fm->fc, &outarg2, outarg, &sx);
> +
> /* Zero nodeid is same as -ENOENT, but with valid timeout */
> - if (err || !outarg->nodeid)
> + if (!outarg->nodeid)
> goto out_put_forget;
> + if (backing_id < 0) {
> + err = backing_id;
> + goto out_send_forget;
> + }
>
> err = -EIO;
> if (fuse_invalid_attr(&outarg->attr))
> @@ -662,19 +691,28 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
> }
>
> *inode = fuse_iget(sb, outarg->nodeid, outarg->generation,
> - &outarg->attr, NULL, ATTR_TIMEOUT(outarg),
> + &outarg->attr, sx, ATTR_TIMEOUT(outarg),
> attr_version, evict_ctr);
> err = -ENOMEM;
> - if (!*inode) {
> - fuse_chan_queue_forget(fm->fc->chan, forget, outarg->nodeid, 1);
> - goto out;
> - }
> + if (!*inode)
> + goto out_send_forget;
> +
> err = 0;
> + if (backing_id) {
> + err = fuse_inode_set_passthrough(*inode, backing_id);
> + if (err) {
> + iput(*inode);
> + *inode = NULL;
> + }
> + }
>
> out_put_forget:
> kfree(forget);
> out:
> return err;
> + out_send_forget:
> + fuse_chan_queue_forget(fm->fc->chan, forget, outarg->nodeid, 1);
> + return err;
> }
>
> static struct dentry *fuse_lookup(struct inode *dir, struct dentry *entry,
> --
> 2.52.0
>
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 14/21] fuse: add passthrough support for entry creation
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (12 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 13/21] fuse: add passthrough lookup Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 15/21] fuse: add passthrough support for create+open Joanne Koong
` (6 subsequent siblings)
20 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Use the new extended fuse_entry2_out reply for entry creation operations
(mknod, mkdir, symlink, link) when FUSE_PASSTHROUGH_INO is enabled.
If the server returns a backing id, the newly created inode will be
automatically associated with the backing file for passthrough
operations. If no backing id is returned (no passthrough), the statx
attributes are cached.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 32 ++++++++++++++++++++++++++++----
1 file changed, 28 insertions(+), 4 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 3a6adae530da..a11f9e4c1999 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -1088,9 +1088,12 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
struct dentry *entry, umode_t mode)
{
struct fuse_entry_out outarg;
+ struct fuse_entry2_out outarg2;
struct inode *inode;
struct dentry *d;
struct fuse_forget_link *forget;
+ struct fuse_statx *sx = NULL;
+ int backing_id;
int epoch, err;
if (fuse_is_bad(dir))
@@ -1102,11 +1105,18 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
if (!forget)
return ERR_PTR(-ENOMEM);
- memset(&outarg, 0, sizeof(outarg));
args->nodeid = get_node_id(dir);
args->out_numargs = 1;
- args->out_args[0].size = sizeof(outarg);
- args->out_args[0].value = &outarg;
+
+ if (fuse_use_entry2(fm->fc)) {
+ memset(&outarg2, 0, sizeof(outarg2));
+ args->out_args[0].size = sizeof(outarg2);
+ args->out_args[0].value = &outarg2;
+ } else {
+ memset(&outarg, 0, sizeof(outarg));
+ args->out_args[0].size = sizeof(outarg);
+ args->out_args[0].value = &outarg;
+ }
if (args->opcode != FUSE_LINK) {
err = get_create_ext(idmap, args, dir, entry, mode);
@@ -1119,21 +1129,35 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
if (err)
goto out_put_forget_req;
+ backing_id = fuse_process_entry2(fm->fc, &outarg2, &outarg, &sx);
+
err = -EIO;
if (invalid_nodeid(outarg.nodeid) || fuse_invalid_attr(&outarg.attr))
goto out_put_forget_req;
+ if (backing_id < 0) {
+ fuse_chan_queue_forget(fm->fc->chan, forget, outarg.nodeid, 1);
+ return ERR_PTR(backing_id);
+ }
if ((outarg.attr.mode ^ mode) & S_IFMT)
goto out_put_forget_req;
inode = fuse_iget(dir->i_sb, outarg.nodeid, outarg.generation,
- &outarg.attr, NULL, ATTR_TIMEOUT(&outarg), 0, 0);
+ &outarg.attr, sx, ATTR_TIMEOUT(&outarg), 0, 0);
if (!inode) {
fuse_chan_queue_forget(fm->fc->chan, forget, outarg.nodeid, 1);
return ERR_PTR(-ENOMEM);
}
kfree(forget);
+ if (backing_id) {
+ err = fuse_inode_set_passthrough(inode, backing_id);
+ if (err) {
+ iput(inode);
+ return ERR_PTR(err);
+ }
+ }
+
d_drop(entry);
d = d_splice_alias(inode, entry);
if (IS_ERR(d))
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* [PATCH v2 15/21] fuse: add passthrough support for create+open
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (13 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 14/21] fuse: add passthrough support for entry creation Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 16/21] fuse: allow backing_id=0 in open to inherit inode's backing file Joanne Koong
` (5 subsequent siblings)
20 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Use the extended fuse_entry2_out reply for FUSE_CREATE and FUSE_TMPFILE
when FUSE_PASSTHROUGH_INO is enabled. If the server returns a backing
id, the newly created inode is associated with the backing file for
passthrough operations before the dentry is instantiated.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 43 +++++++++++++++++++++++++++++++++++--------
1 file changed, 35 insertions(+), 8 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index a11f9e4c1999..4c7e3e1604af 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -934,9 +934,12 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
struct fuse_create_in inarg;
struct fuse_open_out *outopenp;
struct fuse_entry_out outentry;
+ struct fuse_entry2_out outentry2;
+ struct fuse_statx *sx = NULL;
struct fuse_inode *fi;
struct fuse_file *ff;
int epoch, err;
+ int backing_id;
bool trunc = flags & O_TRUNC;
/* Userspace expects S_IFREG in create mode */
@@ -957,7 +960,6 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
flags &= ~O_NOCTTY;
memset(&inarg, 0, sizeof(inarg));
- memset(&outentry, 0, sizeof(outentry));
inarg.flags = flags;
inarg.mode = mode;
inarg.umask = current_umask();
@@ -975,8 +977,15 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
args.in_args[1].size = entry->d_name.len + 1;
args.in_args[1].value = entry->d_name.name;
args.out_numargs = 2;
- args.out_args[0].size = sizeof(outentry);
- args.out_args[0].value = &outentry;
+ if (fuse_use_entry2(fm->fc)) {
+ memset(&outentry2, 0, sizeof(outentry2));
+ args.out_args[0].size = sizeof(outentry2);
+ args.out_args[0].value = &outentry2;
+ } else {
+ memset(&outentry, 0, sizeof(outentry));
+ args.out_args[0].size = sizeof(outentry);
+ args.out_args[0].value = &outentry;
+ }
/* Store outarg for fuse_finish_open() */
outopenp = &ff->args->open_outarg;
args.out_args[1].size = sizeof(*outopenp);
@@ -991,6 +1000,8 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
if (err)
goto out_free_ff;
+ backing_id = fuse_process_entry2(fm->fc, &outentry2, &outentry, &sx);
+
err = -EIO;
if (!S_ISREG(outentry.attr.mode) || invalid_nodeid(outentry.nodeid) ||
fuse_invalid_attr(&outentry.attr))
@@ -999,16 +1010,26 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
ff->fh = outopenp->fh;
ff->nodeid = outentry.nodeid;
ff->open_flags = outopenp->open_flags;
+
+ if (backing_id < 0) {
+ err = backing_id;
+ goto out_queue_forget;
+ }
+
inode = fuse_iget(dir->i_sb, outentry.nodeid, outentry.generation,
- &outentry.attr, NULL, ATTR_TIMEOUT(&outentry), 0, 0);
+ &outentry.attr, sx, ATTR_TIMEOUT(&outentry), 0, 0);
if (!inode) {
- flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
- fuse_sync_release(NULL, ff, flags, false);
- fuse_chan_queue_forget(fm->fc->chan, forget, outentry.nodeid, 1);
err = -ENOMEM;
- goto out_err;
+ goto out_queue_forget;
}
kfree(forget);
+ if (backing_id) {
+ err = fuse_inode_set_passthrough(inode, backing_id);
+ if (err) {
+ iput(inode);
+ goto out_sync_release;
+ }
+ }
d_instantiate(entry, inode);
entry->d_time = epoch;
fuse_change_entry_timeout(entry, &outentry);
@@ -1035,6 +1056,12 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
kfree(forget);
out_err:
return err;
+out_queue_forget:
+ fuse_chan_queue_forget(fm->fc->chan, forget, outentry.nodeid, 1);
+out_sync_release:
+ flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
+ fuse_sync_release(NULL, ff, flags, false);
+ return err;
}
static int fuse_mknod(struct mnt_idmap *, struct inode *, struct dentry *,
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* [PATCH v2 16/21] fuse: allow backing_id=0 in open to inherit inode's backing file
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (14 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 15/21] fuse: add passthrough support for create+open Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 21:07 ` Amir Goldstein
2026-05-16 0:40 ` [PATCH v2 17/21] backing-inode: add backing_inode_copyattr() Joanne Koong
` (4 subsequent siblings)
20 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
With FUSE_PASSTHROUGH_INO, the backing file is associated with the inode
at lookup time. Allow the server to pass in backing_id=0 in the open
response. On the kernel side, this uses the backing file already set up
on the inode.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/passthrough.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index d62a1c751157..ae0137caa06d 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -193,13 +193,17 @@ int fuse_passthrough_readdir(struct file *file, struct dir_context *ctx)
*/
struct fuse_backing *fuse_passthrough_open(struct file *file, int backing_id)
{
+ struct fuse_inode *fi = get_fuse_inode(file_inode(file));
struct fuse_file *ff = file->private_data;
struct fuse_conn *fc = ff->fm->fc;
struct fuse_backing *fb = NULL;
struct file *backing_file;
int err;
- fb = fuse_backing_id_get(fc, backing_id);
+ if (!backing_id && fc->passthrough_ino)
+ fb = fuse_backing_get(fuse_inode_backing(fi));
+ else
+ fb = fuse_backing_id_get(fc, backing_id);
if (IS_ERR_OR_NULL(fb)) {
err = fb ? PTR_ERR(fb) : -ENOENT;
fb = NULL;
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 16/21] fuse: allow backing_id=0 in open to inherit inode's backing file
2026-05-16 0:39 ` [PATCH v2 16/21] fuse: allow backing_id=0 in open to inherit inode's backing file Joanne Koong
@ 2026-05-16 21:07 ` Amir Goldstein
0 siblings, 0 replies; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 21:07 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 2:52 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> With FUSE_PASSTHROUGH_INO, the backing file is associated with the inode
> at lookup time. Allow the server to pass in backing_id=0 in the open
> response. On the kernel side, this uses the backing file already set up
> on the inode.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
> ---
> fs/fuse/passthrough.c | 6 +++++-
> 1 file changed, 5 insertions(+), 1 deletion(-)
>
> diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> index d62a1c751157..ae0137caa06d 100644
> --- a/fs/fuse/passthrough.c
> +++ b/fs/fuse/passthrough.c
> @@ -193,13 +193,17 @@ int fuse_passthrough_readdir(struct file *file, struct dir_context *ctx)
> */
> struct fuse_backing *fuse_passthrough_open(struct file *file, int backing_id)
> {
> + struct fuse_inode *fi = get_fuse_inode(file_inode(file));
> struct fuse_file *ff = file->private_data;
> struct fuse_conn *fc = ff->fm->fc;
> struct fuse_backing *fb = NULL;
> struct file *backing_file;
> int err;
>
> - fb = fuse_backing_id_get(fc, backing_id);
> + if (!backing_id && fc->passthrough_ino)
> + fb = fuse_backing_get(fuse_inode_backing(fi));
> + else
> + fb = fuse_backing_id_get(fc, backing_id);
> if (IS_ERR_OR_NULL(fb)) {
> err = fb ? PTR_ERR(fb) : -ENOENT;
> fb = NULL;
> --
> 2.52.0
>
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 17/21] backing-inode: add backing_inode_copyattr()
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (15 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 16/21] fuse: allow backing_id=0 in open to inherit inode's backing file Joanne Koong
@ 2026-05-16 0:40 ` Joanne Koong
2026-05-16 21:32 ` Amir Goldstein
2026-05-16 0:40 ` [PATCH v2 18/21] backing-inode: add backing_inode_setattr() Joanne Koong
` (3 subsequent siblings)
20 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:40 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Move logic in ovl_copyattr() to a generic backing_inode_copyattr()
function in a new fs/backing-inode.c, which other filesystems that use
backing inodes (eg fuse passthrough) will use.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/Makefile | 2 +-
fs/backing-inode.c | 43 +++++++++++++++++++++++++++++++++++
fs/overlayfs/util.c | 33 +++------------------------
include/linux/backing-inode.h | 14 ++++++++++++
4 files changed, 61 insertions(+), 31 deletions(-)
create mode 100644 fs/backing-inode.c
create mode 100644 include/linux/backing-inode.h
diff --git a/fs/Makefile b/fs/Makefile
index cf4a745e9679..3f8a227d4938 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -40,7 +40,7 @@ obj-$(CONFIG_COMPAT_BINFMT_ELF) += compat_binfmt_elf.o
obj-$(CONFIG_BINFMT_ELF_FDPIC) += binfmt_elf_fdpic.o
obj-$(CONFIG_BINFMT_FLAT) += binfmt_flat.o
-obj-$(CONFIG_FS_STACK) += backing-file.o
+obj-$(CONFIG_FS_STACK) += backing-file.o backing-inode.o
obj-$(CONFIG_FS_MBCACHE) += mbcache.o
obj-$(CONFIG_FS_POSIX_ACL) += posix_acl.o
obj-$(CONFIG_NFS_COMMON) += nfs_common/
diff --git a/fs/backing-inode.c b/fs/backing-inode.c
new file mode 100644
index 000000000000..474770a1fa9d
--- /dev/null
+++ b/fs/backing-inode.c
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Common helpers for stackable filesystems and backing inodes.
+ */
+
+#include <linux/backing-inode.h>
+
+/*
+ * backing_inode_copyattr - copy inode attributes from a backing inode
+ *
+ * When a filesystem copies inode information from a backing layer to its own
+ * inode, it applies the idmapping of the backing mount, ensuring that
+ * the inode ownership will correctly reflect the ownership of the idmapped
+ * backing mount. For example, an idmapped backing mount mapping id 1001 to id
+ * 1000 will take care to map any backing inode owned by id 1001 to id 1000.
+ * These mapping helpers are nops when the backing mount isn't idmapped.
+ */
+void backing_inode_copyattr(struct inode *inode,
+ const struct path *backing_path)
+{
+ struct inode *realinode;
+ struct mnt_idmap *real_idmap;
+ vfsuid_t vfsuid;
+ vfsgid_t vfsgid;
+
+ realinode = d_inode_rcu(backing_path->dentry);
+ real_idmap = mnt_idmap(backing_path->mnt);
+
+ spin_lock(&inode->i_lock);
+ vfsuid = i_uid_into_vfsuid(real_idmap, realinode);
+ vfsgid = i_gid_into_vfsgid(real_idmap, realinode);
+
+ inode->i_uid = vfsuid_into_kuid(vfsuid);
+ inode->i_gid = vfsgid_into_kgid(vfsgid);
+ inode->i_mode = realinode->i_mode;
+ inode_set_atime_to_ts(inode, inode_get_atime(realinode));
+ inode_set_mtime_to_ts(inode, inode_get_mtime(realinode));
+ inode_set_ctime_to_ts(inode, inode_get_ctime(realinode));
+ i_size_write(inode, i_size_read(realinode));
+ spin_unlock(&inode->i_lock);
+}
+EXPORT_SYMBOL_GPL(backing_inode_copyattr);
+
diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c
index 3f1b763a8bb4..3e7d66c26c10 100644
--- a/fs/overlayfs/util.c
+++ b/fs/overlayfs/util.c
@@ -16,6 +16,7 @@
#include <linux/namei.h>
#include <linux/ratelimit.h>
#include <linux/overflow.h>
+#include <linux/backing-inode.h>
#include "overlayfs.h"
/* Get write access to upper mnt - may fail if upper sb was remounted ro */
@@ -1503,38 +1504,10 @@ int ovl_sync_status(struct ovl_fs *ofs)
return errseq_check(&mnt->mnt_sb->s_wb_err, ofs->errseq);
}
-/*
- * ovl_copyattr() - copy inode attributes from layer to ovl inode
- *
- * When overlay copies inode information from an upper or lower layer to the
- * relevant overlay inode it will apply the idmapping of the upper or lower
- * layer when doing so ensuring that the ovl inode ownership will correctly
- * reflect the ownership of the idmapped upper or lower layer. For example, an
- * idmapped upper or lower layer mapping id 1001 to id 1000 will take care to
- * map any lower or upper inode owned by id 1001 to id 1000. These mapping
- * helpers are nops when the relevant layer isn't idmapped.
- */
void ovl_copyattr(struct inode *inode)
{
struct path realpath;
- struct inode *realinode;
- struct mnt_idmap *real_idmap;
- vfsuid_t vfsuid;
- vfsgid_t vfsgid;
- realinode = ovl_i_path_real(inode, &realpath);
- real_idmap = mnt_idmap(realpath.mnt);
-
- spin_lock(&inode->i_lock);
- vfsuid = i_uid_into_vfsuid(real_idmap, realinode);
- vfsgid = i_gid_into_vfsgid(real_idmap, realinode);
-
- inode->i_uid = vfsuid_into_kuid(vfsuid);
- inode->i_gid = vfsgid_into_kgid(vfsgid);
- inode->i_mode = realinode->i_mode;
- inode_set_atime_to_ts(inode, inode_get_atime(realinode));
- inode_set_mtime_to_ts(inode, inode_get_mtime(realinode));
- inode_set_ctime_to_ts(inode, inode_get_ctime(realinode));
- i_size_write(inode, i_size_read(realinode));
- spin_unlock(&inode->i_lock);
+ ovl_i_path_real(inode, &realpath);
+ backing_inode_copyattr(inode, &realpath);
}
diff --git a/include/linux/backing-inode.h b/include/linux/backing-inode.h
new file mode 100644
index 000000000000..6b43cba9fabd
--- /dev/null
+++ b/include/linux/backing-inode.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Common helpers for stackable filesystems and backing inodes.
+ */
+
+#ifndef _LINUX_BACKING_INODE_H
+#define _LINUX_BACKING_INODE_H
+
+#include <linux/fs.h>
+
+void backing_inode_copyattr(struct inode *inode,
+ const struct path *backing_path);
+
+#endif /* _LINUX_BACKING_INODE_H */
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 17/21] backing-inode: add backing_inode_copyattr()
2026-05-16 0:40 ` [PATCH v2 17/21] backing-inode: add backing_inode_copyattr() Joanne Koong
@ 2026-05-16 21:32 ` Amir Goldstein
2026-05-18 23:21 ` Joanne Koong
0 siblings, 1 reply; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 21:32 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs, Christian Brauner
On Sat, May 16, 2026 at 2:53 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Move logic in ovl_copyattr() to a generic backing_inode_copyattr()
> function in a new fs/backing-inode.c, which other filesystems that use
> backing inodes (eg fuse passthrough) will use.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
> fs/Makefile | 2 +-
> fs/backing-inode.c | 43 +++++++++++++++++++++++++++++++++++
> fs/overlayfs/util.c | 33 +++------------------------
> include/linux/backing-inode.h | 14 ++++++++++++
Nice!
Please add these files to
FILESYSTEMS [STACKABLE]
section in MAINTAINERS.
Otherwise, feel free to add:
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
There is one more cleanup patch required, now or later, to get rid of
some awkwardness in kernel fs code.
If you don't want to deal with it, I can take this cleanup later.
d_backing_inode() is a noop from the day it was added and has 177
call sites. Those call sites should be converted to d_inode() and
d_backing_inode() should go away.
backing_inode_security{,_novalidate}() are likewise unneeded
dups of inode_security{,_novalidate}() and should be removed.
Thanks,
Amir.
> 4 files changed, 61 insertions(+), 31 deletions(-)
> create mode 100644 fs/backing-inode.c
> create mode 100644 include/linux/backing-inode.h
>
> diff --git a/fs/Makefile b/fs/Makefile
> index cf4a745e9679..3f8a227d4938 100644
> --- a/fs/Makefile
> +++ b/fs/Makefile
> @@ -40,7 +40,7 @@ obj-$(CONFIG_COMPAT_BINFMT_ELF) += compat_binfmt_elf.o
> obj-$(CONFIG_BINFMT_ELF_FDPIC) += binfmt_elf_fdpic.o
> obj-$(CONFIG_BINFMT_FLAT) += binfmt_flat.o
>
> -obj-$(CONFIG_FS_STACK) += backing-file.o
> +obj-$(CONFIG_FS_STACK) += backing-file.o backing-inode.o
> obj-$(CONFIG_FS_MBCACHE) += mbcache.o
> obj-$(CONFIG_FS_POSIX_ACL) += posix_acl.o
> obj-$(CONFIG_NFS_COMMON) += nfs_common/
> diff --git a/fs/backing-inode.c b/fs/backing-inode.c
> new file mode 100644
> index 000000000000..474770a1fa9d
> --- /dev/null
> +++ b/fs/backing-inode.c
> @@ -0,0 +1,43 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Common helpers for stackable filesystems and backing inodes.
> + */
> +
> +#include <linux/backing-inode.h>
> +
> +/*
> + * backing_inode_copyattr - copy inode attributes from a backing inode
> + *
> + * When a filesystem copies inode information from a backing layer to its own
> + * inode, it applies the idmapping of the backing mount, ensuring that
> + * the inode ownership will correctly reflect the ownership of the idmapped
> + * backing mount. For example, an idmapped backing mount mapping id 1001 to id
> + * 1000 will take care to map any backing inode owned by id 1001 to id 1000.
> + * These mapping helpers are nops when the backing mount isn't idmapped.
> + */
> +void backing_inode_copyattr(struct inode *inode,
> + const struct path *backing_path)
> +{
> + struct inode *realinode;
> + struct mnt_idmap *real_idmap;
> + vfsuid_t vfsuid;
> + vfsgid_t vfsgid;
> +
> + realinode = d_inode_rcu(backing_path->dentry);
> + real_idmap = mnt_idmap(backing_path->mnt);
> +
> + spin_lock(&inode->i_lock);
> + vfsuid = i_uid_into_vfsuid(real_idmap, realinode);
> + vfsgid = i_gid_into_vfsgid(real_idmap, realinode);
> +
> + inode->i_uid = vfsuid_into_kuid(vfsuid);
> + inode->i_gid = vfsgid_into_kgid(vfsgid);
> + inode->i_mode = realinode->i_mode;
> + inode_set_atime_to_ts(inode, inode_get_atime(realinode));
> + inode_set_mtime_to_ts(inode, inode_get_mtime(realinode));
> + inode_set_ctime_to_ts(inode, inode_get_ctime(realinode));
> + i_size_write(inode, i_size_read(realinode));
> + spin_unlock(&inode->i_lock);
> +}
> +EXPORT_SYMBOL_GPL(backing_inode_copyattr);
> +
> diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c
> index 3f1b763a8bb4..3e7d66c26c10 100644
> --- a/fs/overlayfs/util.c
> +++ b/fs/overlayfs/util.c
> @@ -16,6 +16,7 @@
> #include <linux/namei.h>
> #include <linux/ratelimit.h>
> #include <linux/overflow.h>
> +#include <linux/backing-inode.h>
> #include "overlayfs.h"
>
> /* Get write access to upper mnt - may fail if upper sb was remounted ro */
> @@ -1503,38 +1504,10 @@ int ovl_sync_status(struct ovl_fs *ofs)
> return errseq_check(&mnt->mnt_sb->s_wb_err, ofs->errseq);
> }
>
> -/*
> - * ovl_copyattr() - copy inode attributes from layer to ovl inode
> - *
> - * When overlay copies inode information from an upper or lower layer to the
> - * relevant overlay inode it will apply the idmapping of the upper or lower
> - * layer when doing so ensuring that the ovl inode ownership will correctly
> - * reflect the ownership of the idmapped upper or lower layer. For example, an
> - * idmapped upper or lower layer mapping id 1001 to id 1000 will take care to
> - * map any lower or upper inode owned by id 1001 to id 1000. These mapping
> - * helpers are nops when the relevant layer isn't idmapped.
> - */
> void ovl_copyattr(struct inode *inode)
> {
> struct path realpath;
> - struct inode *realinode;
> - struct mnt_idmap *real_idmap;
> - vfsuid_t vfsuid;
> - vfsgid_t vfsgid;
>
> - realinode = ovl_i_path_real(inode, &realpath);
> - real_idmap = mnt_idmap(realpath.mnt);
> -
> - spin_lock(&inode->i_lock);
> - vfsuid = i_uid_into_vfsuid(real_idmap, realinode);
> - vfsgid = i_gid_into_vfsgid(real_idmap, realinode);
> -
> - inode->i_uid = vfsuid_into_kuid(vfsuid);
> - inode->i_gid = vfsgid_into_kgid(vfsgid);
> - inode->i_mode = realinode->i_mode;
> - inode_set_atime_to_ts(inode, inode_get_atime(realinode));
> - inode_set_mtime_to_ts(inode, inode_get_mtime(realinode));
> - inode_set_ctime_to_ts(inode, inode_get_ctime(realinode));
> - i_size_write(inode, i_size_read(realinode));
> - spin_unlock(&inode->i_lock);
> + ovl_i_path_real(inode, &realpath);
> + backing_inode_copyattr(inode, &realpath);
> }
> diff --git a/include/linux/backing-inode.h b/include/linux/backing-inode.h
> new file mode 100644
> index 000000000000..6b43cba9fabd
> --- /dev/null
> +++ b/include/linux/backing-inode.h
> @@ -0,0 +1,14 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +/*
> + * Common helpers for stackable filesystems and backing inodes.
> + */
> +
> +#ifndef _LINUX_BACKING_INODE_H
> +#define _LINUX_BACKING_INODE_H
> +
> +#include <linux/fs.h>
> +
> +void backing_inode_copyattr(struct inode *inode,
> + const struct path *backing_path);
> +
> +#endif /* _LINUX_BACKING_INODE_H */
> --
> 2.52.0
>
^ permalink raw reply [flat|nested] 54+ messages in thread* Re: [PATCH v2 17/21] backing-inode: add backing_inode_copyattr()
2026-05-16 21:32 ` Amir Goldstein
@ 2026-05-18 23:21 ` Joanne Koong
2026-05-19 11:47 ` Amir Goldstein
0 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-18 23:21 UTC (permalink / raw)
To: Amir Goldstein; +Cc: miklos, fuse-devel, linux-unionfs, Christian Brauner
On Sat, May 16, 2026 at 2:33 PM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Sat, May 16, 2026 at 2:53 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > Move logic in ovl_copyattr() to a generic backing_inode_copyattr()
> > function in a new fs/backing-inode.c, which other filesystems that use
> > backing inodes (eg fuse passthrough) will use.
> >
> > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > ---
> > fs/Makefile | 2 +-
> > fs/backing-inode.c | 43 +++++++++++++++++++++++++++++++++++
> > fs/overlayfs/util.c | 33 +++------------------------
> > include/linux/backing-inode.h | 14 ++++++++++++
>
>
> Nice!
>
> Please add these files to
> FILESYSTEMS [STACKABLE]
> section in MAINTAINERS.
>
> Otherwise, feel free to add:
> Reviewed-by: Amir Goldstein <amir73il@gmail.com>
>
> There is one more cleanup patch required, now or later, to get rid of
> some awkwardness in kernel fs code.
> If you don't want to deal with it, I can take this cleanup later.
>
> d_backing_inode() is a noop from the day it was added and has 177
> call sites. Those call sites should be converted to d_inode() and
> d_backing_inode() should go away.
>
> backing_inode_security{,_novalidate}() are likewise unneeded
> dups of inode_security{,_novalidate}() and should be removed.
Awesome! I'm happy to remove these.
You mentioned in v1 that fs/stack.c and the corresponding
include/linux/fs_stack.h are pretty much only used by ecryptfs. It
looks like there are only two pretty minimal functions in there. Do
you think it's also worth moving that logic into ecryptfs and
eliminating fs stack?
Thanks,
Joanne
>
> Thanks,
> Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread* Re: [PATCH v2 17/21] backing-inode: add backing_inode_copyattr()
2026-05-18 23:21 ` Joanne Koong
@ 2026-05-19 11:47 ` Amir Goldstein
0 siblings, 0 replies; 54+ messages in thread
From: Amir Goldstein @ 2026-05-19 11:47 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs, Christian Brauner
On Tue, May 19, 2026 at 1:21 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Sat, May 16, 2026 at 2:33 PM Amir Goldstein <amir73il@gmail.com> wrote:
> >
> > On Sat, May 16, 2026 at 2:53 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> > >
> > > Move logic in ovl_copyattr() to a generic backing_inode_copyattr()
> > > function in a new fs/backing-inode.c, which other filesystems that use
> > > backing inodes (eg fuse passthrough) will use.
> > >
> > > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > > ---
> > > fs/Makefile | 2 +-
> > > fs/backing-inode.c | 43 +++++++++++++++++++++++++++++++++++
> > > fs/overlayfs/util.c | 33 +++------------------------
> > > include/linux/backing-inode.h | 14 ++++++++++++
> >
> >
> > Nice!
> >
> > Please add these files to
> > FILESYSTEMS [STACKABLE]
> > section in MAINTAINERS.
> >
> > Otherwise, feel free to add:
> > Reviewed-by: Amir Goldstein <amir73il@gmail.com>
> >
> > There is one more cleanup patch required, now or later, to get rid of
> > some awkwardness in kernel fs code.
> > If you don't want to deal with it, I can take this cleanup later.
> >
> > d_backing_inode() is a noop from the day it was added and has 177
> > call sites. Those call sites should be converted to d_inode() and
> > d_backing_inode() should go away.
> >
> > backing_inode_security{,_novalidate}() are likewise unneeded
> > dups of inode_security{,_novalidate}() and should be removed.
>
> Awesome! I'm happy to remove these.
>
> You mentioned in v1 that fs/stack.c and the corresponding
> include/linux/fs_stack.h are pretty much only used by ecryptfs. It
> looks like there are only two pretty minimal functions in there. Do
> you think it's also worth moving that logic into ecryptfs and
> eliminating fs stack?
I wanted to do this once, but ecryptfs is not actively maintained
(odd fixes only), so the maintainer did not want to take any changes.
Let's leave it.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 18/21] backing-inode: add backing_inode_setattr()
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (16 preceding siblings ...)
2026-05-16 0:40 ` [PATCH v2 17/21] backing-inode: add backing_inode_copyattr() Joanne Koong
@ 2026-05-16 0:40 ` Joanne Koong
2026-05-16 22:47 ` Amir Goldstein
2026-05-16 0:40 ` [PATCH v2 19/21] fuse: add passthrough setattr Joanne Koong
` (2 subsequent siblings)
20 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:40 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Move logic in ovl_setattr() to a generic backing_inode_setattr() function
in fs/backing-inode.c, which other filesystems that use backing inodes
(eg fuse passthrough) will use.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/backing-inode.c | 54 ++++++++++++++++++++++++++++
fs/overlayfs/inode.c | 66 ++++++++++-------------------------
include/linux/backing-inode.h | 4 +++
3 files changed, 76 insertions(+), 48 deletions(-)
diff --git a/fs/backing-inode.c b/fs/backing-inode.c
index 474770a1fa9d..e72f278789d8 100644
--- a/fs/backing-inode.c
+++ b/fs/backing-inode.c
@@ -41,3 +41,57 @@ void backing_inode_copyattr(struct inode *inode,
}
EXPORT_SYMBOL_GPL(backing_inode_copyattr);
+int backing_inode_setattr(struct dentry *dentry,
+ const struct path *backing_path,
+ struct iattr *attr, const struct cred *cred)
+{
+ struct dentry *backing_dentry = backing_path->dentry;
+ struct inode *winode = NULL;
+ int err;
+
+ if (attr->ia_valid & ATTR_SIZE) {
+ winode = d_inode(backing_dentry);
+ err = get_write_access(winode);
+ if (err)
+ return err;
+ }
+
+ /*
+ * Clear ATTR_MODE to avoid BUG_ON in notify_change(), which does not
+ * allow ATTR_MODE together with ATTR_KILL_SUID/SGID (the first
+ * notify_change() converted ATTR_KILL_SUID/SGID into ATTR_MODE using
+ * @dentry's inode mode).
+ *
+ * This also ensures notify_change() recomputes the mode from the
+ * backing inode's current mode, instead of a potentially stale value
+ * from @dentry's inode.
+ */
+ if (attr->ia_valid & (ATTR_KILL_SUID|ATTR_KILL_SGID))
+ attr->ia_valid &= ~ATTR_MODE;
+
+ /*
+ * The filesystem's file is not meaningful to the backing filesystem.
+ * Clear ATTR_FILE so the backing filesystem does not try to use it.
+ */
+ attr->ia_valid &= ~ATTR_FILE;
+
+ err = mnt_want_write(backing_path->mnt);
+ if (err)
+ goto out_put_write;
+
+ inode_lock(backing_dentry->d_inode);
+ scoped_with_creds(cred)
+ err = notify_change(mnt_idmap(backing_path->mnt),
+ backing_dentry, attr, NULL);
+ if (!err)
+ backing_inode_copyattr(dentry->d_inode, backing_path);
+ inode_unlock(backing_dentry->d_inode);
+ mnt_drop_write(backing_path->mnt);
+
+out_put_write:
+ if (winode)
+ put_write_access(winode);
+
+ return err;
+}
+EXPORT_SYMBOL_GPL(backing_inode_setattr);
diff --git a/fs/overlayfs/inode.c b/fs/overlayfs/inode.c
index 00c69707bda9..f1cd67e6e9c8 100644
--- a/fs/overlayfs/inode.c
+++ b/fs/overlayfs/inode.c
@@ -15,16 +15,17 @@
#include <linux/namei.h>
#include <linux/posix_acl.h>
#include <linux/posix_acl_xattr.h>
+#include <linux/backing-inode.h>
#include "overlayfs.h"
int ovl_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
struct iattr *attr)
{
+ struct path backing_path;
int err;
struct ovl_fs *ofs = OVL_FS(dentry->d_sb);
bool full_copy_up = false;
- struct dentry *upperdentry;
err = setattr_prepare(&nop_mnt_idmap, dentry, attr);
if (err)
@@ -39,57 +40,26 @@ int ovl_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
err = ovl_copy_up(dentry);
else
err = ovl_copy_up_with_data(dentry);
- if (!err) {
- struct inode *winode = NULL;
-
- upperdentry = ovl_dentry_upper(dentry);
-
- if (attr->ia_valid & ATTR_SIZE) {
- winode = d_inode(upperdentry);
- err = get_write_access(winode);
- if (err)
- goto out;
- }
-
- if (attr->ia_valid & (ATTR_KILL_SUID|ATTR_KILL_SGID))
- attr->ia_valid &= ~ATTR_MODE;
- /*
- * We might have to translate ovl file into real file object
- * once use cases emerge. For now, simply don't let underlying
- * filesystem rely on attr->ia_file
- */
- attr->ia_valid &= ~ATTR_FILE;
-
- /*
- * If open(O_TRUNC) is done, VFS calls ->setattr with ATTR_OPEN
- * set. Overlayfs does not pass O_TRUNC flag to underlying
- * filesystem during open -> do not pass ATTR_OPEN. This
- * disables optimization in fuse which assumes open(O_TRUNC)
- * already set file size to 0. But we never passed O_TRUNC to
- * fuse. So by clearing ATTR_OPEN, fuse will be forced to send
- * setattr request to server.
- */
- attr->ia_valid &= ~ATTR_OPEN;
+ if (err)
+ return err;
- err = ovl_want_write(dentry);
- if (err)
- goto out_put_write;
+ /*
+ * If open(O_TRUNC) is done, VFS calls ->setattr with ATTR_OPEN
+ * set. Overlayfs does not pass O_TRUNC flag to underlying
+ * filesystem during open -> do not pass ATTR_OPEN. This
+ * disables optimization in fuse which assumes open(O_TRUNC)
+ * already set file size to 0. But we never passed O_TRUNC to
+ * fuse. So by clearing ATTR_OPEN, fuse will be forced to send
+ * setattr request to server.
+ */
+ attr->ia_valid &= ~ATTR_OPEN;
- inode_lock(upperdentry->d_inode);
- with_ovl_creds(dentry->d_sb)
- err = ovl_do_notify_change(ofs, upperdentry, attr);
- if (!err)
- ovl_copyattr(dentry->d_inode);
- inode_unlock(upperdentry->d_inode);
- ovl_drop_write(dentry);
+ backing_path.dentry = ovl_dentry_upper(dentry);
+ backing_path.mnt = ovl_upper_mnt(ofs);
-out_put_write:
- if (winode)
- put_write_access(winode);
- }
-out:
- return err;
+ return backing_inode_setattr(dentry, &backing_path, attr,
+ ovl_creds(dentry->d_sb));
}
static void ovl_map_dev_ino(struct dentry *dentry, struct kstat *stat, int fsid)
diff --git a/include/linux/backing-inode.h b/include/linux/backing-inode.h
index 6b43cba9fabd..fd02c87e0f99 100644
--- a/include/linux/backing-inode.h
+++ b/include/linux/backing-inode.h
@@ -11,4 +11,8 @@
void backing_inode_copyattr(struct inode *inode,
const struct path *backing_path);
+int backing_inode_setattr(struct dentry *dentry,
+ const struct path *backing_path,
+ struct iattr *attr, const struct cred *cred);
+
#endif /* _LINUX_BACKING_INODE_H */
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 18/21] backing-inode: add backing_inode_setattr()
2026-05-16 0:40 ` [PATCH v2 18/21] backing-inode: add backing_inode_setattr() Joanne Koong
@ 2026-05-16 22:47 ` Amir Goldstein
0 siblings, 0 replies; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 22:47 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 2:53 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Move logic in ovl_setattr() to a generic backing_inode_setattr() function
> in fs/backing-inode.c, which other filesystems that use backing inodes
> (eg fuse passthrough) will use.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
Wow this code brings back memories that I tried hard to forget :)
I hope that fuse passthrough can reap the benefits of all the bugs
and fixes in ovl_setattr()...
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
> ---
> fs/backing-inode.c | 54 ++++++++++++++++++++++++++++
> fs/overlayfs/inode.c | 66 ++++++++++-------------------------
> include/linux/backing-inode.h | 4 +++
> 3 files changed, 76 insertions(+), 48 deletions(-)
>
> diff --git a/fs/backing-inode.c b/fs/backing-inode.c
> index 474770a1fa9d..e72f278789d8 100644
> --- a/fs/backing-inode.c
> +++ b/fs/backing-inode.c
> @@ -41,3 +41,57 @@ void backing_inode_copyattr(struct inode *inode,
> }
> EXPORT_SYMBOL_GPL(backing_inode_copyattr);
>
> +int backing_inode_setattr(struct dentry *dentry,
> + const struct path *backing_path,
> + struct iattr *attr, const struct cred *cred)
> +{
> + struct dentry *backing_dentry = backing_path->dentry;
> + struct inode *winode = NULL;
> + int err;
> +
> + if (attr->ia_valid & ATTR_SIZE) {
> + winode = d_inode(backing_dentry);
> + err = get_write_access(winode);
> + if (err)
> + return err;
> + }
> +
> + /*
> + * Clear ATTR_MODE to avoid BUG_ON in notify_change(), which does not
> + * allow ATTR_MODE together with ATTR_KILL_SUID/SGID (the first
> + * notify_change() converted ATTR_KILL_SUID/SGID into ATTR_MODE using
> + * @dentry's inode mode).
> + *
> + * This also ensures notify_change() recomputes the mode from the
> + * backing inode's current mode, instead of a potentially stale value
> + * from @dentry's inode.
> + */
> + if (attr->ia_valid & (ATTR_KILL_SUID|ATTR_KILL_SGID))
> + attr->ia_valid &= ~ATTR_MODE;
> +
> + /*
> + * The filesystem's file is not meaningful to the backing filesystem.
> + * Clear ATTR_FILE so the backing filesystem does not try to use it.
> + */
> + attr->ia_valid &= ~ATTR_FILE;
> +
> + err = mnt_want_write(backing_path->mnt);
> + if (err)
> + goto out_put_write;
> +
> + inode_lock(backing_dentry->d_inode);
> + scoped_with_creds(cred)
> + err = notify_change(mnt_idmap(backing_path->mnt),
> + backing_dentry, attr, NULL);
> + if (!err)
> + backing_inode_copyattr(dentry->d_inode, backing_path);
> + inode_unlock(backing_dentry->d_inode);
> + mnt_drop_write(backing_path->mnt);
> +
> +out_put_write:
> + if (winode)
> + put_write_access(winode);
> +
> + return err;
> +}
> +EXPORT_SYMBOL_GPL(backing_inode_setattr);
> diff --git a/fs/overlayfs/inode.c b/fs/overlayfs/inode.c
> index 00c69707bda9..f1cd67e6e9c8 100644
> --- a/fs/overlayfs/inode.c
> +++ b/fs/overlayfs/inode.c
> @@ -15,16 +15,17 @@
> #include <linux/namei.h>
> #include <linux/posix_acl.h>
> #include <linux/posix_acl_xattr.h>
> +#include <linux/backing-inode.h>
> #include "overlayfs.h"
>
>
> int ovl_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
> struct iattr *attr)
> {
> + struct path backing_path;
> int err;
> struct ovl_fs *ofs = OVL_FS(dentry->d_sb);
> bool full_copy_up = false;
> - struct dentry *upperdentry;
>
> err = setattr_prepare(&nop_mnt_idmap, dentry, attr);
> if (err)
> @@ -39,57 +40,26 @@ int ovl_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
> err = ovl_copy_up(dentry);
> else
> err = ovl_copy_up_with_data(dentry);
> - if (!err) {
> - struct inode *winode = NULL;
> -
> - upperdentry = ovl_dentry_upper(dentry);
> -
> - if (attr->ia_valid & ATTR_SIZE) {
> - winode = d_inode(upperdentry);
> - err = get_write_access(winode);
> - if (err)
> - goto out;
> - }
> -
> - if (attr->ia_valid & (ATTR_KILL_SUID|ATTR_KILL_SGID))
> - attr->ia_valid &= ~ATTR_MODE;
>
> - /*
> - * We might have to translate ovl file into real file object
> - * once use cases emerge. For now, simply don't let underlying
> - * filesystem rely on attr->ia_file
> - */
> - attr->ia_valid &= ~ATTR_FILE;
> -
> - /*
> - * If open(O_TRUNC) is done, VFS calls ->setattr with ATTR_OPEN
> - * set. Overlayfs does not pass O_TRUNC flag to underlying
> - * filesystem during open -> do not pass ATTR_OPEN. This
> - * disables optimization in fuse which assumes open(O_TRUNC)
> - * already set file size to 0. But we never passed O_TRUNC to
> - * fuse. So by clearing ATTR_OPEN, fuse will be forced to send
> - * setattr request to server.
> - */
> - attr->ia_valid &= ~ATTR_OPEN;
> + if (err)
> + return err;
>
> - err = ovl_want_write(dentry);
> - if (err)
> - goto out_put_write;
> + /*
> + * If open(O_TRUNC) is done, VFS calls ->setattr with ATTR_OPEN
> + * set. Overlayfs does not pass O_TRUNC flag to underlying
> + * filesystem during open -> do not pass ATTR_OPEN. This
> + * disables optimization in fuse which assumes open(O_TRUNC)
> + * already set file size to 0. But we never passed O_TRUNC to
> + * fuse. So by clearing ATTR_OPEN, fuse will be forced to send
> + * setattr request to server.
> + */
> + attr->ia_valid &= ~ATTR_OPEN;
>
> - inode_lock(upperdentry->d_inode);
> - with_ovl_creds(dentry->d_sb)
> - err = ovl_do_notify_change(ofs, upperdentry, attr);
> - if (!err)
> - ovl_copyattr(dentry->d_inode);
> - inode_unlock(upperdentry->d_inode);
> - ovl_drop_write(dentry);
> + backing_path.dentry = ovl_dentry_upper(dentry);
> + backing_path.mnt = ovl_upper_mnt(ofs);
>
> -out_put_write:
> - if (winode)
> - put_write_access(winode);
> - }
> -out:
> - return err;
> + return backing_inode_setattr(dentry, &backing_path, attr,
> + ovl_creds(dentry->d_sb));
> }
>
> static void ovl_map_dev_ino(struct dentry *dentry, struct kstat *stat, int fsid)
> diff --git a/include/linux/backing-inode.h b/include/linux/backing-inode.h
> index 6b43cba9fabd..fd02c87e0f99 100644
> --- a/include/linux/backing-inode.h
> +++ b/include/linux/backing-inode.h
> @@ -11,4 +11,8 @@
> void backing_inode_copyattr(struct inode *inode,
> const struct path *backing_path);
>
> +int backing_inode_setattr(struct dentry *dentry,
> + const struct path *backing_path,
> + struct iattr *attr, const struct cred *cred);
> +
> #endif /* _LINUX_BACKING_INODE_H */
> --
> 2.52.0
>
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 19/21] fuse: add passthrough setattr
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (17 preceding siblings ...)
2026-05-16 0:40 ` [PATCH v2 18/21] backing-inode: add backing_inode_setattr() Joanne Koong
@ 2026-05-16 0:40 ` Joanne Koong
2026-05-16 1:04 ` Joanne Koong
2026-05-16 22:29 ` Amir Goldstein
2026-05-16 0:40 ` [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling Joanne Koong
2026-05-16 0:40 ` [PATCH v2 21/21] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO) Joanne Koong
20 siblings, 2 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:40 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Add passthrough setattr which sets attributes directly on the backing
inode through backing_inode_setattr() instead of sending FUSE_SETATTR to
the server.
Passthrough setattr is checked before the
handle_killpriv/handle_killpriv_v2 suid/sgid stripping because the
stripping is handled natively by notify_change() on the backing inode.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 3 +++
fs/fuse/fuse_i.h | 3 ++-
fs/fuse/passthrough.c | 28 ++++++++++++++++++++++++++++
include/uapi/linux/fuse.h | 1 +
4 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 4c7e3e1604af..b67b3b334e69 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -2506,6 +2506,9 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
if (!fuse_allow_current_process(get_fuse_conn(inode)))
return -EACCES;
+ if (fuse_passthrough_op(inode, FUSE_SETATTR))
+ return fuse_passthrough_setattr(entry, attr);
+
if (attr->ia_valid & (ATTR_KILL_SUID | ATTR_KILL_SGID)) {
attr->ia_valid &= ~(ATTR_KILL_SUID | ATTR_KILL_SGID |
ATTR_MODE);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 89c9333e9702..0d978a9837a0 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1279,7 +1279,7 @@ ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from);
/* Inode passthrough operations for backing file attached to inode */
#define FUSE_PASSTHROUGH_INODE_OPS \
- (FUSE_PASSTHROUGH_OP_GETATTR)
+ (FUSE_PASSTHROUGH_OP_GETATTR | FUSE_PASSTHROUGH_OP_SETATTR)
#define FUSE_BACKING_MAP_OP(map, op) \
((map)->ops_mask & FUSE_PASSTHROUGH_OP(op))
@@ -1367,6 +1367,7 @@ static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
u32 request_mask, unsigned int flags);
+int fuse_passthrough_setattr(struct dentry *entry, struct iattr *attr);
static inline bool fuse_use_entry2(struct fuse_conn *fc)
{
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index ae0137caa06d..c083ab68537e 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -9,6 +9,8 @@
#include <linux/file.h>
#include <linux/backing-file.h>
+#include <linux/backing-inode.h>
+#include <linux/posix_acl.h>
#include <linux/splice.h>
static void fuse_file_accessed(struct file *file)
@@ -275,3 +277,29 @@ int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
return 0;
}
+
+int fuse_passthrough_setattr(struct dentry *entry, struct iattr *attr)
+{
+ struct inode *inode = d_inode(entry);
+ struct fuse_conn *fc = get_fuse_conn(inode);
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_backing *fb = fuse_inode_backing(fi);
+ struct path path;
+ int err;
+
+ err = setattr_prepare(&nop_mnt_idmap, entry, attr);
+ if (err)
+ return err;
+
+ path.mnt = fb->file->f_path.mnt;
+ path.dentry = fb->file->f_path.dentry;
+
+ err = backing_inode_setattr(entry, &path, attr, fb->cred);
+ if (err)
+ return err;
+
+ if (fc->posix_acl)
+ forget_all_cached_acls(inode);
+
+ return 0;
+}
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 3963631558f9..040fee549bb9 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -1158,6 +1158,7 @@ struct fuse_backing_map {
#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
#define FUSE_PASSTHROUGH_OP_READDIR FUSE_PASSTHROUGH_OP(FUSE_READDIR)
#define FUSE_PASSTHROUGH_OP_GETATTR FUSE_PASSTHROUGH_OP(FUSE_GETATTR)
+#define FUSE_PASSTHROUGH_OP_SETATTR FUSE_PASSTHROUGH_OP(FUSE_SETATTR)
/* Device ioctls: */
#define FUSE_DEV_IOC_MAGIC 229
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 19/21] fuse: add passthrough setattr
2026-05-16 0:40 ` [PATCH v2 19/21] fuse: add passthrough setattr Joanne Koong
@ 2026-05-16 1:04 ` Joanne Koong
2026-05-16 23:03 ` Amir Goldstein
2026-05-16 22:29 ` Amir Goldstein
1 sibling, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 1:04 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
On Fri, May 15, 2026 at 5:53 PM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Add passthrough setattr which sets attributes directly on the backing
> inode through backing_inode_setattr() instead of sending FUSE_SETATTR to
> the server.
>
> Passthrough setattr is checked before the
> handle_killpriv/handle_killpriv_v2 suid/sgid stripping because the
> stripping is handled natively by notify_change() on the backing inode.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
> fs/fuse/dir.c | 3 +++
> fs/fuse/fuse_i.h | 3 ++-
> fs/fuse/passthrough.c | 28 ++++++++++++++++++++++++++++
> include/uapi/linux/fuse.h | 1 +
> 4 files changed, 34 insertions(+), 1 deletion(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index 4c7e3e1604af..b67b3b334e69 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -2506,6 +2506,9 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
> if (!fuse_allow_current_process(get_fuse_conn(inode)))
> return -EACCES;
>
> + if (fuse_passthrough_op(inode, FUSE_SETATTR))
> + return fuse_passthrough_setattr(entry, attr);
In the v1 discussion [1], there was a suggestion about gating
FUSE_PASSTHROUGH_INO on FUSE_HANDLE_KILLPRIV_V2 to avoid the
non-atomic killpriv path, but I think we were only encountering the
non-atomic killpriv path because I was calling
fuse_passthrough_setattr() in the wrong place. I think we can avoid
this altogether by just having the backing filesystem handle all of
the suid/sgid stripping atomically through its own notify_change()
path, which will let us skip all the fuse killpriv handling logic in
this function.
Thanks,
Joanne
[1] https://lore.kernel.org/fuse-devel/CAOQ4uxipQJt5zskb_THxueGi_MXpFdywFiGpd_nmWeY_sMHwzQ@mail.gmail.com/
> +
> if (attr->ia_valid & (ATTR_KILL_SUID | ATTR_KILL_SGID)) {
> attr->ia_valid &= ~(ATTR_KILL_SUID | ATTR_KILL_SGID |
> ATTR_MODE);
^ permalink raw reply [flat|nested] 54+ messages in thread* Re: [PATCH v2 19/21] fuse: add passthrough setattr
2026-05-16 1:04 ` Joanne Koong
@ 2026-05-16 23:03 ` Amir Goldstein
2026-05-18 23:47 ` Joanne Koong
0 siblings, 1 reply; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 23:03 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 3:04 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Fri, May 15, 2026 at 5:53 PM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > Add passthrough setattr which sets attributes directly on the backing
> > inode through backing_inode_setattr() instead of sending FUSE_SETATTR to
> > the server.
> >
> > Passthrough setattr is checked before the
> > handle_killpriv/handle_killpriv_v2 suid/sgid stripping because the
> > stripping is handled natively by notify_change() on the backing inode.
> >
> > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > ---
> > fs/fuse/dir.c | 3 +++
> > fs/fuse/fuse_i.h | 3 ++-
> > fs/fuse/passthrough.c | 28 ++++++++++++++++++++++++++++
> > include/uapi/linux/fuse.h | 1 +
> > 4 files changed, 34 insertions(+), 1 deletion(-)
> >
> > diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> > index 4c7e3e1604af..b67b3b334e69 100644
> > --- a/fs/fuse/dir.c
> > +++ b/fs/fuse/dir.c
> > @@ -2506,6 +2506,9 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
> > if (!fuse_allow_current_process(get_fuse_conn(inode)))
> > return -EACCES;
> >
> > + if (fuse_passthrough_op(inode, FUSE_SETATTR))
> > + return fuse_passthrough_setattr(entry, attr);
>
> In the v1 discussion [1], there was a suggestion about gating
> FUSE_PASSTHROUGH_INO on FUSE_HANDLE_KILLPRIV_V2 to avoid the
> non-atomic killpriv path, but I think we were only encountering the
> non-atomic killpriv path because I was calling
> fuse_passthrough_setattr() in the wrong place. I think we can avoid
> this altogether by just having the backing filesystem handle all of
> the suid/sgid stripping atomically through its own notify_change()
> path, which will let us skip all the fuse killpriv handling logic in
> this function.
I think it still matters, but to the only-passthrough-GETATTR case
and I am still in favor of making this change.
My point was why have the discussion whether getattr
for the purpose of killpriv should be passthrough or not (next patch)?
Why not drop the next patch instead because it will become moot.
If FUSE_PASSTHROUGH_INO requires FUSE_HANDLE_KILLPRIV_V2
it means that FUSE kernel is never trying to play games removing privs
itself, not with passthrough and not without passthrough.
Quoting your comment on next patch
"I think there might be some use cases
though where having only getattr passed through and not setattr is
useful (eg server wants fast stat() but needs to intercept attribute
changes for access control or policy enforcement)."
Valid use case, but in this valid use case, said server may reject/allow
attribute changes, but eventually if it allows them it will likely apply
them to the very file which was set as backing inode, so essentially
FUSE_HANDLE_KILLPRIV_V2 is the natural choice for a
FUSE_PASSTHROUGH_INO server, even when it wants to introspect
setattr.
I think the same is true for ATOMIC_O_TRUNC.
Either backing fs deals with O_TRUNC or server deals with O_TRUNC.
But this is my opinion - I am open to hearing other opinions.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 19/21] fuse: add passthrough setattr
2026-05-16 23:03 ` Amir Goldstein
@ 2026-05-18 23:47 ` Joanne Koong
0 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-18 23:47 UTC (permalink / raw)
To: Amir Goldstein; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 4:03 PM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Sat, May 16, 2026 at 3:04 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > On Fri, May 15, 2026 at 5:53 PM Joanne Koong <joannelkoong@gmail.com> wrote:
> > >
> > > Add passthrough setattr which sets attributes directly on the backing
> > > inode through backing_inode_setattr() instead of sending FUSE_SETATTR to
> > > the server.
> > >
> > > Passthrough setattr is checked before the
> > > handle_killpriv/handle_killpriv_v2 suid/sgid stripping because the
> > > stripping is handled natively by notify_change() on the backing inode.
> > >
> > > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > > ---
> > > fs/fuse/dir.c | 3 +++
> > > fs/fuse/fuse_i.h | 3 ++-
> > > fs/fuse/passthrough.c | 28 ++++++++++++++++++++++++++++
> > > include/uapi/linux/fuse.h | 1 +
> > > 4 files changed, 34 insertions(+), 1 deletion(-)
> > >
> > > diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> > > index 4c7e3e1604af..b67b3b334e69 100644
> > > --- a/fs/fuse/dir.c
> > > +++ b/fs/fuse/dir.c
> > > @@ -2506,6 +2506,9 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
> > > if (!fuse_allow_current_process(get_fuse_conn(inode)))
> > > return -EACCES;
> > >
> > > + if (fuse_passthrough_op(inode, FUSE_SETATTR))
> > > + return fuse_passthrough_setattr(entry, attr);
> >
> > In the v1 discussion [1], there was a suggestion about gating
> > FUSE_PASSTHROUGH_INO on FUSE_HANDLE_KILLPRIV_V2 to avoid the
> > non-atomic killpriv path, but I think we were only encountering the
> > non-atomic killpriv path because I was calling
> > fuse_passthrough_setattr() in the wrong place. I think we can avoid
> > this altogether by just having the backing filesystem handle all of
> > the suid/sgid stripping atomically through its own notify_change()
> > path, which will let us skip all the fuse killpriv handling logic in
> > this function.
>
> I think it still matters, but to the only-passthrough-GETATTR case
> and I am still in favor of making this change.
> My point was why have the discussion whether getattr
> for the purpose of killpriv should be passthrough or not (next patch)?
> Why not drop the next patch instead because it will become moot.
>
> If FUSE_PASSTHROUGH_INO requires FUSE_HANDLE_KILLPRIV_V2
> it means that FUSE kernel is never trying to play games removing privs
> itself, not with passthrough and not without passthrough.
>
> Quoting your comment on next patch
> "I think there might be some use cases
> though where having only getattr passed through and not setattr is
> useful (eg server wants fast stat() but needs to intercept attribute
> changes for access control or policy enforcement)."
>
> Valid use case, but in this valid use case, said server may reject/allow
> attribute changes, but eventually if it allows them it will likely apply
> them to the very file which was set as backing inode, so essentially
> FUSE_HANDLE_KILLPRIV_V2 is the natural choice for a
> FUSE_PASSTHROUGH_INO server, even when it wants to introspect
> setattr.
>
> I think the same is true for ATOMIC_O_TRUNC.
> Either backing fs deals with O_TRUNC or server deals with O_TRUNC.
>
> But this is my opinion - I am open to hearing other opinions.
I was thinking from the user perspective, it'd be nicer to not couple
things unless it was absolutely necessary, especially since
passthrough is a per-inode/per-file attribute whereas KILLPRIV_V2 is a
global server configuration (eg server might want passthrough on just
a few files without taking on killpriv responsibility for the whole
filesystem).
But I think everything you wrote makes sense and I don't think
handling the stripping stuff is too burdensome to ask of the server.
For v3, I'll make the change to require FUSE_KILLPRIV_V2 for
FUSE_PASSTHROUGH_INO.
Thanks,
Joanne
>
> Thanks,
> Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 19/21] fuse: add passthrough setattr
2026-05-16 0:40 ` [PATCH v2 19/21] fuse: add passthrough setattr Joanne Koong
2026-05-16 1:04 ` Joanne Koong
@ 2026-05-16 22:29 ` Amir Goldstein
1 sibling, 0 replies; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 22:29 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 2:53 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Add passthrough setattr which sets attributes directly on the backing
> inode through backing_inode_setattr() instead of sending FUSE_SETATTR to
> the server.
>
> Passthrough setattr is checked before the
> handle_killpriv/handle_killpriv_v2 suid/sgid stripping because the
> stripping is handled natively by notify_change() on the backing inode.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
> ---
> fs/fuse/dir.c | 3 +++
> fs/fuse/fuse_i.h | 3 ++-
> fs/fuse/passthrough.c | 28 ++++++++++++++++++++++++++++
> include/uapi/linux/fuse.h | 1 +
> 4 files changed, 34 insertions(+), 1 deletion(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index 4c7e3e1604af..b67b3b334e69 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -2506,6 +2506,9 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
> if (!fuse_allow_current_process(get_fuse_conn(inode)))
> return -EACCES;
>
> + if (fuse_passthrough_op(inode, FUSE_SETATTR))
> + return fuse_passthrough_setattr(entry, attr);
> +
> if (attr->ia_valid & (ATTR_KILL_SUID | ATTR_KILL_SGID)) {
> attr->ia_valid &= ~(ATTR_KILL_SUID | ATTR_KILL_SGID |
> ATTR_MODE);
> diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> index 89c9333e9702..0d978a9837a0 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -1279,7 +1279,7 @@ ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from);
>
> /* Inode passthrough operations for backing file attached to inode */
> #define FUSE_PASSTHROUGH_INODE_OPS \
> - (FUSE_PASSTHROUGH_OP_GETATTR)
> + (FUSE_PASSTHROUGH_OP_GETATTR | FUSE_PASSTHROUGH_OP_SETATTR)
>
> #define FUSE_BACKING_MAP_OP(map, op) \
> ((map)->ops_mask & FUSE_PASSTHROUGH_OP(op))
> @@ -1367,6 +1367,7 @@ static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
>
> int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
> u32 request_mask, unsigned int flags);
> +int fuse_passthrough_setattr(struct dentry *entry, struct iattr *attr);
>
> static inline bool fuse_use_entry2(struct fuse_conn *fc)
> {
> diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> index ae0137caa06d..c083ab68537e 100644
> --- a/fs/fuse/passthrough.c
> +++ b/fs/fuse/passthrough.c
> @@ -9,6 +9,8 @@
>
> #include <linux/file.h>
> #include <linux/backing-file.h>
> +#include <linux/backing-inode.h>
> +#include <linux/posix_acl.h>
> #include <linux/splice.h>
>
> static void fuse_file_accessed(struct file *file)
> @@ -275,3 +277,29 @@ int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
>
> return 0;
> }
> +
> +int fuse_passthrough_setattr(struct dentry *entry, struct iattr *attr)
> +{
> + struct inode *inode = d_inode(entry);
> + struct fuse_conn *fc = get_fuse_conn(inode);
> + struct fuse_inode *fi = get_fuse_inode(inode);
> + struct fuse_backing *fb = fuse_inode_backing(fi);
> + struct path path;
> + int err;
> +
> + err = setattr_prepare(&nop_mnt_idmap, entry, attr);
> + if (err)
> + return err;
> +
> + path.mnt = fb->file->f_path.mnt;
> + path.dentry = fb->file->f_path.dentry;
> +
> + err = backing_inode_setattr(entry, &path, attr, fb->cred);
> + if (err)
> + return err;
> +
> + if (fc->posix_acl)
> + forget_all_cached_acls(inode);
> +
> + return 0;
> +}
> diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
> index 3963631558f9..040fee549bb9 100644
> --- a/include/uapi/linux/fuse.h
> +++ b/include/uapi/linux/fuse.h
> @@ -1158,6 +1158,7 @@ struct fuse_backing_map {
> #define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
> #define FUSE_PASSTHROUGH_OP_READDIR FUSE_PASSTHROUGH_OP(FUSE_READDIR)
> #define FUSE_PASSTHROUGH_OP_GETATTR FUSE_PASSTHROUGH_OP(FUSE_GETATTR)
> +#define FUSE_PASSTHROUGH_OP_SETATTR FUSE_PASSTHROUGH_OP(FUSE_SETATTR)
>
> /* Device ioctls: */
> #define FUSE_DEV_IOC_MAGIC 229
> --
> 2.52.0
>
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (18 preceding siblings ...)
2026-05-16 0:40 ` [PATCH v2 19/21] fuse: add passthrough setattr Joanne Koong
@ 2026-05-16 0:40 ` Joanne Koong
2026-05-16 1:20 ` Joanne Koong
2026-05-16 0:40 ` [PATCH v2 21/21] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO) Joanne Koong
20 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:40 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
When refreshing i_mode for suid/sgid kill during setattr, use
passthrough getattr if the inode has that enabled.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index b67b3b334e69..fd1b3fd86968 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -2524,7 +2524,11 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
* ia_mode calculation may have used stale i_mode.
* Refresh and recalculate.
*/
- ret = fuse_do_getattr(idmap, inode, NULL, file);
+ if (fuse_passthrough_op(inode, FUSE_GETATTR))
+ ret = fuse_passthrough_getattr(inode, NULL,
+ STATX_MODE, 0);
+ else
+ ret = fuse_do_getattr(idmap, inode, NULL, file);
if (ret)
return ret;
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling
2026-05-16 0:40 ` [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling Joanne Koong
@ 2026-05-16 1:20 ` Joanne Koong
2026-05-16 22:23 ` Amir Goldstein
0 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 1:20 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
On Fri, May 15, 2026 at 5:53 PM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> When refreshing i_mode for suid/sgid kill during setattr, use
> passthrough getattr if the inode has that enabled.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
> fs/fuse/dir.c | 6 +++++-
> 1 file changed, 5 insertions(+), 1 deletion(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index b67b3b334e69..fd1b3fd86968 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -2524,7 +2524,11 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
> * ia_mode calculation may have used stale i_mode.
> * Refresh and recalculate.
> */
> - ret = fuse_do_getattr(idmap, inode, NULL, file);
> + if (fuse_passthrough_op(inode, FUSE_GETATTR))
> + ret = fuse_passthrough_getattr(inode, NULL,
> + STATX_MODE, 0);
> + else
> + ret = fuse_do_getattr(idmap, inode, NULL, file);
I left this part untouched from v1 but there was a discussion [1]
about whether to enforce that setattr must be passed through if
getattr is passed through [1]. I think there might be some use cases
though where having only getattr passed through and not setattr is
useful (eg server wants fast stat() but needs to intercept attribute
changes for access control or policy enforcement). I'm not sure if you
still feel it's better to keep it simple with enforcing that
getattr+setattr rmust always be passed through together, Amir, but if
so I'm happy to add that in.
Thanks,
Joanne
[1] https://lore.kernel.org/fuse-devel/CAOQ4uxg7jaA0FdLW6uzZBRX=m1Xw4v-k7zeQx7kYcpFwFDpf7A@mail.gmail.com/
> if (ret)
> return ret;
>
> --
> 2.52.0
>
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling
2026-05-16 1:20 ` Joanne Koong
@ 2026-05-16 22:23 ` Amir Goldstein
2026-05-19 0:01 ` Joanne Koong
0 siblings, 1 reply; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 22:23 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 3:20 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Fri, May 15, 2026 at 5:53 PM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > When refreshing i_mode for suid/sgid kill during setattr, use
> > passthrough getattr if the inode has that enabled.
> >
> > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > ---
> > fs/fuse/dir.c | 6 +++++-
> > 1 file changed, 5 insertions(+), 1 deletion(-)
> >
> > diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> > index b67b3b334e69..fd1b3fd86968 100644
> > --- a/fs/fuse/dir.c
> > +++ b/fs/fuse/dir.c
> > @@ -2524,7 +2524,11 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
> > * ia_mode calculation may have used stale i_mode.
> > * Refresh and recalculate.
> > */
> > - ret = fuse_do_getattr(idmap, inode, NULL, file);
> > + if (fuse_passthrough_op(inode, FUSE_GETATTR))
> > + ret = fuse_passthrough_getattr(inode, NULL,
> > + STATX_MODE, 0);
> > + else
> > + ret = fuse_do_getattr(idmap, inode, NULL, file);
>
> I left this part untouched from v1 but there was a discussion [1]
> about whether to enforce that setattr must be passed through if
> getattr is passed through [1]. I think there might be some use cases
> though where having only getattr passed through and not setattr is
> useful (eg server wants fast stat() but needs to intercept attribute
> changes for access control or policy enforcement).
Of course there is. I think I was misunderstood.
What I meant was that *specifically* for the getattr for this purpose
of killpriv, it may make sense to ask the server's mode for
calculating the setattr mode for the server.
But this is just a hunch.
I could be wrong and I have no strong feelings either way.
> I'm not sure if you
> still feel it's better to keep it simple with enforcing that
> getattr+setattr rmust always be passed through together, Amir, but if
> so I'm happy to add that in.
No need.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling
2026-05-16 22:23 ` Amir Goldstein
@ 2026-05-19 0:01 ` Joanne Koong
0 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-19 0:01 UTC (permalink / raw)
To: Amir Goldstein; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 3:23 PM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Sat, May 16, 2026 at 3:20 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > On Fri, May 15, 2026 at 5:53 PM Joanne Koong <joannelkoong@gmail.com> wrote:
> > >
> > > When refreshing i_mode for suid/sgid kill during setattr, use
> > > passthrough getattr if the inode has that enabled.
> > >
> > > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > > ---
> > > fs/fuse/dir.c | 6 +++++-
> > > 1 file changed, 5 insertions(+), 1 deletion(-)
> > >
> > > diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> > > index b67b3b334e69..fd1b3fd86968 100644
> > > --- a/fs/fuse/dir.c
> > > +++ b/fs/fuse/dir.c
> > > @@ -2524,7 +2524,11 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
> > > * ia_mode calculation may have used stale i_mode.
> > > * Refresh and recalculate.
> > > */
> > > - ret = fuse_do_getattr(idmap, inode, NULL, file);
> > > + if (fuse_passthrough_op(inode, FUSE_GETATTR))
> > > + ret = fuse_passthrough_getattr(inode, NULL,
> > > + STATX_MODE, 0);
> > > + else
> > > + ret = fuse_do_getattr(idmap, inode, NULL, file);
> >
> > I left this part untouched from v1 but there was a discussion [1]
> > about whether to enforce that setattr must be passed through if
> > getattr is passed through [1]. I think there might be some use cases
> > though where having only getattr passed through and not setattr is
> > useful (eg server wants fast stat() but needs to intercept attribute
> > changes for access control or policy enforcement).
>
> Of course there is. I think I was misunderstood.
>
> What I meant was that *specifically* for the getattr for this purpose
> of killpriv, it may make sense to ask the server's mode for
> calculating the setattr mode for the server.
Ah gotcha, I misinterpreted your comment from v1. Thanks for clarifying.
Thanks again for reviewing this series.
>
> But this is just a hunch.
> I could be wrong and I have no strong feelings either way.
>
> > I'm not sure if you
> > still feel it's better to keep it simple with enforcing that
> > getattr+setattr rmust always be passed through together, Amir, but if
> > so I'm happy to add that in.
>
> No need.
>
> Thanks,
> Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 21/21] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO)
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (19 preceding siblings ...)
2026-05-16 0:40 ` [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling Joanne Koong
@ 2026-05-16 0:40 ` Joanne Koong
2026-05-16 18:23 ` Amir Goldstein
20 siblings, 1 reply; 54+ messages in thread
From: Joanne Koong @ 2026-05-16 0:40 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Add section about extended passthrough (FUSE_PASSTHROUGH_INO) mode.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
.../filesystems/fuse/fuse-passthrough.rst | 126 ++++++++++++++++++
1 file changed, 126 insertions(+)
diff --git a/Documentation/filesystems/fuse/fuse-passthrough.rst b/Documentation/filesystems/fuse/fuse-passthrough.rst
index 2b0e7c2da54a..751d27c6fc5c 100644
--- a/Documentation/filesystems/fuse/fuse-passthrough.rst
+++ b/Documentation/filesystems/fuse/fuse-passthrough.rst
@@ -25,6 +25,11 @@ operations.
Currently, passthrough is supported for operations like ``read(2)``/``write(2)``
(via ``read_iter``/``write_iter``), ``splice(2)``, and ``mmap(2)``.
+With the extended ``FUSE_PASSTHROUGH_INO`` mode, passthrough is also supported
+for inode operations (getattr, setattr) and directory operations (readdir).
+In this mode, a backing file is attached to a fuse inode for its entire
+lifetime.
+
Enabling Passthrough
====================
@@ -46,6 +51,127 @@ To use FUSE passthrough:
the ``backing_id`` to release the kernel's reference to the backing file
when it's no longer needed for passthrough setups.
+Extended Passthrough (FUSE_PASSTHROUGH_INO)
+============================================
+
+``FUSE_PASSTHROUGH_INO`` is a stricter variant of ``FUSE_PASSTHROUGH`` in
+which the backing file inode number must match the fuse inode number, enforcing
+a one-to-one mapping. The kernel offers this flag during ``FUSE_INIT`` if
+``CONFIG_FUSE_PASSTHROUGH`` is enabled and the architecture has 64-bit
+``ino_t``. The daemon accepts by returning it back in the init reply.
+
+Enabling Extended Passthrough
+-----------------------------
+
+To use extended passthrough:
+
+ 1. Follow steps 1-2 from `Enabling Passthrough`_ above. The daemon must
+ also negotiate the ``FUSE_PASSTHROUGH_INO`` capability during
+ ``FUSE_INIT``.
+ 2. When registering a backing file via ``FUSE_DEV_IOC_BACKING_OPEN``, set
+ the ``ops_mask`` field in ``struct fuse_backing_map`` to declare which
+ operations should be passed through. At minimum,
+ ``FUSE_PASSTHROUGH_OP_GETATTR`` must be set for any inode-level
+ passthrough.
+ 3. When handling a ``LOOKUP``, ``CREATE``, ``MKNOD``, ``MKDIR``,
+ ``SYMLINK``, or ``LINK`` request, the daemon responds with a
+ ``fuse_entry2_out`` (instead of ``fuse_entry_out``). To enable
+ passthrough on the inode, set ``backing_id`` to the id returned by
+ ``FUSE_DEV_IOC_BACKING_OPEN``. Set ``backing_id`` to 0 for inodes
+ that should not use passthrough. The ``nodeid`` in the response must
+ be the backing file's inode number (``i_ino``). If they don't match,
+ the kernel rejects the passthrough setup with ``-EIO``.
+ 4. When handling an ``OPEN`` request for a FUSE file, the daemon
+ replies with the ``FOPEN_PASSTHROUGH`` flag set in
+ ``fuse_open_out::open_flags`` and provides the corresponding ``backing_id``
+ in ``fuse_open_out::backing_id`` or leaves ``fuse_open_out::backing_id``
+ blank. If the daemon would like to opt out of passthrough when the inode
+ is already in passthrough mode, it may additionally set
+ ``FOPEN_DIRECT_IO``, which will forward read/write operations directly to
+ the daemon.
+ 5. The FUSE daemon should eventually call ``FUSE_DEV_IOC_BACKING_CLOSE`` with
+ the ``backing_id`` to release the kernel's reference to the backing file
+ when it's no longer needed for passthrough setups.
+
+Passthrough Operations Mask
+---------------------------
+
+When registering a backing file via ``FUSE_DEV_IOC_BACKING_OPEN``, the daemon
+sets ``ops_mask`` in ``struct fuse_backing_map`` to declare which operations
+should be passed through::
+
+ FUSE_PASSTHROUGH_OP_READ
+ FUSE_PASSTHROUGH_OP_WRITE
+ FUSE_PASSTHROUGH_OP_READDIR
+ FUSE_PASSTHROUGH_OP_GETATTR
+ FUSE_PASSTHROUGH_OP_SETATTR
+
+Operations fall into two categories, which can be combined:
+
+**Inode operations** (getattr, setattr): Activated on lookup when the daemon
+returns a ``backing_id`` in the ``fuse_entry2_out`` response. The backing
+file reference persists for the lifetime of the fuse inode. Getattr is the
+minimum required inode operation.
+
+**File operations** (read, write, readdir): Require the file to be opened with
+``FOPEN_PASSTHROUGH`` in the daemon's open response. Read and write can be set
+independently for partial passthrough. If only one direction is set, the other
+falls back to direct IO and mmap is disabled.
+
+Extended Entry Reply
+--------------------
+
+When ``FUSE_PASSTHROUGH_INO`` is negotiated, the kernel uses
+``fuse_entry2_out`` instead of ``fuse_entry_out`` for entry responses.
+This struct carries a ``backing_id`` and ``fuse_statx`` attributes instead
+of ``fuse_attr``.
+
+When ``backing_id > 0``, the kernel associates the inode with the backing file
+for passthrough inode operations. Statx attributes are not cached because
+passthrough getattr fetches them directly from the backing inode.
+
+When ``backing_id == 0`` (no passthrough), the statx attributes from the reply
+are cached normally.
+
+A negative ``backing_id`` is treated as an error. The kernel sends
+``FUSE_FORGET`` for the returned nodeid and fails the operation.
+
+IO Mode State Machine
+---------------------
+
+The ``iocachectr`` field in ``struct fuse_inode`` prevents conflicting access
+modes on the same inode (page-cache I/O and passthrough I/O cannot coexist)::
+
+ iocachectr > 0 Cached mode
+ iocachectr == 0 Idle. No files open, no passthrough
+ iocachectr < 0 Uncached/passthrough mode
+
+Each open file in passthrough mode holds one reference (``iocachectr--``).
+The inode-level passthrough setup holds one additional long-lived reference
+if the backing has inode ops (getattr/setattr). This long-lived reference is
+released on inode eviction.
+
+Cached mode and passthrough mode are mutually exclusive. Attempting either
+while the other is active returns ``-ETXTBSY``.
+
+For directories, the same mechanism arbitrates between cached readdir
+(``FOPEN_CACHE_DIR``) and passthrough readdir. A directory opened without
+``FOPEN_CACHE_DIR`` and without ``FOPEN_PASSTHROUGH`` is treated as direct I/O
+and does not affect io mode.
+
+Things to note
+--------------
+
+- ``FUSE_PASSTHROUGH_INO`` requires 64-bit ``ino_t``.
+- Readdirplus does not set up inode passthrough. Inodes created via readdirplus
+ use normal FUSE operations until a fresh lookup occurs.
+- An inode's backing association is set once and cannot be changed.
+- Passthrough and cached I/O cannot coexist on the same inode.
+- If any inode operations are passed through, this means all opened files need
+ to set the ``FOPEN_PASSTHROUGH`` flag in the open response, even if reads
+ and writes are not passed through. If reads and writes are not passed
+ through, they will go directly to the daemon.
+
Privilege Requirements
======================
--
2.52.0
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 21/21] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO)
2026-05-16 0:40 ` [PATCH v2 21/21] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO) Joanne Koong
@ 2026-05-16 18:23 ` Amir Goldstein
2026-05-16 20:59 ` Amir Goldstein
0 siblings, 1 reply; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 18:23 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 2:53 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Add section about extended passthrough (FUSE_PASSTHROUGH_INO) mode.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
> .../filesystems/fuse/fuse-passthrough.rst | 126 ++++++++++++++++++
> 1 file changed, 126 insertions(+)
>
> diff --git a/Documentation/filesystems/fuse/fuse-passthrough.rst b/Documentation/filesystems/fuse/fuse-passthrough.rst
> index 2b0e7c2da54a..751d27c6fc5c 100644
> --- a/Documentation/filesystems/fuse/fuse-passthrough.rst
> +++ b/Documentation/filesystems/fuse/fuse-passthrough.rst
> @@ -25,6 +25,11 @@ operations.
> Currently, passthrough is supported for operations like ``read(2)``/``write(2)``
> (via ``read_iter``/``write_iter``), ``splice(2)``, and ``mmap(2)``.
>
> +With the extended ``FUSE_PASSTHROUGH_INO`` mode, passthrough is also supported
> +for inode operations (getattr, setattr) and directory operations (readdir).
> +In this mode, a backing file is attached to a fuse inode for its entire
> +lifetime.
> +
> Enabling Passthrough
> ====================
>
> @@ -46,6 +51,127 @@ To use FUSE passthrough:
> the ``backing_id`` to release the kernel's reference to the backing file
> when it's no longer needed for passthrough setups.
>
> +Extended Passthrough (FUSE_PASSTHROUGH_INO)
> +============================================
> +
> +``FUSE_PASSTHROUGH_INO`` is a stricter variant of ``FUSE_PASSTHROUGH`` in
> +which the backing file inode number must match the fuse inode number, enforcing
> +a one-to-one mapping. The kernel offers this flag during ``FUSE_INIT`` if
> +``CONFIG_FUSE_PASSTHROUGH`` is enabled and the architecture has 64-bit
> +``ino_t``. The daemon accepts by returning it back in the init reply.
IIUC, ino_t is now always 64-bit.
> +
> +Enabling Extended Passthrough
> +-----------------------------
> +
> +To use extended passthrough:
> +
> + 1. Follow steps 1-2 from `Enabling Passthrough`_ above. The daemon must
> + also negotiate the ``FUSE_PASSTHROUGH_INO`` capability during
> + ``FUSE_INIT``.
> + 2. When registering a backing file via ``FUSE_DEV_IOC_BACKING_OPEN``, set
> + the ``ops_mask`` field in ``struct fuse_backing_map`` to declare which
> + operations should be passed through. At minimum,
> + ``FUSE_PASSTHROUGH_OP_GETATTR`` must be set for any inode-level
> + passthrough.
> + 3. When handling a ``LOOKUP``, ``CREATE``, ``MKNOD``, ``MKDIR``,
> + ``SYMLINK``, or ``LINK`` request, the daemon responds with a
> + ``fuse_entry2_out`` (instead of ``fuse_entry_out``). To enable
> + passthrough on the inode, set ``backing_id`` to the id returned by
> + ``FUSE_DEV_IOC_BACKING_OPEN``. Set ``backing_id`` to 0 for inodes
> + that should not use passthrough. The ``nodeid`` in the response must
> + be the backing file's inode number (``i_ino``). If they don't match,
> + the kernel rejects the passthrough setup with ``-EIO``.
> + 4. When handling an ``OPEN`` request for a FUSE file, the daemon
> + replies with the ``FOPEN_PASSTHROUGH`` flag set in
> + ``fuse_open_out::open_flags`` and provides the corresponding ``backing_id``
> + in ``fuse_open_out::backing_id`` or leaves ``fuse_open_out::backing_id``
> + blank. If the daemon would like to opt out of passthrough when the inode
> + is already in passthrough mode, it may additionally set
> + ``FOPEN_DIRECT_IO``, which will forward read/write operations directly to
> + the daemon.
> + 5. The FUSE daemon should eventually call ``FUSE_DEV_IOC_BACKING_CLOSE`` with
> + the ``backing_id`` to release the kernel's reference to the backing file
> + when it's no longer needed for passthrough setups.
> +
> +Passthrough Operations Mask
> +---------------------------
> +
> +When registering a backing file via ``FUSE_DEV_IOC_BACKING_OPEN``, the daemon
> +sets ``ops_mask`` in ``struct fuse_backing_map`` to declare which operations
> +should be passed through::
> +
> + FUSE_PASSTHROUGH_OP_READ
> + FUSE_PASSTHROUGH_OP_WRITE
> + FUSE_PASSTHROUGH_OP_READDIR
> + FUSE_PASSTHROUGH_OP_GETATTR
> + FUSE_PASSTHROUGH_OP_SETATTR
> +
> +Operations fall into two categories, which can be combined:
> +
> +**Inode operations** (getattr, setattr): Activated on lookup when the daemon
> +returns a ``backing_id`` in the ``fuse_entry2_out`` response. The backing
> +file reference persists for the lifetime of the fuse inode. Getattr is the
> +minimum required inode operation.
> +
> +**File operations** (read, write, readdir): Require the file to be opened with
> +``FOPEN_PASSTHROUGH`` in the daemon's open response. Read and write can be set
> +independently for partial passthrough. If only one direction is set, the other
> +falls back to direct IO and mmap is disabled.
> +
> +Extended Entry Reply
> +--------------------
> +
> +When ``FUSE_PASSTHROUGH_INO`` is negotiated, the kernel uses
> +``fuse_entry2_out`` instead of ``fuse_entry_out`` for entry responses.
> +This struct carries a ``backing_id`` and ``fuse_statx`` attributes instead
> +of ``fuse_attr``.
> +
> +When ``backing_id > 0``, the kernel associates the inode with the backing file
> +for passthrough inode operations. Statx attributes are not cached because
> +passthrough getattr fetches them directly from the backing inode.
> +
> +When ``backing_id == 0`` (no passthrough), the statx attributes from the reply
> +are cached normally.
> +
> +A negative ``backing_id`` is treated as an error. The kernel sends
> +``FUSE_FORGET`` for the returned nodeid and fails the operation.
> +
> +IO Mode State Machine
> +---------------------
> +
> +The ``iocachectr`` field in ``struct fuse_inode`` prevents conflicting access
> +modes on the same inode (page-cache I/O and passthrough I/O cannot coexist)::
> +
> + iocachectr > 0 Cached mode
> + iocachectr == 0 Idle. No files open, no passthrough
More accurately, no open files in caching or passthough.
files open with FOPEN_DIRECT_IO may exist and they
do not deny entering either caching or passthrough iomode.
> + iocachectr < 0 Uncached/passthrough mode
> +
> +Each open file in passthrough mode holds one reference (``iocachectr--``).
> +The inode-level passthrough setup holds one additional long-lived reference
> +if the backing has inode ops (getattr/setattr). This long-lived reference is
> +released on inode eviction.
> +
> +Cached mode and passthrough mode are mutually exclusive. Attempting either
> +while the other is active returns ``-ETXTBSY``.
> +
> +For directories, the same mechanism arbitrates between cached readdir
> +(``FOPEN_CACHE_DIR``) and passthrough readdir. A directory opened without
> +``FOPEN_CACHE_DIR`` and without ``FOPEN_PASSTHROUGH`` is treated as direct I/O
> +and does not affect io mode.
> +
> +Things to note
> +--------------
> +
> +- ``FUSE_PASSTHROUGH_INO`` requires 64-bit ``ino_t``.
Not relevant I think.
> +- Readdirplus does not set up inode passthrough. Inodes created via readdirplus
> + use normal FUSE operations until a fresh lookup occurs.
> +- An inode's backing association is set once and cannot be changed.
> +- Passthrough and cached I/O cannot coexist on the same inode.
> +- If any inode operations are passed through, this means all opened files need
> + to set the ``FOPEN_PASSTHROUGH`` flag in the open response, even if reads
> + and writes are not passed through. If reads and writes are not passed
> + through, they will go directly to the daemon.
I had not considered this.
Setting OP_GETATTR/SETATTR in LOOKUP without any OP_RW bits
and then requiring that the server open files with FOPEN_PASSTHROUGH
with the backing_id sounds strange -
I understand why it was done this way, but for users this could be perplexing.
The thing is that if the server is only doing GETATTR passthrough there is not
really a reason to deny open file in caching mode (I think?).
The limitation that requires FOPEN_PASSTHROUGH is encoded in
fuse_file_cached_io_open()
fuse_is_io_cache_wait()
!fuse_inode_backing()
We have two options.
Either we decide that server passing through GETATTR and opening
files in caching mode is not interesting to support (for now) and then just
maybe we need to better document the reason that server needs to
use FOPEN_PASSTHROUGH despite not doing io passthrough.
Or we relax the limitation w.r.t cached io mode, something like this
(completely untested most likely broken based or earlier review
suggestion regarding fuse_passthrough_op):
Thanks,
Amir.
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index c5f3e2201c6ab..7c33e7a974f6c 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -20,7 +20,8 @@ static inline bool fuse_is_io_cache_wait(struct inode *inode)
struct fuse_inode *fi = get_fuse_inode(inode);
return S_ISREG(inode->i_mode) &&
- READ_ONCE(fi->iocachectr) < 0 && !fuse_inode_backing(fi);
+ READ_ONCE(fi->iocachectr) < 0 &&
+ !fuse_passthrough_op(inode, FUSE_PASSTHROUGH_RW_OPS);
}
/*
@@ -54,7 +55,7 @@ int fuse_file_cached_io_open(struct inode *inode,
struct fuse_file *ff)
* Check if inode entered passthrough io mode while waiting for parallel
* dio write completion.
*/
- if (fuse_inode_backing(fi)) {
+ if (fuse_passthrough_op(inode, FUSE_PASSTHROUGH_RW_OPS)) {
clear_bit(FUSE_I_CACHE_IO_MODE, &fi->state);
spin_unlock(&fi->lock);
return -ETXTBSY;
@@ -285,7 +286,8 @@ int fuse_file_io_open(struct file *file, struct
inode *inode)
* which is already open for passthrough.
*/
err = -EINVAL;
- if (fuse_inode_backing(fi) && !(ff->open_flags & FOPEN_PASSTHROUGH))
+ if (fuse_passthrough_op(inode, FUSE_PASSTHROUGH_RW_OPS) &&
+ !(ff->open_flags & FOPEN_PASSTHROUGH))
goto fail;
/*
^ permalink raw reply related [flat|nested] 54+ messages in thread* Re: [PATCH v2 21/21] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO)
2026-05-16 18:23 ` Amir Goldstein
@ 2026-05-16 20:59 ` Amir Goldstein
2026-05-18 21:23 ` Joanne Koong
0 siblings, 1 reply; 54+ messages in thread
From: Amir Goldstein @ 2026-05-16 20:59 UTC (permalink / raw)
To: Joanne Koong; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 8:23 PM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Sat, May 16, 2026 at 2:53 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > Add section about extended passthrough (FUSE_PASSTHROUGH_INO) mode.
> >
> > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > ---
> > .../filesystems/fuse/fuse-passthrough.rst | 126 ++++++++++++++++++
> > 1 file changed, 126 insertions(+)
> >
> > diff --git a/Documentation/filesystems/fuse/fuse-passthrough.rst b/Documentation/filesystems/fuse/fuse-passthrough.rst
> > index 2b0e7c2da54a..751d27c6fc5c 100644
> > --- a/Documentation/filesystems/fuse/fuse-passthrough.rst
> > +++ b/Documentation/filesystems/fuse/fuse-passthrough.rst
> > @@ -25,6 +25,11 @@ operations.
> > Currently, passthrough is supported for operations like ``read(2)``/``write(2)``
> > (via ``read_iter``/``write_iter``), ``splice(2)``, and ``mmap(2)``.
> >
> > +With the extended ``FUSE_PASSTHROUGH_INO`` mode, passthrough is also supported
> > +for inode operations (getattr, setattr) and directory operations (readdir).
> > +In this mode, a backing file is attached to a fuse inode for its entire
> > +lifetime.
> > +
> > Enabling Passthrough
> > ====================
> >
> > @@ -46,6 +51,127 @@ To use FUSE passthrough:
> > the ``backing_id`` to release the kernel's reference to the backing file
> > when it's no longer needed for passthrough setups.
> >
> > +Extended Passthrough (FUSE_PASSTHROUGH_INO)
> > +============================================
> > +
> > +``FUSE_PASSTHROUGH_INO`` is a stricter variant of ``FUSE_PASSTHROUGH`` in
> > +which the backing file inode number must match the fuse inode number, enforcing
> > +a one-to-one mapping. The kernel offers this flag during ``FUSE_INIT`` if
> > +``CONFIG_FUSE_PASSTHROUGH`` is enabled and the architecture has 64-bit
> > +``ino_t``. The daemon accepts by returning it back in the init reply.
>
> IIUC, ino_t is now always 64-bit.
>
> > +
> > +Enabling Extended Passthrough
> > +-----------------------------
> > +
> > +To use extended passthrough:
> > +
> > + 1. Follow steps 1-2 from `Enabling Passthrough`_ above. The daemon must
> > + also negotiate the ``FUSE_PASSTHROUGH_INO`` capability during
> > + ``FUSE_INIT``.
> > + 2. When registering a backing file via ``FUSE_DEV_IOC_BACKING_OPEN``, set
> > + the ``ops_mask`` field in ``struct fuse_backing_map`` to declare which
> > + operations should be passed through. At minimum,
> > + ``FUSE_PASSTHROUGH_OP_GETATTR`` must be set for any inode-level
> > + passthrough.
> > + 3. When handling a ``LOOKUP``, ``CREATE``, ``MKNOD``, ``MKDIR``,
> > + ``SYMLINK``, or ``LINK`` request, the daemon responds with a
> > + ``fuse_entry2_out`` (instead of ``fuse_entry_out``). To enable
> > + passthrough on the inode, set ``backing_id`` to the id returned by
> > + ``FUSE_DEV_IOC_BACKING_OPEN``. Set ``backing_id`` to 0 for inodes
> > + that should not use passthrough. The ``nodeid`` in the response must
> > + be the backing file's inode number (``i_ino``). If they don't match,
> > + the kernel rejects the passthrough setup with ``-EIO``.
> > + 4. When handling an ``OPEN`` request for a FUSE file, the daemon
> > + replies with the ``FOPEN_PASSTHROUGH`` flag set in
> > + ``fuse_open_out::open_flags`` and provides the corresponding ``backing_id``
> > + in ``fuse_open_out::backing_id`` or leaves ``fuse_open_out::backing_id``
> > + blank. If the daemon would like to opt out of passthrough when the inode
> > + is already in passthrough mode, it may additionally set
> > + ``FOPEN_DIRECT_IO``, which will forward read/write operations directly to
> > + the daemon.
> > + 5. The FUSE daemon should eventually call ``FUSE_DEV_IOC_BACKING_CLOSE`` with
> > + the ``backing_id`` to release the kernel's reference to the backing file
> > + when it's no longer needed for passthrough setups.
> > +
> > +Passthrough Operations Mask
> > +---------------------------
> > +
> > +When registering a backing file via ``FUSE_DEV_IOC_BACKING_OPEN``, the daemon
> > +sets ``ops_mask`` in ``struct fuse_backing_map`` to declare which operations
> > +should be passed through::
> > +
> > + FUSE_PASSTHROUGH_OP_READ
> > + FUSE_PASSTHROUGH_OP_WRITE
> > + FUSE_PASSTHROUGH_OP_READDIR
> > + FUSE_PASSTHROUGH_OP_GETATTR
> > + FUSE_PASSTHROUGH_OP_SETATTR
> > +
> > +Operations fall into two categories, which can be combined:
> > +
> > +**Inode operations** (getattr, setattr): Activated on lookup when the daemon
> > +returns a ``backing_id`` in the ``fuse_entry2_out`` response. The backing
> > +file reference persists for the lifetime of the fuse inode. Getattr is the
> > +minimum required inode operation.
> > +
> > +**File operations** (read, write, readdir): Require the file to be opened with
> > +``FOPEN_PASSTHROUGH`` in the daemon's open response. Read and write can be set
> > +independently for partial passthrough. If only one direction is set, the other
> > +falls back to direct IO and mmap is disabled.
> > +
> > +Extended Entry Reply
> > +--------------------
> > +
> > +When ``FUSE_PASSTHROUGH_INO`` is negotiated, the kernel uses
> > +``fuse_entry2_out`` instead of ``fuse_entry_out`` for entry responses.
> > +This struct carries a ``backing_id`` and ``fuse_statx`` attributes instead
> > +of ``fuse_attr``.
> > +
> > +When ``backing_id > 0``, the kernel associates the inode with the backing file
> > +for passthrough inode operations. Statx attributes are not cached because
> > +passthrough getattr fetches them directly from the backing inode.
> > +
> > +When ``backing_id == 0`` (no passthrough), the statx attributes from the reply
> > +are cached normally.
> > +
> > +A negative ``backing_id`` is treated as an error. The kernel sends
> > +``FUSE_FORGET`` for the returned nodeid and fails the operation.
> > +
> > +IO Mode State Machine
> > +---------------------
> > +
> > +The ``iocachectr`` field in ``struct fuse_inode`` prevents conflicting access
> > +modes on the same inode (page-cache I/O and passthrough I/O cannot coexist)::
> > +
> > + iocachectr > 0 Cached mode
> > + iocachectr == 0 Idle. No files open, no passthrough
>
> More accurately, no open files in caching or passthough.
> files open with FOPEN_DIRECT_IO may exist and they
> do not deny entering either caching or passthrough iomode.
>
> > + iocachectr < 0 Uncached/passthrough mode
> > +
> > +Each open file in passthrough mode holds one reference (``iocachectr--``).
> > +The inode-level passthrough setup holds one additional long-lived reference
> > +if the backing has inode ops (getattr/setattr). This long-lived reference is
> > +released on inode eviction.
> > +
> > +Cached mode and passthrough mode are mutually exclusive. Attempting either
> > +while the other is active returns ``-ETXTBSY``.
> > +
> > +For directories, the same mechanism arbitrates between cached readdir
> > +(``FOPEN_CACHE_DIR``) and passthrough readdir. A directory opened without
> > +``FOPEN_CACHE_DIR`` and without ``FOPEN_PASSTHROUGH`` is treated as direct I/O
> > +and does not affect io mode.
> > +
> > +Things to note
> > +--------------
> > +
> > +- ``FUSE_PASSTHROUGH_INO`` requires 64-bit ``ino_t``.
>
> Not relevant I think.
>
> > +- Readdirplus does not set up inode passthrough. Inodes created via readdirplus
> > + use normal FUSE operations until a fresh lookup occurs.
> > +- An inode's backing association is set once and cannot be changed.
> > +- Passthrough and cached I/O cannot coexist on the same inode.
> > +- If any inode operations are passed through, this means all opened files need
> > + to set the ``FOPEN_PASSTHROUGH`` flag in the open response, even if reads
> > + and writes are not passed through. If reads and writes are not passed
> > + through, they will go directly to the daemon.
>
> I had not considered this.
> Setting OP_GETATTR/SETATTR in LOOKUP without any OP_RW bits
> and then requiring that the server open files with FOPEN_PASSTHROUGH
> with the backing_id sounds strange -
> I understand why it was done this way, but for users this could be perplexing.
>
> The thing is that if the server is only doing GETATTR passthrough there is not
> really a reason to deny open file in caching mode (I think?).
>
> The limitation that requires FOPEN_PASSTHROUGH is encoded in
> fuse_file_cached_io_open()
> fuse_is_io_cache_wait()
> !fuse_inode_backing()
>
> We have two options.
>
> Either we decide that server passing through GETATTR and opening
> files in caching mode is not interesting to support (for now) and then just
> maybe we need to better document the reason that server needs to
> use FOPEN_PASSTHROUGH despite not doing io passthrough.
>
> Or we relax the limitation w.r.t cached io mode, something like this
> (completely untested most likely broken based or earlier review
> suggestion regarding fuse_passthrough_op):
>
> Thanks,
> Amir.
>
> diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
> index c5f3e2201c6ab..7c33e7a974f6c 100644
> --- a/fs/fuse/iomode.c
> +++ b/fs/fuse/iomode.c
> @@ -20,7 +20,8 @@ static inline bool fuse_is_io_cache_wait(struct inode *inode)
> struct fuse_inode *fi = get_fuse_inode(inode);
>
> return S_ISREG(inode->i_mode) &&
> - READ_ONCE(fi->iocachectr) < 0 && !fuse_inode_backing(fi);
> + READ_ONCE(fi->iocachectr) < 0 &&
> + !fuse_passthrough_op(inode, FUSE_PASSTHROUGH_RW_OPS);
> }
Obviously, this was naive and not close to enough, because
fuse_inode_set_passthrough() still takes the negative iocachectr
refcount.
Please ignore this. Supporting caching iomode and GETATTR
passthrough is left for a future exercise, if anyone is ever interested.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 21/21] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO)
2026-05-16 20:59 ` Amir Goldstein
@ 2026-05-18 21:23 ` Joanne Koong
0 siblings, 0 replies; 54+ messages in thread
From: Joanne Koong @ 2026-05-18 21:23 UTC (permalink / raw)
To: Amir Goldstein; +Cc: miklos, fuse-devel, linux-unionfs
On Sat, May 16, 2026 at 1:59 PM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Sat, May 16, 2026 at 8:23 PM Amir Goldstein <amir73il@gmail.com> wrote:
> >
> > On Sat, May 16, 2026 at 2:53 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> > >
> > > Add section about extended passthrough (FUSE_PASSTHROUGH_INO) mode.
> > >
> > > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > > ---
> > > .../filesystems/fuse/fuse-passthrough.rst | 126 ++++++++++++++++++
> > > 1 file changed, 126 insertions(+)
> > >
> > > diff --git a/Documentation/filesystems/fuse/fuse-passthrough.rst b/Documentation/filesystems/fuse/fuse-passthrough.rst
> > > index 2b0e7c2da54a..751d27c6fc5c 100644
> > > --- a/Documentation/filesystems/fuse/fuse-passthrough.rst
> > > +++ b/Documentation/filesystems/fuse/fuse-passthrough.rst
> > > @@ -25,6 +25,11 @@ operations.
> > >
> > > +Extended Passthrough (FUSE_PASSTHROUGH_INO)
> > > +============================================
> > > +
> > > +``FUSE_PASSTHROUGH_INO`` is a stricter variant of ``FUSE_PASSTHROUGH`` in
> > > +which the backing file inode number must match the fuse inode number, enforcing
> > > +a one-to-one mapping. The kernel offers this flag during ``FUSE_INIT`` if
> > > +``CONFIG_FUSE_PASSTHROUGH`` is enabled and the architecture has 64-bit
> > > +``ino_t``. The daemon accepts by returning it back in the init reply.
> >
> > IIUC, ino_t is now always 64-bit.
When Jeff's changes for u64 inode->i_ino land in the fuse tree, I'll
get rid of this and update the 1st patch accordingly.
> >
> > > +
> > > +IO Mode State Machine
> > > +---------------------
> > > +
> > > +The ``iocachectr`` field in ``struct fuse_inode`` prevents conflicting access
> > > +modes on the same inode (page-cache I/O and passthrough I/O cannot coexist)::
> > > +
> > > + iocachectr > 0 Cached mode
> > > + iocachectr == 0 Idle. No files open, no passthrough
> >
> > More accurately, no open files in caching or passthough.
> > files open with FOPEN_DIRECT_IO may exist and they
> > do not deny entering either caching or passthrough iomode.
Good point, I'll update this to be more precise.
> >
> > > + iocachectr < 0 Uncached/passthrough mode
> > > +
> > > +Each open file in passthrough mode holds one reference (``iocachectr--``).
> > > +The inode-level passthrough setup holds one additional long-lived reference
> > > +if the backing has inode ops (getattr/setattr). This long-lived reference is
> > > +released on inode eviction.
> > > +
> > > +Cached mode and passthrough mode are mutually exclusive. Attempting either
> > > +while the other is active returns ``-ETXTBSY``.
> > > +
> > > +For directories, the same mechanism arbitrates between cached readdir
> > > +(``FOPEN_CACHE_DIR``) and passthrough readdir. A directory opened without
> > > +``FOPEN_CACHE_DIR`` and without ``FOPEN_PASSTHROUGH`` is treated as direct I/O
> > > +and does not affect io mode.
> > > +
> > > +Things to note
> > > +--------------
> > > +
> > > +- ``FUSE_PASSTHROUGH_INO`` requires 64-bit ``ino_t``.
> >
> > Not relevant I think.
> >
> > > +- Readdirplus does not set up inode passthrough. Inodes created via readdirplus
> > > + use normal FUSE operations until a fresh lookup occurs.
> > > +- An inode's backing association is set once and cannot be changed.
> > > +- Passthrough and cached I/O cannot coexist on the same inode.
> > > +- If any inode operations are passed through, this means all opened files need
> > > + to set the ``FOPEN_PASSTHROUGH`` flag in the open response, even if reads
> > > + and writes are not passed through. If reads and writes are not passed
> > > + through, they will go directly to the daemon.
> >
> > I had not considered this.
> > Setting OP_GETATTR/SETATTR in LOOKUP without any OP_RW bits
> > and then requiring that the server open files with FOPEN_PASSTHROUGH
> > with the backing_id sounds strange -
> > I understand why it was done this way, but for users this could be perplexing.
> >
> > The thing is that if the server is only doing GETATTR passthrough there is not
> > really a reason to deny open file in caching mode (I think?).
> >
> > The limitation that requires FOPEN_PASSTHROUGH is encoded in
> > fuse_file_cached_io_open()
> > fuse_is_io_cache_wait()
> > !fuse_inode_backing()
> >
> > We have two options.
> >
> > Either we decide that server passing through GETATTR and opening
> > files in caching mode is not interesting to support (for now) and then just
> > maybe we need to better document the reason that server needs to
> > use FOPEN_PASSTHROUGH despite not doing io passthrough.
> >
> > Or we relax the limitation w.r.t cached io mode, something like this
> > (completely untested most likely broken based or earlier review
> > suggestion regarding fuse_passthrough_op):
> >
> > Thanks,
> > Amir.
> >
> > diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
> > index c5f3e2201c6ab..7c33e7a974f6c 100644
> > --- a/fs/fuse/iomode.c
> > +++ b/fs/fuse/iomode.c
> > @@ -20,7 +20,8 @@ static inline bool fuse_is_io_cache_wait(struct inode *inode)
> > struct fuse_inode *fi = get_fuse_inode(inode);
> >
> > return S_ISREG(inode->i_mode) &&
> > - READ_ONCE(fi->iocachectr) < 0 && !fuse_inode_backing(fi);
> > + READ_ONCE(fi->iocachectr) < 0 &&
> > + !fuse_passthrough_op(inode, FUSE_PASSTHROUGH_RW_OPS);
> > }
>
> Obviously, this was naive and not close to enough, because
> fuse_inode_set_passthrough() still takes the negative iocachectr
> refcount.
>
> Please ignore this. Supporting caching iomode and GETATTR
> passthrough is left for a future exercise, if anyone is ever interested.
Sounds good, I will continue to leave this unsupported for now.
Thanks for reviewing these patches.
>
> Thanks,
> Amir.
^ permalink raw reply [flat|nested] 54+ messages in thread