FILESYSTEM IN USERSPACE (FUSE) development
 help / color / mirror / Atom feed
* [PATCH v1 00/17] fuse: extend passthrough to inode operations
@ 2026-04-20 22:16 Joanne Koong
  2026-04-20 22:16 ` [PATCH v1 01/17] fuse: introduce FUSE_PASSTHROUGH_INO mode Joanne Koong
                   ` (17 more replies)
  0 siblings, 18 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

This series extends fuse passthrough to support inode operations (getattr,
setattr), directory readdir, and kernel-initiated open on backing files.

The existing FUSE_PASSTHROUGH mode attaches a backing file to a fuse inode
only when a passthrough file is open. This series introduces
FUSE_PASSTHROUGH_INO, a stricter mode requiring one-to-one inode number
mapping, which allows attaching a backing file for the lifetime of the fuse
inode.

Future work includes passthrough for additional operations (rename, unlink,
readdirplus) and full subtree passthrough, which will be part of a separate
series.

Patches 1-7 are from Amir from his git tree in [1]. There were a few
modifications made (removed separate FUSE_PASSTHROUGH_OP_STATX op,
modified fuse_kstat_to_attr() - the full diff of changes is in [2]); if there
are any mistakes, those are mine and not Amir's.

For testing/debugging, this was done using the passthrough_hp server with the
changes from [3]. readdirplus passthrough is not yet implemented, so after an
ls, subsequent operations on listed files will go through the server until
their dentries expire and a fresh lookup sets up passthrough. During
testing/debugging, I avoided ls and accessed files directly.

[1] https://github.com/amir73il/linux/commits/fuse-backing-inode-wip/
[2] https://gist.github.com/joannekoong/1e55f90c355a928eb5fa0ac9972c1d0e
[3] https://github.com/joannekoong/libfuse/commits/extended_passthrough

Amir Goldstein (7):
  fuse: introduce FUSE_PASSTHROUGH_INO mode
  fuse: prepare for passthrough of inode operations
  fuse: prepare for readdir passthrough on directories
  fuse: implement passthrough for readdir
  fuse: prepare for long lived reference on backing file
  fuse: implement passthrough for getattr/statx
  fuse: prepare to setup backing inode passthrough on lookup

Joanne Koong (10):
  fuse: add passthrough ops gating
  fuse: prepare to cache statx attributes from entry replies
  fuse: add struct fuse_entry2_out and helpers for extended entry
    replies
  fuse: add passthrough lookup
  fuse: add passthrough support for entry creation
  fuse: add passthrough support for atomic file creation
  fuse: use passthrough getattr in setattr suid/sgid handling
  fuse: add passthrough setattr
  fuse: add passthrough open
  docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO)

 .../filesystems/fuse/fuse-passthrough.rst     | 131 ++++++++
 fs/fuse/backing.c                             |  58 +++-
 fs/fuse/cuse.c                                |   2 +-
 fs/fuse/dir.c                                 | 293 +++++++++++++++---
 fs/fuse/file.c                                |  61 +++-
 fs/fuse/fuse_i.h                              | 112 ++++++-
 fs/fuse/inode.c                               |  27 +-
 fs/fuse/iomode.c                              | 114 +++++--
 fs/fuse/passthrough.c                         | 170 +++++++++-
 fs/fuse/readdir.c                             |   5 +-
 include/uapi/linux/fuse.h                     |  33 +-
 11 files changed, 871 insertions(+), 135 deletions(-)

-- 
2.52.0


^ permalink raw reply	[flat|nested] 56+ messages in thread

* [PATCH v1 01/17] fuse: introduce FUSE_PASSTHROUGH_INO mode
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 21:11   ` Darrick J. Wong
  2026-04-20 22:16 ` [PATCH v1 02/17] fuse: prepare for passthrough of inode operations Joanne Koong
                   ` (16 subsequent siblings)
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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 7294bd347412..f239c8a888cb 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1431,7 +1431,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) {
@@ -1446,7 +1445,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 23a241f18623..86fdf873d639 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -913,6 +913,9 @@ struct fuse_conn {
 	/** Passthrough support for read/write IO */
 	unsigned int passthrough:1;
 
+	/** One-to-one mapping between fuse ino to backing ino */
+	unsigned int passthrough_ino:1;
+
 	/* Use pages instead of pointer for kernel I/O */
 	unsigned int use_pages_for_kvec_io:1;
 
@@ -1535,8 +1538,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 8b64034ab0bb..014b9af42909 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -1445,6 +1445,8 @@ static void process_init_reply(struct fuse_mount *fm, struct fuse_args *args,
 				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;
@@ -1518,8 +1520,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] 56+ messages in thread

* [PATCH v1 02/17] fuse: prepare for passthrough of inode operations
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
  2026-04-20 22:16 ` [PATCH v1 01/17] fuse: introduce FUSE_PASSTHROUGH_INO mode Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 21:16   ` Darrick J. Wong
  2026-04-20 22:16 ` [PATCH v1 03/17] fuse: prepare for readdir passthrough on directories Joanne Koong
                   ` (15 subsequent siblings)
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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          | 40 +++++++++++++++++++++++++++++++++++++++
 fs/fuse/iomode.c          |  5 +++++
 include/uapi/linux/fuse.h |  9 ++++++++-
 4 files changed, 63 insertions(+), 4 deletions(-)

diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index d95dfa48483f..830cbe2a4200 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -86,7 +86,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;
@@ -94,7 +95,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);
@@ -104,7 +109,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;
@@ -119,6 +125,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 86fdf873d639..eb974739dd5e 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -107,6 +107,7 @@ struct fuse_submount_lookup {
 struct fuse_backing {
 	struct file *file;
 	struct cred *cred;
+	u64 ops_mask;
 
 	/** refcount */
 	refcount_t count;
@@ -244,6 +245,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;
@@ -1550,6 +1553,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);
@@ -1619,6 +1641,24 @@ 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 struct fuse_backing *fuse_inode_passthrough(struct fuse_inode *fi)
+{
+#ifdef CONFIG_FUSE_PASSTHROUGH
+	if (test_bit(FUSE_I_PASSTHROUGH, &fi->state))
+		return fuse_inode_backing(fi);
+#endif
+	return NULL;
+}
+
+static inline bool fuse_inode_passthrough_op(struct inode *inode,
+					     enum fuse_opcode op)
+{
+	struct fuse_inode *fi = get_fuse_inode(inode);
+	struct fuse_backing *fb = fuse_inode_passthrough(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..6815b4506007 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) {
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] 56+ messages in thread

* [PATCH v1 03/17] fuse: prepare for readdir passthrough on directories
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
  2026-04-20 22:16 ` [PATCH v1 01/17] fuse: introduce FUSE_PASSTHROUGH_INO mode Joanne Koong
  2026-04-20 22:16 ` [PATCH v1 02/17] fuse: prepare for passthrough of inode operations Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 21:17   ` Darrick J. Wong
  2026-04-20 22:16 ` [PATCH v1 04/17] fuse: implement passthrough for readdir Joanne Koong
                   ` (14 subsequent siblings)
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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 | 12 ++++++------
 fs/fuse/inode.c  |  2 +-
 fs/fuse/iomode.c | 31 ++++++++++++++++++-------------
 6 files changed, 34 insertions(+), 28 deletions(-)

diff --git a/fs/fuse/cuse.c b/fs/fuse/cuse.c
index dfcb98a654d8..e168740351a0 100644
--- a/fs/fuse/cuse.c
+++ b/fs/fuse/cuse.c
@@ -147,7 +147,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 b658b6baf72f..015f0c103d06 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_queue_forget(fm->fc, 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 f239c8a888cb..3da4ce73e11b 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);
@@ -1456,13 +1458,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 eb974739dd5e..1c4646ad7c25 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -161,9 +161,6 @@ struct fuse_inode {
 			 * (FUSE_NOWRITE) means more writes are blocked */
 			int writectr;
 
-			/** Number of files/maps using page cache */
-			int iocachectr;
-
 			/* Waitq for writepage completion */
 			wait_queue_head_t page_waitq;
 
@@ -206,6 +203,9 @@ struct fuse_inode {
 	/** Lock to protect write related fields */
 	spinlock_t lock;
 
+	/** Number of files/maps using page cache (negative for passthrough) */
+	int iocachectr;
+
 #ifdef CONFIG_FUSE_DAX
 	/*
 	 * Dax specific inode data
@@ -238,7 +238,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
@@ -1192,7 +1192,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
@@ -1542,7 +1542,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 014b9af42909..bdc135f9fe3e 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -192,10 +192,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 6815b4506007..2360b32793c2 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);
 }
 
 /*
@@ -267,8 +274,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.
@@ -278,10 +283,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] 56+ messages in thread

* [PATCH v1 04/17] fuse: implement passthrough for readdir
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (2 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 03/17] fuse: prepare for readdir passthrough on directories Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-20 22:16 ` [PATCH v1 05/17] fuse: prepare for long lived reference on backing file Joanne Koong
                   ` (13 subsequent siblings)
  17 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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, 68 insertions(+), 3 deletions(-)

diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index 830cbe2a4200..516f58e5ae18 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -113,6 +113,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 015f0c103d06..16b266b51702 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 3da4ce73e11b..0db6abcf1033 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 1c4646ad7c25..99798a9de3ed 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1559,9 +1559,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)
@@ -1640,6 +1644,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 struct fuse_backing *fuse_inode_passthrough(struct fuse_inode *fi)
 {
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index 2360b32793c2..3753b78cbb56 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -200,11 +200,18 @@ static int fuse_file_passthrough_open(struct inode *inode, struct file *file)
 	if (IS_ERR(fb))
 		return PTR_ERR(fb);
 
+	/* Readdir passthrough requires opt-in on backing file setup */
+	err = -EOPNOTSUPP;
+	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)
 		return 0;
 
+fail:
 	fuse_passthrough_release(ff, fb);
 	fuse_backing_put(fb);
 
@@ -242,6 +249,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] 56+ messages in thread

* [PATCH v1 05/17] fuse: prepare for long lived reference on backing file
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (3 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 04/17] fuse: implement passthrough for readdir Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-20 22:16 ` [PATCH v1 06/17] fuse: implement passthrough for getattr/statx Joanne Koong
                   ` (12 subsequent siblings)
  17 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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 0db6abcf1033..e719c54c12d2 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1444,6 +1444,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) {
@@ -1458,7 +1459,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 99798a9de3ed..73de8a9c079c 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -245,7 +245,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,
 };
 
@@ -1541,7 +1541,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 bdc135f9fe3e..c80ec5d2ce82 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -195,6 +195,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 3753b78cbb56..54259ead41a4 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] 56+ messages in thread

* [PATCH v1 06/17] fuse: implement passthrough for getattr/statx
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (4 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 05/17] fuse: prepare for long lived reference on backing file Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-20 22:16 ` [PATCH v1 07/17] fuse: prepare to setup backing inode passthrough on lookup Joanne Koong
                   ` (11 subsequent siblings)
  17 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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     | 34 ++++++++++++++++++++++++++++++++++
 include/uapi/linux/fuse.h |  1 +
 4 files changed, 74 insertions(+), 1 deletion(-)

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 16b266b51702..2d037591a3ab 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_inode_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_inode_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 73de8a9c079c..1f279b5b618a 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1234,6 +1234,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);
@@ -1569,7 +1571,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))
@@ -1665,6 +1668,9 @@ static inline bool fuse_inode_passthrough_op(struct inode *inode,
 	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..1baac4f0cb68 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -219,3 +219,37 @@ 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_passthrough(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 with FUSE dev */
+	stat->dev = inode->i_sb->s_dev;
+	/* 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] 56+ messages in thread

* [PATCH v1 07/17] fuse: prepare to setup backing inode passthrough on lookup
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (5 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 06/17] fuse: implement passthrough for getattr/statx Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-20 22:16 ` [PATCH v1 08/17] fuse: add passthrough ops gating Joanne Koong
                   ` (10 subsequent siblings)
  17 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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      | 39 +++++++++++++++++++++++++++++++++++++++
 fs/fuse/passthrough.c | 12 +++++-------
 4 files changed, 68 insertions(+), 21 deletions(-)

diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index 516f58e5ae18..85ac6f917779 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -16,6 +16,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);
@@ -177,15 +197,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 1f279b5b618a..a15fb508fd28 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1584,7 +1584,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)
@@ -1595,7 +1595,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;
@@ -1650,6 +1650,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 struct fuse_backing *fuse_inode_passthrough(struct fuse_inode *fi)
 {
 #ifdef CONFIG_FUSE_PASSTHROUGH
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index 54259ead41a4..7c4053160b8a 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -181,6 +181,45 @@ 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 *fb;
+	int err;
+
+	if (!IS_ENABLED(CONFIG_FUSE_PASSTHROUGH) || !fc->passthrough_ino)
+		return 0;
+
+	/* backing inode is set once for the lifetime of the inode */
+	if (fuse_inode_passthrough(fi))
+		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;
+	}
+
+	/* 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 1baac4f0cb68..514e4af46d79 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] 56+ messages in thread

* [PATCH v1 08/17] fuse: add passthrough ops gating
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (6 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 07/17] fuse: prepare to setup backing inode passthrough on lookup Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 10:48   ` Amir Goldstein
  2026-04-20 22:16 ` [PATCH v1 09/17] fuse: prepare to cache statx attributes from entry replies Joanne Koong
                   ` (9 subsequent siblings)
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

Route existing callers through a passthrough ops mask check to verify
whether passthrough can be called for the operation or not. The check is
only done when FUSE_PASSTHROUGH_INO mode is enabled, which preserves
backwards compatibility with prior passthrough behavior.

It is safe to get the backing file by accessing ff->passthrough directly
in passthrough.c because passthrough.c is only compiled with
CONFIG_FUSE_PASSTHROUGH=y and the caller has already done the ops check.

Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
 fs/fuse/file.c        | 14 ++++++++------
 fs/fuse/fuse_i.h      | 14 +++++++++++++-
 fs/fuse/passthrough.c | 12 ++++++------
 fs/fuse/readdir.c     |  2 +-
 4 files changed, 28 insertions(+), 14 deletions(-)

diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index e719c54c12d2..a1de70bc589d 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -324,7 +324,7 @@ static void fuse_prepare_release(struct fuse_inode *fi, struct fuse_file *ff,
 	struct fuse_conn *fc = ff->fm->fc;
 	struct fuse_release_args *ra = &ff->args->release_args;
 
-	if (fuse_file_passthrough(ff))
+	if (fuse_file_passthrough(&fi->inode, ff, 0))
 		fuse_passthrough_release(ff, fuse_inode_backing(fi));
 
 	/* Inode is NULL on error path of fuse_create_open() */
@@ -1838,7 +1838,7 @@ static ssize_t fuse_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
 	/* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
 	if (ff->open_flags & FOPEN_DIRECT_IO)
 		return fuse_direct_read_iter(iocb, to);
-	else if (fuse_file_passthrough(ff))
+	else if (fuse_file_passthrough(inode, ff, FUSE_PASSTHROUGH_OP_READ))
 		return fuse_passthrough_read_iter(iocb, to);
 	else
 		return fuse_cache_read_iter(iocb, to);
@@ -1859,7 +1859,7 @@ static ssize_t fuse_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
 	/* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
 	if (ff->open_flags & FOPEN_DIRECT_IO)
 		return fuse_direct_write_iter(iocb, from);
-	else if (fuse_file_passthrough(ff))
+	else if (fuse_file_passthrough(inode, ff, FUSE_PASSTHROUGH_OP_WRITE))
 		return fuse_passthrough_write_iter(iocb, from);
 	else
 		return fuse_cache_write_iter(iocb, from);
@@ -1872,7 +1872,8 @@ static ssize_t fuse_splice_read(struct file *in, loff_t *ppos,
 	struct fuse_file *ff = in->private_data;
 
 	/* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
-	if (fuse_file_passthrough(ff) && !(ff->open_flags & FOPEN_DIRECT_IO))
+	if (fuse_file_passthrough(file_inode(in), ff, FUSE_PASSTHROUGH_OP_READ) &&
+	    !(ff->open_flags & FOPEN_DIRECT_IO))
 		return fuse_passthrough_splice_read(in, ppos, pipe, len, flags);
 	else
 		return filemap_splice_read(in, ppos, pipe, len, flags);
@@ -1884,7 +1885,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_file_passthrough(file_inode(out), ff, FUSE_PASSTHROUGH_OP_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);
@@ -2397,7 +2399,7 @@ static int fuse_file_mmap(struct file *file, struct vm_area_struct *vma)
 	 * in passthrough mode, either mmap to backing file or fail mmap,
 	 * because mixing cached mmap and passthrough io mode is not allowed.
 	 */
-	if (fuse_file_passthrough(ff))
+	if (fuse_file_passthrough(inode, ff, FUSE_PASSTHROUGH_RW_OPS))
 		return fuse_passthrough_mmap(file, vma);
 	else if (fuse_inode_backing(get_fuse_inode(inode)))
 		return -ENODEV;
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index a15fb508fd28..45d9184d0f7a 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1630,9 +1630,21 @@ static inline struct fuse_backing *fuse_inode_backing_set(struct fuse_inode *fi,
 struct fuse_backing *fuse_passthrough_open(struct file *file, int backing_id);
 void fuse_passthrough_release(struct fuse_file *ff, struct fuse_backing *fb);
 
-static inline struct file *fuse_file_passthrough(struct fuse_file *ff)
+static inline struct file *fuse_file_passthrough(struct inode *inode,
+						 struct fuse_file *ff,
+						 u64 passthrough_ops)
 {
 #ifdef CONFIG_FUSE_PASSTHROUGH
+	if (!ff->passthrough)
+		return NULL;
+
+	if (ff->fm->fc->passthrough_ino && passthrough_ops) {
+		struct fuse_backing *fb = fuse_inode_backing(get_fuse_inode(inode));
+
+		if (!fb || (fb->ops_mask & passthrough_ops) != passthrough_ops)
+			return NULL;
+	}
+
 	return ff->passthrough;
 #else
 	return NULL;
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index 514e4af46d79..8ecce2a97ee8 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -29,7 +29,7 @@ ssize_t fuse_passthrough_read_iter(struct kiocb *iocb, struct iov_iter *iter)
 {
 	struct file *file = iocb->ki_filp;
 	struct fuse_file *ff = file->private_data;
-	struct file *backing_file = fuse_file_passthrough(ff);
+	struct file *backing_file = ff->passthrough;
 	size_t count = iov_iter_count(iter);
 	ssize_t ret;
 	struct backing_file_ctx ctx = {
@@ -56,7 +56,7 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
 	struct file *file = iocb->ki_filp;
 	struct inode *inode = file_inode(file);
 	struct fuse_file *ff = file->private_data;
-	struct file *backing_file = fuse_file_passthrough(ff);
+	struct file *backing_file = ff->passthrough;
 	size_t count = iov_iter_count(iter);
 	ssize_t ret;
 	struct backing_file_ctx ctx = {
@@ -83,7 +83,7 @@ ssize_t fuse_passthrough_splice_read(struct file *in, loff_t *ppos,
 				     size_t len, unsigned int flags)
 {
 	struct fuse_file *ff = in->private_data;
-	struct file *backing_file = fuse_file_passthrough(ff);
+	struct file *backing_file = ff->passthrough;
 	struct backing_file_ctx ctx = {
 		.cred = ff->cred,
 		.accessed = fuse_file_accessed,
@@ -107,8 +107,8 @@ ssize_t fuse_passthrough_splice_write(struct pipe_inode_info *pipe,
 				      size_t len, unsigned int flags)
 {
 	struct fuse_file *ff = out->private_data;
-	struct file *backing_file = fuse_file_passthrough(ff);
 	struct inode *inode = file_inode(out);
+	struct file *backing_file = ff->passthrough;
 	ssize_t ret;
 	struct backing_file_ctx ctx = {
 		.cred = ff->cred,
@@ -132,7 +132,7 @@ ssize_t fuse_passthrough_splice_write(struct pipe_inode_info *pipe,
 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 file *backing_file = ff->passthrough;
 	struct backing_file_ctx ctx = {
 		.cred = ff->cred,
 		.accessed = fuse_file_accessed,
@@ -150,7 +150,7 @@ int fuse_passthrough_readdir(struct file *file, struct dir_context *ctx)
 	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);
+	struct file *backing_file = ff->passthrough;
 	bool locked;
 
 	pr_debug("%s: backing_file=0x%p, pos=%lld\n", __func__,
diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
index 49226f022339..1f2ff63cd317 100644
--- a/fs/fuse/readdir.c
+++ b/fs/fuse/readdir.c
@@ -593,7 +593,7 @@ int fuse_readdir(struct file *file, struct dir_context *ctx)
 	if (fuse_is_bad(inode))
 		return -EIO;
 
-	if (fuse_file_passthrough(ff))
+	if (fuse_file_passthrough(inode, ff, FUSE_PASSTHROUGH_OP_READDIR))
 		return fuse_passthrough_readdir(file, ctx);
 
 	err = UNCACHED;
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 56+ messages in thread

* [PATCH v1 09/17] fuse: prepare to cache statx attributes from entry replies
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (7 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 08/17] fuse: add passthrough ops gating Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 12:26   ` Amir Goldstein
  2026-04-20 22:16 ` [PATCH v1 10/17] fuse: add struct fuse_entry2_out and helpers for extended " Joanne Koong
                   ` (8 subsequent siblings)
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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.

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 2d037591a3ab..94ab05cb89ce 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -589,7 +589,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_queue_forget(fm->fc, forget, outarg.nodeid, 1);
 		return ERR_PTR(-ENOMEM);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 45d9184d0f7a..e422d3dc2202 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1147,10 +1147,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 c80ec5d2ce82..3b71685e7148 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -469,8 +469,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;
@@ -532,7 +532,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);
@@ -1070,7 +1070,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 {
@@ -1755,8 +1755,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 1f2ff63cd317..7bd17b3389e6 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] 56+ messages in thread

* [PATCH v1 10/17] fuse: add struct fuse_entry2_out and helpers for extended entry replies
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (8 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 09/17] fuse: prepare to cache statx attributes from entry replies Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 12:25   ` Amir Goldstein
  2026-04-20 22:16 ` [PATCH v1 11/17] fuse: add passthrough lookup Joanne Koong
                   ` (7 subsequent siblings)
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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             | 119 ++++++++++++++++++++++++++++++--------
 fs/fuse/fuse_i.h          |   5 ++
 include/uapi/linux/fuse.h |  14 +++++
 3 files changed, 113 insertions(+), 25 deletions(-)

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 94ab05cb89ce..0eacfef52164 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -354,9 +354,9 @@ 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_simple(struct fuse_args *args, u64 nodeid,
+				    const struct qstr *name,
+				    struct fuse_entry_out *outarg)
 {
 	memset(outarg, 0, sizeof(struct fuse_entry_out));
 	args->opcode = FUSE_LOOKUP;
@@ -372,6 +372,95 @@ static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
 	args->out_args[0].value = outarg;
 }
 
+static __maybe_unused 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)
+{
+	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;
+	fuse_set_zero_arg0(args);
+	args->in_args[1].size = name->len;
+	args->in_args[1].value = name->name;
+	args->in_args[2].size = 1;
+	args->in_args[2].value = "";
+	args->out_numargs = 1;
+
+	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);
+}
+
+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;
+
+	fuse_entry2_to_entry(outarg2, outarg);
+
+	/* 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;
+}
+
 /*
  * Check whether the dentry is still valid
  *
@@ -421,7 +510,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
 
 		attr_version = fuse_get_attr_version(fm->fc);
 
-		fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
+		fuse_lookup_init_simple(&args, get_node_id(dir), name, &outarg);
 		ret = fuse_simple_request(fm, &args);
 		/* Zero nodeid is same as -ENOENT */
 		if (!ret && !outarg.nodeid)
@@ -574,7 +663,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_simple(&args, nodeid, name, outarg);
 	err = fuse_simple_request(fm, &args);
 	/* Zero nodeid is same as -ENOENT, but with valid timeout */
 	if (err || !outarg->nodeid)
@@ -1394,26 +1483,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 e422d3dc2202..daf7b664e1b9 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1684,6 +1684,11 @@ static inline bool fuse_inode_passthrough_op(struct inode *inode,
 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] 56+ messages in thread

* [PATCH v1 11/17] fuse: add passthrough lookup
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (9 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 10/17] fuse: add struct fuse_entry2_out and helpers for extended " Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 13:23   ` Amir Goldstein
  2026-04-20 22:16 ` [PATCH v1 12/17] fuse: add passthrough support for entry creation Joanne Koong
                   ` (6 subsequent siblings)
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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 | 92 +++++++++++++++++++++++++++++++--------------------
 1 file changed, 57 insertions(+), 35 deletions(-)

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 0eacfef52164..9e9b77942dcd 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -354,29 +354,10 @@ static void fuse_invalidate_entry(struct dentry *entry)
 	fuse_invalidate_entry_cache(entry);
 }
 
-static void fuse_lookup_init_simple(struct fuse_args *args, u64 nodeid,
-				    const struct qstr *name,
-				    struct fuse_entry_out *outarg)
-{
-	memset(outarg, 0, sizeof(struct fuse_entry_out));
-	args->opcode = FUSE_LOOKUP;
-	args->nodeid = nodeid;
-	args->in_numargs = 3;
-	fuse_set_zero_arg0(args);
-	args->in_args[1].size = name->len;
-	args->in_args[1].value = name->name;
-	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;
-}
-
-static __maybe_unused 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)
+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)
 {
 	bool use_entry2 = fuse_use_entry2(fc);
 
@@ -437,10 +418,10 @@ static void fuse_entry2_to_entry(struct fuse_entry2_out *outarg2,
 	fuse_statx_to_attr(&outarg2->statx, &outarg->attr);
 }
 
-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;
@@ -489,9 +470,12 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
 	else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
 		 (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {
 		struct fuse_entry_out outarg;
+		struct fuse_entry2_out outarg2;
+		struct fuse_statx *sx = NULL;
 		FUSE_ARGS(args);
 		struct fuse_forget_link *forget;
 		u64 attr_version;
+		int backing_id = 0;
 
 		/* For negative dentries, always do a fresh lookup */
 		if (!inode)
@@ -510,11 +494,21 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
 
 		attr_version = fuse_get_attr_version(fm->fc);
 
-		fuse_lookup_init_simple(&args, get_node_id(dir), name, &outarg);
+		fuse_lookup_init(fc, &args, get_node_id(dir), name, &outarg,
+				 &outarg2);
 		ret = fuse_simple_request(fm, &args);
-		/* Zero nodeid is same as -ENOENT */
-		if (!ret && !outarg.nodeid)
-			ret = -ENOENT;
+		if (!ret) {
+			backing_id = fuse_process_entry2(fm->fc, &outarg2, &outarg, &sx);
+
+			/* Zero nodeid is same as -ENOENT */
+			if (!outarg.nodeid)
+				ret = -ENOENT;
+			else if (backing_id < 0) {
+				fuse_queue_forget(fm->fc, forget,
+						  outarg.nodeid, 1);
+				return backing_id;
+			}
+		}
 		if (!ret) {
 			fi = get_fuse_inode(inode);
 			if (outarg.nodeid != get_node_id(inode) ||
@@ -523,6 +517,14 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
 						  outarg.nodeid, 1);
 				goto invalid;
 			}
+			if (backing_id) {
+				ret = fuse_inode_set_passthrough(inode, backing_id);
+				if (ret) {
+					fuse_queue_forget(fm->fc, forget,
+							  outarg.nodeid, 1);
+					return ret;
+				}
+			}
 			spin_lock(&fi->lock);
 			fi->nlookup++;
 			spin_unlock(&fi->lock);
@@ -535,7 +537,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
 			goto invalid;
 
 		forget_all_cached_acls(inode);
-		fuse_change_attributes(inode, &outarg.attr, NULL,
+		fuse_change_attributes(inode, &outarg.attr, sx,
 				       ATTR_TIMEOUT(&outarg),
 				       attr_version);
 		fuse_change_entry_timeout(entry, &outarg);
@@ -644,9 +646,12 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
 		     struct fuse_entry_out *outarg, struct inode **inode)
 {
 	struct fuse_mount *fm = get_fuse_mount_super(sb);
+	struct fuse_entry2_out outarg2;
 	FUSE_ARGS(args);
 	struct fuse_forget_link *forget;
 	u64 attr_version, evict_ctr;
+	struct fuse_statx *sx = NULL;
+	int backing_id;
 	int err;
 
 	*inode = NULL;
@@ -663,11 +668,20 @@ 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_simple(&args, nodeid, name, outarg);
+	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) {
+		fuse_queue_forget(fm->fc, forget, outarg->nodeid, 1);
+		return backing_id;
+	}
 
 	err = -EIO;
 	if (fuse_invalid_attr(&outarg->attr))
@@ -678,7 +692,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, NULL, ATTR_TIMEOUT(outarg),
+			   &outarg->attr, sx, ATTR_TIMEOUT(outarg),
 			   attr_version, evict_ctr);
 	err = -ENOMEM;
 	if (!*inode) {
@@ -687,6 +701,14 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
 	}
 	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:
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 56+ messages in thread

* [PATCH v1 12/17] fuse: add passthrough support for entry creation
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (10 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 11/17] fuse: add passthrough lookup Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 14:08   ` Amir Goldstein
  2026-04-20 22:16 ` [PATCH v1 13/17] fuse: add passthrough support for atomic file creation Joanne Koong
                   ` (5 subsequent siblings)
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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 | 33 +++++++++++++++++++++++++++++----
 1 file changed, 29 insertions(+), 4 deletions(-)

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 9e9b77942dcd..e5a7640fcd30 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -1088,10 +1088,14 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
 				       struct fuse_args *args, struct inode *dir,
 				       struct dentry *entry, umode_t mode)
 {
+	bool use_entry2 = fuse_use_entry2(fm->fc);
 	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))
@@ -1103,11 +1107,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 (use_entry2) {
+		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);
@@ -1120,21 +1131,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_queue_forget(fm->fc, 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_queue_forget(fm->fc, 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] 56+ messages in thread

* [PATCH v1 13/17] fuse: add passthrough support for atomic file creation
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (11 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 12/17] fuse: add passthrough support for entry creation Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 19:51   ` Amir Goldstein
  2026-04-20 22:16 ` [PATCH v1 14/17] fuse: use passthrough getattr in setattr suid/sgid handling Joanne Koong
                   ` (4 subsequent siblings)
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

Route FUSE_CREATE (atomic create + open) through fuse_entry2_out when
FUSE_PASSTHROUGH_INO is enabled. If the server returns a backing id,
the newly created inode will be 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 | 44 ++++++++++++++++++++++++++++++++++++--------
 1 file changed, 36 insertions(+), 8 deletions(-)

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index e5a7640fcd30..637761de2c5b 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -929,14 +929,18 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
 {
 	struct inode *inode;
 	struct fuse_mount *fm = get_fuse_mount(dir);
+	bool use_entry2 = fuse_use_entry2(fm->fc);
 	FUSE_ARGS(args);
 	struct fuse_forget_link *forget;
 	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 */
@@ -958,7 +962,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();
@@ -976,8 +979,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 (use_entry2) {
+		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);
@@ -992,6 +1002,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))
@@ -1000,16 +1012,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_queue_forget(fm->fc, 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);
@@ -1036,6 +1058,12 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
 	kfree(forget);
 out_err:
 	return err;
+out_queue_forget:
+	fuse_queue_forget(fm->fc, 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] 56+ messages in thread

* [PATCH v1 14/17] fuse: use passthrough getattr in setattr suid/sgid handling
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (12 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 13/17] fuse: add passthrough support for atomic file creation Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 14:25   ` Amir Goldstein
  2026-04-20 22:16 ` [PATCH v1 15/17] fuse: add passthrough setattr Joanne Koong
                   ` (3 subsequent siblings)
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

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 637761de2c5b..ff9a92d8a496 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_inode_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] 56+ messages in thread

* [PATCH v1 15/17] fuse: add passthrough setattr
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (13 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 14/17] fuse: use passthrough getattr in setattr suid/sgid handling Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 14:20   ` Amir Goldstein
  2026-04-20 22:16 ` [PATCH v1 16/17] fuse: add passthrough open Joanne Koong
                   ` (2 subsequent siblings)
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

Add passthrough setattr for setting the attributes directly on a
backing inode via notify_change() instead of issuing a FUSE_SETATTR
request to the server.

After updating the backing inode, sync the fuse inode's vfs-visible
attributes with a passthrough getattr (which is guaranteed to be
supported, as getattr must be passed through in order to use inode
passthrough) so that it matches the attributes of the backing file.

Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
 fs/fuse/dir.c             |  3 +++
 fs/fuse/fuse_i.h          |  3 ++-
 fs/fuse/passthrough.c     | 31 +++++++++++++++++++++++++++++++
 include/uapi/linux/fuse.h |  1 +
 4 files changed, 37 insertions(+), 1 deletion(-)

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index ff9a92d8a496..6dea0f8e384f 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -2546,6 +2546,9 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
 	if (!attr->ia_valid)
 		return 0;
 
+	if (fuse_inode_passthrough_op(inode, FUSE_SETATTR))
+		return fuse_passthrough_setattr(inode, attr);
+
 	ret = fuse_do_setattr(idmap, entry, attr, file);
 	if (!ret) {
 		/*
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index daf7b664e1b9..d722382676e2 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1571,7 +1571,7 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff,
 
 /* 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))
@@ -1683,6 +1683,7 @@ static inline bool fuse_inode_passthrough_op(struct inode *inode,
 
 int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
 			     u32 request_mask, unsigned int flags);
+int fuse_passthrough_setattr(struct inode *inode, 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 8ecce2a97ee8..c70478f07d6a 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -9,6 +9,7 @@
 
 #include <linux/file.h>
 #include <linux/backing-file.h>
+#include <linux/posix_acl.h>
 #include <linux/splice.h>
 
 static void fuse_file_accessed(struct file *file)
@@ -251,3 +252,33 @@ int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
 
 	return 0;
 }
+
+int fuse_passthrough_setattr(struct inode *inode, struct iattr *attr)
+{
+	struct fuse_conn *fc = get_fuse_conn(inode);
+	struct fuse_inode *fi = get_fuse_inode(inode);
+	struct fuse_backing *fb = fuse_inode_passthrough(fi);
+	const struct cred *old_cred;
+	struct dentry *backing_entry = fb->file->f_path.dentry;
+	struct mnt_idmap *backing_idmap = mnt_idmap(fb->file->f_path.mnt);
+	int err;
+
+	old_cred = override_creds(fb->cred);
+	inode_lock(d_inode(backing_entry));
+	err = notify_change(backing_idmap, backing_entry, attr, NULL);
+	inode_unlock(d_inode(backing_entry));
+	revert_creds(old_cred);
+
+	if (err)
+		return err;
+
+	if (fc->posix_acl)
+		forget_all_cached_acls(inode);
+
+	/*
+	 * Sync the fuse inode's cached attributes with the backing inode after
+	 * updating the backing inode's attributes. The fuse inode's vfs-visible
+	 * fields need to be in sync with the backing inode
+	 */
+	return fuse_passthrough_getattr(inode, NULL, STATX_BASIC_STATS, 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] 56+ messages in thread

* [PATCH v1 16/17] fuse: add passthrough open
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (14 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 15/17] fuse: add passthrough setattr Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 20:20   ` Amir Goldstein
  2026-04-20 22:16 ` [PATCH v1 17/17] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO) Joanne Koong
  2026-04-21  9:37 ` [PATCH v1 00/17] fuse: extend passthrough to inode operations Amir Goldstein
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

Add FUSE_PASSTHROUGH_OP_OPEN which the server may set in the backing
map's ops mask to indicate that the kernel should skip sending
FUSE_OPEN/FUSE_OPENDIR to the server and open the backing file directly.
FUSE_RELEASE/FUSE_RELEASEDIR is also skipped when the file is released.
FUSE_FLUSH is skipped as well.

If the file is a directory, this automatically implies passthrough
readdir. If the file is a regular file, this automatically implies
passthrough read/write.

For FUSE_ATOMIC_O_TRUNC, the server typically handles the truncating
logic when it handles FUSE_OPEN. With passthrough open, the vfs layer
will call handle_truncate() which will trigger fuse_setattr(). If
setattr is passed through, then the backing file will be truncated
directly or if not, then a FUSE_SETATTR will be sent to the server for
the truncation.

Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
 fs/fuse/backing.c         | 12 ++++++--
 fs/fuse/dir.c             | 15 +++++++++-
 fs/fuse/file.c            | 17 ++++++++++-
 fs/fuse/fuse_i.h          | 13 ++++++++-
 fs/fuse/iomode.c          |  5 ++--
 fs/fuse/passthrough.c     | 59 +++++++++++++++++++++++++++++++++++++++
 include/uapi/linux/fuse.h |  1 +
 7 files changed, 114 insertions(+), 8 deletions(-)

diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index 85ac6f917779..5bc5581dd27f 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -118,8 +118,9 @@ 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)
+	/* For now passthrough operations on backing inodes require FUSE_PASSTHROUGH_INO */
+	if (!fc->passthrough_ino &&
+	    (map->ops_mask & (FUSE_PASSTHROUGH_INODE_OPS | FUSE_PASSTHROUGH_OP_OPEN)))
 		goto out;
 
 	file = fget_raw(map->fd);
@@ -127,6 +128,13 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
 	if (!file)
 		goto out;
 
+	if (map->ops_mask & FUSE_PASSTHROUGH_OP_OPEN) {
+		if (d_is_reg(file->f_path.dentry))
+			map->ops_mask |= FUSE_PASSTHROUGH_RW_OPS;
+		else
+			map->ops_mask |= FUSE_PASSTHROUGH_DIR_OPS;
+	}
+
 	/* read/write/splice/mmap passthrough only relevant for regular files */
 	res = d_is_dir(file->f_path.dentry) ? -EISDIR : -EINVAL;
 	if (!(map->ops_mask & ~FUSE_PASSTHROUGH_RW_OPS) &&
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 6dea0f8e384f..816cb7d7aeb1 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -2077,6 +2077,9 @@ static int fuse_dir_open(struct inode *inode, struct file *file)
 	if (err)
 		return err;
 
+	if (fuse_inode_passthrough_op(inode, FUSE_OPEN))
+		return fuse_passthrough_open_inode(inode, file);
+
 	err = fuse_do_open(fm, get_node_id(inode), file, true);
 	if (!err) {
 		struct fuse_file *ff = file->private_data;
@@ -2113,11 +2116,15 @@ static int fuse_dir_fsync(struct file *file, loff_t start, loff_t end,
 {
 	struct inode *inode = file->f_mapping->host;
 	struct fuse_conn *fc = get_fuse_conn(inode);
+	struct fuse_file *ff = file->private_data;
 	int err;
 
 	if (fuse_is_bad(inode))
 		return -EIO;
 
+	if (ff->passthrough_open)
+		return vfs_fsync_range(ff->passthrough, start, end, datasync);
+
 	if (fc->no_fsyncdir)
 		return 0;
 
@@ -2362,7 +2369,13 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 		/* This is coming from open(..., ... | O_TRUNC); */
 		WARN_ON(!(attr->ia_valid & ATTR_SIZE));
 		WARN_ON(attr->ia_size != 0);
-		if (fc->atomic_o_trunc) {
+		/*
+		 * With open passthrough, FUSE_OPEN was never sent to the
+		 * server, which means the server didn't get a chance to handle
+		 * the truncation, so we need to send a FUSE_SETATTR
+		 */
+		if (fc->atomic_o_trunc &&
+		    !fuse_inode_passthrough_op(inode, FUSE_OPEN)) {
 			/*
 			 * No need to send request to userspace, since actual
 			 * truncation has already been done by OPEN.  But still
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index a1de70bc589d..5cbf240064f9 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -111,7 +111,12 @@ static void fuse_file_put(struct fuse_file *ff, bool sync)
 
 		if (!args) {
 			/* Do nothing when server does not implement 'opendir' */
-		} else if (args->opcode == FUSE_RELEASE && ff->fm->fc->no_open) {
+		} else if ((args->opcode == FUSE_RELEASE && ff->fm->fc->no_open) ||
+			   ff->passthrough_open) {
+			/*
+			 * With open passthrough, no FUSE_OPEN was sent. Skip
+			 * sending a FUSE_RELEASE
+			 */
 			fuse_release_end(ff->fm, args, 0);
 		} else if (sync) {
 			fuse_simple_request(ff->fm, args);
@@ -278,6 +283,9 @@ static int fuse_open(struct inode *inode, struct file *file)
 	if (err)
 		return err;
 
+	if (fuse_inode_passthrough_op(inode, FUSE_OPEN))
+		return fuse_passthrough_open_inode(inode, file);
+
 	if (is_wb_truncate || dax_truncate)
 		inode_lock(inode);
 
@@ -485,6 +493,9 @@ static int fuse_flush(struct file *file, fl_owner_t id)
 	if (fuse_is_bad(inode))
 		return -EIO;
 
+	if (ff->passthrough_open)
+		return 0;
+
 	if (ff->open_flags & FOPEN_NOFLUSH && !fm->fc->writeback_cache)
 		return 0;
 
@@ -551,11 +562,15 @@ static int fuse_fsync(struct file *file, loff_t start, loff_t end,
 {
 	struct inode *inode = file->f_mapping->host;
 	struct fuse_conn *fc = get_fuse_conn(inode);
+	struct fuse_file *ff = file->private_data;
 	int err;
 
 	if (fuse_is_bad(inode))
 		return -EIO;
 
+	if (ff->passthrough_open)
+		return vfs_fsync_range(ff->passthrough, start, end, datasync);
+
 	inode_lock(inode);
 
 	/*
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index d722382676e2..726b0004fce7 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -307,6 +307,12 @@ struct fuse_file {
 	const struct cred *cred;
 #endif
 
+	/*
+	 * Was file opened via passthrough (no FUSE_OPEN/FUSE_OPENDIR
+	 * sent)?
+	 */
+	bool passthrough_open:1;
+
 	/** Has flock been performed on this file? */
 	bool flock:1;
 };
@@ -1548,6 +1554,8 @@ 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);
+int fuse_file_uncached_io_open(struct inode *inode, struct fuse_file *ff,
+			       struct fuse_backing *fb);
 
 /* file.c */
 struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
@@ -1577,7 +1585,8 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff,
 	((map)->ops_mask & FUSE_PASSTHROUGH_OP(op))
 
 #define FUSE_BACKING_MAP_VALID_OPS \
-	(FUSE_PASSTHROUGH_FILE_OPS | FUSE_PASSTHROUGH_INODE_OPS)
+	(FUSE_PASSTHROUGH_FILE_OPS | FUSE_PASSTHROUGH_INODE_OPS | \
+	 FUSE_PASSTHROUGH_OP_OPEN)
 
 /* backing.c */
 #ifdef CONFIG_FUSE_PASSTHROUGH
@@ -1685,6 +1694,8 @@ int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
 			     u32 request_mask, unsigned int flags);
 int fuse_passthrough_setattr(struct inode *inode, struct iattr *attr);
 
+int fuse_passthrough_open_inode(struct inode *inode, struct file *file);
+
 static inline bool fuse_use_entry2(struct fuse_conn *fc)
 {
 	return fc->passthrough_ino;
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index 7c4053160b8a..5a4091e7e275 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -139,9 +139,8 @@ int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_file *ff,
 }
 
 /* Takes uncached_io inode mode reference to be dropped on file release */
-static int fuse_file_uncached_io_open(struct inode *inode,
-				      struct fuse_file *ff,
-				      struct fuse_backing *fb)
+int fuse_file_uncached_io_open(struct inode *inode, struct fuse_file *ff,
+			       struct fuse_backing *fb)
 {
 	int err;
 
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index c70478f07d6a..eb55c4b42a4e 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -282,3 +282,62 @@ int fuse_passthrough_setattr(struct inode *inode, struct iattr *attr)
 	 */
 	return fuse_passthrough_getattr(inode, NULL, STATX_BASIC_STATS, 0);
 }
+
+/*
+ * Open a file in passthrough mode using the inode's backing reference.
+ * Called when FUSE_PASSTHROUGH_OP_OPEN is set on the backing map's ops.
+ * This skips sending a FUSE_OPEN/FUSE_OPENDIR request to the server.
+ * FUSE_RELEASE/FUSE_RELEASEDIR will be skipped as well when the file is
+ * released.
+ */
+int fuse_passthrough_open_inode(struct inode *inode, struct file *file)
+{
+	struct fuse_mount *fm = get_fuse_mount(inode);
+	struct fuse_inode *fi = get_fuse_inode(inode);
+	struct file *backing_file;
+	struct fuse_backing *fb;
+	struct fuse_file *ff;
+	int err;
+
+	/*
+	 * This should always pass true for the release arg because when the
+	 * last refcount on the file is dropped (see fuse_file_put()),
+	 * fuse_io_release() needs to be called on the inode stashed in
+	 * ff->args->release_args.
+	 */
+	ff = fuse_file_alloc(fm, true);
+	if (!ff)
+		return -ENOMEM;
+
+	ff->nodeid = get_node_id(inode);
+	ff->open_flags = FOPEN_PASSTHROUGH;
+	ff->passthrough_open = true;
+
+	err = -EINVAL;
+	fb = fuse_backing_get(fuse_inode_passthrough(fi));
+	if (!fb)
+		goto error_free;
+
+	backing_file = backing_file_open(&file->f_path, file->f_flags,
+					  &fb->file->f_path, fb->cred);
+	if (IS_ERR(backing_file)) {
+		err = PTR_ERR(backing_file);
+		goto error_put;
+	}
+
+	ff->passthrough = backing_file;
+	ff->cred = get_cred(fb->cred);
+
+	err = fuse_file_uncached_io_open(inode, ff, fb);
+	if (!err) {
+		file->private_data = ff;
+		return 0;
+	}
+
+	fuse_passthrough_release(ff, fb);
+error_put:
+	fuse_backing_put(fb);
+error_free:
+	fuse_file_free(ff);
+	return err;
+}
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 040fee549bb9..bfb30278f324 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -1159,6 +1159,7 @@ struct fuse_backing_map {
 #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)
+#define FUSE_PASSTHROUGH_OP_OPEN	FUSE_PASSTHROUGH_OP(FUSE_OPEN)
 
 /* Device ioctls: */
 #define FUSE_DEV_IOC_MAGIC		229
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 56+ messages in thread

* [PATCH v1 17/17] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO)
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (15 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 16/17] fuse: add passthrough open Joanne Koong
@ 2026-04-20 22:16 ` Joanne Koong
  2026-04-21 11:09   ` Amir Goldstein
  2026-04-21  9:37 ` [PATCH v1 00/17] fuse: extend passthrough to inode operations Amir Goldstein
  17 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-20 22:16 UTC (permalink / raw)
  To: miklos; +Cc: amir73il, fuse-devel, luis

Add section about extended passthrough (FUSE_PASSTHROUGH_INO) mode.

Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
 .../filesystems/fuse/fuse-passthrough.rst     | 131 ++++++++++++++++++
 1 file changed, 131 insertions(+)

diff --git a/Documentation/filesystems/fuse/fuse-passthrough.rst b/Documentation/filesystems/fuse/fuse-passthrough.rst
index 2b0e7c2da54a..c7ccea597324 100644
--- a/Documentation/filesystems/fuse/fuse-passthrough.rst
+++ b/Documentation/filesystems/fuse/fuse-passthrough.rst
@@ -25,6 +25,12 @@ 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), directory operations (readdir), and
+kernel-initiated open (bypassing ``FUSE_OPEN``). In this mode, a backing file
+can be attached to a fuse inode for its entire lifetime, not just while a file
+is open.
+
 Enabling Passthrough
 ====================
 
@@ -46,6 +52,131 @@ 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 server 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 server 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 server 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. For passthrough open (``FUSE_PASSTHROUGH_OP_OPEN`` in ``ops_mask``),
+     no further action is needed. The kernel will open the backing file
+     directly without sending ``FUSE_OPEN`` / ``FUSE_OPENDIR`` to the
+     server. ``FUSE_RELEASE`` / ``FUSE_RELEASEDIR`` is also skipped.
+  5. For server-initiated passthrough open (without
+     ``FUSE_PASSTHROUGH_OP_OPEN``), the server handles ``FUSE_OPEN`` /
+     ``FUSE_OPENDIR`` as before and returns ``FOPEN_PASSTHROUGH`` with the
+     ``backing_id`` in the open response. A ``backing_id`` is required even
+     if the inode already has passthrough set up from lookup. The server
+     must use the same ``backing_id``. If the inode has passthrough,
+     the server must set ``FOPEN_PASSTHROUGH`` or ``FOPEN_DIRECT_IO``
+     on open as cached I/O mode is not allowed on passthrough inodes.
+  6. The server should call ``FUSE_DEV_IOC_BACKING_CLOSE`` to release the
+     backing file when it is no longer needed.
+
+Passthrough Operations Mask
+---------------------------
+
+When registering a backing file via ``FUSE_DEV_IOC_BACKING_OPEN``, the server
+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
+    FUSE_PASSTHROUGH_OP_OPEN
+
+Operations fall into three categories:
+
+**File operations** (read, write, readdir): Activated per-open when the
+server returns ``FOPEN_PASSTHROUGH`` in its open response. The backing file
+reference exists only while the file is open.
+
+**Inode operations** (getattr, setattr): Activated on lookup when the server
+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.
+
+**Open passthrough**: The kernel opens the backing file directly without
+sending ``FUSE_OPEN`` / ``FUSE_OPENDIR`` to the server. ``FUSE_RELEASE`` /
+``FUSE_RELEASEDIR`` is also skipped. For regular files this implies passthrough
+read/write; for directories it implies passthrough readdir.
+
+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. N files using page cache.
+    iocachectr == 0   Idle. No files open, no passthrough.
+    iocachectr < 0    Uncached/passthrough mode. |N| references held.
+
+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.
+
 Privilege Requirements
 ======================
 
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 00/17] fuse: extend passthrough to inode operations
  2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
                   ` (16 preceding siblings ...)
  2026-04-20 22:16 ` [PATCH v1 17/17] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO) Joanne Koong
@ 2026-04-21  9:37 ` Amir Goldstein
  2026-04-21 13:55   ` Amir Goldstein
  17 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21  9:37 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> This series extends fuse passthrough to support inode operations (getattr,
> setattr), directory readdir, and kernel-initiated open on backing files.
>
> The existing FUSE_PASSTHROUGH mode attaches a backing file to a fuse inode
> only when a passthrough file is open. This series introduces
> FUSE_PASSTHROUGH_INO, a stricter mode requiring one-to-one inode number
> mapping, which allows attaching a backing file for the lifetime of the fuse
> inode.
>
> Future work includes passthrough for additional operations (rename, unlink,
> readdirplus) and full subtree passthrough, which will be part of a separate
> series.
>
> Patches 1-7 are from Amir from his git tree in [1]. There were a few
> modifications made (removed separate FUSE_PASSTHROUGH_OP_STATX op,
> modified fuse_kstat_to_attr() - the full diff of changes is in [2]); if there
> are any mistakes, those are mine and not Amir's.

FWIW these changes look good to me.
I don't know what I was thinking.
The statx code that you removed looks very silly :)

>
> For testing/debugging, this was done using the passthrough_hp server with the
> changes from [3]. readdirplus passthrough is not yet implemented, so after an
> ls, subsequent operations on listed files will go through the server until
> their dentries expire and a fresh lookup sets up passthrough. During
> testing/debugging, I avoided ls and accessed files directly.

Maybe better to disable readdirplus for the time being when
FUSE_PASSTHROUGH_OP_READDIR is set on directory inode
to have deterministic test results.

Thanks,
Amir.

>
> [1] https://github.com/amir73il/linux/commits/fuse-backing-inode-wip/
> [2] https://gist.github.com/joannekoong/1e55f90c355a928eb5fa0ac9972c1d0e
> [3] https://github.com/joannekoong/libfuse/commits/extended_passthrough
>
> Amir Goldstein (7):
>   fuse: introduce FUSE_PASSTHROUGH_INO mode
>   fuse: prepare for passthrough of inode operations
>   fuse: prepare for readdir passthrough on directories
>   fuse: implement passthrough for readdir
>   fuse: prepare for long lived reference on backing file
>   fuse: implement passthrough for getattr/statx
>   fuse: prepare to setup backing inode passthrough on lookup
>
> Joanne Koong (10):
>   fuse: add passthrough ops gating
>   fuse: prepare to cache statx attributes from entry replies
>   fuse: add struct fuse_entry2_out and helpers for extended entry
>     replies
>   fuse: add passthrough lookup
>   fuse: add passthrough support for entry creation
>   fuse: add passthrough support for atomic file creation
>   fuse: use passthrough getattr in setattr suid/sgid handling
>   fuse: add passthrough setattr
>   fuse: add passthrough open
>   docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO)
>
>  .../filesystems/fuse/fuse-passthrough.rst     | 131 ++++++++
>  fs/fuse/backing.c                             |  58 +++-
>  fs/fuse/cuse.c                                |   2 +-
>  fs/fuse/dir.c                                 | 293 +++++++++++++++---
>  fs/fuse/file.c                                |  61 +++-
>  fs/fuse/fuse_i.h                              | 112 ++++++-
>  fs/fuse/inode.c                               |  27 +-
>  fs/fuse/iomode.c                              | 114 +++++--
>  fs/fuse/passthrough.c                         | 170 +++++++++-
>  fs/fuse/readdir.c                             |   5 +-
>  include/uapi/linux/fuse.h                     |  33 +-
>  11 files changed, 871 insertions(+), 135 deletions(-)
>
> --
> 2.52.0
>

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 08/17] fuse: add passthrough ops gating
  2026-04-20 22:16 ` [PATCH v1 08/17] fuse: add passthrough ops gating Joanne Koong
@ 2026-04-21 10:48   ` Amir Goldstein
  2026-04-22  2:57     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 10:48 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Route existing callers through a passthrough ops mask check to verify
> whether passthrough can be called for the operation or not. The check is
> only done when FUSE_PASSTHROUGH_INO mode is enabled, which preserves
> backwards compatibility with prior passthrough behavior.
>
> It is safe to get the backing file by accessing ff->passthrough directly
> in passthrough.c because passthrough.c is only compiled with
> CONFIG_FUSE_PASSTHROUGH=y and the caller has already done the ops check.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
>  fs/fuse/file.c        | 14 ++++++++------
>  fs/fuse/fuse_i.h      | 14 +++++++++++++-
>  fs/fuse/passthrough.c | 12 ++++++------
>  fs/fuse/readdir.c     |  2 +-
>  4 files changed, 28 insertions(+), 14 deletions(-)
>
> diff --git a/fs/fuse/file.c b/fs/fuse/file.c
> index e719c54c12d2..a1de70bc589d 100644
> --- a/fs/fuse/file.c
> +++ b/fs/fuse/file.c
> @@ -324,7 +324,7 @@ static void fuse_prepare_release(struct fuse_inode *fi, struct fuse_file *ff,
>         struct fuse_conn *fc = ff->fm->fc;
>         struct fuse_release_args *ra = &ff->args->release_args;
>
> -       if (fuse_file_passthrough(ff))
> +       if (fuse_file_passthrough(&fi->inode, ff, 0))
>                 fuse_passthrough_release(ff, fuse_inode_backing(fi));
>
>         /* Inode is NULL on error path of fuse_create_open() */
> @@ -1838,7 +1838,7 @@ static ssize_t fuse_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
>         /* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
>         if (ff->open_flags & FOPEN_DIRECT_IO)
>                 return fuse_direct_read_iter(iocb, to);
> -       else if (fuse_file_passthrough(ff))
> +       else if (fuse_file_passthrough(inode, ff, FUSE_PASSTHROUGH_OP_READ))
>                 return fuse_passthrough_read_iter(iocb, to);
>         else
>                 return fuse_cache_read_iter(iocb, to);
> @@ -1859,7 +1859,7 @@ static ssize_t fuse_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
>         /* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
>         if (ff->open_flags & FOPEN_DIRECT_IO)
>                 return fuse_direct_write_iter(iocb, from);
> -       else if (fuse_file_passthrough(ff))
> +       else if (fuse_file_passthrough(inode, ff, FUSE_PASSTHROUGH_OP_WRITE))
>                 return fuse_passthrough_write_iter(iocb, from);
>         else
>                 return fuse_cache_write_iter(iocb, from);
> @@ -1872,7 +1872,8 @@ static ssize_t fuse_splice_read(struct file *in, loff_t *ppos,
>         struct fuse_file *ff = in->private_data;
>
>         /* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
> -       if (fuse_file_passthrough(ff) && !(ff->open_flags & FOPEN_DIRECT_IO))
> +       if (fuse_file_passthrough(file_inode(in), ff, FUSE_PASSTHROUGH_OP_READ) &&
> +           !(ff->open_flags & FOPEN_DIRECT_IO))
>                 return fuse_passthrough_splice_read(in, ppos, pipe, len, flags);
>         else
>                 return filemap_splice_read(in, ppos, pipe, len, flags);
> @@ -1884,7 +1885,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_file_passthrough(file_inode(out), ff, FUSE_PASSTHROUGH_OP_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);
> @@ -2397,7 +2399,7 @@ static int fuse_file_mmap(struct file *file, struct vm_area_struct *vma)
>          * in passthrough mode, either mmap to backing file or fail mmap,
>          * because mixing cached mmap and passthrough io mode is not allowed.
>          */
> -       if (fuse_file_passthrough(ff))
> +       if (fuse_file_passthrough(inode, ff, FUSE_PASSTHROUGH_RW_OPS))
>                 return fuse_passthrough_mmap(file, vma);
>         else if (fuse_inode_backing(get_fuse_inode(inode)))
>                 return -ENODEV;
> diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> index a15fb508fd28..45d9184d0f7a 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -1630,9 +1630,21 @@ static inline struct fuse_backing *fuse_inode_backing_set(struct fuse_inode *fi,
>  struct fuse_backing *fuse_passthrough_open(struct file *file, int backing_id);
>  void fuse_passthrough_release(struct fuse_file *ff, struct fuse_backing *fb);
>
> -static inline struct file *fuse_file_passthrough(struct fuse_file *ff)
> +static inline struct file *fuse_file_passthrough(struct inode *inode,
> +                                                struct fuse_file *ff,
> +                                                u64 passthrough_ops)
>  {
>  #ifdef CONFIG_FUSE_PASSTHROUGH
> +       if (!ff->passthrough)
> +               return NULL;
> +
> +       if (ff->fm->fc->passthrough_ino && passthrough_ops) {
> +               struct fuse_backing *fb = fuse_inode_backing(get_fuse_inode(inode));
> +
> +               if (!fb || (fb->ops_mask & passthrough_ops) != passthrough_ops)
> +                       return NULL;
> +       }
> +
>         return ff->passthrough;
>  #else
>         return NULL;


Please document the relationship between  FOPEN_PASSTHROUGH
and FUSE_PASSTHROUGH_RW_OPS.

Does a server need to opt-in both for the read/write operations passthrough
and the specific file (I guess so)?

The concept behind the inode iomodes is that it is not allowed to mix cached
with passthrough ops on the same inode, because there be dragons.

If the server wants to opt-out of passthrough for a specific opened file,
which already has a backing inode, it must explicitly request the combination
FOPEN_DIRECT_IO | FOPEN_PASSTHROUGH
and then direct io will be performed, despite the inode having a backing inode.

What you have done here is a private case of passthrough opt-out
not per file, but per operation.

The bottom line is if the fuse inode has a backing inode, then you must not
fallback to the fuse_cache_ operations in case the op is not in the mask,
but you may fallback to the fuse_direct_ operations.

I hope this does not complicate things for you too much,
but I also don't know (yet) what you intend this gating for.

Otherwise, this looks ok.

Thanks,
Amir.

> diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> index 514e4af46d79..8ecce2a97ee8 100644
> --- a/fs/fuse/passthrough.c
> +++ b/fs/fuse/passthrough.c
> @@ -29,7 +29,7 @@ ssize_t fuse_passthrough_read_iter(struct kiocb *iocb, struct iov_iter *iter)
>  {
>         struct file *file = iocb->ki_filp;
>         struct fuse_file *ff = file->private_data;
> -       struct file *backing_file = fuse_file_passthrough(ff);
> +       struct file *backing_file = ff->passthrough;
>         size_t count = iov_iter_count(iter);
>         ssize_t ret;
>         struct backing_file_ctx ctx = {
> @@ -56,7 +56,7 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
>         struct file *file = iocb->ki_filp;
>         struct inode *inode = file_inode(file);
>         struct fuse_file *ff = file->private_data;
> -       struct file *backing_file = fuse_file_passthrough(ff);
> +       struct file *backing_file = ff->passthrough;
>         size_t count = iov_iter_count(iter);
>         ssize_t ret;
>         struct backing_file_ctx ctx = {
> @@ -83,7 +83,7 @@ ssize_t fuse_passthrough_splice_read(struct file *in, loff_t *ppos,
>                                      size_t len, unsigned int flags)
>  {
>         struct fuse_file *ff = in->private_data;
> -       struct file *backing_file = fuse_file_passthrough(ff);
> +       struct file *backing_file = ff->passthrough;
>         struct backing_file_ctx ctx = {
>                 .cred = ff->cred,
>                 .accessed = fuse_file_accessed,
> @@ -107,8 +107,8 @@ ssize_t fuse_passthrough_splice_write(struct pipe_inode_info *pipe,
>                                       size_t len, unsigned int flags)
>  {
>         struct fuse_file *ff = out->private_data;
> -       struct file *backing_file = fuse_file_passthrough(ff);
>         struct inode *inode = file_inode(out);
> +       struct file *backing_file = ff->passthrough;
>         ssize_t ret;
>         struct backing_file_ctx ctx = {
>                 .cred = ff->cred,
> @@ -132,7 +132,7 @@ ssize_t fuse_passthrough_splice_write(struct pipe_inode_info *pipe,
>  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 file *backing_file = ff->passthrough;
>         struct backing_file_ctx ctx = {
>                 .cred = ff->cred,
>                 .accessed = fuse_file_accessed,
> @@ -150,7 +150,7 @@ int fuse_passthrough_readdir(struct file *file, struct dir_context *ctx)
>         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);
> +       struct file *backing_file = ff->passthrough;
>         bool locked;
>
>         pr_debug("%s: backing_file=0x%p, pos=%lld\n", __func__,
> diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
> index 49226f022339..1f2ff63cd317 100644
> --- a/fs/fuse/readdir.c
> +++ b/fs/fuse/readdir.c
> @@ -593,7 +593,7 @@ int fuse_readdir(struct file *file, struct dir_context *ctx)
>         if (fuse_is_bad(inode))
>                 return -EIO;
>
> -       if (fuse_file_passthrough(ff))
> +       if (fuse_file_passthrough(inode, ff, FUSE_PASSTHROUGH_OP_READDIR))
>                 return fuse_passthrough_readdir(file, ctx);
>
>         err = UNCACHED;
> --
> 2.52.0
>

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 17/17] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO)
  2026-04-20 22:16 ` [PATCH v1 17/17] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO) Joanne Koong
@ 2026-04-21 11:09   ` Amir Goldstein
  2026-04-22  1:04     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 11:09 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:19 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     | 131 ++++++++++++++++++
>  1 file changed, 131 insertions(+)
>
> diff --git a/Documentation/filesystems/fuse/fuse-passthrough.rst b/Documentation/filesystems/fuse/fuse-passthrough.rst
> index 2b0e7c2da54a..c7ccea597324 100644
> --- a/Documentation/filesystems/fuse/fuse-passthrough.rst
> +++ b/Documentation/filesystems/fuse/fuse-passthrough.rst
> @@ -25,6 +25,12 @@ 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), directory operations (readdir), and
> +kernel-initiated open (bypassing ``FUSE_OPEN``). In this mode, a backing file
> +can be attached to a fuse inode for its entire lifetime, not just while a file
> +is open.
> +
>  Enabling Passthrough
>  ====================
>
> @@ -46,6 +52,131 @@ 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 server 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 server 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 server 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. For passthrough open (``FUSE_PASSTHROUGH_OP_OPEN`` in ``ops_mask``),
> +     no further action is needed. The kernel will open the backing file
> +     directly without sending ``FUSE_OPEN`` / ``FUSE_OPENDIR`` to the
> +     server. ``FUSE_RELEASE`` / ``FUSE_RELEASEDIR`` is also skipped.
> +  5. For server-initiated passthrough open (without
> +     ``FUSE_PASSTHROUGH_OP_OPEN``), the server handles ``FUSE_OPEN`` /
> +     ``FUSE_OPENDIR`` as before and returns ``FOPEN_PASSTHROUGH`` with the
> +     ``backing_id`` in the open response. A ``backing_id`` is required even
> +     if the inode already has passthrough set up from lookup. The server
> +     must use the same ``backing_id``. If the inode has passthrough,
> +     the server must set ``FOPEN_PASSTHROUGH`` or ``FOPEN_DIRECT_IO``

IIUC (and please do double check this), the server MUST always set
``FOPEN_PASSTHROUGH`` if inode is in passthrough mode and it MAY set
``FOPEN_DIRECT_IO`` *additionally* to opt-out of passthrough when inode
is already in passthrough mode.

> +     on open as cached I/O mode is not allowed on passthrough inodes.
> +  6. The server should call ``FUSE_DEV_IOC_BACKING_CLOSE`` to release the
> +     backing file when it is no longer needed.
> +
> +Passthrough Operations Mask
> +---------------------------
> +
> +When registering a backing file via ``FUSE_DEV_IOC_BACKING_OPEN``, the server
> +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
> +    FUSE_PASSTHROUGH_OP_OPEN
> +
> +Operations fall into three categories:
> +
> +**File operations** (read, write, readdir): Activated per-open when the
> +server returns ``FOPEN_PASSTHROUGH`` in its open response. The backing file
> +reference exists only while the file is open.
> +
> +**Inode operations** (getattr, setattr): Activated on lookup when the server
> +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.
> +
> +**Open passthrough**: The kernel opens the backing file directly without
> +sending ``FUSE_OPEN`` / ``FUSE_OPENDIR`` to the server. ``FUSE_RELEASE`` /
> +``FUSE_RELEASEDIR`` is also skipped. For regular files this implies passthrough
> +read/write; for directories it implies passthrough readdir.
> +
> +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. N files using page cache.
> +    iocachectr == 0   Idle. No files open, no passthrough.
> +    iocachectr < 0    Uncached/passthrough mode. |N| references held.
> +
> +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.
> +

Looks nice.
Adds a few things (e.g. iomodes) that are missing in the doc
for the non-extended passthrough.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 10/17] fuse: add struct fuse_entry2_out and helpers for extended entry replies
  2026-04-20 22:16 ` [PATCH v1 10/17] fuse: add struct fuse_entry2_out and helpers for extended " Joanne Koong
@ 2026-04-21 12:25   ` Amir Goldstein
  2026-04-22  0:50     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 12:25 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:18 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.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
>  fs/fuse/dir.c             | 119 ++++++++++++++++++++++++++++++--------
>  fs/fuse/fuse_i.h          |   5 ++
>  include/uapi/linux/fuse.h |  14 +++++
>  3 files changed, 113 insertions(+), 25 deletions(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index 94ab05cb89ce..0eacfef52164 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -354,9 +354,9 @@ 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_simple(struct fuse_args *args, u64 nodeid,
> +                                   const struct qstr *name,
> +                                   struct fuse_entry_out *outarg)

Nit: patch 11 would be nicer if you add the arg now and pass NULL from callers
skipping this temporary fuse_lookup_init_simple() step.

>  {
>         memset(outarg, 0, sizeof(struct fuse_entry_out));
>         args->opcode = FUSE_LOOKUP;
> @@ -372,6 +372,95 @@ static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
>         args->out_args[0].value = outarg;
>  }
>
> +static __maybe_unused 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)
> +{
> +       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;
> +       fuse_set_zero_arg0(args);
> +       args->in_args[1].size = name->len;
> +       args->in_args[1].value = name->name;
> +       args->in_args[2].size = 1;
> +       args->in_args[2].value = "";

I know you just carried this, but do you know what this is?
some sort of backward compat inarg?

> +       args->out_numargs = 1;
> +
> +       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);
> +}
> +
> +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;
> +
> +       fuse_entry2_to_entry(outarg2, outarg);
> +
> +       /* 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;
> +}
> +

Need to check that outargs2->flags and ->reserved are 0.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 09/17] fuse: prepare to cache statx attributes from entry replies
  2026-04-20 22:16 ` [PATCH v1 09/17] fuse: prepare to cache statx attributes from entry replies Joanne Koong
@ 2026-04-21 12:26   ` Amir Goldstein
  0 siblings, 0 replies; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 12:26 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> 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.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>

Looks fine to me

Reviewed-by: Amir Goldstein <amir73il@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 2d037591a3ab..94ab05cb89ce 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -589,7 +589,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_queue_forget(fm->fc, forget, outarg.nodeid, 1);
>                 return ERR_PTR(-ENOMEM);
> diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> index 45d9184d0f7a..e422d3dc2202 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -1147,10 +1147,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 c80ec5d2ce82..3b71685e7148 100644
> --- a/fs/fuse/inode.c
> +++ b/fs/fuse/inode.c
> @@ -469,8 +469,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;
> @@ -532,7 +532,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);
> @@ -1070,7 +1070,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 {
> @@ -1755,8 +1755,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 1f2ff63cd317..7bd17b3389e6 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	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 11/17] fuse: add passthrough lookup
  2026-04-20 22:16 ` [PATCH v1 11/17] fuse: add passthrough lookup Joanne Koong
@ 2026-04-21 13:23   ` Amir Goldstein
  2026-04-22  3:17     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 13:23 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:18 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.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
>  fs/fuse/dir.c | 92 +++++++++++++++++++++++++++++++--------------------
>  1 file changed, 57 insertions(+), 35 deletions(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index 0eacfef52164..9e9b77942dcd 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -354,29 +354,10 @@ static void fuse_invalidate_entry(struct dentry *entry)
>         fuse_invalidate_entry_cache(entry);
>  }
>
> -static void fuse_lookup_init_simple(struct fuse_args *args, u64 nodeid,
> -                                   const struct qstr *name,
> -                                   struct fuse_entry_out *outarg)
> -{
> -       memset(outarg, 0, sizeof(struct fuse_entry_out));
> -       args->opcode = FUSE_LOOKUP;
> -       args->nodeid = nodeid;
> -       args->in_numargs = 3;
> -       fuse_set_zero_arg0(args);
> -       args->in_args[1].size = name->len;
> -       args->in_args[1].value = name->name;
> -       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;
> -}
> -
> -static __maybe_unused 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)

As I wrote on patch 10, this back and forth with
fuse_lookup_init_simple is not needed.

> +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)
>  {
>         bool use_entry2 = fuse_use_entry2(fc);
>
> @@ -437,10 +418,10 @@ static void fuse_entry2_to_entry(struct fuse_entry2_out *outarg2,
>         fuse_statx_to_attr(&outarg2->statx, &outarg->attr);
>  }
>
> -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;
> @@ -489,9 +470,12 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
>         else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
>                  (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {


Not changed by your patch but relevant to my other comments.
The flow of this function is terrible.
My eyes cannot stand non-matching {} they are review traps
and there is no need for the else after goto anyway:

        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))) {
                /* For negative dentries, always do a fresh lookup */
                if (!inode)
                        goto invalid;

                /* This code is deeply nested - and even more so after
this patch */

        } else if (inode) {
                ...
        }
        ret = 1;
out:
        return ret;
invalid:
        ret = 0;
        goto out;

These goto labels are useless.

Perhaps:

        bool need_reval = time_before64(fuse_dentry_time(entry),
get_jiffies_64()) ||
                 (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET));

        if (inode && fuse_is_bad(inode))
                return 0;

        if (!inode && need_reval)
                return 0;

        if (inode && !need_reval) {
                ...
                return 1;
        }

The rest here inline or call helper fuse_do_dentry_revalidate()
because its a pretty
big and proper coding style is that functions fit in a single terminal screen.

>                 struct fuse_entry_out outarg;
> +               struct fuse_entry2_out outarg2;
> +               struct fuse_statx *sx = NULL;
>                 FUSE_ARGS(args);
>                 struct fuse_forget_link *forget;
>                 u64 attr_version;
> +               int backing_id = 0;
>
>                 /* For negative dentries, always do a fresh lookup */
>                 if (!inode)
> @@ -510,11 +494,21 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
>
>                 attr_version = fuse_get_attr_version(fm->fc);
>
> -               fuse_lookup_init_simple(&args, get_node_id(dir), name, &outarg);
> +               fuse_lookup_init(fc, &args, get_node_id(dir), name, &outarg,
> +                                &outarg2);
>                 ret = fuse_simple_request(fm, &args);
> -               /* Zero nodeid is same as -ENOENT */
> -               if (!ret && !outarg.nodeid)
> -                       ret = -ENOENT;
> +               if (!ret) {
> +                       backing_id = fuse_process_entry2(fm->fc, &outarg2, &outarg, &sx);
> +
> +                       /* Zero nodeid is same as -ENOENT */
> +                       if (!outarg.nodeid)
> +                               ret = -ENOENT;
> +                       else if (backing_id < 0) {

Again, the unforgivable sin of {} mismatch

> +                               fuse_queue_forget(fm->fc, forget,
> +                                                 outarg.nodeid, 1);

This line just goes to show that the nesting is too deep.

> +                               return backing_id;
> +                       }
> +               }
>                 if (!ret) {
>                         fi = get_fuse_inode(inode);
>                         if (outarg.nodeid != get_node_id(inode) ||
> @@ -523,6 +517,14 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
>                                                   outarg.nodeid, 1);
>                                 goto invalid;
>                         }
> +                       if (backing_id) {
> +                               ret = fuse_inode_set_passthrough(inode, backing_id);
> +                               if (ret) {
> +                                       fuse_queue_forget(fm->fc, forget,
> +                                                         outarg.nodeid, 1);
> +                                       return ret;
> +                               }
> +                       }

This is revalidate so the inode already exists and likely already has a backing
inode so this is almost certainly bound to fail.

And the same thing can happen in fuse_lookup_name() when fuse_iget()
finds an existing inode - for example on lookup of a hardlink alias.

I see a few options:
1. Move fuse_inode_set_passthrough into fuse_iget() and otherwise
    ignore the backing_id for non inode instantiating lookups
2. Teach fuse_inode_set_passthrough() to be happy if existing backing inode
    exist and the new backing_id refers to the same fuse_backing object

I am leaning towards #2 as this is exactly what fuse_inode_uncached_io_start()
does when many FOPEN_PASSTHROUGH are called for the same inode.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 00/17] fuse: extend passthrough to inode operations
  2026-04-21  9:37 ` [PATCH v1 00/17] fuse: extend passthrough to inode operations Amir Goldstein
@ 2026-04-21 13:55   ` Amir Goldstein
  2026-04-21 21:05     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 13:55 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 11:37 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > This series extends fuse passthrough to support inode operations (getattr,
> > setattr), directory readdir, and kernel-initiated open on backing files.
> >
> > The existing FUSE_PASSTHROUGH mode attaches a backing file to a fuse inode
> > only when a passthrough file is open. This series introduces
> > FUSE_PASSTHROUGH_INO, a stricter mode requiring one-to-one inode number
> > mapping, which allows attaching a backing file for the lifetime of the fuse
> > inode.
> >
> > Future work includes passthrough for additional operations (rename, unlink,
> > readdirplus) and full subtree passthrough, which will be part of a separate
> > series.

Joanne,

Thanks a lot for picking up my abandoned wip!

Could you add a few words about the intended use case in Meta?

Is the target fuse filesystem expected to be used by arbitrary users/apps
or by a controlled set of users/apps where a posix_fadvise() may make
sense to configure readdir vs. readdirplus?

What is the minimal set of passthrough ops that would make this useful
to your internal customers? Is readdirplus in this set?

Do you have profiling numbers to indicate which ops generate the most
overhead for would-be passthrough ops routed to the server for the
target workloads?
And how do these numbers improve when using io_uring channel?

Regarding "full subtree passthrough", what does that mean?
That lookup op itself is passthrough?

In that case, would there be no way to escape (opt-out of) passthrough
inside the subtree?

The semantics sound challenging especially when considering
moves of directories in and out of subtrees.

Anyway, no wrong answers here, just trying to understand the
first mile store you are aiming at and the expected improvement.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 12/17] fuse: add passthrough support for entry creation
  2026-04-20 22:16 ` [PATCH v1 12/17] fuse: add passthrough support for entry creation Joanne Koong
@ 2026-04-21 14:08   ` Amir Goldstein
  2026-04-22  3:01     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 14:08 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> 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 | 33 +++++++++++++++++++++++++++++----
>  1 file changed, 29 insertions(+), 4 deletions(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index 9e9b77942dcd..e5a7640fcd30 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -1088,10 +1088,14 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
>                                        struct fuse_args *args, struct inode *dir,
>                                        struct dentry *entry, umode_t mode)
>  {
> +       bool use_entry2 = fuse_use_entry2(fm->fc);
>         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))
> @@ -1103,11 +1107,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 (use_entry2) {
> +               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);
> @@ -1120,21 +1131,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_queue_forget(fm->fc, 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_queue_forget(fm->fc, 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);
> +               }
> +       }
> +


I think we need to verify that the backing file is of the same type as
requested entry.

This was also true for lookup which instantiates the inode according to the
mode in fuse_attr.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 15/17] fuse: add passthrough setattr
  2026-04-20 22:16 ` [PATCH v1 15/17] fuse: add passthrough setattr Joanne Koong
@ 2026-04-21 14:20   ` Amir Goldstein
  2026-04-21 14:32     ` Amir Goldstein
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 14:20 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Add passthrough setattr for setting the attributes directly on a
> backing inode via notify_change() instead of issuing a FUSE_SETATTR
> request to the server.
>
> After updating the backing inode, sync the fuse inode's vfs-visible
> attributes with a passthrough getattr (which is guaranteed to be
> supported, as getattr must be passed through in order to use inode
> passthrough) so that it matches the attributes of the backing file.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
>  fs/fuse/dir.c             |  3 +++
>  fs/fuse/fuse_i.h          |  3 ++-
>  fs/fuse/passthrough.c     | 31 +++++++++++++++++++++++++++++++
>  include/uapi/linux/fuse.h |  1 +
>  4 files changed, 37 insertions(+), 1 deletion(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index ff9a92d8a496..6dea0f8e384f 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -2546,6 +2546,9 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
>         if (!attr->ia_valid)
>                 return 0;
>
> +       if (fuse_inode_passthrough_op(inode, FUSE_SETATTR))
> +               return fuse_passthrough_setattr(inode, attr);
> +
>         ret = fuse_do_setattr(idmap, entry, attr, file);
>         if (!ret) {
>                 /*
> diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> index daf7b664e1b9..d722382676e2 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -1571,7 +1571,7 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff,
>
>  /* 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))
> @@ -1683,6 +1683,7 @@ static inline bool fuse_inode_passthrough_op(struct inode *inode,
>
>  int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
>                              u32 request_mask, unsigned int flags);
> +int fuse_passthrough_setattr(struct inode *inode, 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 8ecce2a97ee8..c70478f07d6a 100644
> --- a/fs/fuse/passthrough.c
> +++ b/fs/fuse/passthrough.c
> @@ -9,6 +9,7 @@
>
>  #include <linux/file.h>
>  #include <linux/backing-file.h>
> +#include <linux/posix_acl.h>
>  #include <linux/splice.h>
>
>  static void fuse_file_accessed(struct file *file)
> @@ -251,3 +252,33 @@ int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
>
>         return 0;
>  }
> +
> +int fuse_passthrough_setattr(struct inode *inode, struct iattr *attr)
> +{
> +       struct fuse_conn *fc = get_fuse_conn(inode);
> +       struct fuse_inode *fi = get_fuse_inode(inode);
> +       struct fuse_backing *fb = fuse_inode_passthrough(fi);
> +       const struct cred *old_cred;
> +       struct dentry *backing_entry = fb->file->f_path.dentry;
> +       struct mnt_idmap *backing_idmap = mnt_idmap(fb->file->f_path.mnt);
> +       int err;
> +
> +       old_cred = override_creds(fb->cred);
> +       inode_lock(d_inode(backing_entry));
> +       err = notify_change(backing_idmap, backing_entry, attr, NULL);
> +       inode_unlock(d_inode(backing_entry));
> +       revert_creds(old_cred);

Looking at ovl_setattr() (overlayfs should be the reference to all
passthrough ops)
it looks like mnt_want_write() is missing and possibly many other tweaks

It is generally advisable to keep heavy logic as this in
fs/backing_file.c helpers,
which overlayfs and fuse can share, but I did not look to see how much of
ovl_setattr() is relevant for fuse passthrough.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 14/17] fuse: use passthrough getattr in setattr suid/sgid handling
  2026-04-20 22:16 ` [PATCH v1 14/17] fuse: use passthrough getattr in setattr suid/sgid handling Joanne Koong
@ 2026-04-21 14:25   ` Amir Goldstein
  2026-04-22  3:48     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 14:25 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:18 AM 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 637761de2c5b..ff9a92d8a496 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c

                /*
                 * The only sane way to reliably kill suid/sgid is to do it in
                 * the userspace filesystem
                 *
                 * This should be done on write(), truncate() and chown().
                 */
                if (!fc->handle_killpriv && !fc->handle_killpriv_v2) {

We should probably enforce a dependency between those two and
passthrough of SETATTR and maybe GETATTR - not sure what
the rules should be though.


> @@ -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_inode_passthrough_op(inode, FUSE_GETATTR))
> +                               ret = fuse_passthrough_getattr(inode, NULL,
> +                                                              STATX_MODE, 0);
> +                       else
> +                               ret = fuse_do_getattr(idmap, inode, NULL, file);

It does not feel right to passthrough GETATTR if not passthrough SETATTR
in this case - I could be wrong.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 15/17] fuse: add passthrough setattr
  2026-04-21 14:20   ` Amir Goldstein
@ 2026-04-21 14:32     ` Amir Goldstein
  2026-04-22  1:09       ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 14:32 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

> > +int fuse_passthrough_setattr(struct inode *inode, struct iattr *attr)
> > +{
> > +       struct fuse_conn *fc = get_fuse_conn(inode);
> > +       struct fuse_inode *fi = get_fuse_inode(inode);
> > +       struct fuse_backing *fb = fuse_inode_passthrough(fi);
> > +       const struct cred *old_cred;
> > +       struct dentry *backing_entry = fb->file->f_path.dentry;
> > +       struct mnt_idmap *backing_idmap = mnt_idmap(fb->file->f_path.mnt);
> > +       int err;
> > +
> > +       old_cred = override_creds(fb->cred);
> > +       inode_lock(d_inode(backing_entry));
> > +       err = notify_change(backing_idmap, backing_entry, attr, NULL);
> > +       inode_unlock(d_inode(backing_entry));
> > +       revert_creds(old_cred);
>
> Looking at ovl_setattr() (overlayfs should be the reference to all
> passthrough ops)
> it looks like mnt_want_write() is missing and possibly many other tweaks
>
> It is generally advisable to keep heavy logic as this in
> fs/backing_file.c helpers,
> which overlayfs and fuse can share, but I did not look to see how much of
> ovl_setattr() is relevant for fuse passthrough.
>

Also setattr is not a file op, so if we move to common code it
should probably be fs/backing_inode.c or something (*).

Thanks,
Amir.

(*) there are a few common helpers in fs/stack.c, but there are out dated
and only used by ecryptfs, so better start a new lib.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 13/17] fuse: add passthrough support for atomic file creation
  2026-04-20 22:16 ` [PATCH v1 13/17] fuse: add passthrough support for atomic file creation Joanne Koong
@ 2026-04-21 19:51   ` Amir Goldstein
  2026-04-22  0:40     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 19:51 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Route FUSE_CREATE (atomic create + open) through fuse_entry2_out when
> FUSE_PASSTHROUGH_INO is enabled. If the server returns a backing id,
> the newly created inode will be 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 | 44 ++++++++++++++++++++++++++++++++++++--------
>  1 file changed, 36 insertions(+), 8 deletions(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index e5a7640fcd30..637761de2c5b 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -929,14 +929,18 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
>  {
>         struct inode *inode;
>         struct fuse_mount *fm = get_fuse_mount(dir);
> +       bool use_entry2 = fuse_use_entry2(fm->fc);
>         FUSE_ARGS(args);
>         struct fuse_forget_link *forget;
>         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 */
> @@ -958,7 +962,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();
> @@ -976,8 +979,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 (use_entry2) {
> +               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);
> @@ -992,6 +1002,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))
> @@ -1000,16 +1012,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;

Unless I am missing something, you need to set FOPEN_PASSTHROUGH
in ff->open_flags otherwise finish_open will not open the ff->passthrough
file (with the correct RW mode).

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 16/17] fuse: add passthrough open
  2026-04-20 22:16 ` [PATCH v1 16/17] fuse: add passthrough open Joanne Koong
@ 2026-04-21 20:20   ` Amir Goldstein
  2026-04-22  4:19     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-21 20:20 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:19 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Add FUSE_PASSTHROUGH_OP_OPEN which the server may set in the backing
> map's ops mask to indicate that the kernel should skip sending
> FUSE_OPEN/FUSE_OPENDIR to the server and open the backing file directly.
> FUSE_RELEASE/FUSE_RELEASEDIR is also skipped when the file is released.
> FUSE_FLUSH is skipped as well.
>
> If the file is a directory, this automatically implies passthrough
> readdir. If the file is a regular file, this automatically implies
> passthrough read/write.
>
> For FUSE_ATOMIC_O_TRUNC, the server typically handles the truncating
> logic when it handles FUSE_OPEN. With passthrough open, the vfs layer
> will call handle_truncate() which will trigger fuse_setattr(). If
> setattr is passed through, then the backing file will be truncated
> directly or if not, then a FUSE_SETATTR will be sent to the server for
> the truncation.

I think we need to reduce complexity.
If we do open passthrough, then the server should not handle truncate.
same for write, if we passthrough write, server should not handle killpriv.
If this means that PASSTHROUGH_INO conflicts with some other
fuse configs, then I think we should make those configs mutually exclusive,
at least for the start.

>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
>  fs/fuse/backing.c         | 12 ++++++--
>  fs/fuse/dir.c             | 15 +++++++++-
>  fs/fuse/file.c            | 17 ++++++++++-
>  fs/fuse/fuse_i.h          | 13 ++++++++-
>  fs/fuse/iomode.c          |  5 ++--
>  fs/fuse/passthrough.c     | 59 +++++++++++++++++++++++++++++++++++++++
>  include/uapi/linux/fuse.h |  1 +
>  7 files changed, 114 insertions(+), 8 deletions(-)
>
> diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
> index 85ac6f917779..5bc5581dd27f 100644
> --- a/fs/fuse/backing.c
> +++ b/fs/fuse/backing.c
> @@ -118,8 +118,9 @@ 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)
> +       /* For now passthrough operations on backing inodes require FUSE_PASSTHROUGH_INO */
> +       if (!fc->passthrough_ino &&
> +           (map->ops_mask & (FUSE_PASSTHROUGH_INODE_OPS | FUSE_PASSTHROUGH_OP_OPEN)))
>                 goto out;

I would argue that in this context, passthrough open *is* an inode operation,
just like CREATE, because it operates on the backing inode, not ff->passthrough.
But this POV is confusing when considering file_operations and inode_operations
so let it stay as you did.

>
>         file = fget_raw(map->fd);
> @@ -127,6 +128,13 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
>         if (!file)
>                 goto out;
>
> +       if (map->ops_mask & FUSE_PASSTHROUGH_OP_OPEN) {
> +               if (d_is_reg(file->f_path.dentry))
> +                       map->ops_mask |= FUSE_PASSTHROUGH_RW_OPS;
> +               else
> +                       map->ops_mask |= FUSE_PASSTHROUGH_DIR_OPS;
> +       }
> +
>         /* read/write/splice/mmap passthrough only relevant for regular files */
>         res = d_is_dir(file->f_path.dentry) ? -EISDIR : -EINVAL;
>         if (!(map->ops_mask & ~FUSE_PASSTHROUGH_RW_OPS) &&
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index 6dea0f8e384f..816cb7d7aeb1 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -2077,6 +2077,9 @@ static int fuse_dir_open(struct inode *inode, struct file *file)
>         if (err)
>                 return err;
>
> +       if (fuse_inode_passthrough_op(inode, FUSE_OPEN))
> +               return fuse_passthrough_open_inode(inode, file);
> +
>         err = fuse_do_open(fm, get_node_id(inode), file, true);
>         if (!err) {
>                 struct fuse_file *ff = file->private_data;
> @@ -2113,11 +2116,15 @@ static int fuse_dir_fsync(struct file *file, loff_t start, loff_t end,
>  {
>         struct inode *inode = file->f_mapping->host;
>         struct fuse_conn *fc = get_fuse_conn(inode);
> +       struct fuse_file *ff = file->private_data;
>         int err;
>
>         if (fuse_is_bad(inode))
>                 return -EIO;
>
> +       if (ff->passthrough_open)
> +               return vfs_fsync_range(ff->passthrough, start, end, datasync);
> +

ovl_fsync has override_cred maybe needed here?

>         if (fc->no_fsyncdir)
>                 return 0;
>
> @@ -2362,7 +2369,13 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
>                 /* This is coming from open(..., ... | O_TRUNC); */
>                 WARN_ON(!(attr->ia_valid & ATTR_SIZE));
>                 WARN_ON(attr->ia_size != 0);
> -               if (fc->atomic_o_trunc) {
> +               /*
> +                * With open passthrough, FUSE_OPEN was never sent to the
> +                * server, which means the server didn't get a chance to handle
> +                * the truncation, so we need to send a FUSE_SETATTR
> +                */

Do we need to do that?
Is the backing inode being opened with O_TRUNC? maybe not?
If it does it will be truncated and fuse will read the size attribute. no?

> +               if (fc->atomic_o_trunc &&
> +                   !fuse_inode_passthrough_op(inode, FUSE_OPEN)) {
>                         /*
>                          * No need to send request to userspace, since actual
>                          * truncation has already been done by OPEN.  But still
> diff --git a/fs/fuse/file.c b/fs/fuse/file.c
> index a1de70bc589d..5cbf240064f9 100644
> --- a/fs/fuse/file.c
> +++ b/fs/fuse/file.c
> @@ -111,7 +111,12 @@ static void fuse_file_put(struct fuse_file *ff, bool sync)
>
>                 if (!args) {
>                         /* Do nothing when server does not implement 'opendir' */
> -               } else if (args->opcode == FUSE_RELEASE && ff->fm->fc->no_open) {
> +               } else if ((args->opcode == FUSE_RELEASE && ff->fm->fc->no_open) ||
> +                          ff->passthrough_open) {
> +                       /*
> +                        * With open passthrough, no FUSE_OPEN was sent. Skip
> +                        * sending a FUSE_RELEASE
> +                        */
>                         fuse_release_end(ff->fm, args, 0);
>                 } else if (sync) {
>                         fuse_simple_request(ff->fm, args);
> @@ -278,6 +283,9 @@ static int fuse_open(struct inode *inode, struct file *file)
>         if (err)
>                 return err;
>
> +       if (fuse_inode_passthrough_op(inode, FUSE_OPEN))
> +               return fuse_passthrough_open_inode(inode, file);
> +
>         if (is_wb_truncate || dax_truncate)
>                 inode_lock(inode);
>
> @@ -485,6 +493,9 @@ static int fuse_flush(struct file *file, fl_owner_t id)
>         if (fuse_is_bad(inode))
>                 return -EIO;
>
> +       if (ff->passthrough_open)
> +               return 0;
> +
>         if (ff->open_flags & FOPEN_NOFLUSH && !fm->fc->writeback_cache)
>                 return 0;
>
> @@ -551,11 +562,15 @@ static int fuse_fsync(struct file *file, loff_t start, loff_t end,
>  {
>         struct inode *inode = file->f_mapping->host;
>         struct fuse_conn *fc = get_fuse_conn(inode);
> +       struct fuse_file *ff = file->private_data;
>         int err;
>
>         if (fuse_is_bad(inode))
>                 return -EIO;
>
> +       if (ff->passthrough_open)
> +               return vfs_fsync_range(ff->passthrough, start, end, datasync);
> +

ovl_fsync() has override_cred - not sure if it is a must here...

>         inode_lock(inode);
>
>         /*
> diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> index d722382676e2..726b0004fce7 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -307,6 +307,12 @@ struct fuse_file {
>         const struct cred *cred;
>  #endif
>
> +       /*
> +        * Was file opened via passthrough (no FUSE_OPEN/FUSE_OPENDIR
> +        * sent)?
> +        */
> +       bool passthrough_open:1;
> +
>         /** Has flock been performed on this file? */
>         bool flock:1;
>  };
> @@ -1548,6 +1554,8 @@ 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);
> +int fuse_file_uncached_io_open(struct inode *inode, struct fuse_file *ff,
> +                              struct fuse_backing *fb);
>
>  /* file.c */
>  struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
> @@ -1577,7 +1585,8 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff,
>         ((map)->ops_mask & FUSE_PASSTHROUGH_OP(op))
>
>  #define FUSE_BACKING_MAP_VALID_OPS \
> -       (FUSE_PASSTHROUGH_FILE_OPS | FUSE_PASSTHROUGH_INODE_OPS)
> +       (FUSE_PASSTHROUGH_FILE_OPS | FUSE_PASSTHROUGH_INODE_OPS | \
> +        FUSE_PASSTHROUGH_OP_OPEN)
>
>  /* backing.c */
>  #ifdef CONFIG_FUSE_PASSTHROUGH
> @@ -1685,6 +1694,8 @@ int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
>                              u32 request_mask, unsigned int flags);
>  int fuse_passthrough_setattr(struct inode *inode, struct iattr *attr);
>
> +int fuse_passthrough_open_inode(struct inode *inode, struct file *file);
> +
>  static inline bool fuse_use_entry2(struct fuse_conn *fc)
>  {
>         return fc->passthrough_ino;
> diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
> index 7c4053160b8a..5a4091e7e275 100644
> --- a/fs/fuse/iomode.c
> +++ b/fs/fuse/iomode.c
> @@ -139,9 +139,8 @@ int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_file *ff,
>  }
>
>  /* Takes uncached_io inode mode reference to be dropped on file release */
> -static int fuse_file_uncached_io_open(struct inode *inode,
> -                                     struct fuse_file *ff,
> -                                     struct fuse_backing *fb)
> +int fuse_file_uncached_io_open(struct inode *inode, struct fuse_file *ff,
> +                              struct fuse_backing *fb)
>  {
>         int err;
>
> diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> index c70478f07d6a..eb55c4b42a4e 100644
> --- a/fs/fuse/passthrough.c
> +++ b/fs/fuse/passthrough.c
> @@ -282,3 +282,62 @@ int fuse_passthrough_setattr(struct inode *inode, struct iattr *attr)
>          */
>         return fuse_passthrough_getattr(inode, NULL, STATX_BASIC_STATS, 0);
>  }
> +
> +/*
> + * Open a file in passthrough mode using the inode's backing reference.
> + * Called when FUSE_PASSTHROUGH_OP_OPEN is set on the backing map's ops.
> + * This skips sending a FUSE_OPEN/FUSE_OPENDIR request to the server.
> + * FUSE_RELEASE/FUSE_RELEASEDIR will be skipped as well when the file is
> + * released.
> + */
> +int fuse_passthrough_open_inode(struct inode *inode, struct file *file)
> +{
> +       struct fuse_mount *fm = get_fuse_mount(inode);
> +       struct fuse_inode *fi = get_fuse_inode(inode);
> +       struct file *backing_file;
> +       struct fuse_backing *fb;
> +       struct fuse_file *ff;
> +       int err;
> +
> +       /*
> +        * This should always pass true for the release arg because when the
> +        * last refcount on the file is dropped (see fuse_file_put()),
> +        * fuse_io_release() needs to be called on the inode stashed in
> +        * ff->args->release_args.
> +        */
> +       ff = fuse_file_alloc(fm, true);
> +       if (!ff)
> +               return -ENOMEM;
> +
> +       ff->nodeid = get_node_id(inode);
> +       ff->open_flags = FOPEN_PASSTHROUGH;
> +       ff->passthrough_open = true;


I think if the server did not see the open and did not provide ff->fh
then all the file ops that carry fh cannot be sent to the server. Right?
So implementing passthrough open may need a bit more work.

> +
> +       err = -EINVAL;
> +       fb = fuse_backing_get(fuse_inode_passthrough(fi));
> +       if (!fb)
> +               goto error_free;
> +
> +       backing_file = backing_file_open(&file->f_path, file->f_flags,
> +                                         &fb->file->f_path, fb->cred);
> +       if (IS_ERR(backing_file)) {
> +               err = PTR_ERR(backing_file);
> +               goto error_put;
> +       }
> +
> +       ff->passthrough = backing_file;
> +       ff->cred = get_cred(fb->cred);
> +
> +       err = fuse_file_uncached_io_open(inode, ff, fb);
> +       if (!err) {
> +               file->private_data = ff;
> +               return 0;
> +       }
> +
> +       fuse_passthrough_release(ff, fb);
> +error_put:
> +       fuse_backing_put(fb);
> +error_free:
> +       fuse_file_free(ff);
> +       return err;
> +}

Joanne,

I am done with this round of review.
Nice work!
Thanks for getting this out the door.
Let's see if we find a way to tackle the issues found so far
without over complicating the code.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 00/17] fuse: extend passthrough to inode operations
  2026-04-21 13:55   ` Amir Goldstein
@ 2026-04-21 21:05     ` Joanne Koong
  2026-04-22  6:02       ` Amir Goldstein
  0 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-21 21:05 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 6:55 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 11:37 AM Amir Goldstein <amir73il@gmail.com> wrote:
> >
> > On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> > >
> > > This series extends fuse passthrough to support inode operations (getattr,
> > > setattr), directory readdir, and kernel-initiated open on backing files.
> > >
> > > The existing FUSE_PASSTHROUGH mode attaches a backing file to a fuse inode
> > > only when a passthrough file is open. This series introduces
> > > FUSE_PASSTHROUGH_INO, a stricter mode requiring one-to-one inode number
> > > mapping, which allows attaching a backing file for the lifetime of the fuse
> > > inode.
> > >
> > > Future work includes passthrough for additional operations (rename, unlink,
> > > readdirplus) and full subtree passthrough, which will be part of a separate
> > > series.
>

Thanks for taking the time to look at this, Amir!

> Joanne,
>
> Thanks a lot for picking up my abandoned wip!
>
> Could you add a few words about the intended use case in Meta?

The main intended use case at Meta is to use passthrough to reduce
startup latency for short‑lived services and batch jobs that need to
fetch large packaged artifacts over the network. The idea is to add a
fuse-based lazy materialization layer that supports on-demand fetching
for cold data but that once the required content has been fetched
locally, can switch to passthrough for near-native performance with
minimal userspace overhead.

>
> Is the target fuse filesystem expected to be used by arbitrary users/apps
> or by a controlled set of users/apps where a posix_fadvise() may make
> sense to configure readdir vs. readdirplus?

I think it'll mostly be used by different containerized/batch
workloads and services, so for Meta's use case, I don't think it can
assume users/apps can be modified to use posix_fadvise().

>
> What is the minimal set of passthrough ops that would make this useful
> to your internal customers? Is readdirplus in this set?

The minimal set of useful passthrough ops would be open + read
passthrough, getattr/statx passthrough, and readdirplus. The internal
use case is read-only. readdirplus would be useful because the
workloads often do directory walking followed by metadata lookups for
the entries

>
> Do you have profiling numbers to indicate which ops generate the most
> overhead for would-be passthrough ops routed to the server for the
> target workloads?
> And how do these numbers improve when using io_uring channel?

I do not, but if you recommend getting them, I can run some benchmarks.

>
> Regarding "full subtree passthrough", what does that mean?
> That lookup op itself is passthrough?

Yes I was thinking this would be something where once a directory is
marked as full passthrough, the fuse server never sees any requests
for paths within that subtree and everything is fully passed through
to the backing directory.

>
> In that case, would there be no way to escape (opt-out of) passthrough
> inside the subtree?

Yes I was thinking this would only be used when all files in a
directory map 1:1 to a backing filesystem.

>
> The semantics sound challenging especially when considering
> moves of directories in and out of subtrees.

I was thinking cross-boundary moves would just return -EXDEV, but
there are probably other semantics I am glossing over here.

>
> Anyway, no wrong answers here, just trying to understand the
> first mile store you are aiming at and the expected improvement.

The first goal I have in mind is eliminating the getattr and open
round trips. I was envisioning the passthrough work to be a
multi-series effort where
everything would get merged together at once only when all the pieces
were ready.

Thanks,
Joanne

>
> Thanks,
> Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 01/17] fuse: introduce FUSE_PASSTHROUGH_INO mode
  2026-04-20 22:16 ` [PATCH v1 01/17] fuse: introduce FUSE_PASSTHROUGH_INO mode Joanne Koong
@ 2026-04-21 21:11   ` Darrick J. Wong
  2026-04-21 23:38     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Darrick J. Wong @ 2026-04-21 21:11 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, amir73il, fuse-devel, luis

On Mon, Apr 20, 2026 at 03:16:21PM -0700, Joanne Koong wrote:
> 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 7294bd347412..f239c8a888cb 100644
> --- a/fs/fuse/file.c
> +++ b/fs/fuse/file.c
> @@ -1431,7 +1431,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) {
> @@ -1446,7 +1445,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 23a241f18623..86fdf873d639 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -913,6 +913,9 @@ struct fuse_conn {
>  	/** Passthrough support for read/write IO */
>  	unsigned int passthrough:1;
>  
> +	/** One-to-one mapping between fuse ino to backing ino */
> +	unsigned int passthrough_ino:1;
> +
>  	/* Use pages instead of pointer for kernel I/O */
>  	unsigned int use_pages_for_kvec_io:1;
>  
> @@ -1535,8 +1538,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 8b64034ab0bb..014b9af42909 100644
> --- a/fs/fuse/inode.c
> +++ b/fs/fuse/inode.c
> @@ -1445,6 +1445,8 @@ static void process_init_reply(struct fuse_mount *fm, struct fuse_args *args,
>  				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;
> @@ -1518,8 +1520,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))

Now that struct inode::i_ino is u64 and not ino_t, does this check need
adjusting?  I think it's the case that the inode hash will work fine
with a 64-bit inumber.a  The fuse uapi defines ino and nodeid fields to
be uint64_t, so I think it should work even on a 32-bit kernel.

That said, ino_t remains kernel_ulong_t in 7.1, so this still won't be
enabled for 32-bit.  OTOH maybe none of us care. ;)

--D

> +			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	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 02/17] fuse: prepare for passthrough of inode operations
  2026-04-20 22:16 ` [PATCH v1 02/17] fuse: prepare for passthrough of inode operations Joanne Koong
@ 2026-04-21 21:16   ` Darrick J. Wong
  2026-04-22  1:12     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Darrick J. Wong @ 2026-04-21 21:16 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, amir73il, fuse-devel, luis

On Mon, Apr 20, 2026 at 03:16:22PM -0700, Joanne Koong 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          | 40 +++++++++++++++++++++++++++++++++++++++
>  fs/fuse/iomode.c          |  5 +++++
>  include/uapi/linux/fuse.h |  9 ++++++++-
>  4 files changed, 63 insertions(+), 4 deletions(-)
> 
> diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
> index d95dfa48483f..830cbe2a4200 100644
> --- a/fs/fuse/backing.c
> +++ b/fs/fuse/backing.c
> @@ -86,7 +86,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;
> @@ -94,7 +95,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);
> @@ -104,7 +109,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;
> @@ -119,6 +125,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 86fdf873d639..eb974739dd5e 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -107,6 +107,7 @@ struct fuse_submount_lookup {
>  struct fuse_backing {
>  	struct file *file;
>  	struct cred *cred;
> +	u64 ops_mask;
>  
>  	/** refcount */
>  	refcount_t count;
> @@ -244,6 +245,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;
> @@ -1550,6 +1553,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);
> @@ -1619,6 +1641,24 @@ 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 struct fuse_backing *fuse_inode_passthrough(struct fuse_inode *fi)
> +{
> +#ifdef CONFIG_FUSE_PASSTHROUGH
> +	if (test_bit(FUSE_I_PASSTHROUGH, &fi->state))
> +		return fuse_inode_backing(fi);
> +#endif
> +	return NULL;
> +}
> +
> +static inline bool fuse_inode_passthrough_op(struct inode *inode,
> +					     enum fuse_opcode op)
> +{
> +	struct fuse_inode *fi = get_fuse_inode(inode);
> +	struct fuse_backing *fb = fuse_inode_passthrough(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..6815b4506007 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) {
> 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)

FUSE_READ==15 and FUSE_WRITE==16; this is a bit wasteful of ops_mask
bits, of which there are only 64.

--D

> +
>  /* 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	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 03/17] fuse: prepare for readdir passthrough on directories
  2026-04-20 22:16 ` [PATCH v1 03/17] fuse: prepare for readdir passthrough on directories Joanne Koong
@ 2026-04-21 21:17   ` Darrick J. Wong
  2026-04-21 23:12     ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Darrick J. Wong @ 2026-04-21 21:17 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, amir73il, fuse-devel, luis

On Mon, Apr 20, 2026 at 03:16:23PM -0700, Joanne Koong wrote:
> 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 | 12 ++++++------
>  fs/fuse/inode.c  |  2 +-
>  fs/fuse/iomode.c | 31 ++++++++++++++++++-------------
>  6 files changed, 34 insertions(+), 28 deletions(-)
> 
> diff --git a/fs/fuse/cuse.c b/fs/fuse/cuse.c
> index dfcb98a654d8..e168740351a0 100644
> --- a/fs/fuse/cuse.c
> +++ b/fs/fuse/cuse.c
> @@ -147,7 +147,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 b658b6baf72f..015f0c103d06 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_queue_forget(fm->fc, 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 f239c8a888cb..3da4ce73e11b 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;

Can't you figure this out from S_ISDIR(fi->inode.i_mode), and thereby
obviate the need for the extra parameter?

--D

> +
>  	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);
> @@ -1456,13 +1458,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 eb974739dd5e..1c4646ad7c25 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -161,9 +161,6 @@ struct fuse_inode {
>  			 * (FUSE_NOWRITE) means more writes are blocked */
>  			int writectr;
>  
> -			/** Number of files/maps using page cache */
> -			int iocachectr;
> -
>  			/* Waitq for writepage completion */
>  			wait_queue_head_t page_waitq;
>  
> @@ -206,6 +203,9 @@ struct fuse_inode {
>  	/** Lock to protect write related fields */
>  	spinlock_t lock;
>  
> +	/** Number of files/maps using page cache (negative for passthrough) */
> +	int iocachectr;
> +
>  #ifdef CONFIG_FUSE_DAX
>  	/*
>  	 * Dax specific inode data
> @@ -238,7 +238,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
> @@ -1192,7 +1192,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
> @@ -1542,7 +1542,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 014b9af42909..bdc135f9fe3e 100644
> --- a/fs/fuse/inode.c
> +++ b/fs/fuse/inode.c
> @@ -192,10 +192,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 6815b4506007..2360b32793c2 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);
>  }
>  
>  /*
> @@ -267,8 +274,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.
> @@ -278,10 +283,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	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 03/17] fuse: prepare for readdir passthrough on directories
  2026-04-21 21:17   ` Darrick J. Wong
@ 2026-04-21 23:12     ` Joanne Koong
  0 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-21 23:12 UTC (permalink / raw)
  To: Darrick J. Wong; +Cc: miklos, amir73il, fuse-devel, luis

On Tue, Apr 21, 2026 at 2:17 PM Darrick J. Wong <djwong@kernel.org> wrote:
>
> On Mon, Apr 20, 2026 at 03:16:23PM -0700, Joanne Koong wrote:
> > 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 | 12 ++++++------
> >  fs/fuse/inode.c  |  2 +-
> >  fs/fuse/iomode.c | 31 ++++++++++++++++++-------------
> >  6 files changed, 34 insertions(+), 28 deletions(-)
> >
> > diff --git a/fs/fuse/cuse.c b/fs/fuse/cuse.c
> > index dfcb98a654d8..e168740351a0 100644
> > --- a/fs/fuse/cuse.c
> > +++ b/fs/fuse/cuse.c
> > @@ -147,7 +147,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 b658b6baf72f..015f0c103d06 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_queue_forget(fm->fc, 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 f239c8a888cb..3da4ce73e11b 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;
>
> Can't you figure this out from S_ISDIR(fi->inode.i_mode), and thereby
> obviate the need for the extra parameter?
>
> --D
>

Thanks for taking a look at this series. I don't think we can do that
here unfortunately because some callers pass in a null fuse_inode
pointer (eg fuse_create_open() if the fuse_iget() call fails).

Thanks,
Joanne

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 01/17] fuse: introduce FUSE_PASSTHROUGH_INO mode
  2026-04-21 21:11   ` Darrick J. Wong
@ 2026-04-21 23:38     ` Joanne Koong
  0 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-21 23:38 UTC (permalink / raw)
  To: Darrick J. Wong; +Cc: miklos, amir73il, fuse-devel, luis

On Tue, Apr 21, 2026 at 2:11 PM Darrick J. Wong <djwong@kernel.org> wrote:
>
> On Mon, Apr 20, 2026 at 03:16:21PM -0700, Joanne Koong wrote:
> > 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 7294bd347412..f239c8a888cb 100644
> > --- a/fs/fuse/file.c
> > +++ b/fs/fuse/file.c
> > @@ -1431,7 +1431,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) {
> > @@ -1446,7 +1445,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 23a241f18623..86fdf873d639 100644
> > --- a/fs/fuse/fuse_i.h
> > +++ b/fs/fuse/fuse_i.h
> > @@ -913,6 +913,9 @@ struct fuse_conn {
> >       /** Passthrough support for read/write IO */
> >       unsigned int passthrough:1;
> >
> > +     /** One-to-one mapping between fuse ino to backing ino */
> > +     unsigned int passthrough_ino:1;
> > +
> >       /* Use pages instead of pointer for kernel I/O */
> >       unsigned int use_pages_for_kvec_io:1;
> >
> > @@ -1535,8 +1538,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 8b64034ab0bb..014b9af42909 100644
> > --- a/fs/fuse/inode.c
> > +++ b/fs/fuse/inode.c
> > @@ -1518,8 +1520,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))
>
> Now that struct inode::i_ino is u64 and not ino_t, does this check need
> adjusting?  I think it's the case that the inode hash will work fine
> with a 64-bit inumber.a  The fuse uapi defines ino and nodeid fields to
> be uint64_t, so I think it should work even on a 32-bit kernel.
>
> That said, ino_t remains kernel_ulong_t in 7.1, so this still won't be
> enabled for 32-bit.  OTOH maybe none of us care. ;)

Ahh great point, I remember seeing the patches for that. I agree with
you, once i_ino is switched to u64, we can just drop this check
altogether.

Thanks,
Joanne
>
> --D

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 13/17] fuse: add passthrough support for atomic file creation
  2026-04-21 19:51   ` Amir Goldstein
@ 2026-04-22  0:40     ` Joanne Koong
  2026-04-22  5:10       ` Amir Goldstein
  0 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-22  0:40 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 12:51 PM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > Route FUSE_CREATE (atomic create + open) through fuse_entry2_out when
> > FUSE_PASSTHROUGH_INO is enabled. If the server returns a backing id,
> > the newly created inode will be 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 | 44 ++++++++++++++++++++++++++++++++++++--------
> >  1 file changed, 36 insertions(+), 8 deletions(-)
> >
> > diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> > index e5a7640fcd30..637761de2c5b 100644
> > --- a/fs/fuse/dir.c
> > +++ b/fs/fuse/dir.c
> > @@ -976,8 +979,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 (use_entry2) {
> > +               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);
> > @@ -992,6 +1002,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))
> > @@ -1000,16 +1012,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;
>
> Unless I am missing something, you need to set FOPEN_PASSTHROUGH
> in ff->open_flags otherwise finish_open will not open the ff->passthrough
> file (with the correct RW mode).

Currently in this series, the kernel enforces that if there is a
backing file registered for the inode, the server is still expected to
set FOPEN_PASSTHROUGH and provide the backing id in the FUSE_OPEN /
FUSE_CREATE reply. My original motivation was for keeping the api
contract consistent between FUSE_PASSTHROUGH_INO and the regular
passthrough mode, but I think you're right that this is just overkill.
I'll remove this restriction in v2.

Thanks,
Joanne

>
> Thanks,
> Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 10/17] fuse: add struct fuse_entry2_out and helpers for extended entry replies
  2026-04-21 12:25   ` Amir Goldstein
@ 2026-04-22  0:50     ` Joanne Koong
  0 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-22  0:50 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 5:25 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 12:18 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.
> >
> > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > ---
> >  fs/fuse/dir.c             | 119 ++++++++++++++++++++++++++++++--------
> >  fs/fuse/fuse_i.h          |   5 ++
> >  include/uapi/linux/fuse.h |  14 +++++
> >  3 files changed, 113 insertions(+), 25 deletions(-)
> >
> > diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> > index 94ab05cb89ce..0eacfef52164 100644
> > --- a/fs/fuse/dir.c
> > +++ b/fs/fuse/dir.c
> > @@ -354,9 +354,9 @@ 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_simple(struct fuse_args *args, u64 nodeid,
> > +                                   const struct qstr *name,
> > +                                   struct fuse_entry_out *outarg)
>
> Nit: patch 11 would be nicer if you add the arg now and pass NULL from callers
> skipping this temporary fuse_lookup_init_simple() step.

That is a much better way of doing it, will do this in v2.

>
> >  {
> >         memset(outarg, 0, sizeof(struct fuse_entry_out));
> >         args->opcode = FUSE_LOOKUP;
> > @@ -372,6 +372,95 @@ static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
> >         args->out_args[0].value = outarg;
> >  }
> >
> > +static __maybe_unused 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)
> > +{
> > +       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;
> > +       fuse_set_zero_arg0(args);
> > +       args->in_args[1].size = name->len;
> > +       args->in_args[1].value = name->name;
> > +       args->in_args[2].size = 1;
> > +       args->in_args[2].value = "";
>
> I know you just carried this, but do you know what this is?
> some sort of backward compat inarg?

I believe this is for the null terminator (to properly null-terminate
the name string that gets sent to the server)

>
> > +       args->out_numargs = 1;
> > +
> > +       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);
> > +}
> > +
> > +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;
> > +
> > +       fuse_entry2_to_entry(outarg2, outarg);
> > +
> > +       /* 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;
> > +}
> > +
>
> Need to check that outargs2->flags and ->reserved are 0.

Good idea, I'll add checking for this in v2.

Thanks,
Joanne
>
> Thanks,
> Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 17/17] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO)
  2026-04-21 11:09   ` Amir Goldstein
@ 2026-04-22  1:04     ` Joanne Koong
  0 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-22  1:04 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 4:09 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 12:19 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     | 131 ++++++++++++++++++
> >  1 file changed, 131 insertions(+)
> >
> > diff --git a/Documentation/filesystems/fuse/fuse-passthrough.rst b/Documentation/filesystems/fuse/fuse-passthrough.rst
> > index 2b0e7c2da54a..c7ccea597324 100644
> > --- a/Documentation/filesystems/fuse/fuse-passthrough.rst
> > +++ b/Documentation/filesystems/fuse/fuse-passthrough.rst
> > @@ -25,6 +25,12 @@ 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), directory operations (readdir), and
> > +kernel-initiated open (bypassing ``FUSE_OPEN``). In this mode, a backing file
> > +can be attached to a fuse inode for its entire lifetime, not just while a file
> > +is open.
> > +
> >  Enabling Passthrough
> >  ====================
> >
> > @@ -46,6 +52,131 @@ 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 server 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 server 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 server 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. For passthrough open (``FUSE_PASSTHROUGH_OP_OPEN`` in ``ops_mask``),
> > +     no further action is needed. The kernel will open the backing file
> > +     directly without sending ``FUSE_OPEN`` / ``FUSE_OPENDIR`` to the
> > +     server. ``FUSE_RELEASE`` / ``FUSE_RELEASEDIR`` is also skipped.
> > +  5. For server-initiated passthrough open (without
> > +     ``FUSE_PASSTHROUGH_OP_OPEN``), the server handles ``FUSE_OPEN`` /
> > +     ``FUSE_OPENDIR`` as before and returns ``FOPEN_PASSTHROUGH`` with the
> > +     ``backing_id`` in the open response. A ``backing_id`` is required even
> > +     if the inode already has passthrough set up from lookup. The server
> > +     must use the same ``backing_id``. If the inode has passthrough,
> > +     the server must set ``FOPEN_PASSTHROUGH`` or ``FOPEN_DIRECT_IO``
>
> IIUC (and please do double check this), the server MUST always set
> ``FOPEN_PASSTHROUGH`` if inode is in passthrough mode and it MAY set
> ``FOPEN_DIRECT_IO`` *additionally* to opt-out of passthrough when inode
> is already in passthrough mode.
>

Yes, you're correct. I'll change this to "must set
``FOPEN_PASSTHROUGH`` (and optionally ``FOPEN_DIRECT_IO``) on open as
cached..."

I'll add a separate section about the hybrid FOPEN_PASSTHROUGH |
FOPEN_DIRECT_IO mode to make it more clear that direct io supersedes
passthrough for read/write but not mmap.

Thanks,
Joanne

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 15/17] fuse: add passthrough setattr
  2026-04-21 14:32     ` Amir Goldstein
@ 2026-04-22  1:09       ` Joanne Koong
  0 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-22  1:09 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 7:32 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> > > +int fuse_passthrough_setattr(struct inode *inode, struct iattr *attr)
> > > +{
> > > +       struct fuse_conn *fc = get_fuse_conn(inode);
> > > +       struct fuse_inode *fi = get_fuse_inode(inode);
> > > +       struct fuse_backing *fb = fuse_inode_passthrough(fi);
> > > +       const struct cred *old_cred;
> > > +       struct dentry *backing_entry = fb->file->f_path.dentry;
> > > +       struct mnt_idmap *backing_idmap = mnt_idmap(fb->file->f_path.mnt);
> > > +       int err;
> > > +
> > > +       old_cred = override_creds(fb->cred);
> > > +       inode_lock(d_inode(backing_entry));
> > > +       err = notify_change(backing_idmap, backing_entry, attr, NULL);
> > > +       inode_unlock(d_inode(backing_entry));
> > > +       revert_creds(old_cred);
> >
> > Looking at ovl_setattr() (overlayfs should be the reference to all
> > passthrough ops)
> > it looks like mnt_want_write() is missing and possibly many other tweaks

Awesome, I will take a look at the overlayfs code and see how it's done there.

> >
> > It is generally advisable to keep heavy logic as this in
> > fs/backing_file.c helpers,
> > which overlayfs and fuse can share, but I did not look to see how much of
> > ovl_setattr() is relevant for fuse passthrough.
> >
>
> Also setattr is not a file op, so if we move to common code it
> should probably be fs/backing_inode.c or something (*).
>
> Thanks,
> Amir.
>
> (*) there are a few common helpers in fs/stack.c, but there are out dated
> and only used by ecryptfs, so better start a new lib.

Great, I will take a look at fs/backing_file.c and fs/stack.c as well.

Thanks,
Joanne

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 02/17] fuse: prepare for passthrough of inode operations
  2026-04-21 21:16   ` Darrick J. Wong
@ 2026-04-22  1:12     ` Joanne Koong
  0 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-22  1:12 UTC (permalink / raw)
  To: Darrick J. Wong; +Cc: miklos, amir73il, fuse-devel, luis

On Tue, Apr 21, 2026 at 2:16 PM Darrick J. Wong <djwong@kernel.org> wrote:
>
> On Mon, Apr 20, 2026 at 03:16:22PM -0700, Joanne Koong 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          | 40 +++++++++++++++++++++++++++++++++++++++
> >  fs/fuse/iomode.c          |  5 +++++
> >  include/uapi/linux/fuse.h |  9 ++++++++-
> >  4 files changed, 63 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 @@
> >   *
> >   *  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)
>
> FUSE_READ==15 and FUSE_WRITE==16; this is a bit wasteful of ops_mask
> bits, of which there are only 64.
>
> --D

That's a good point but I think we're unlikely to need more than 64
passthrough ops, so I think this should be okay.

Thanks,
Joanne

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 08/17] fuse: add passthrough ops gating
  2026-04-21 10:48   ` Amir Goldstein
@ 2026-04-22  2:57     ` Joanne Koong
  2026-04-22  7:27       ` Amir Goldstein
  0 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-22  2:57 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 3:48 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > Route existing callers through a passthrough ops mask check to verify
> > whether passthrough can be called for the operation or not. The check is
> > only done when FUSE_PASSTHROUGH_INO mode is enabled, which preserves
> > backwards compatibility with prior passthrough behavior.
> >
> > It is safe to get the backing file by accessing ff->passthrough directly
> > in passthrough.c because passthrough.c is only compiled with
> > CONFIG_FUSE_PASSTHROUGH=y and the caller has already done the ops check.
> >
> > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > ---
> >  fs/fuse/file.c        | 14 ++++++++------
> >  fs/fuse/fuse_i.h      | 14 +++++++++++++-
> >  fs/fuse/passthrough.c | 12 ++++++------
> >  fs/fuse/readdir.c     |  2 +-
> >  4 files changed, 28 insertions(+), 14 deletions(-)
> >
> > diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> > index a15fb508fd28..45d9184d0f7a 100644
> > --- a/fs/fuse/fuse_i.h
> > +++ b/fs/fuse/fuse_i.h
> > @@ -1630,9 +1630,21 @@ static inline struct fuse_backing *fuse_inode_backing_set(struct fuse_inode *fi,
> >  struct fuse_backing *fuse_passthrough_open(struct file *file, int backing_id);
> >  void fuse_passthrough_release(struct fuse_file *ff, struct fuse_backing *fb);
> >
> > -static inline struct file *fuse_file_passthrough(struct fuse_file *ff)
> > +static inline struct file *fuse_file_passthrough(struct inode *inode,
> > +                                                struct fuse_file *ff,
> > +                                                u64 passthrough_ops)
> >  {
> >  #ifdef CONFIG_FUSE_PASSTHROUGH
> > +       if (!ff->passthrough)
> > +               return NULL;
> > +
> > +       if (ff->fm->fc->passthrough_ino && passthrough_ops) {
> > +               struct fuse_backing *fb = fuse_inode_backing(get_fuse_inode(inode));
> > +
> > +               if (!fb || (fb->ops_mask & passthrough_ops) != passthrough_ops)
> > +                       return NULL;
> > +       }
> > +
> >         return ff->passthrough;
> >  #else
> >         return NULL;
>
>
> Please document the relationship between  FOPEN_PASSTHROUGH
> and FUSE_PASSTHROUGH_RW_OPS.

I will add a part about this to the documentation patch. In this
current series, FOPEN_PASSTHROUGH under FUSE_PASSTHROUGH_INO does not
imply FUSE_PASSTHROUGH_RW_OPS. FOPEN_PASSTHROUGH can be set with just
FUSE_PASSTHROUGH_OP_READ or FUSE_PASSTHROUGH_OP_WRITE and does not
require both (eg server wants to intercept writes for logging or
encryption but reads can be passed through). For mmap, it would only
be passed through if both passthrough read and passthrough write are
set. In the non-FUSE_PASSTHROUGH_INO model, FOPEN_PASSTHROUGH implies
both passthrough read and passthrough write.

Do you think this makes sense or am I overcomplicating things?

>
> Does a server need to opt-in both for the read/write operations passthrough
> and the specific file (I guess so)?

Yes, in this current series, to use passthrough read/write, the server
is expected to set read/write ops in ops_mask when registering the
backing file (inode level) and set FOPEN_PASSTHROUGH when opening the
specific file (file level).

I left a comment [1] on your other comment about dropping the
expectation that the server needs to set FOPEN_PASSTHROUGH in the
FUSE_PASSTHROUGH_INO case if it provided a backing id in the lookup
and set passthrough read / write in the ops mask - I'm not sure if
that was what you were hinting at or not.

[1] https://lore.kernel.org/fuse-devel/CAJnrk1b=5vRgm1otKAiaeFJr9_J-aLnoVTp4MHAz2mBKtcE2dw@mail.gmail.com/

>
> The concept behind the inode iomodes is that it is not allowed to mix cached
> with passthrough ops on the same inode, because there be dragons.
>
> If the server wants to opt-out of passthrough for a specific opened file,
> which already has a backing inode, it must explicitly request the combination
> FOPEN_DIRECT_IO | FOPEN_PASSTHROUGH
> and then direct io will be performed, despite the inode having a backing inode.
>
> What you have done here is a private case of passthrough opt-out
> not per file, but per operation.
>
> The bottom line is if the fuse inode has a backing inode, then you must not
> fallback to the fuse_cache_ operations in case the op is not in the mask,
> but you may fallback to the fuse_direct_ operations.

Thanks for pointing this out. I will add this enforcement at the
kernel level for calling back to direct operations.

This is what I have in my head for how it'll work:
- server opts into passthrough for both reads and writes
* server sets FOPEN_DIRECT_IO: reads/writes will be forwarded to the
server, but mmap will be passed through to the backing file
* server does not set FOPEN_DIRECT_IO: reads/writes/mmap are passed
through to backing file
This is the same behavior as the non-FUSE_PASSTHROUGH_INO case

- server opts into passthrough for reads but not writes (or vice versa)
* mmap is never passed through, mmap will always go to the server
* no data caching will be done whatsoever
* server does not set FOPEN_DIRECT_IO: reads are passed through,
writes are forwarded to the server
* server sets FOPEN_DIRECT_IO: no-op (same behavior as if server did
not set FOPEN_DIRECT_IO)

Do you think this makes sense?

>
> I hope this does not complicate things for you too much,
> but I also don't know (yet) what you intend this gating for.

Thank you for your feedback, it was all very helpful!

The intention of this patch was to support cases where the server sets
only read passthrough or write passthrough but not both. In this
scenario we cannot rely on "ff->passthrough != NULL" to know whether
to pass through the read or the write, so we need to check the
ops_mask. My intended use case doesn't really need this, but imo I
think it's overall a useful feature that other servers will want, but
if you disagree with this, please let me know.

Thanks,
Joanne

>
> Otherwise, this looks ok.
>
> Thanks,
> Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 12/17] fuse: add passthrough support for entry creation
  2026-04-21 14:08   ` Amir Goldstein
@ 2026-04-22  3:01     ` Joanne Koong
  0 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-22  3:01 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 7:08 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > 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 | 33 +++++++++++++++++++++++++++++----
> >  1 file changed, 29 insertions(+), 4 deletions(-)
> >
> > diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> > index 9e9b77942dcd..e5a7640fcd30 100644
> > --- a/fs/fuse/dir.c
> > +++ b/fs/fuse/dir.c
> > @@ -1088,10 +1088,14 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
> >
> >         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_queue_forget(fm->fc, 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);
> > +               }
> > +       }
> > +
>
>
> I think we need to verify that the backing file is of the same type as
> requested entry.
>
> This was also true for lookup which instantiates the inode according to the
> mode in fuse_attr.

Great point, I'll add these checks in for v2.

Thanks,
Joanne
>
> Thanks,
> Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 11/17] fuse: add passthrough lookup
  2026-04-21 13:23   ` Amir Goldstein
@ 2026-04-22  3:17     ` Joanne Koong
  0 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-22  3:17 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 6:23 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 12:18 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.
> >
> > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > ---
> >  fs/fuse/dir.c | 92 +++++++++++++++++++++++++++++++--------------------
> >  1 file changed, 57 insertions(+), 35 deletions(-)
> >
> > diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> > index 0eacfef52164..9e9b77942dcd 100644
> > --- a/fs/fuse/dir.c
> > +++ b/fs/fuse/dir.c
> > @@ -489,9 +470,12 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
> >         else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
> >                  (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {
>
>
> Not changed by your patch but relevant to my other comments.
> The flow of this function is terrible.
> My eyes cannot stand non-matching {} they are review traps
> and there is no need for the else after goto anyway:

I will clean this up and submit this separately.

>
>         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))) {
>                 /* For negative dentries, always do a fresh lookup */
>                 if (!inode)
>                         goto invalid;
>
>                 /* This code is deeply nested - and even more so after
> this patch */
>
>         } else if (inode) {
>                 ...
>         }
>         ret = 1;
> out:
>         return ret;
> invalid:
>         ret = 0;
>         goto out;
>
> These goto labels are useless.
>
> Perhaps:
>
>         bool need_reval = time_before64(fuse_dentry_time(entry),
> get_jiffies_64()) ||
>                  (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET));
>
>         if (inode && fuse_is_bad(inode))
>                 return 0;
>
>         if (!inode && need_reval)
>                 return 0;
>
>         if (inode && !need_reval) {
>                 ...
>                 return 1;
>         }
>
> The rest here inline or call helper fuse_do_dentry_revalidate()
> because its a pretty
> big and proper coding style is that functions fit in a single terminal screen.
>
> >                 struct fuse_entry_out outarg;
> > +               struct fuse_entry2_out outarg2;
> > +               struct fuse_statx *sx = NULL;
> >                 FUSE_ARGS(args);
> >                 struct fuse_forget_link *forget;
> >                 u64 attr_version;
> > +               int backing_id = 0;
> >
> >                 /* For negative dentries, always do a fresh lookup */
> >                 if (!inode)
> > @@ -510,11 +494,21 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
> >
> >                 attr_version = fuse_get_attr_version(fm->fc);
> >
> > -               fuse_lookup_init_simple(&args, get_node_id(dir), name, &outarg);
> > +               fuse_lookup_init(fc, &args, get_node_id(dir), name, &outarg,
> > +                                &outarg2);
> >                 ret = fuse_simple_request(fm, &args);
> > -               /* Zero nodeid is same as -ENOENT */
> > -               if (!ret && !outarg.nodeid)
> > -                       ret = -ENOENT;
> > +               if (!ret) {
> > +                       backing_id = fuse_process_entry2(fm->fc, &outarg2, &outarg, &sx);
> > +
> > +                       /* Zero nodeid is same as -ENOENT */
> > +                       if (!outarg.nodeid)
> > +                               ret = -ENOENT;
> > +                       else if (backing_id < 0) {
>
> Again, the unforgivable sin of {} mismatch
>
> > +                               fuse_queue_forget(fm->fc, forget,
> > +                                                 outarg.nodeid, 1);
>
> This line just goes to show that the nesting is too deep.
>
> > +                               return backing_id;
> > +                       }
> > +               }
> >                 if (!ret) {
> >                         fi = get_fuse_inode(inode);
> >                         if (outarg.nodeid != get_node_id(inode) ||
> > @@ -523,6 +517,14 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
> >                                                   outarg.nodeid, 1);
> >                                 goto invalid;
> >                         }
> > +                       if (backing_id) {
> > +                               ret = fuse_inode_set_passthrough(inode, backing_id);
> > +                               if (ret) {
> > +                                       fuse_queue_forget(fm->fc, forget,
> > +                                                         outarg.nodeid, 1);
> > +                                       return ret;
> > +                               }
> > +                       }
>
> This is revalidate so the inode already exists and likely already has a backing
> inode so this is almost certainly bound to fail.
>
> And the same thing can happen in fuse_lookup_name() when fuse_iget()
> finds an existing inode - for example on lookup of a hardlink alias.
>
> I see a few options:
> 1. Move fuse_inode_set_passthrough into fuse_iget() and otherwise
>     ignore the backing_id for non inode instantiating lookups
> 2. Teach fuse_inode_set_passthrough() to be happy if existing backing inode
>     exist and the new backing_id refers to the same fuse_backing object
>
> I am leaning towards #2 as this is exactly what fuse_inode_uncached_io_start()
> does when many FOPEN_PASSTHROUGH are called for the same inode.

That's a really good point I should have considered. I'll go with #2
and fix this up for v2.

Thanks,
Joanne
>
> Thanks,
> Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 14/17] fuse: use passthrough getattr in setattr suid/sgid handling
  2026-04-21 14:25   ` Amir Goldstein
@ 2026-04-22  3:48     ` Joanne Koong
  2026-04-22  5:22       ` Amir Goldstein
  0 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-22  3:48 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 7:25 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 12:18 AM 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 637761de2c5b..ff9a92d8a496 100644
> > --- a/fs/fuse/dir.c
> > +++ b/fs/fuse/dir.c
>
>                 /*
>                  * The only sane way to reliably kill suid/sgid is to do it in
>                  * the userspace filesystem
>                  *
>                  * This should be done on write(), truncate() and chown().
>                  */
>                 if (!fc->handle_killpriv && !fc->handle_killpriv_v2) {
>
> We should probably enforce a dependency between those two and
> passthrough of SETATTR and maybe GETATTR - not sure what
> the rules should be though.

Not sure if this is what you had in mind, but skip the manual
stripping entirely if setattr is passed through since the backing
filesystem will handle it? eg

  if (!fc->handle_killpriv && !fc->handle_killpriv_v2 &&
      !fuse_inode_passthrough_op(inode, FUSE_SETATTR)) {

>
>
> > @@ -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_inode_passthrough_op(inode, FUSE_GETATTR))
> > +                               ret = fuse_passthrough_getattr(inode, NULL,
> > +                                                              STATX_MODE, 0);
> > +                       else
> > +                               ret = fuse_do_getattr(idmap, inode, NULL, file);
>
> It does not feel right to passthrough GETATTR if not passthrough SETATTR
> in this case - I could be wrong.

My mental model was that if a server wishes to pass through getattr
but not setattr or vice versa, they are responsible for keeping the
state in sync with the backing file. I'm happy to add the check in
though if you think it's better to add this.

Thanks,
Joanne

>
> Thanks,
> Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 16/17] fuse: add passthrough open
  2026-04-21 20:20   ` Amir Goldstein
@ 2026-04-22  4:19     ` Joanne Koong
  2026-04-22  4:23       ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-22  4:19 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 1:21 PM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 12:19 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > Add FUSE_PASSTHROUGH_OP_OPEN which the server may set in the backing
> > map's ops mask to indicate that the kernel should skip sending
> > FUSE_OPEN/FUSE_OPENDIR to the server and open the backing file directly.
> > FUSE_RELEASE/FUSE_RELEASEDIR is also skipped when the file is released.
> > FUSE_FLUSH is skipped as well.
> >
> > If the file is a directory, this automatically implies passthrough
> > readdir. If the file is a regular file, this automatically implies
> > passthrough read/write.
> >
> > For FUSE_ATOMIC_O_TRUNC, the server typically handles the truncating
> > logic when it handles FUSE_OPEN. With passthrough open, the vfs layer
> > will call handle_truncate() which will trigger fuse_setattr(). If
> > setattr is passed through, then the backing file will be truncated
> > directly or if not, then a FUSE_SETATTR will be sent to the server for
> > the truncation.
>
> I think we need to reduce complexity.
> If we do open passthrough, then the server should not handle truncate.
> same for write, if we passthrough write, server should not handle killpriv.
> If this means that PASSTHROUGH_INO conflicts with some other
> fuse configs, then I think we should make those configs mutually exclusive,
> at least for the start.

That makes a lot of sense and will make things simpler. I think that's
a great idea.

>
> >
> > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > ---
> >  fs/fuse/backing.c         | 12 ++++++--
> >  fs/fuse/dir.c             | 15 +++++++++-
> >  fs/fuse/file.c            | 17 ++++++++++-
> >  fs/fuse/fuse_i.h          | 13 ++++++++-
> >  fs/fuse/iomode.c          |  5 ++--
> >  fs/fuse/passthrough.c     | 59 +++++++++++++++++++++++++++++++++++++++
> >  include/uapi/linux/fuse.h |  1 +
> >  7 files changed, 114 insertions(+), 8 deletions(-)
> >
> > diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
> > index 85ac6f917779..5bc5581dd27f 100644
> > --- a/fs/fuse/backing.c
> > +++ b/fs/fuse/backing.c
> > @@ -118,8 +118,9 @@ 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)
> > +       /* For now passthrough operations on backing inodes require FUSE_PASSTHROUGH_INO */
> > +       if (!fc->passthrough_ino &&
> > +           (map->ops_mask & (FUSE_PASSTHROUGH_INODE_OPS | FUSE_PASSTHROUGH_OP_OPEN)))
> >                 goto out;
>
> I would argue that in this context, passthrough open *is* an inode operation,
> just like CREATE, because it operates on the backing inode, not ff->passthrough.
> But this POV is confusing when considering file_operations and inode_operations
> so let it stay as you did.
>
> >
> >         file = fget_raw(map->fd);
> > @@ -127,6 +128,13 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
> >         if (!file)
> >                 goto out;
> >
> > +       if (map->ops_mask & FUSE_PASSTHROUGH_OP_OPEN) {
> > +               if (d_is_reg(file->f_path.dentry))
> > +                       map->ops_mask |= FUSE_PASSTHROUGH_RW_OPS;
> > +               else
> > +                       map->ops_mask |= FUSE_PASSTHROUGH_DIR_OPS;
> > +       }
> > +
> >         /* read/write/splice/mmap passthrough only relevant for regular files */
> >         res = d_is_dir(file->f_path.dentry) ? -EISDIR : -EINVAL;
> >         if (!(map->ops_mask & ~FUSE_PASSTHROUGH_RW_OPS) &&
> > diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> > index 6dea0f8e384f..816cb7d7aeb1 100644
> > --- a/fs/fuse/dir.c
> > +++ b/fs/fuse/dir.c
> > @@ -2077,6 +2077,9 @@ static int fuse_dir_open(struct inode *inode, struct file *file)
> >         if (err)
> >                 return err;
> >
> > +       if (fuse_inode_passthrough_op(inode, FUSE_OPEN))
> > +               return fuse_passthrough_open_inode(inode, file);
> > +
> >         err = fuse_do_open(fm, get_node_id(inode), file, true);
> >         if (!err) {
> >                 struct fuse_file *ff = file->private_data;
> > @@ -2113,11 +2116,15 @@ static int fuse_dir_fsync(struct file *file, loff_t start, loff_t end,
> >  {
> >         struct inode *inode = file->f_mapping->host;
> >         struct fuse_conn *fc = get_fuse_conn(inode);
> > +       struct fuse_file *ff = file->private_data;
> >         int err;
> >
> >         if (fuse_is_bad(inode))
> >                 return -EIO;
> >
> > +       if (ff->passthrough_open)
> > +               return vfs_fsync_range(ff->passthrough, start, end, datasync);
> > +
>
> ovl_fsync has override_cred maybe needed here?
>
> >         if (fc->no_fsyncdir)
> >                 return 0;
> >
> > @@ -2362,7 +2369,13 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
> >                 /* This is coming from open(..., ... | O_TRUNC); */
> >                 WARN_ON(!(attr->ia_valid & ATTR_SIZE));
> >                 WARN_ON(attr->ia_size != 0);
> > -               if (fc->atomic_o_trunc) {
> > +               /*
> > +                * With open passthrough, FUSE_OPEN was never sent to the
> > +                * server, which means the server didn't get a chance to handle
> > +                * the truncation, so we need to send a FUSE_SETATTR
> > +                */
>
> Do we need to do that?
> Is the backing inode being opened with O_TRUNC? maybe not?
> If it does it will be truncated and fuse will read the size attribute. no?

You're right, the truncation will already be handled by the backing
filesystem. I'll fix this in v2.

>
> > +               if (fc->atomic_o_trunc &&
> > +                   !fuse_inode_passthrough_op(inode, FUSE_OPEN)) {
> >                         /*
> >                          * No need to send request to userspace, since actual
> >                          * truncation has already been done by OPEN.  But still
> > diff --git a/fs/fuse/file.c b/fs/fuse/file.c
> > index a1de70bc589d..5cbf240064f9 100644
> > --- a/fs/fuse/file.c
> > +++ b/fs/fuse/file.c
> > @@ -111,7 +111,12 @@ static void fuse_file_put(struct fuse_file *ff, bool sync)
> >
> > @@ -551,11 +562,15 @@ static int fuse_fsync(struct file *file, loff_t start, loff_t end,
> >  {
> >         struct inode *inode = file->f_mapping->host;
> >         struct fuse_conn *fc = get_fuse_conn(inode);
> > +       struct fuse_file *ff = file->private_data;
> >         int err;
> >
> >         if (fuse_is_bad(inode))
> >                 return -EIO;
> >
> > +       if (ff->passthrough_open)
> > +               return vfs_fsync_range(ff->passthrough, start, end, datasync);
> > +
>
> ovl_fsync() has override_cred - not sure if it is a must here...

I'll look at the overlayfs layer and do some research on this. Thanks
for pointing this out

>
> >         inode_lock(inode);
> >
> >         /*
> > diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> > index c70478f07d6a..eb55c4b42a4e 100644
> > --- a/fs/fuse/passthrough.c
> > +++ b/fs/fuse/passthrough.c
> > @@ -282,3 +282,62 @@ int fuse_passthrough_setattr(struct inode *inode, struct iattr *attr)
> >          */
> >         return fuse_passthrough_getattr(inode, NULL, STATX_BASIC_STATS, 0);
> >  }
> > +
> > +/*
> > + * Open a file in passthrough mode using the inode's backing reference.
> > + * Called when FUSE_PASSTHROUGH_OP_OPEN is set on the backing map's ops.
> > + * This skips sending a FUSE_OPEN/FUSE_OPENDIR request to the server.
> > + * FUSE_RELEASE/FUSE_RELEASEDIR will be skipped as well when the file is
> > + * released.
> > + */
> > +int fuse_passthrough_open_inode(struct inode *inode, struct file *file)
> > +{
> > +       struct fuse_mount *fm = get_fuse_mount(inode);
> > +       struct fuse_inode *fi = get_fuse_inode(inode);
> > +       struct file *backing_file;
> > +       struct fuse_backing *fb;
> > +       struct fuse_file *ff;
> > +       int err;
> > +
> > +       /*
> > +        * This should always pass true for the release arg because when the
> > +        * last refcount on the file is dropped (see fuse_file_put()),
> > +        * fuse_io_release() needs to be called on the inode stashed in
> > +        * ff->args->release_args.
> > +        */
> > +       ff = fuse_file_alloc(fm, true);
> > +       if (!ff)
> > +               return -ENOMEM;
> > +
> > +       ff->nodeid = get_node_id(inode);
> > +       ff->open_flags = FOPEN_PASSTHROUGH;
> > +       ff->passthrough_open = true;
>
>
> I think if the server did not see the open and did not provide ff->fh
> then all the file ops that carry fh cannot be sent to the server. Right?

I need to research more into this but my understanding was that the
logic in the patch avoids sending any ops that require ff->fh. Though
maybe I'm missing something here...

> So implementing passthrough open may need a bit more work.
>
> > +
> > +       err = -EINVAL;
> > +       fb = fuse_backing_get(fuse_inode_passthrough(fi));
> > +       if (!fb)
> > +               goto error_free;
> > +
> > +       backing_file = backing_file_open(&file->f_path, file->f_flags,
> > +                                         &fb->file->f_path, fb->cred);
> > +       if (IS_ERR(backing_file)) {
> > +               err = PTR_ERR(backing_file);
> > +               goto error_put;
> > +       }
> > +
> > +       ff->passthrough = backing_file;
> > +       ff->cred = get_cred(fb->cred);
> > +
> > +       err = fuse_file_uncached_io_open(inode, ff, fb);
> > +       if (!err) {
> > +               file->private_data = ff;
> > +               return 0;
> > +       }
> > +
> > +       fuse_passthrough_release(ff, fb);
> > +error_put:
> > +       fuse_backing_put(fb);
> > +error_free:
> > +       fuse_file_free(ff);
> > +       return err;
> > +}
>
> Joanne,
>
> I am done with this round of review.
> Nice work!
> Thanks for getting this out the door.
> Let's see if we find a way to tackle the issues found so far
> without over complicating the code.

Thank you for reviewing this series, Amir. All your comments were
immensely helpful.

Thanks,
Joanne
>
> Thanks,
> Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 16/17] fuse: add passthrough open
  2026-04-22  4:19     ` Joanne Koong
@ 2026-04-22  4:23       ` Joanne Koong
  2026-04-22  6:51         ` Amir Goldstein
  0 siblings, 1 reply; 56+ messages in thread
From: Joanne Koong @ 2026-04-22  4:23 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 9:19 PM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 1:21 PM Amir Goldstein <amir73il@gmail.com> wrote:
> >
> > > diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> > > index c70478f07d6a..eb55c4b42a4e 100644
> > > --- a/fs/fuse/passthrough.c
> > > +++ b/fs/fuse/passthrough.c
> > > @@ -282,3 +282,62 @@ int fuse_passthrough_setattr(struct inode *inode, struct iattr *attr)
> > >          */
> > >         return fuse_passthrough_getattr(inode, NULL, STATX_BASIC_STATS, 0);
> > >  }
> > > +
> > > +/*
> > > + * Open a file in passthrough mode using the inode's backing reference.
> > > + * Called when FUSE_PASSTHROUGH_OP_OPEN is set on the backing map's ops.
> > > + * This skips sending a FUSE_OPEN/FUSE_OPENDIR request to the server.
> > > + * FUSE_RELEASE/FUSE_RELEASEDIR will be skipped as well when the file is
> > > + * released.
> > > + */
> > > +int fuse_passthrough_open_inode(struct inode *inode, struct file *file)
> > > +{
> > > +       struct fuse_mount *fm = get_fuse_mount(inode);
> > > +       struct fuse_inode *fi = get_fuse_inode(inode);
> > > +       struct file *backing_file;
> > > +       struct fuse_backing *fb;
> > > +       struct fuse_file *ff;
> > > +       int err;
> > > +
> > > +       /*
> > > +        * This should always pass true for the release arg because when the
> > > +        * last refcount on the file is dropped (see fuse_file_put()),
> > > +        * fuse_io_release() needs to be called on the inode stashed in
> > > +        * ff->args->release_args.
> > > +        */
> > > +       ff = fuse_file_alloc(fm, true);
> > > +       if (!ff)
> > > +               return -ENOMEM;
> > > +
> > > +       ff->nodeid = get_node_id(inode);
> > > +       ff->open_flags = FOPEN_PASSTHROUGH;
> > > +       ff->passthrough_open = true;
> >
> >
> > I think if the server did not see the open and did not provide ff->fh
> > then all the file ops that carry fh cannot be sent to the server. Right?
>
> I need to research more into this but my understanding was that the
> logic in the patch avoids sending any ops that require ff->fh. Though
> maybe I'm missing something here...
>
Ahh okay I see, I'm missing FUSE_IOCTL, FUSE_LSEEK, FUSE_FLOCK and
some others. Sorry for missing that, I'll fix that up in v2.

Thanks,
Joanne

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 13/17] fuse: add passthrough support for atomic file creation
  2026-04-22  0:40     ` Joanne Koong
@ 2026-04-22  5:10       ` Amir Goldstein
  0 siblings, 0 replies; 56+ messages in thread
From: Amir Goldstein @ 2026-04-22  5:10 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Wed, Apr 22, 2026 at 2:40 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 12:51 PM Amir Goldstein <amir73il@gmail.com> wrote:
> >
> > On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> > >
> > > Route FUSE_CREATE (atomic create + open) through fuse_entry2_out when
> > > FUSE_PASSTHROUGH_INO is enabled. If the server returns a backing id,
> > > the newly created inode will be 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 | 44 ++++++++++++++++++++++++++++++++++++--------
> > >  1 file changed, 36 insertions(+), 8 deletions(-)
> > >
> > > diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> > > index e5a7640fcd30..637761de2c5b 100644
> > > --- a/fs/fuse/dir.c
> > > +++ b/fs/fuse/dir.c
> > > @@ -976,8 +979,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 (use_entry2) {
> > > +               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);
> > > @@ -992,6 +1002,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))
> > > @@ -1000,16 +1012,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;
> >
> > Unless I am missing something, you need to set FOPEN_PASSTHROUGH
> > in ff->open_flags otherwise finish_open will not open the ff->passthrough
> > file (with the correct RW mode).
>
> Currently in this series, the kernel enforces that if there is a
> backing file registered for the inode, the server is still expected to
> set FOPEN_PASSTHROUGH and provide the backing id in the FUSE_OPEN /
> FUSE_CREATE reply. My original motivation was for keeping the api
> contract consistent between FUSE_PASSTHROUGH_INO and the regular
> passthrough mode, but I think you're right that this is just overkill.
> I'll remove this restriction in v2.

Actually, that's not what I meant.
I had a silly braino reading the subject
"add passthrough support for atomic file creation" and thinking that we want
to passthrough the CREATE op :)
so you may ignore this comment in the scope of this patch.

As for the api, I am not sure if it's an overkill.
I kind of like it the way it is - IMO keeping the dialect over explicit
is better than being ambiguous, but am open to other opinions.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 14/17] fuse: use passthrough getattr in setattr suid/sgid handling
  2026-04-22  3:48     ` Joanne Koong
@ 2026-04-22  5:22       ` Amir Goldstein
  2026-04-23  0:03         ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-22  5:22 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Wed, Apr 22, 2026 at 5:48 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 7:25 AM Amir Goldstein <amir73il@gmail.com> wrote:
> >
> > On Tue, Apr 21, 2026 at 12:18 AM 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 637761de2c5b..ff9a92d8a496 100644
> > > --- a/fs/fuse/dir.c
> > > +++ b/fs/fuse/dir.c
> >
> >                 /*
> >                  * The only sane way to reliably kill suid/sgid is to do it in
> >                  * the userspace filesystem
> >                  *
> >                  * This should be done on write(), truncate() and chown().
> >                  */
> >                 if (!fc->handle_killpriv && !fc->handle_killpriv_v2) {
> >
> > We should probably enforce a dependency between those two and
> > passthrough of SETATTR and maybe GETATTR - not sure what
> > the rules should be though.
>
> Not sure if this is what you had in mind, but skip the manual
> stripping entirely if setattr is passed through since the backing
> filesystem will handle it? eg
>
>   if (!fc->handle_killpriv && !fc->handle_killpriv_v2 &&
>       !fuse_inode_passthrough_op(inode, FUSE_SETATTR)) {
>

I was hoping that this will not be needed because
PASSTHROUGH_INO feature will require HANDLE_KILLPRIV_V2
and then either server handles killpriv or killpriv happens natively
on passthrough write/trunc ops, but never need this explicit non
atomic code to run.

> >
> >
> > > @@ -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_inode_passthrough_op(inode, FUSE_GETATTR))
> > > +                               ret = fuse_passthrough_getattr(inode, NULL,
> > > +                                                              STATX_MODE, 0);
> > > +                       else
> > > +                               ret = fuse_do_getattr(idmap, inode, NULL, file);
> >
> > It does not feel right to passthrough GETATTR if not passthrough SETATTR
> > in this case - I could be wrong.
>
> My mental model was that if a server wishes to pass through getattr
> but not setattr or vice versa, they are responsible for keeping the
> state in sync with the backing file. I'm happy to add the check in
> though if you think it's better to add this.

Though question - I am hoping that we can avert it by never reaching
this code with PASSTHROUGH_INO enabled.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 00/17] fuse: extend passthrough to inode operations
  2026-04-21 21:05     ` Joanne Koong
@ 2026-04-22  6:02       ` Amir Goldstein
  2026-04-23  1:02         ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-22  6:02 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 11:05 PM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 6:55 AM Amir Goldstein <amir73il@gmail.com> wrote:
> >
> > On Tue, Apr 21, 2026 at 11:37 AM Amir Goldstein <amir73il@gmail.com> wrote:
> > >
> > > On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> > > >
> > > > This series extends fuse passthrough to support inode operations (getattr,
> > > > setattr), directory readdir, and kernel-initiated open on backing files.
> > > >
> > > > The existing FUSE_PASSTHROUGH mode attaches a backing file to a fuse inode
> > > > only when a passthrough file is open. This series introduces
> > > > FUSE_PASSTHROUGH_INO, a stricter mode requiring one-to-one inode number
> > > > mapping, which allows attaching a backing file for the lifetime of the fuse
> > > > inode.
> > > >
> > > > Future work includes passthrough for additional operations (rename, unlink,
> > > > readdirplus) and full subtree passthrough, which will be part of a separate
> > > > series.
> >
>
> Thanks for taking the time to look at this, Amir!
>
> > Joanne,
> >
> > Thanks a lot for picking up my abandoned wip!
> >
> > Could you add a few words about the intended use case in Meta?
>
> The main intended use case at Meta is to use passthrough to reduce
> startup latency for short‑lived services and batch jobs that need to
> fetch large packaged artifacts over the network. The idea is to add a
> fuse-based lazy materialization layer that supports on-demand fetching
> for cold data but that once the required content has been fetched
> locally, can switch to passthrough for near-native performance with
> minimal userspace overhead.
>

Yes, I figured.
I wonder if Meta plans to completely phase out the fanotify
pre-content events based solution.
I myself am putting money on both horses so not advocating
for one or the other, just curious.

> >
> > Is the target fuse filesystem expected to be used by arbitrary users/apps
> > or by a controlled set of users/apps where a posix_fadvise() may make
> > sense to configure readdir vs. readdirplus?
>
> I think it'll mostly be used by different containerized/batch
> workloads and services, so for Meta's use case, I don't think it can
> assume users/apps can be modified to use posix_fadvise().
>
> >
> > What is the minimal set of passthrough ops that would make this useful
> > to your internal customers? Is readdirplus in this set?
>
> The minimal set of useful passthrough ops would be open + read
> passthrough, getattr/statx passthrough, and readdirplus. The internal

Do you not see any getxattr in this readonly workload?
Are posix acl checks not in play?
passthrough of getxattr/listxattr is quite low hanging IIRC.
I actually had it in the wip branch at some point.
Unfortunately, I don't remember what was the reason I removed it.

FWIW, for my internal customers passthrough of
readdir,getattr,getxattr and access() is used to speed up
file browsing workloads (especially for huge directories).

readdirplus is of course desired for this workload.

Passthrough of open, I am not sure why this is so important?
As fast as passthrough read/write is I expect it would amortize the
cost of OPEN calls to the server, unless the workload is open
of many small files??

> use case is read-only. readdirplus would be useful because the
> workloads often do directory walking followed by metadata lookups for
> the entries
>
> >
> > Do you have profiling numbers to indicate which ops generate the most
> > overhead for would-be passthrough ops routed to the server for the
> > target workloads?
> > And how do these numbers improve when using io_uring channel?
>
> I do not, but if you recommend getting them, I can run some benchmarks.
>

Up to you.
My intuition says that passthrough ops are a big win anyway,
but I never checked to what extent uring ops are to closing this gap.

> >
> > Regarding "full subtree passthrough", what does that mean?
> > That lookup op itself is passthrough?
>
> Yes I was thinking this would be something where once a directory is
> marked as full passthrough, the fuse server never sees any requests
> for paths within that subtree and everything is fully passed through
> to the backing directory.
>

Do your internal customers have this use case?
A subtree which is known to be fully materialized?
I wouldn't advise trying to implement such a complex feature
without at least one real customer to justify a real world use case
where this feature would actually be helpful.

Is the cost of reducing a single server call per cold cache directory
really worth the extra complexity?

I could understand "passthrough all immediate non-dir children"
may be useful and not add so much complexity but again
better base this on some real world use case.

> >
> > In that case, would there be no way to escape (opt-out of) passthrough
> > inside the subtree?
>
> Yes I was thinking this would only be used when all files in a
> directory map 1:1 to a backing filesystem.
>

I remember at the time that Android FUSE passthrough/BPF patches
were discussed, that the 1:1 config was mentioned as a good way
to test passthrough ops, but this could be a simple per fc config
which does not raise all the hard questions about subtrees and
renames.

> >
> > The semantics sound challenging especially when considering
> > moves of directories in and out of subtrees.
>
> I was thinking cross-boundary moves would just return -EXDEV, but
> there are probably other semantics I am glossing over here.
>

What is the "boundary"? Who checks this? kernel or server?
Those are questions best avoided unless we have a good enough
justifications to answer them.

> >
> > Anyway, no wrong answers here, just trying to understand the
> > first mile store you are aiming at and the expected improvement.
>
> The first goal I have in mind is eliminating the getattr and open
> round trips. I was envisioning the passthrough work to be a
> multi-series effort where
> everything would get merged together at once only when all the pieces
> were ready.

Everything being the read-only ops? or everything everything?
The main reason I introduced the ops mask was so that we
can chew this huge effort in small bites over many kernel releases
and slowly gain confidence and field experience with the practical
aspects of fuse passthrough - and this includes the api questions
and which opt-out combinations are important and which are not.

I suggest that we try to focus on shipping a useful feature to
users that are going to use it - that is - your internal customers,
my internal customers and whoever else chimes in to test and
provide feedback.

All the rest of the plans of subtree passthrough and directory
modify ops should not block the first useful feature release.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 16/17] fuse: add passthrough open
  2026-04-22  4:23       ` Joanne Koong
@ 2026-04-22  6:51         ` Amir Goldstein
  0 siblings, 0 replies; 56+ messages in thread
From: Amir Goldstein @ 2026-04-22  6:51 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Wed, Apr 22, 2026 at 6:23 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 9:19 PM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > On Tue, Apr 21, 2026 at 1:21 PM Amir Goldstein <amir73il@gmail.com> wrote:
> > >
> > > > diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> > > > index c70478f07d6a..eb55c4b42a4e 100644
> > > > --- a/fs/fuse/passthrough.c
> > > > +++ b/fs/fuse/passthrough.c
> > > > @@ -282,3 +282,62 @@ int fuse_passthrough_setattr(struct inode *inode, struct iattr *attr)
> > > >          */
> > > >         return fuse_passthrough_getattr(inode, NULL, STATX_BASIC_STATS, 0);
> > > >  }
> > > > +
> > > > +/*
> > > > + * Open a file in passthrough mode using the inode's backing reference.
> > > > + * Called when FUSE_PASSTHROUGH_OP_OPEN is set on the backing map's ops.
> > > > + * This skips sending a FUSE_OPEN/FUSE_OPENDIR request to the server.
> > > > + * FUSE_RELEASE/FUSE_RELEASEDIR will be skipped as well when the file is
> > > > + * released.
> > > > + */
> > > > +int fuse_passthrough_open_inode(struct inode *inode, struct file *file)
> > > > +{
> > > > +       struct fuse_mount *fm = get_fuse_mount(inode);
> > > > +       struct fuse_inode *fi = get_fuse_inode(inode);
> > > > +       struct file *backing_file;
> > > > +       struct fuse_backing *fb;
> > > > +       struct fuse_file *ff;
> > > > +       int err;
> > > > +
> > > > +       /*
> > > > +        * This should always pass true for the release arg because when the
> > > > +        * last refcount on the file is dropped (see fuse_file_put()),
> > > > +        * fuse_io_release() needs to be called on the inode stashed in
> > > > +        * ff->args->release_args.
> > > > +        */
> > > > +       ff = fuse_file_alloc(fm, true);
> > > > +       if (!ff)
> > > > +               return -ENOMEM;
> > > > +
> > > > +       ff->nodeid = get_node_id(inode);
> > > > +       ff->open_flags = FOPEN_PASSTHROUGH;
> > > > +       ff->passthrough_open = true;
> > >
> > >
> > > I think if the server did not see the open and did not provide ff->fh
> > > then all the file ops that carry fh cannot be sent to the server. Right?
> >
> > I need to research more into this but my understanding was that the
> > logic in the patch avoids sending any ops that require ff->fh. Though
> > maybe I'm missing something here...
> >
> Ahh okay I see, I'm missing FUSE_IOCTL, FUSE_LSEEK, FUSE_FLOCK and
> some others. Sorry for missing that, I'll fix that up in v2.
>

FWIW, those are all optional operations that already have
a default kernel implementation, so I suggest that OPEN passthrough
implies that we just skip them, same as you did for FLUSH.

It is worth mentioning that the atomic open code path still goes
through the server, which means that server could return
FUSE_PASSTHROUGH_OP_OPEN entry and either opt-in
for FOPEN_PASSTHROUGH or not and also that the server does
provide ff->fh and does expect release in this case.

Semantically this seems fine, the only thing to watch out for is
that because FUSE_PASSTHROUGH_OP_OPEN is for the lifetime
of the inode, it is almost as if ff->passthrough_open is redundant,
only it is not, for this first atomic open.

Maybe the mental model would be simpler if atomic open
would send RELEASE to the server and use the
FUSE_PASSTHROUGH_OP_OPEN semantics
(force FOPEN_PASSTHROUGH and no lseek/ioctl/flock) for this
first open as well, to be more consistent.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 08/17] fuse: add passthrough ops gating
  2026-04-22  2:57     ` Joanne Koong
@ 2026-04-22  7:27       ` Amir Goldstein
  2026-04-23  1:47         ` Joanne Koong
  0 siblings, 1 reply; 56+ messages in thread
From: Amir Goldstein @ 2026-04-22  7:27 UTC (permalink / raw)
  To: Joanne Koong; +Cc: miklos, fuse-devel, luis

On Wed, Apr 22, 2026 at 4:57 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 3:48 AM Amir Goldstein <amir73il@gmail.com> wrote:
> >
> > On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> > >
> > > Route existing callers through a passthrough ops mask check to verify
> > > whether passthrough can be called for the operation or not. The check is
> > > only done when FUSE_PASSTHROUGH_INO mode is enabled, which preserves
> > > backwards compatibility with prior passthrough behavior.
> > >
> > > It is safe to get the backing file by accessing ff->passthrough directly
> > > in passthrough.c because passthrough.c is only compiled with
> > > CONFIG_FUSE_PASSTHROUGH=y and the caller has already done the ops check.
> > >
> > > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > > ---
> > >  fs/fuse/file.c        | 14 ++++++++------
> > >  fs/fuse/fuse_i.h      | 14 +++++++++++++-
> > >  fs/fuse/passthrough.c | 12 ++++++------
> > >  fs/fuse/readdir.c     |  2 +-
> > >  4 files changed, 28 insertions(+), 14 deletions(-)
> > >
> > > diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> > > index a15fb508fd28..45d9184d0f7a 100644
> > > --- a/fs/fuse/fuse_i.h
> > > +++ b/fs/fuse/fuse_i.h
> > > @@ -1630,9 +1630,21 @@ static inline struct fuse_backing *fuse_inode_backing_set(struct fuse_inode *fi,
> > >  struct fuse_backing *fuse_passthrough_open(struct file *file, int backing_id);
> > >  void fuse_passthrough_release(struct fuse_file *ff, struct fuse_backing *fb);
> > >
> > > -static inline struct file *fuse_file_passthrough(struct fuse_file *ff)
> > > +static inline struct file *fuse_file_passthrough(struct inode *inode,
> > > +                                                struct fuse_file *ff,
> > > +                                                u64 passthrough_ops)
> > >  {
> > >  #ifdef CONFIG_FUSE_PASSTHROUGH
> > > +       if (!ff->passthrough)
> > > +               return NULL;
> > > +
> > > +       if (ff->fm->fc->passthrough_ino && passthrough_ops) {
> > > +               struct fuse_backing *fb = fuse_inode_backing(get_fuse_inode(inode));
> > > +
> > > +               if (!fb || (fb->ops_mask & passthrough_ops) != passthrough_ops)
> > > +                       return NULL;
> > > +       }
> > > +
> > >         return ff->passthrough;
> > >  #else
> > >         return NULL;
> >
> >
> > Please document the relationship between  FOPEN_PASSTHROUGH
> > and FUSE_PASSTHROUGH_RW_OPS.
>
> I will add a part about this to the documentation patch. In this
> current series, FOPEN_PASSTHROUGH under FUSE_PASSTHROUGH_INO does not
> imply FUSE_PASSTHROUGH_RW_OPS. FOPEN_PASSTHROUGH can be set with just
> FUSE_PASSTHROUGH_OP_READ or FUSE_PASSTHROUGH_OP_WRITE and does not
> require both (eg server wants to intercept writes for logging or
> encryption but reads can be passed through). For mmap, it would only
> be passed through if both passthrough read and passthrough write are
> set. In the non-FUSE_PASSTHROUGH_INO model, FOPEN_PASSTHROUGH implies
> both passthrough read and passthrough write.
>
> Do you think this makes sense or am I overcomplicating things?
>

I think it makes sense.
My internal customers have this use case (passthrough only read).

I have a library (libfuse_passthough) that implements fuse passthrough
template fs in userspace and this library currently uses kernel passthrough
only if both passthrough_read and passthrough_write have been requested:

https://github.com/amir73il/libfuse/blob/fuse_passthrough/passthrough/fuse_passthrough.cpp#L997

Note that the library provides this granularity at open time, not lookup
but we can also think about extending the FOPEN_PASSTHROUGH
api later to allow this.

> >
> > Does a server need to opt-in both for the read/write operations passthrough
> > and the specific file (I guess so)?
>
> Yes, in this current series, to use passthrough read/write, the server
> is expected to set read/write ops in ops_mask when registering the
> backing file (inode level) and set FOPEN_PASSTHROUGH when opening the
> specific file (file level).
>
> I left a comment [1] on your other comment about dropping the
> expectation that the server needs to set FOPEN_PASSTHROUGH in the
> FUSE_PASSTHROUGH_INO case if it provided a backing id in the lookup
> and set passthrough read / write in the ops mask - I'm not sure if
> that was what you were hinting at or not.

I actually did not mean that, but now that I think about it,
FOPEN_PASSTHROUGH + backing_id could mean as today
"request passthrough for lifetime of file"
FOPEN_PASSTHROUGH + 0 could mean
"request passthrough to existing backing inode"

IIRC, the value of 0 as backing_id was intentionally reserved
for this purpose.

> [1] https://lore.kernel.org/fuse-devel/CAJnrk1b=5vRgm1otKAiaeFJr9_J-aLnoVTp4MHAz2mBKtcE2dw@mail.gmail.com/
>
> >
> > The concept behind the inode iomodes is that it is not allowed to mix cached
> > with passthrough ops on the same inode, because there be dragons.
> >
> > If the server wants to opt-out of passthrough for a specific opened file,
> > which already has a backing inode, it must explicitly request the combination
> > FOPEN_DIRECT_IO | FOPEN_PASSTHROUGH
> > and then direct io will be performed, despite the inode having a backing inode.
> >
> > What you have done here is a private case of passthrough opt-out
> > not per file, but per operation.
> >
> > The bottom line is if the fuse inode has a backing inode, then you must not
> > fallback to the fuse_cache_ operations in case the op is not in the mask,
> > but you may fallback to the fuse_direct_ operations.
>
> Thanks for pointing this out. I will add this enforcement at the
> kernel level for calling back to direct operations.
>
> This is what I have in my head for how it'll work:
> - server opts into passthrough for both reads and writes
> * server sets FOPEN_DIRECT_IO: reads/writes will be forwarded to the
> server, but mmap will be passed through to the backing file
> * server does not set FOPEN_DIRECT_IO: reads/writes/mmap are passed
> through to backing file
> This is the same behavior as the non-FUSE_PASSTHROUGH_INO case

yes.

>
> - server opts into passthrough for reads but not writes (or vice versa)
> * mmap is never passed through, mmap will always go to the server
> * no data caching will be done whatsoever

mmap cannot go to the server without page cache
and this is what we were trying to avoid.

> * server does not set FOPEN_DIRECT_IO: reads are passed through,
> writes are forwarded to the server
> * server sets FOPEN_DIRECT_IO: no-op (same behavior as if server did
> not set FOPEN_DIRECT_IO)
>
> Do you think this makes sense?
>

Unfortunately not.
Splitting reads from write logic does not work for mmap at all.
User always read/write to the same memory, either mapped
to backing inode or to fuse page cache.
The logic of iomode.c is that if a backing inode exists mmap
always maps the backing file.

Server could request to get normal READ or normal WRITE or
both by not setting FUSE_PASSTHROUGH_OP_READ/WRITE in
ops mask, but it cannot opt-out of passthrough mmap.

> >
> > I hope this does not complicate things for you too much,
> > but I also don't know (yet) what you intend this gating for.
>
> Thank you for your feedback, it was all very helpful!
>
> The intention of this patch was to support cases where the server sets
> only read passthrough or write passthrough but not both. In this
> scenario we cannot rely on "ff->passthrough != NULL" to know whether
> to pass through the read or the write, so we need to check the
> ops_mask. My intended use case doesn't really need this, but imo I
> think it's overall a useful feature that other servers will want, but
> if you disagree with this, please let me know.

I think it is a useful and common use case and the implementation
should be quite trivial following the lead of the FOPEN_DIRECT_IO
per-file opt-out.

Thanks,
Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 14/17] fuse: use passthrough getattr in setattr suid/sgid handling
  2026-04-22  5:22       ` Amir Goldstein
@ 2026-04-23  0:03         ` Joanne Koong
  0 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-23  0:03 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 10:23 PM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Wed, Apr 22, 2026 at 5:48 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > On Tue, Apr 21, 2026 at 7:25 AM Amir Goldstein <amir73il@gmail.com> wrote:
> > >
> > > On Tue, Apr 21, 2026 at 12:18 AM 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 637761de2c5b..ff9a92d8a496 100644
> > > > --- a/fs/fuse/dir.c
> > > > +++ b/fs/fuse/dir.c
> > >
> > >                 /*
> > >                  * The only sane way to reliably kill suid/sgid is to do it in
> > >                  * the userspace filesystem
> > >                  *
> > >                  * This should be done on write(), truncate() and chown().
> > >                  */
> > >                 if (!fc->handle_killpriv && !fc->handle_killpriv_v2) {
> > >
> > > We should probably enforce a dependency between those two and
> > > passthrough of SETATTR and maybe GETATTR - not sure what
> > > the rules should be though.
> >
> > Not sure if this is what you had in mind, but skip the manual
> > stripping entirely if setattr is passed through since the backing
> > filesystem will handle it? eg
> >
> >   if (!fc->handle_killpriv && !fc->handle_killpriv_v2 &&
> >       !fuse_inode_passthrough_op(inode, FUSE_SETATTR)) {
> >
>
> I was hoping that this will not be needed because
> PASSTHROUGH_INO feature will require HANDLE_KILLPRIV_V2
> and then either server handles killpriv or killpriv happens natively
> on passthrough write/trunc ops, but never need this explicit non
> atomic code to run.

That's a good idea and I think that makes things simplest. It seems
like supporting stripping logic on the server side would not be too
hard. I'll drop this patch and make it explicit that
handle_killpriv_v2 support would need to be enabled to use
PASSTHROUGH_INO.

Thanks,
Joanne

>
> > >
> > >
> > > > @@ -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_inode_passthrough_op(inode, FUSE_GETATTR))
> > > > +                               ret = fuse_passthrough_getattr(inode, NULL,
> > > > +                                                              STATX_MODE, 0);
> > > > +                       else
> > > > +                               ret = fuse_do_getattr(idmap, inode, NULL, file);
> > >
> > > It does not feel right to passthrough GETATTR if not passthrough SETATTR
> > > in this case - I could be wrong.
> >
> > My mental model was that if a server wishes to pass through getattr
> > but not setattr or vice versa, they are responsible for keeping the
> > state in sync with the backing file. I'm happy to add the check in
> > though if you think it's better to add this.
>
> Though question - I am hoping that we can avert it by never reaching
> this code with PASSTHROUGH_INO enabled.
>
> Thanks,
> Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 00/17] fuse: extend passthrough to inode operations
  2026-04-22  6:02       ` Amir Goldstein
@ 2026-04-23  1:02         ` Joanne Koong
  0 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-23  1:02 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Tue, Apr 21, 2026 at 11:02 PM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Tue, Apr 21, 2026 at 11:05 PM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > The main intended use case at Meta is to use passthrough to reduce
> > startup latency for short‑lived services and batch jobs that need to
> > fetch large packaged artifacts over the network. The idea is to add a
> > fuse-based lazy materialization layer that supports on-demand fetching
> > for cold data but that once the required content has been fetched
> > locally, can switch to passthrough for near-native performance with
> > minimal userspace overhead.
> >
>
> Yes, I figured.
> I wonder if Meta plans to completely phase out the fanotify
> pre-content events based solution.
> I myself am putting money on both horses so not advocating
> for one or the other, just curious.
>

Yes the plan/hope is to have this fuse server be the replacement for
the current fanotify implementation.

> > >
> > > Is the target fuse filesystem expected to be used by arbitrary users/apps
> > > or by a controlled set of users/apps where a posix_fadvise() may make
> > > sense to configure readdir vs. readdirplus?
> >
> > I think it'll mostly be used by different containerized/batch
> > workloads and services, so for Meta's use case, I don't think it can
> > assume users/apps can be modified to use posix_fadvise().
> >
> > >
> > > What is the minimal set of passthrough ops that would make this useful
> > > to your internal customers? Is readdirplus in this set?
> >
> > The minimal set of useful passthrough ops would be open + read
> > passthrough, getattr/statx passthrough, and readdirplus. The internal
>
> Do you not see any getxattr in this readonly workload?
> Are posix acl checks not in play?
> passthrough of getxattr/listxattr is quite low hanging IIRC.
> I actually had it in the wip branch at some point.
> Unfortunately, I don't remember what was the reason I removed it.

The team I talked with did not mention anything about extended
attributes but I'll double check with them. Thanks for the suggestion.

>
> FWIW, for my internal customers passthrough of
> readdir,getattr,getxattr and access() is used to speed up
> file browsing workloads (especially for huge directories).
>
> readdirplus is of course desired for this workload.
>
> Passthrough of open, I am not sure why this is so important?
> As fast as passthrough read/write is I expect it would amortize the
> cost of OPEN calls to the server, unless the workload is open
> of many small files??

This wasn't requested by our users but it was something I added
because I thought it'd be useful and it had seemed straightforward. I
don't think I have the requisite vfs knowledge for this yet though, so
I'm going to drop it from this series and bring it back once I study
up on the vfs side of things, if it does prove to be useful.

>
> > use case is read-only. readdirplus would be useful because the
> > workloads often do directory walking followed by metadata lookups for
> > the entries
> >
> > >
> > > Do you have profiling numbers to indicate which ops generate the most
> > > overhead for would-be passthrough ops routed to the server for the
> > > target workloads?
> > > And how do these numbers improve when using io_uring channel?
> >
> > I do not, but if you recommend getting them, I can run some benchmarks.
> >
>
> Up to you.
> My intuition says that passthrough ops are a big win anyway,
> but I never checked to what extent uring ops are to closing this gap.
>
> > >
> > > Regarding "full subtree passthrough", what does that mean?
> > > That lookup op itself is passthrough?
> >
> > Yes I was thinking this would be something where once a directory is
> > marked as full passthrough, the fuse server never sees any requests
> > for paths within that subtree and everything is fully passed through
> > to the backing directory.
> >
>
> Do your internal customers have this use case?
> A subtree which is known to be fully materialized?
> I wouldn't advise trying to implement such a complex feature
> without at least one real customer to justify a real world use case
> where this feature would actually be helpful.
>
> Is the cost of reducing a single server call per cold cache directory
> really worth the extra complexity?
>

There is no concrete internal use case for this. I was more just
thinking this would be useful to have for situations where a
subdirectory has already been entirely downloaded over the network and
can be passed through from the start, but I agree it's not worth the
complexity. I will drop this.

> I could understand "passthrough all immediate non-dir children"
> may be useful and not add so much complexity but again
> better base this on some real world use case.
>
> > >
> > > In that case, would there be no way to escape (opt-out of) passthrough
> > > inside the subtree?
> >
> > Yes I was thinking this would only be used when all files in a
> > directory map 1:1 to a backing filesystem.
> >
>
> I remember at the time that Android FUSE passthrough/BPF patches
> were discussed, that the 1:1 config was mentioned as a good way
> to test passthrough ops, but this could be a simple per fc config
> which does not raise all the hard questions about subtrees and
> renames.
>
> > >
> > > The semantics sound challenging especially when considering
> > > moves of directories in and out of subtrees.
> >
> > I was thinking cross-boundary moves would just return -EXDEV, but
> > there are probably other semantics I am glossing over here.
> >
>
> What is the "boundary"? Who checks this? kernel or server?
> Those are questions best avoided unless we have a good enough
> justifications to answer them.
>
> > >
> > > Anyway, no wrong answers here, just trying to understand the
> > > first mile store you are aiming at and the expected improvement.
> >
> > The first goal I have in mind is eliminating the getattr and open
> > round trips. I was envisioning the passthrough work to be a
> > multi-series effort where
> > everything would get merged together at once only when all the pieces
> > were ready.
>
> Everything being the read-only ops? or everything everything?
> The main reason I introduced the ops mask was so that we
> can chew this huge effort in small bites over many kernel releases
> and slowly gain confidence and field experience with the practical
> aspects of fuse passthrough - and this includes the api questions
> and which opt-out combinations are important and which are not.
>
> I suggest that we try to focus on shipping a useful feature to
> users that are going to use it - that is - your internal customers,
> my internal customers and whoever else chimes in to test and
> provide feedback.

That makes sense. My original thinking was to land
FUSE_PASSTHROUGH_INO at once with all the features so that when the
init flag is set, servers didn't have to probe which ops are supported
vs not. I think what you're saying makes a lot of sense and is the
better way to go.

Thanks,
Joanne

>
> All the rest of the plans of subtree passthrough and directory
> modify ops should not block the first useful feature release.
>
> Thanks,
> Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

* Re: [PATCH v1 08/17] fuse: add passthrough ops gating
  2026-04-22  7:27       ` Amir Goldstein
@ 2026-04-23  1:47         ` Joanne Koong
  0 siblings, 0 replies; 56+ messages in thread
From: Joanne Koong @ 2026-04-23  1:47 UTC (permalink / raw)
  To: Amir Goldstein; +Cc: miklos, fuse-devel, luis

On Wed, Apr 22, 2026 at 12:27 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Wed, Apr 22, 2026 at 4:57 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> >
> > On Tue, Apr 21, 2026 at 3:48 AM Amir Goldstein <amir73il@gmail.com> wrote:
> > >
> > > On Tue, Apr 21, 2026 at 12:18 AM Joanne Koong <joannelkoong@gmail.com> wrote:
> > > >
> > > > Route existing callers through a passthrough ops mask check to verify
> > > > whether passthrough can be called for the operation or not. The check is
> > > > only done when FUSE_PASSTHROUGH_INO mode is enabled, which preserves
> > > > backwards compatibility with prior passthrough behavior.
> > > >
> > > > It is safe to get the backing file by accessing ff->passthrough directly
> > > > in passthrough.c because passthrough.c is only compiled with
> > > > CONFIG_FUSE_PASSTHROUGH=y and the caller has already done the ops check.
> > > >
> > > > Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> > > > ---
> > > >  fs/fuse/file.c        | 14 ++++++++------
> > > >  fs/fuse/fuse_i.h      | 14 +++++++++++++-
> > > >  fs/fuse/passthrough.c | 12 ++++++------
> > > >  fs/fuse/readdir.c     |  2 +-
> > > >  4 files changed, 28 insertions(+), 14 deletions(-)
> > > >
> > > > diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> > > > index a15fb508fd28..45d9184d0f7a 100644
> > > > --- a/fs/fuse/fuse_i.h
> > > > +++ b/fs/fuse/fuse_i.h
> > > Please document the relationship between  FOPEN_PASSTHROUGH
> > > and FUSE_PASSTHROUGH_RW_OPS.
> >
> > I will add a part about this to the documentation patch. In this
> > current series, FOPEN_PASSTHROUGH under FUSE_PASSTHROUGH_INO does not
> > imply FUSE_PASSTHROUGH_RW_OPS. FOPEN_PASSTHROUGH can be set with just
> > FUSE_PASSTHROUGH_OP_READ or FUSE_PASSTHROUGH_OP_WRITE and does not
> > require both (eg server wants to intercept writes for logging or
> > encryption but reads can be passed through). For mmap, it would only
> > be passed through if both passthrough read and passthrough write are
> > set. In the non-FUSE_PASSTHROUGH_INO model, FOPEN_PASSTHROUGH implies
> > both passthrough read and passthrough write.
> >
> > Do you think this makes sense or am I overcomplicating things?
> >
>
> I think it makes sense.
> My internal customers have this use case (passthrough only read).
>
> I have a library (libfuse_passthough) that implements fuse passthrough
> template fs in userspace and this library currently uses kernel passthrough
> only if both passthrough_read and passthrough_write have been requested:
>
> https://github.com/amir73il/libfuse/blob/fuse_passthrough/passthrough/fuse_passthrough.cpp#L997
>
> Note that the library provides this granularity at open time, not lookup
> but we can also think about extending the FOPEN_PASSTHROUGH
> api later to allow this.
>
> > >
> > > Does a server need to opt-in both for the read/write operations passthrough
> > > and the specific file (I guess so)?
> >
> > Yes, in this current series, to use passthrough read/write, the server
> > is expected to set read/write ops in ops_mask when registering the
> > backing file (inode level) and set FOPEN_PASSTHROUGH when opening the
> > specific file (file level).
> >
> > I left a comment [1] on your other comment about dropping the
> > expectation that the server needs to set FOPEN_PASSTHROUGH in the
> > FUSE_PASSTHROUGH_INO case if it provided a backing id in the lookup
> > and set passthrough read / write in the ops mask - I'm not sure if
> > that was what you were hinting at or not.
>
> I actually did not mean that, but now that I think about it,
> FOPEN_PASSTHROUGH + backing_id could mean as today
> "request passthrough for lifetime of file"
> FOPEN_PASSTHROUGH + 0 could mean
> "request passthrough to existing backing inode"
>
> IIRC, the value of 0 as backing_id was intentionally reserved
> for this purpose.
>
> > [1] https://lore.kernel.org/fuse-devel/CAJnrk1b=5vRgm1otKAiaeFJr9_J-aLnoVTp4MHAz2mBKtcE2dw@mail.gmail.com/
> >
> > >
> > > The concept behind the inode iomodes is that it is not allowed to mix cached
> > > with passthrough ops on the same inode, because there be dragons.
> > >
> > > If the server wants to opt-out of passthrough for a specific opened file,
> > > which already has a backing inode, it must explicitly request the combination
> > > FOPEN_DIRECT_IO | FOPEN_PASSTHROUGH
> > > and then direct io will be performed, despite the inode having a backing inode.
> > >
> > > What you have done here is a private case of passthrough opt-out
> > > not per file, but per operation.
> > >
> > > The bottom line is if the fuse inode has a backing inode, then you must not
> > > fallback to the fuse_cache_ operations in case the op is not in the mask,
> > > but you may fallback to the fuse_direct_ operations.
> >
> > Thanks for pointing this out. I will add this enforcement at the
> > kernel level for calling back to direct operations.
> >
> > This is what I have in my head for how it'll work:
> > - server opts into passthrough for both reads and writes
> > * server sets FOPEN_DIRECT_IO: reads/writes will be forwarded to the
> > server, but mmap will be passed through to the backing file
> > * server does not set FOPEN_DIRECT_IO: reads/writes/mmap are passed
> > through to backing file
> > This is the same behavior as the non-FUSE_PASSTHROUGH_INO case
>
> yes.
>
> >
> > - server opts into passthrough for reads but not writes (or vice versa)
> > * mmap is never passed through, mmap will always go to the server
> > * no data caching will be done whatsoever
>
> mmap cannot go to the server without page cache
> and this is what we were trying to avoid.
>
> > * server does not set FOPEN_DIRECT_IO: reads are passed through,
> > writes are forwarded to the server
> > * server sets FOPEN_DIRECT_IO: no-op (same behavior as if server did
> > not set FOPEN_DIRECT_IO)
> >
> > Do you think this makes sense?
> >
>
> Unfortunately not.
> Splitting reads from write logic does not work for mmap at all.
> User always read/write to the same memory, either mapped
> to backing inode or to fuse page cache.
> The logic of iomode.c is that if a backing inode exists mmap
> always maps the backing file.
>
> Server could request to get normal READ or normal WRITE or
> both by not setting FUSE_PASSTHROUGH_OP_READ/WRITE in
> ops mask, but it cannot opt-out of passthrough mmap.

Agh that's right, mmap directly operates on the page cache. The
corrected model for passthrough read + non-passthrough write is then:
* reads and mmaps are always passed through, writes are always
forwarded directly to the server

Thanks,
Joanne
>
> > >
> > > I hope this does not complicate things for you too much,
> > > but I also don't know (yet) what you intend this gating for.
> >
> > Thank you for your feedback, it was all very helpful!
> >
> > The intention of this patch was to support cases where the server sets
> > only read passthrough or write passthrough but not both. In this
> > scenario we cannot rely on "ff->passthrough != NULL" to know whether
> > to pass through the read or the write, so we need to check the
> > ops_mask. My intended use case doesn't really need this, but imo I
> > think it's overall a useful feature that other servers will want, but
> > if you disagree with this, please let me know.
>
> I think it is a useful and common use case and the implementation
> should be quite trivial following the lead of the FOPEN_DIRECT_IO
> per-file opt-out.
>
> Thanks,
> Amir.

^ permalink raw reply	[flat|nested] 56+ messages in thread

end of thread, other threads:[~2026-04-23  1:47 UTC | newest]

Thread overview: 56+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-20 22:16 [PATCH v1 00/17] fuse: extend passthrough to inode operations Joanne Koong
2026-04-20 22:16 ` [PATCH v1 01/17] fuse: introduce FUSE_PASSTHROUGH_INO mode Joanne Koong
2026-04-21 21:11   ` Darrick J. Wong
2026-04-21 23:38     ` Joanne Koong
2026-04-20 22:16 ` [PATCH v1 02/17] fuse: prepare for passthrough of inode operations Joanne Koong
2026-04-21 21:16   ` Darrick J. Wong
2026-04-22  1:12     ` Joanne Koong
2026-04-20 22:16 ` [PATCH v1 03/17] fuse: prepare for readdir passthrough on directories Joanne Koong
2026-04-21 21:17   ` Darrick J. Wong
2026-04-21 23:12     ` Joanne Koong
2026-04-20 22:16 ` [PATCH v1 04/17] fuse: implement passthrough for readdir Joanne Koong
2026-04-20 22:16 ` [PATCH v1 05/17] fuse: prepare for long lived reference on backing file Joanne Koong
2026-04-20 22:16 ` [PATCH v1 06/17] fuse: implement passthrough for getattr/statx Joanne Koong
2026-04-20 22:16 ` [PATCH v1 07/17] fuse: prepare to setup backing inode passthrough on lookup Joanne Koong
2026-04-20 22:16 ` [PATCH v1 08/17] fuse: add passthrough ops gating Joanne Koong
2026-04-21 10:48   ` Amir Goldstein
2026-04-22  2:57     ` Joanne Koong
2026-04-22  7:27       ` Amir Goldstein
2026-04-23  1:47         ` Joanne Koong
2026-04-20 22:16 ` [PATCH v1 09/17] fuse: prepare to cache statx attributes from entry replies Joanne Koong
2026-04-21 12:26   ` Amir Goldstein
2026-04-20 22:16 ` [PATCH v1 10/17] fuse: add struct fuse_entry2_out and helpers for extended " Joanne Koong
2026-04-21 12:25   ` Amir Goldstein
2026-04-22  0:50     ` Joanne Koong
2026-04-20 22:16 ` [PATCH v1 11/17] fuse: add passthrough lookup Joanne Koong
2026-04-21 13:23   ` Amir Goldstein
2026-04-22  3:17     ` Joanne Koong
2026-04-20 22:16 ` [PATCH v1 12/17] fuse: add passthrough support for entry creation Joanne Koong
2026-04-21 14:08   ` Amir Goldstein
2026-04-22  3:01     ` Joanne Koong
2026-04-20 22:16 ` [PATCH v1 13/17] fuse: add passthrough support for atomic file creation Joanne Koong
2026-04-21 19:51   ` Amir Goldstein
2026-04-22  0:40     ` Joanne Koong
2026-04-22  5:10       ` Amir Goldstein
2026-04-20 22:16 ` [PATCH v1 14/17] fuse: use passthrough getattr in setattr suid/sgid handling Joanne Koong
2026-04-21 14:25   ` Amir Goldstein
2026-04-22  3:48     ` Joanne Koong
2026-04-22  5:22       ` Amir Goldstein
2026-04-23  0:03         ` Joanne Koong
2026-04-20 22:16 ` [PATCH v1 15/17] fuse: add passthrough setattr Joanne Koong
2026-04-21 14:20   ` Amir Goldstein
2026-04-21 14:32     ` Amir Goldstein
2026-04-22  1:09       ` Joanne Koong
2026-04-20 22:16 ` [PATCH v1 16/17] fuse: add passthrough open Joanne Koong
2026-04-21 20:20   ` Amir Goldstein
2026-04-22  4:19     ` Joanne Koong
2026-04-22  4:23       ` Joanne Koong
2026-04-22  6:51         ` Amir Goldstein
2026-04-20 22:16 ` [PATCH v1 17/17] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO) Joanne Koong
2026-04-21 11:09   ` Amir Goldstein
2026-04-22  1:04     ` Joanne Koong
2026-04-21  9:37 ` [PATCH v1 00/17] fuse: extend passthrough to inode operations Amir Goldstein
2026-04-21 13:55   ` Amir Goldstein
2026-04-21 21:05     ` Joanne Koong
2026-04-22  6:02       ` Amir Goldstein
2026-04-23  1:02         ` Joanne Koong

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox