* [PATCH v2 00/21] fuse: extend passthrough to inode operations
@ 2026-05-16 0:39 Joanne Koong
2026-05-16 0:39 ` [PATCH v2 01/21] fuse: introduce FUSE_PASSTHROUGH_INO mode Joanne Koong
` (20 more replies)
0 siblings, 21 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
This series extends fuse passthrough to support inode operations (getattr +
setattr) and directory readdir.
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.
This is based off Amir's work. Patches 1-7 are from his git tree in [1].
For testing/debugging, this was done using the passthrough_hp server with the
changes in [2] applied.
This series was run through fstests using passthrough_hp in [2]. There were no
regressions seen compared to passthrough_hp --nopassthrough and an improvement
in generic 683, 684, and 685 (dropping suid/sgid) now passing. Compared to
passthrough_hp without inode operations passed through, generic tests 080 and
215 (mapped write timestamp updates) now pass.
This is on top of commit ea2ff881bb8e ("fuse: reduce attributes invalidated on
directory change") + a locally applied patch [3], in Miklos's fuse tree.
[1] https://github.com/amir73il/linux/commits/fuse-backing-inode-wip/
[2] https://github.com/joannekoong/libfuse/commits/extended_passthrough_v2/
[3] https://lore.kernel.org/fuse-devel/20260513231437.1806724-1-joannelkoong@gmail.com/T/#u
Changelog
---------
v1: https://lore.kernel.org/fuse-devel/CAOQ4uxjBZL6CY0mJEiHrw8aR28MHg5pU+Pfr9fUqGdKPDOdwcw@mail.gmail.com/T/
v1 -> v2:
- Address Amir's feedback (especially about not mixing cached io mode with
passthrough mode, dropping the passthrough open patch, modeling some of the
logic after overlayfs)
- Diff between v2 and v1 for Amir's patches 1-7:
https://gist.github.com/joannekoong/451eb02e716fa6321a622a61a95aea04
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 (14):
fuse: handle zero ops_mask in FUSE_DEV_IOC_BACKING_OPEN
fuse: handle partial io passthrough for read/write, splice, and mmap
fuse: prepare to cache statx attributes from entry replies
fuse: clean up fuse_dentry_revalidate()
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 create+open
fuse: allow backing_id=0 in open to inherit inode's backing file
backing-inode: add backing_inode_copyattr()
backing-inode: add backing_inode_setattr()
fuse: add passthrough setattr
fuse: use passthrough getattr in setattr suid/sgid handling
docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO)
.../filesystems/fuse/fuse-passthrough.rst | 126 ++++++
fs/Makefile | 2 +-
fs/backing-inode.c | 97 +++++
fs/fuse/backing.c | 64 ++-
fs/fuse/cuse.c | 2 +-
fs/fuse/dir.c | 391 +++++++++++++-----
fs/fuse/file.c | 37 +-
fs/fuse/fuse_i.h | 83 +++-
fs/fuse/inode.c | 27 +-
fs/fuse/iomode.c | 122 +++++-
fs/fuse/passthrough.c | 122 +++++-
fs/fuse/readdir.c | 5 +-
fs/overlayfs/inode.c | 66 +--
fs/overlayfs/util.c | 33 +-
include/linux/backing-inode.h | 18 +
include/uapi/linux/fuse.h | 32 +-
16 files changed, 969 insertions(+), 258 deletions(-)
create mode 100644 fs/backing-inode.c
create mode 100644 include/linux/backing-inode.h
--
2.52.0
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v2 01/21] fuse: introduce FUSE_PASSTHROUGH_INO mode
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 02/21] fuse: prepare for passthrough of inode operations Joanne Koong
` (19 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
This is a more strict variant of FUSE_PASSTHROUGH mode, in which the
backing file inode number must match the fuse inode number.
This mode will allow the kernel to instantiate fuse inodes by
passthrough lookup and passthrough readdirplus and notify about those
inodes to the server, using the backing file inode number as a unique
identifier for fuse inodes across kernel and server.
This mode limits the possibility to map multiple fuse inodes to the same
backing file, unless they are all hardlinks.
This mode is only supported on 64bit arch, where ino_t is u64.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/file.c | 3 +--
fs/fuse/fuse_i.h | 6 ++++--
fs/fuse/inode.c | 8 +++++++-
fs/fuse/iomode.c | 13 ++++++++++---
include/uapi/linux/fuse.h | 6 +++++-
5 files changed, 27 insertions(+), 9 deletions(-)
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 7a35ac3e0023..06dc8cfe3c7b 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1429,7 +1429,6 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
bool *exclusive)
{
struct inode *inode = file_inode(iocb->ki_filp);
- struct fuse_inode *fi = get_fuse_inode(inode);
*exclusive = fuse_dio_wr_exclusive_lock(iocb, from);
if (*exclusive) {
@@ -1444,7 +1443,7 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
* have raced, so check it again.
*/
if (fuse_io_past_eof(iocb, from) ||
- fuse_inode_uncached_io_start(fi, NULL) != 0) {
+ fuse_inode_uncached_io_start(inode, NULL) != 0) {
inode_unlock_shared(inode);
inode_lock(inode);
*exclusive = true;
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 3a7ac74a23ed..0b925ac3e195 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -698,6 +698,9 @@ struct fuse_conn {
/** @passthrough: Passthrough support for read/write IO */
unsigned int passthrough:1;
+ /** @passthrough_ino: One-to-one mapping between fuse ino to backing ino */
+ unsigned int passthrough_ino:1;
+
/** @use_pages_for_kvec_io: Use pages instead of pointer for kernel I/O */
unsigned int use_pages_for_kvec_io:1;
@@ -1237,8 +1240,7 @@ int fuse_fileattr_set(struct mnt_idmap *idmap,
/* iomode.c */
int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff);
-int fuse_inode_uncached_io_start(struct fuse_inode *fi,
- struct fuse_backing *fb);
+int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb);
void fuse_inode_uncached_io_end(struct fuse_inode *fi);
int fuse_file_io_open(struct file *file, struct inode *inode);
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index 33009227e91d..b2a5892a4dc3 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -1397,6 +1397,8 @@ static void process_init_reply(struct fuse_args *args, int error)
fc->passthrough = 1;
fc->max_stack_depth = arg->max_stack_depth;
fm->sb->s_stack_depth = arg->max_stack_depth;
+ if (flags & FUSE_PASSTHROUGH_INO)
+ fc->passthrough_ino = 1;
}
if (flags & FUSE_NO_EXPORT_SUPPORT)
fm->sb->s_export_op = &fuse_export_fid_operations;
@@ -1476,8 +1478,12 @@ static struct fuse_init_args *fuse_new_init(struct fuse_mount *fm)
#endif
if (fm->fc->auto_submounts)
flags |= FUSE_SUBMOUNTS;
- if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH))
+ if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH)) {
flags |= FUSE_PASSTHROUGH;
+ /* one-to-one ino mapping requires 64bit ino */
+ if (sizeof(ino_t) == sizeof(u64))
+ flags |= FUSE_PASSTHROUGH_INO;
+ }
/*
* This is just an information flag for fuse server. No need to check
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index 3728933188f3..ca3b28597722 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -82,8 +82,10 @@ static void fuse_file_cached_io_release(struct fuse_file *ff,
}
/* Start strictly uncached io mode where cache access is not allowed */
-int fuse_inode_uncached_io_start(struct fuse_inode *fi, struct fuse_backing *fb)
+int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb)
{
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_conn *fc = get_fuse_conn(inode);
struct fuse_backing *oldfb;
int err = 0;
@@ -94,6 +96,12 @@ int fuse_inode_uncached_io_start(struct fuse_inode *fi, struct fuse_backing *fb)
err = -EBUSY;
goto unlock;
}
+ /* With FUSE_PASSTHROUGH_INO, fuse and backing ino must match */
+ if (fb && fc->passthrough_ino &&
+ fb->file->f_inode->i_ino != inode->i_ino) {
+ err = -EIO;
+ goto unlock;
+ }
if (fi->iocachectr > 0) {
err = -ETXTBSY;
goto unlock;
@@ -117,10 +125,9 @@ static int fuse_file_uncached_io_open(struct inode *inode,
struct fuse_file *ff,
struct fuse_backing *fb)
{
- struct fuse_inode *fi = get_fuse_inode(inode);
int err;
- err = fuse_inode_uncached_io_start(fi, fb);
+ err = fuse_inode_uncached_io_start(inode, fb);
if (err)
return err;
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index c13e1f9a2f12..4be9ccc5b3ff 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -240,6 +240,9 @@
* - add FUSE_COPY_FILE_RANGE_64
* - add struct fuse_copy_file_range_out
* - add FUSE_NOTIFY_PRUNE
+ *
+ * 7.46
+ * - add FUSE_PASSTHROUGH_INO
*/
#ifndef _LINUX_FUSE_H
@@ -275,7 +278,7 @@
#define FUSE_KERNEL_VERSION 7
/** Minor version number of this interface */
-#define FUSE_KERNEL_MINOR_VERSION 45
+#define FUSE_KERNEL_MINOR_VERSION 46
/** The node ID of the root inode */
#define FUSE_ROOT_ID 1
@@ -495,6 +498,7 @@ struct fuse_file_lock {
#define FUSE_ALLOW_IDMAP (1ULL << 40)
#define FUSE_OVER_IO_URING (1ULL << 41)
#define FUSE_REQUEST_TIMEOUT (1ULL << 42)
+#define FUSE_PASSTHROUGH_INO (1ULL << 43)
/**
* CUSE INIT request/reply flags
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 02/21] fuse: prepare for passthrough of inode operations
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
2026-05-16 0:39 ` [PATCH v2 01/21] fuse: introduce FUSE_PASSTHROUGH_INO mode Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 1:34 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 03/21] fuse: prepare for readdir passthrough on directories Joanne Koong
` (18 subsequent siblings)
20 siblings, 1 reply; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
So far, fuse passthrough was implemented for read/write/splice/mmap
operations for regular files opened with FOPEN_PASSTHROUGH.
A backing file is attached to a fuse inode, but only for as long as
there are FOPEN_PASSTHROUGH files opened on this inode.
We would like to attach a backing file to fuse inode also without an
open file to allow passthrough of some inode operations.
Add field ops_mask to the input argument of FUSE_DEV_IOC_BACKING_OPEN
ioctl to declare the operations that would passthrough to the backing
file once it has been attached to the fuse inode on lookup.
Setting the FUSE_READ/FUSE_WRITE operations in the ops_mask is not
required because those operations are implied by FOPEN_PASSTHROUGH.
When setting operations other than FUSE_READ/FUSE_WRITE in ops_mask,
non-regular backing files are allowed, so we need to verify when
attaching a backing file to a fuse inode, that their file types match.
For simplification of inode attribute caching, for now, require a
filesystem with FUSE_PASSTHROUGH_INO (one-to-one mapping from fuse inode
to backing inode) for setting up passthrough of any inode operations.
We may consider relaxing this requirement for some inode operations
in the future.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/backing.c | 13 ++++++++++---
fs/fuse/fuse_i.h | 30 ++++++++++++++++++++++++++++++
fs/fuse/iomode.c | 16 ++++++++++++++++
include/uapi/linux/fuse.h | 9 ++++++++-
4 files changed, 64 insertions(+), 4 deletions(-)
diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index 3d3f49c2dd42..1fbb4d876eec 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -87,7 +87,8 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
struct fuse_backing *fb = NULL;
int res;
- pr_debug("%s: fd=%d flags=0x%x\n", __func__, map->fd, map->flags);
+ pr_debug("%s: fd=%d flags=0x%x ops_mask=0x%llx\n", __func__,
+ map->fd, map->flags, map->ops_mask);
/* TODO: relax CAP_SYS_ADMIN once backing files are visible to lsof */
res = -EPERM;
@@ -95,7 +96,11 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
goto out;
res = -EINVAL;
- if (map->flags || map->padding)
+ if (map->flags || map->ops_mask & ~FUSE_BACKING_MAP_VALID_OPS)
+ goto out;
+
+ /* For now passthrough inode operations requires FUSE_PASSTHROUGH_INO */
+ if (!fc->passthrough_ino && map->ops_mask & FUSE_PASSTHROUGH_INODE_OPS)
goto out;
file = fget_raw(map->fd);
@@ -105,7 +110,8 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
/* read/write/splice/mmap passthrough only relevant for regular files */
res = d_is_dir(file->f_path.dentry) ? -EISDIR : -EINVAL;
- if (!d_is_reg(file->f_path.dentry))
+ if (!(map->ops_mask & ~FUSE_PASSTHROUGH_RW_OPS) &&
+ !d_is_reg(file->f_path.dentry))
goto out_fput;
backing_sb = file_inode(file)->i_sb;
@@ -120,6 +126,7 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
fb->file = file;
fb->cred = prepare_creds();
+ fb->ops_mask = map->ops_mask;
refcount_set(&fb->count, 1);
res = fuse_backing_id_alloc(fc, fb);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 0b925ac3e195..8e54dc7d56af 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -92,6 +92,7 @@ struct fuse_submount_lookup {
struct fuse_backing {
struct file *file;
struct cred *cred;
+ u64 ops_mask;
/* refcount */
refcount_t count;
@@ -254,6 +255,8 @@ enum {
* or the fuse server has an exclusive "lease" on distributed fs
*/
FUSE_I_EXCLUSIVE,
+ /* Has backing file for inode ops passthrough */
+ FUSE_I_PASSTHROUGH,
};
struct fuse_conn;
@@ -1252,6 +1255,25 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
void fuse_file_release(struct inode *inode, struct fuse_file *ff,
unsigned int open_flags, fl_owner_t id, bool isdir);
+/* passthrough.c */
+
+/* READ/WRITE are implied by FOPEN_PASSTHROUGH, but defined for completeness */
+#define FUSE_PASSTHROUGH_RW_OPS \
+ (FUSE_PASSTHROUGH_OP_READ | FUSE_PASSTHROUGH_OP_WRITE)
+
+/* File passthrough operations require a file opened with FOPEN_PASSTHROUGH */
+#define FUSE_PASSTHROUGH_FILE_OPS \
+ (FUSE_PASSTHROUGH_RW_OPS)
+
+/* Inode passthrough operations for backing file attached to inode */
+#define FUSE_PASSTHROUGH_INODE_OPS (0)
+
+#define FUSE_BACKING_MAP_OP(map, op) \
+ ((map)->ops_mask & FUSE_PASSTHROUGH_OP(op))
+
+#define FUSE_BACKING_MAP_VALID_OPS \
+ (FUSE_PASSTHROUGH_FILE_OPS | FUSE_PASSTHROUGH_INODE_OPS)
+
/* backing.c */
#ifdef CONFIG_FUSE_PASSTHROUGH
struct fuse_backing *fuse_backing_get(struct fuse_backing *fb);
@@ -1319,6 +1341,14 @@ ssize_t fuse_passthrough_splice_write(struct pipe_inode_info *pipe,
size_t len, unsigned int flags);
ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma);
+static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
+{
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_backing *fb = fuse_inode_backing(fi);
+
+ return fb && fb->ops_mask & FUSE_PASSTHROUGH_OP(op);
+}
+
#ifdef CONFIG_SYSCTL
extern int fuse_sysctl_register(void);
extern void fuse_sysctl_unregister(void);
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index ca3b28597722..5517711a3eca 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -96,6 +96,11 @@ int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb)
err = -EBUSY;
goto unlock;
}
+ /* fuse and backing file types must match */
+ if (fb && ((fb->file->f_inode->i_mode ^ inode->i_mode) & S_IFMT)) {
+ err = -EIO;
+ goto unlock;
+ }
/* With FUSE_PASSTHROUGH_INO, fuse and backing ino must match */
if (fb && fc->passthrough_ino &&
fb->file->f_inode->i_ino != inode->i_ino) {
@@ -176,6 +181,7 @@ static int fuse_file_passthrough_open(struct inode *inode, struct file *file)
{
struct fuse_file *ff = file->private_data;
struct fuse_conn *fc = get_fuse_conn(inode);
+ struct fuse_inode *fi = get_fuse_inode(inode);
struct fuse_backing *fb;
int err;
@@ -188,11 +194,21 @@ static int fuse_file_passthrough_open(struct inode *inode, struct file *file)
if (IS_ERR(fb))
return PTR_ERR(fb);
+ /*
+ * Inode ops passthrough requires backing file setup at
+ * creation/lookup time.
+ */
+ err = -EOPNOTSUPP;
+ if (!fuse_inode_backing(fi) &&
+ (fb->ops_mask & FUSE_PASSTHROUGH_INODE_OPS))
+ goto fail;
+
/* First passthrough file open denies caching inode io mode */
err = fuse_file_uncached_io_open(inode, ff, fb);
if (!err)
return 0;
+fail:
fuse_passthrough_release(ff, fb);
fuse_backing_put(fb);
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 4be9ccc5b3ff..0f1e1c1ec367 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -243,6 +243,7 @@
*
* 7.46
* - add FUSE_PASSTHROUGH_INO
+ * - add ops_mask field to struct fuse_backing_map
*/
#ifndef _LINUX_FUSE_H
@@ -1133,9 +1134,15 @@ struct fuse_notify_prune_out {
struct fuse_backing_map {
int32_t fd;
uint32_t flags;
- uint64_t padding;
+ uint64_t ops_mask;
};
+#define FUSE_PASSTHROUGH_OP(op) (1ULL << ((op) - 1))
+
+/* op bits for fuse_backing_map ops_mask */
+#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
+#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
+
/* Device ioctls: */
#define FUSE_DEV_IOC_MAGIC 229
#define FUSE_DEV_IOC_CLONE _IOR(FUSE_DEV_IOC_MAGIC, 0, uint32_t)
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 03/21] fuse: prepare for readdir passthrough on directories
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
2026-05-16 0:39 ` [PATCH v2 01/21] fuse: introduce FUSE_PASSTHROUGH_INO mode Joanne Koong
2026-05-16 0:39 ` [PATCH v2 02/21] fuse: prepare for passthrough of inode operations Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 04/21] fuse: implement passthrough for readdir Joanne Koong
` (17 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
In preparation for readdir passthrough, allow the inode iomode state
to be applicable to directory inodes and prepare the helper
fuse_sync_release() for directories.
Directory inodes will support cached mode, "direct" uncached readdir
mode and readdir passthrough mode, but will not need to wait for
parallel dio like regular files.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/cuse.c | 2 +-
fs/fuse/dir.c | 4 ++--
fs/fuse/file.c | 11 ++++++-----
fs/fuse/fuse_i.h | 15 +++++++++------
fs/fuse/inode.c | 2 +-
fs/fuse/iomode.c | 31 ++++++++++++++++++-------------
6 files changed, 37 insertions(+), 28 deletions(-)
diff --git a/fs/fuse/cuse.c b/fs/fuse/cuse.c
index bac327cfc7f1..9c4d7a1993e8 100644
--- a/fs/fuse/cuse.c
+++ b/fs/fuse/cuse.c
@@ -148,7 +148,7 @@ static int cuse_release(struct inode *inode, struct file *file)
struct fuse_file *ff = file->private_data;
struct fuse_mount *fm = ff->fm;
- fuse_sync_release(NULL, ff, file->f_flags);
+ fuse_sync_release(NULL, ff, file->f_flags, false);
fuse_conn_put(fm->fc);
return 0;
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index be41c14ef329..937165788e79 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -893,7 +893,7 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
&outentry.attr, ATTR_TIMEOUT(&outentry), 0, 0);
if (!inode) {
flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
- fuse_sync_release(NULL, ff, flags);
+ fuse_sync_release(NULL, ff, flags, false);
fuse_chan_queue_forget(fm->fc->chan, forget, outentry.nodeid, 1);
err = -ENOMEM;
goto out_err;
@@ -910,7 +910,7 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
}
if (err) {
fi = get_fuse_inode(inode);
- fuse_sync_release(fi, ff, flags);
+ fuse_sync_release(fi, ff, flags, false);
} else {
if (fm->fc->atomic_o_trunc && trunc)
truncate_pagecache(inode, 0);
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 06dc8cfe3c7b..332032cd5622 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -285,7 +285,7 @@ static int fuse_open(struct inode *inode, struct file *file)
ff = file->private_data;
err = fuse_finish_open(inode, file);
if (err)
- fuse_sync_release(fi, ff, file->f_flags);
+ fuse_sync_release(fi, ff, file->f_flags, false);
else if (is_truncate)
fuse_truncate_update_attr(inode, file);
}
@@ -408,10 +408,12 @@ static int fuse_release(struct inode *inode, struct file *file)
}
void fuse_sync_release(struct fuse_inode *fi, struct fuse_file *ff,
- unsigned int flags)
+ unsigned int flags, bool isdir)
{
+ int opcode = isdir ? FUSE_RELEASEDIR : FUSE_RELEASE;
+
WARN_ON(refcount_read(&ff->count) > 1);
- fuse_prepare_release(fi, ff, flags, FUSE_RELEASE, true);
+ fuse_prepare_release(fi, ff, flags, opcode, true);
fuse_file_put(ff, true);
}
EXPORT_SYMBOL_GPL(fuse_sync_release);
@@ -1454,13 +1456,12 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
static void fuse_dio_unlock(struct kiocb *iocb, bool exclusive)
{
struct inode *inode = file_inode(iocb->ki_filp);
- struct fuse_inode *fi = get_fuse_inode(inode);
if (exclusive) {
inode_unlock(inode);
} else {
/* Allow opens in caching mode after last parallel dio end */
- fuse_inode_uncached_io_end(fi);
+ fuse_inode_uncached_io_end(inode);
inode_unlock_shared(inode);
}
}
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 8e54dc7d56af..2436219338b3 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -160,9 +160,6 @@ struct fuse_inode {
*/
int writectr;
- /** @iocachectr: Number of files/maps using page cache */
- int iocachectr;
-
/** @page_waitq: Waitq for writepage completion */
wait_queue_head_t page_waitq;
@@ -216,6 +213,12 @@ struct fuse_inode {
/** @lock: Lock to protect write-related fields */
spinlock_t lock;
+ /**
+ * @iocachectr: Number of files/maps using page cache (negative for
+ * passthrough)
+ */
+ int iocachectr;
+
#ifdef CONFIG_FUSE_DAX
/**
* @dax: Dax specific inode data
@@ -248,7 +251,7 @@ enum {
FUSE_I_BAD,
/* Has btime */
FUSE_I_BTIME,
- /* Wants or already has page cache IO */
+ /* Regular file wants or already has page cache IO */
FUSE_I_CACHE_IO_MODE,
/*
* Client has exclusive access to the inode, either because fs is local
@@ -946,7 +949,7 @@ void fuse_file_free(struct fuse_file *ff);
int fuse_finish_open(struct inode *inode, struct file *file);
void fuse_sync_release(struct fuse_inode *fi, struct fuse_file *ff,
- unsigned int flags);
+ unsigned int flags, bool isdir);
/*
* Send RELEASE or RELEASEDIR request
@@ -1244,7 +1247,7 @@ int fuse_fileattr_set(struct mnt_idmap *idmap,
/* iomode.c */
int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff);
int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb);
-void fuse_inode_uncached_io_end(struct fuse_inode *fi);
+void fuse_inode_uncached_io_end(struct inode *inode);
int fuse_file_io_open(struct file *file, struct inode *inode);
void fuse_file_io_release(struct fuse_file *ff, struct inode *inode);
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index b2a5892a4dc3..cef8b853b3d8 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -180,10 +180,10 @@ static void fuse_evict_inode(struct inode *inode)
atomic64_inc(&fc->evict_ctr);
}
if (S_ISREG(inode->i_mode) && !fuse_is_bad(inode)) {
- WARN_ON(fi->iocachectr != 0);
WARN_ON(!list_empty(&fi->write_files));
WARN_ON(!list_empty(&fi->queued_writes));
}
+ WARN_ON(fi->iocachectr != 0);
}
static int fuse_reconfigure(struct fs_context *fsc)
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index 5517711a3eca..e2e7fbaac9af 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -15,9 +15,12 @@
/*
* Return true if need to wait for new opens in caching mode.
*/
-static inline bool fuse_is_io_cache_wait(struct fuse_inode *fi)
+static inline bool fuse_is_io_cache_wait(struct inode *inode)
{
- return READ_ONCE(fi->iocachectr) < 0 && !fuse_inode_backing(fi);
+ struct fuse_inode *fi = get_fuse_inode(inode);
+
+ return S_ISREG(inode->i_mode) &&
+ READ_ONCE(fi->iocachectr) < 0 && !fuse_inode_backing(fi);
}
/*
@@ -40,10 +43,10 @@ int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff)
* Setting the bit advises new direct-io writes to use an exclusive
* lock - without it the wait below might be forever.
*/
- while (fuse_is_io_cache_wait(fi)) {
+ while (fuse_is_io_cache_wait(inode)) {
set_bit(FUSE_I_CACHE_IO_MODE, &fi->state);
spin_unlock(&fi->lock);
- wait_event(fi->direct_io_waitq, !fuse_is_io_cache_wait(fi));
+ wait_event(fi->direct_io_waitq, !fuse_is_io_cache_wait(inode));
spin_lock(&fi->lock);
}
@@ -69,8 +72,10 @@ int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff)
}
static void fuse_file_cached_io_release(struct fuse_file *ff,
- struct fuse_inode *fi)
+ struct inode *inode)
{
+ struct fuse_inode *fi = get_fuse_inode(inode);
+
spin_lock(&fi->lock);
WARN_ON(fi->iocachectr <= 0);
WARN_ON(ff->iomode != IOM_CACHED);
@@ -141,15 +146,17 @@ static int fuse_file_uncached_io_open(struct inode *inode,
return 0;
}
-void fuse_inode_uncached_io_end(struct fuse_inode *fi)
+void fuse_inode_uncached_io_end(struct inode *inode)
{
+ struct fuse_inode *fi = get_fuse_inode(inode);
struct fuse_backing *oldfb = NULL;
spin_lock(&fi->lock);
WARN_ON(fi->iocachectr >= 0);
fi->iocachectr++;
if (!fi->iocachectr) {
- wake_up(&fi->direct_io_waitq);
+ if (S_ISREG(inode->i_mode))
+ wake_up(&fi->direct_io_waitq);
oldfb = fuse_inode_backing_set(fi, NULL);
}
spin_unlock(&fi->lock);
@@ -159,11 +166,11 @@ void fuse_inode_uncached_io_end(struct fuse_inode *fi)
/* Drop uncached_io reference from passthrough open */
static void fuse_file_uncached_io_release(struct fuse_file *ff,
- struct fuse_inode *fi)
+ struct inode *inode)
{
WARN_ON(ff->iomode != IOM_UNCACHED);
ff->iomode = IOM_NONE;
- fuse_inode_uncached_io_end(fi);
+ fuse_inode_uncached_io_end(inode);
}
/*
@@ -278,8 +285,6 @@ int fuse_file_io_open(struct file *file, struct inode *inode)
/* No more pending io and no new io possible to inode via open/mmapped file */
void fuse_file_io_release(struct fuse_file *ff, struct inode *inode)
{
- struct fuse_inode *fi = get_fuse_inode(inode);
-
/*
* Last passthrough file close allows caching inode io mode.
* Last caching file close exits caching inode io mode.
@@ -289,10 +294,10 @@ void fuse_file_io_release(struct fuse_file *ff, struct inode *inode)
/* Nothing to do */
break;
case IOM_UNCACHED:
- fuse_file_uncached_io_release(ff, fi);
+ fuse_file_uncached_io_release(ff, inode);
break;
case IOM_CACHED:
- fuse_file_cached_io_release(ff, fi);
+ fuse_file_cached_io_release(ff, inode);
break;
}
}
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 04/21] fuse: implement passthrough for readdir
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (2 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 03/21] fuse: prepare for readdir passthrough on directories Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 05/21] fuse: prepare for long lived reference on backing file Joanne Koong
` (16 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
Requires both requesting the passthrough inode op READDIR when setting
up the backing file and opening the dir with FOPEN_PASSTHROUGH.
The dir opened with FOPEN_PASSTHROUGH must not request cached readdir
with FOPEN_CACHE_DIR flag.
Like regular files, a directory inode cannot be opened at the same time
for cached readdir and readdir passthrough.
A directory opened without both FOPEN_CACHE_DIR and FOPEN_PASSTHROUGH
is marked as FOPEN_DIRECT_IO and does not affect io mode.
Note that opt-in for passthrough of READDIR operation means that there
is no READDIRPLUS call to server.
For the ls -l use case, the cost of FUSE_LOOKUP to server will be
paid for every entry, so passthrough of READDIR is not always a
performance win.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/backing.c | 5 +++++
fs/fuse/dir.c | 7 +++++++
fs/fuse/file.c | 15 +++++++++++++--
fs/fuse/fuse_i.h | 7 ++++++-
fs/fuse/iomode.c | 9 ++++++++-
fs/fuse/passthrough.c | 24 ++++++++++++++++++++++++
fs/fuse/readdir.c | 3 +++
include/uapi/linux/fuse.h | 1 +
8 files changed, 67 insertions(+), 4 deletions(-)
diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index 1fbb4d876eec..52fccb9ff283 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -114,6 +114,11 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
!d_is_reg(file->f_path.dentry))
goto out_fput;
+ res = -ENOTDIR;
+ if (map->ops_mask & FUSE_PASSTHROUGH_DIR_OPS &&
+ !d_is_dir(file->f_path.dentry))
+ goto out_fput;
+
backing_sb = file_inode(file)->i_sb;
res = -ELOOP;
if (backing_sb->s_stack_depth >= fc->max_stack_depth)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 937165788e79..05f69b8fa4c4 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -1891,6 +1891,7 @@ static const char *fuse_get_link(struct dentry *dentry, struct inode *inode,
static int fuse_dir_open(struct inode *inode, struct file *file)
{
struct fuse_mount *fm = get_fuse_mount(inode);
+ struct fuse_inode *fi = get_fuse_inode(inode);
int err;
if (fuse_is_bad(inode))
@@ -1904,6 +1905,12 @@ static int fuse_dir_open(struct inode *inode, struct file *file)
if (!err) {
struct fuse_file *ff = file->private_data;
+ err = fuse_file_io_open(file, inode);
+ if (err) {
+ fuse_sync_release(fi, ff, file->f_flags, true);
+ return err;
+ }
+
/*
* Keep handling FOPEN_STREAM and FOPEN_NONSEEKABLE for
* directories for backward compatibility, though it's unlikely
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 332032cd5622..828e3fc0ec31 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -180,8 +180,19 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
}
}
- if (isdir)
- ff->open_flags &= ~FOPEN_DIRECT_IO;
+ /*
+ * Mark an uncached dir open as FOPEN_DIRECT_IO for fuse_file_io_open().
+ * The explicit combination of FOPEN_PASSTHROUGH | FOPEN_DIRECT_IO is
+ * allowed and it means (similar to regular files) a request from the
+ * server for uncached readdir open for an inode that already has files
+ * open in passthrough readdir mode.
+ */
+ if (isdir) {
+ if (ff->open_flags & FOPEN_CACHE_DIR)
+ ff->open_flags &= ~FOPEN_DIRECT_IO;
+ else if (!(ff->open_flags & FOPEN_PASSTHROUGH))
+ ff->open_flags |= FOPEN_DIRECT_IO;
+ }
ff->nodeid = nodeid;
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 2436219338b3..23d2510f8e93 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1264,9 +1264,13 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff,
#define FUSE_PASSTHROUGH_RW_OPS \
(FUSE_PASSTHROUGH_OP_READ | FUSE_PASSTHROUGH_OP_WRITE)
+/* Passthrough operations for directories */
+#define FUSE_PASSTHROUGH_DIR_OPS \
+ (FUSE_PASSTHROUGH_OP_READDIR)
+
/* File passthrough operations require a file opened with FOPEN_PASSTHROUGH */
#define FUSE_PASSTHROUGH_FILE_OPS \
- (FUSE_PASSTHROUGH_RW_OPS)
+ (FUSE_PASSTHROUGH_RW_OPS | FUSE_PASSTHROUGH_OP_READDIR)
/* Inode passthrough operations for backing file attached to inode */
#define FUSE_PASSTHROUGH_INODE_OPS (0)
@@ -1343,6 +1347,7 @@ ssize_t fuse_passthrough_splice_write(struct pipe_inode_info *pipe,
struct file *out, loff_t *ppos,
size_t len, unsigned int flags);
ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma);
+int fuse_passthrough_readdir(struct file *file, struct dir_context *ctx);
static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
{
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index e2e7fbaac9af..c83a070cd834 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -201,15 +201,20 @@ static int fuse_file_passthrough_open(struct inode *inode, struct file *file)
if (IS_ERR(fb))
return PTR_ERR(fb);
+ err = -EOPNOTSUPP;
/*
* Inode ops passthrough requires backing file setup at
* creation/lookup time.
*/
- err = -EOPNOTSUPP;
if (!fuse_inode_backing(fi) &&
(fb->ops_mask & FUSE_PASSTHROUGH_INODE_OPS))
goto fail;
+ /* Readdir passthrough requires opt-in on backing file setup */
+ if (S_ISDIR(inode->i_mode) &&
+ !(fb->ops_mask & FUSE_PASSTHROUGH_OP_READDIR))
+ goto fail;
+
/* First passthrough file open denies caching inode io mode */
err = fuse_file_uncached_io_open(inode, ff, fb);
if (!err)
@@ -253,6 +258,8 @@ int fuse_file_io_open(struct file *file, struct inode *inode)
/*
* First passthrough file open denies caching inode io mode.
* First caching file open enters caching inode io mode.
+ * A directory opened without FOPEN_CACHE_DIR is marked with
+ * FOPEN_DIRECT_IO and like regular file dio, does not affect io mode.
*
* Note that if user opens a file open with O_DIRECT, but server did
* not specify FOPEN_DIRECT_IO, a later fcntl() could remove O_DIRECT,
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index 72de97c03d0e..a1d87ed51a94 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -144,6 +144,30 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
return backing_file_mmap(backing_file, vma, &ctx);
}
+int fuse_passthrough_readdir(struct file *file, struct dir_context *ctx)
+{
+ int ret;
+ const struct cred *old_cred;
+ struct inode *inode = file_inode(file);
+ struct fuse_file *ff = file->private_data;
+ struct file *backing_file = fuse_file_passthrough(ff);
+ bool locked;
+
+ pr_debug("%s: backing_file=0x%p, pos=%lld\n", __func__,
+ backing_file, ctx->pos);
+
+ old_cred = override_creds(ff->cred);
+ locked = fuse_lock_inode(inode);
+ /* Respect seekdir() on fuse dir */
+ vfs_llseek(backing_file, ctx->pos, SEEK_SET);
+ ret = iterate_dir(backing_file, ctx);
+ fuse_invalidate_atime(inode);
+ fuse_unlock_inode(inode, locked);
+ revert_creds(old_cred);
+
+ return ret;
+}
+
/*
* Setup passthrough to a backing file.
*
diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
index c88194e52d18..49226f022339 100644
--- a/fs/fuse/readdir.c
+++ b/fs/fuse/readdir.c
@@ -593,6 +593,9 @@ int fuse_readdir(struct file *file, struct dir_context *ctx)
if (fuse_is_bad(inode))
return -EIO;
+ if (fuse_file_passthrough(ff))
+ return fuse_passthrough_readdir(file, ctx);
+
err = UNCACHED;
if (ff->open_flags & FOPEN_CACHE_DIR)
err = fuse_readdir_cached(file, ctx);
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 0f1e1c1ec367..df366e390f0c 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -1142,6 +1142,7 @@ struct fuse_backing_map {
/* op bits for fuse_backing_map ops_mask */
#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
+#define FUSE_PASSTHROUGH_OP_READDIR FUSE_PASSTHROUGH_OP(FUSE_READDIR)
/* Device ioctls: */
#define FUSE_DEV_IOC_MAGIC 229
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 05/21] fuse: prepare for long lived reference on backing file
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (3 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 04/21] fuse: implement passthrough for readdir Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 06/21] fuse: implement passthrough for getattr/statx Joanne Koong
` (15 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
Currently backing file is attached to fuse inode on the first
passthrough open of the inode and detached on last passthrough close.
In preparation for attaching a backing file to inode with no open file,
allow attaching a single long lived reference on fuse inode backing file
that will be used for passthrough of inode operations and detached on
fuse inode evict.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/file.c | 3 ++-
fs/fuse/fuse_i.h | 5 +++--
fs/fuse/inode.c | 5 +++++
fs/fuse/iomode.c | 16 ++++++++++++----
4 files changed, 22 insertions(+), 7 deletions(-)
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 828e3fc0ec31..1173811b2ea7 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1442,6 +1442,7 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
bool *exclusive)
{
struct inode *inode = file_inode(iocb->ki_filp);
+ struct fuse_file *ff = iocb->ki_filp->private_data;
*exclusive = fuse_dio_wr_exclusive_lock(iocb, from);
if (*exclusive) {
@@ -1456,7 +1457,7 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
* have raced, so check it again.
*/
if (fuse_io_past_eof(iocb, from) ||
- fuse_inode_uncached_io_start(inode, NULL) != 0) {
+ fuse_inode_uncached_io_start(inode, ff, NULL) != 0) {
inode_unlock_shared(inode);
inode_lock(inode);
*exclusive = true;
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 23d2510f8e93..502b3b3f7e89 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -258,7 +258,7 @@ enum {
* or the fuse server has an exclusive "lease" on distributed fs
*/
FUSE_I_EXCLUSIVE,
- /* Has backing file for inode ops passthrough */
+ /* Has long lived backing file for inode ops passthrough */
FUSE_I_PASSTHROUGH,
};
@@ -1246,7 +1246,8 @@ int fuse_fileattr_set(struct mnt_idmap *idmap,
/* iomode.c */
int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff);
-int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb);
+int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_file *ff,
+ struct fuse_backing *fb);
void fuse_inode_uncached_io_end(struct inode *inode);
int fuse_file_io_open(struct file *file, struct inode *inode);
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index cef8b853b3d8..9e9c20e846e7 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -183,6 +183,11 @@ static void fuse_evict_inode(struct inode *inode)
WARN_ON(!list_empty(&fi->write_files));
WARN_ON(!list_empty(&fi->queued_writes));
}
+ /* fuse inode may have a long lived reference to backing file */
+ if (fuse_inode_backing(fi)) {
+ WARN_ON(!test_bit(FUSE_I_PASSTHROUGH, &fi->state));
+ fuse_inode_uncached_io_end(inode);
+ }
WARN_ON(fi->iocachectr != 0);
}
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index c83a070cd834..6e2ddfe384b3 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -87,7 +87,8 @@ static void fuse_file_cached_io_release(struct fuse_file *ff,
}
/* Start strictly uncached io mode where cache access is not allowed */
-int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb)
+int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_file *ff,
+ struct fuse_backing *fb)
{
struct fuse_inode *fi = get_fuse_inode(inode);
struct fuse_conn *fc = get_fuse_conn(inode);
@@ -116,12 +117,19 @@ int fuse_inode_uncached_io_start(struct inode *inode, struct fuse_backing *fb)
err = -ETXTBSY;
goto unlock;
}
- fi->iocachectr--;
+ /* every open file holds a single refcount of backing file... */
+ if (ff)
+ fi->iocachectr--;
- /* fuse inode holds a single refcount of backing file */
if (fb && !oldfb) {
oldfb = fuse_inode_backing_set(fi, fb);
WARN_ON_ONCE(oldfb != NULL);
+ /* ...and an optional extra refcount for inode ops */
+ if ((fb->ops_mask & FUSE_PASSTHROUGH_INODE_OPS)) {
+ WARN_ON_ONCE(test_bit(FUSE_I_PASSTHROUGH, &fi->state));
+ set_bit(FUSE_I_PASSTHROUGH, &fi->state);
+ fi->iocachectr--;
+ }
} else {
fuse_backing_put(fb);
}
@@ -137,7 +145,7 @@ static int fuse_file_uncached_io_open(struct inode *inode,
{
int err;
- err = fuse_inode_uncached_io_start(inode, fb);
+ err = fuse_inode_uncached_io_start(inode, ff, fb);
if (err)
return err;
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 06/21] fuse: implement passthrough for getattr/statx
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (4 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 05/21] fuse: prepare for long lived reference on backing file Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 07/21] fuse: prepare to setup backing inode passthrough on lookup Joanne Koong
` (14 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
Call vfs_getattr() on backing inode to respond to a user statx(2)
request and update the fuse inode attributes with the response.
For now, we assume that calling vfs_getattr() on backing inode is cheap,
so we never use cached attributed and always update fuse inode
attributes from backing attributes in fuse_permission() and
fuse_update_attributes().
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/dir.c | 32 ++++++++++++++++++++++++++++++++
fs/fuse/fuse_i.h | 8 +++++++-
fs/fuse/passthrough.c | 36 ++++++++++++++++++++++++++++++++++++
include/uapi/linux/fuse.h | 1 +
4 files changed, 76 insertions(+), 1 deletion(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 05f69b8fa4c4..9a8e525f4d2b 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -1414,6 +1414,28 @@ static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr)
attr->blksize = sx->blksize;
}
+void fuse_kstat_to_attr(struct fuse_conn *fc, const struct kstat *stat,
+ struct fuse_attr *attr)
+{
+ memset(attr, 0, sizeof(*attr));
+
+ attr->ino = stat->ino;
+ attr->size = stat->size;
+ attr->blocks = stat->blocks;
+ attr->atime = stat->atime.tv_sec;
+ attr->mtime = stat->mtime.tv_sec;
+ attr->ctime = stat->ctime.tv_sec;
+ attr->atimensec = stat->atime.tv_nsec;
+ attr->mtimensec = stat->mtime.tv_nsec;
+ attr->ctimensec = stat->ctime.tv_nsec;
+ attr->mode = stat->mode;
+ attr->nlink = stat->nlink;
+ attr->uid = from_kuid(fc->user_ns, stat->uid);
+ attr->gid = from_kgid(fc->user_ns, stat->gid);
+ attr->rdev = new_encode_dev(MKDEV(MAJOR(stat->rdev), MINOR(stat->rdev)));
+ attr->blksize = stat->blksize;
+}
+
static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode,
struct file *file, struct kstat *stat)
{
@@ -1539,6 +1561,12 @@ static int fuse_update_get_attr(struct mnt_idmap *idmap, struct inode *inode,
if (fc->no_statx)
request_mask &= STATX_BASIC_STATS;
+ if (fuse_passthrough_op(inode, FUSE_GETATTR)) {
+ forget_all_cached_acls(inode);
+ return fuse_passthrough_getattr(inode, stat, request_mask,
+ flags);
+ }
+
if (!request_mask)
sync = false;
else if (flags & AT_STATX_FORCE_SYNC)
@@ -1737,6 +1765,10 @@ static int fuse_perm_getattr(struct inode *inode, int mask)
return -ECHILD;
forget_all_cached_acls(inode);
+ if (fuse_passthrough_op(inode, FUSE_GETATTR))
+ return fuse_passthrough_getattr(inode, NULL,
+ STATX_BASIC_STATS, 0);
+
return fuse_do_getattr(&nop_mnt_idmap, inode, NULL, NULL);
}
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 502b3b3f7e89..389e28497dc7 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -991,6 +991,8 @@ void fuse_init_symlink(struct inode *inode);
/*
* Change attributes of an inode
*/
+void fuse_kstat_to_attr(struct fuse_conn *fc, const struct kstat *stat,
+ struct fuse_attr *attr);
void fuse_change_attributes(struct inode *inode, struct fuse_attr *attr,
struct fuse_statx *sx,
u64 attr_valid, u64 attr_version);
@@ -1274,7 +1276,8 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff,
(FUSE_PASSTHROUGH_RW_OPS | FUSE_PASSTHROUGH_OP_READDIR)
/* Inode passthrough operations for backing file attached to inode */
-#define FUSE_PASSTHROUGH_INODE_OPS (0)
+#define FUSE_PASSTHROUGH_INODE_OPS \
+ (FUSE_PASSTHROUGH_OP_GETATTR)
#define FUSE_BACKING_MAP_OP(map, op) \
((map)->ops_mask & FUSE_PASSTHROUGH_OP(op))
@@ -1358,6 +1361,9 @@ static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
return fb && fb->ops_mask & FUSE_PASSTHROUGH_OP(op);
}
+int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
+ u32 request_mask, unsigned int flags);
+
#ifdef CONFIG_SYSCTL
extern int fuse_sysctl_register(void);
extern void fuse_sysctl_unregister(void);
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index a1d87ed51a94..7de038960b2f 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -219,3 +219,39 @@ void fuse_passthrough_release(struct fuse_file *ff, struct fuse_backing *fb)
put_cred(ff->cred);
ff->cred = NULL;
}
+
+/*
+ * Inode passthrough operations for backing file attached on lookup.
+ */
+int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
+ u32 request_mask, unsigned int flags)
+{
+ struct fuse_conn *fc = get_fuse_conn(inode);
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_backing *fb = fuse_inode_backing(fi);
+ u64 attr_version = fuse_get_attr_version(fc);
+ const struct path *fb_path = &fb->file->f_path;
+ const struct cred *old_cred;
+ struct kstat backing_stat;
+ struct fuse_attr attr;
+ int err;
+
+ if (!stat)
+ stat = &backing_stat;
+
+ old_cred = override_creds(fb->cred);
+ err = vfs_getattr(fb_path, stat, request_mask, flags);
+ revert_creds(old_cred);
+ if (err)
+ return err;
+
+ /* Always override st_dev and st_ino with FUSE dev and ino */
+ stat->dev = inode->i_sb->s_dev;
+ stat->ino = inode->i_ino;
+
+ /* Fill fuse inode attrs from backing inode stat */
+ fuse_kstat_to_attr(fc, stat, &attr);
+ fuse_change_attributes(inode, &attr, NULL, 0, attr_version);
+
+ return 0;
+}
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index df366e390f0c..6404ed95c758 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -1143,6 +1143,7 @@ struct fuse_backing_map {
#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
#define FUSE_PASSTHROUGH_OP_READDIR FUSE_PASSTHROUGH_OP(FUSE_READDIR)
+#define FUSE_PASSTHROUGH_OP_GETATTR FUSE_PASSTHROUGH_OP(FUSE_GETATTR)
/* Device ioctls: */
#define FUSE_DEV_IOC_MAGIC 229
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 07/21] fuse: prepare to setup backing inode passthrough on lookup
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (5 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 06/21] fuse: implement passthrough for getattr/statx Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 08/21] fuse: handle zero ops_mask in FUSE_DEV_IOC_BACKING_OPEN Joanne Koong
` (13 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
From: Amir Goldstein <amir73il@gmail.com>
Add a helper for requesting to associate a backing inode for inode
passthrough operations.
This helper is expected to be used to setup backing inode on lookup
response.
The minimal requirement for the backing inode is to support the
GETATTR passthrough op.
Unlike setting of passthrough from open, this mapping is created for
the entire lifetime of the inode and cannot be changed later.
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
fs/fuse/backing.c | 32 ++++++++++++++++++++------------
fs/fuse/fuse_i.h | 6 ++++--
fs/fuse/iomode.c | 43 +++++++++++++++++++++++++++++++++++++++++++
fs/fuse/passthrough.c | 12 +++++-------
4 files changed, 72 insertions(+), 21 deletions(-)
diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index 52fccb9ff283..2234ab47406a 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -17,6 +17,26 @@ struct fuse_backing *fuse_backing_get(struct fuse_backing *fb)
return NULL;
}
+/*
+ * Get fuse backing object by backing id.
+ *
+ * Returns an fb object with elevated refcount to be stored in fuse inode.
+ */
+struct fuse_backing *fuse_backing_id_get(struct fuse_conn *fc, int backing_id)
+{
+ struct fuse_backing *fb;
+
+ if (backing_id <= 0)
+ return ERR_PTR(-EINVAL);
+
+ rcu_read_lock();
+ fb = idr_find(&fc->backing_files_map, backing_id);
+ fb = fuse_backing_get(fb);
+ rcu_read_unlock();
+
+ return fb;
+}
+
static void fuse_backing_free(struct fuse_backing *fb)
{
pr_debug("%s: fb=0x%p\n", __func__, fb);
@@ -178,15 +198,3 @@ int fuse_backing_close(struct fuse_conn *fc, int backing_id)
return err;
}
-
-struct fuse_backing *fuse_backing_lookup(struct fuse_conn *fc, int backing_id)
-{
- struct fuse_backing *fb;
-
- rcu_read_lock();
- fb = idr_find(&fc->backing_files_map, backing_id);
- fb = fuse_backing_get(fb);
- rcu_read_unlock();
-
- return fb;
-}
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 389e28497dc7..9e5142a94a09 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1289,7 +1289,7 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff,
#ifdef CONFIG_FUSE_PASSTHROUGH
struct fuse_backing *fuse_backing_get(struct fuse_backing *fb);
void fuse_backing_put(struct fuse_backing *fb);
-struct fuse_backing *fuse_backing_lookup(struct fuse_conn *fc, int backing_id);
+struct fuse_backing *fuse_backing_id_get(struct fuse_conn *fc, int backing_id);
#else
static inline struct fuse_backing *fuse_backing_get(struct fuse_backing *fb)
@@ -1300,7 +1300,7 @@ static inline struct fuse_backing *fuse_backing_get(struct fuse_backing *fb)
static inline void fuse_backing_put(struct fuse_backing *fb)
{
}
-static inline struct fuse_backing *fuse_backing_lookup(struct fuse_conn *fc,
+static inline struct fuse_backing *fuse_backing_id_get(struct fuse_conn *fc,
int backing_id)
{
return NULL;
@@ -1353,6 +1353,8 @@ ssize_t fuse_passthrough_splice_write(struct pipe_inode_info *pipe,
ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma);
int fuse_passthrough_readdir(struct file *file, struct dir_context *ctx);
+int fuse_inode_set_passthrough(struct inode *inode, int backing_id);
+
static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
{
struct fuse_inode *fi = get_fuse_inode(inode);
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index 6e2ddfe384b3..3483aaec698e 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -181,6 +181,49 @@ static void fuse_file_uncached_io_release(struct fuse_file *ff,
fuse_inode_uncached_io_end(inode);
}
+/* Setup passthrough for inode operations without an open file */
+int fuse_inode_set_passthrough(struct inode *inode, int backing_id)
+{
+ struct fuse_conn *fc = get_fuse_conn(inode);
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_backing *current_fb, *fb;
+ int err;
+
+ if (!IS_ENABLED(CONFIG_FUSE_PASSTHROUGH) || !fc->passthrough_ino)
+ return 0;
+
+ fb = fuse_backing_id_get(fc, backing_id);
+ if (IS_ERR_OR_NULL(fb)) {
+ err = fb ? PTR_ERR(fb) : -ENOENT;
+ fb = NULL;
+ goto fail;
+ }
+
+ current_fb = fuse_inode_backing(fi);
+ if (current_fb) {
+ /* Different backing file on same inode is not allowed */
+ err = current_fb == fb ? 0 : -EBUSY;
+ fuse_backing_put(fb);
+ return err;
+ }
+
+ /* Backing inode requires at least GETATTR op passthrough */
+ err = -EOPNOTSUPP;
+ if (!(fb->ops_mask & FUSE_PASSTHROUGH_OP_GETATTR))
+ goto fail;
+
+ err = fuse_inode_uncached_io_start(inode, NULL, fb);
+ if (err)
+ goto fail;
+
+ return 0;
+fail:
+ fuse_backing_put(fb);
+ pr_debug("failed to setup backing inode (ino=%lu, backing_id=%d, err=%i).\n",
+ inode->i_ino, backing_id, err);
+ return err;
+}
+
/*
* Open flags that are allowed in combination with FOPEN_PASSTHROUGH.
* A combination of FOPEN_PASSTHROUGH and FOPEN_DIRECT_IO means that read/write
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index 7de038960b2f..f2025772c9c1 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -181,14 +181,12 @@ struct fuse_backing *fuse_passthrough_open(struct file *file, int backing_id)
struct file *backing_file;
int err;
- err = -EINVAL;
- if (backing_id <= 0)
- goto out;
-
- err = -ENOENT;
- fb = fuse_backing_lookup(fc, backing_id);
- if (!fb)
+ fb = fuse_backing_id_get(fc, backing_id);
+ if (IS_ERR_OR_NULL(fb)) {
+ err = fb ? PTR_ERR(fb) : -ENOENT;
+ fb = NULL;
goto out;
+ }
/* Allocate backing file per fuse file to store fuse path */
backing_file = backing_file_open(&file->f_path, file->f_flags,
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 08/21] fuse: handle zero ops_mask in FUSE_DEV_IOC_BACKING_OPEN
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (6 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 07/21] fuse: prepare to setup backing inode passthrough on lookup Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap Joanne Koong
` (12 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Servers that pre-date the ops_mask field will pass a zero ops_mask in
FUSE_DEV_IOC_BACKING_OPEN. Default this to FUSE_PASSTHROUGH_RW_OPS to
maintain backwards compatibility.
For FUSE_PASSTHROUGH_INO servers, an ops_mask must be set.
FUSE_PASSTHROUGH_INO is a new feature with no backwards compatibliity
requirements.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/backing.c | 20 +++++++++++++++++---
1 file changed, 17 insertions(+), 3 deletions(-)
diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index 2234ab47406a..b499860e1185 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -119,9 +119,23 @@ int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map)
if (map->flags || map->ops_mask & ~FUSE_BACKING_MAP_VALID_OPS)
goto out;
- /* For now passthrough inode operations requires FUSE_PASSTHROUGH_INO */
- if (!fc->passthrough_ino && map->ops_mask & FUSE_PASSTHROUGH_INODE_OPS)
- goto out;
+ if (!fc->passthrough_ino) {
+ /*
+ * For now passthrough inode operations requires
+ * FUSE_PASSTHROUGH_INO
+ */
+ if (map->ops_mask & FUSE_PASSTHROUGH_INODE_OPS)
+ goto out;
+ /*
+ * To maintain backwards compatibility with servers that
+ * pre-date the ops_mask field, a zero ops_mask defaults
+ * to passing through both reads and writes
+ */
+ if (!map->ops_mask)
+ map->ops_mask |= FUSE_PASSTHROUGH_RW_OPS;
+ } else if (!map->ops_mask) {
+ return -EINVAL;
+ }
file = fget_raw(map->fd);
res = -EBADF;
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (7 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 08/21] fuse: handle zero ops_mask in FUSE_DEV_IOC_BACKING_OPEN Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 10/21] fuse: prepare to cache statx attributes from entry replies Joanne Koong
` (11 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Servers can now pass through only reads or writes instead of being
required to pass through both. When the read or write operation is not
passed through, fall back to direct io for handling it. This avoids
cache coherency issues from mixing the backing file's page cache with
the fuse inode's page cache.
Reject mmap when passthrough is partial or not set for read/writes since
mmap requires both read and write access to the backing page cache.
The FOPEN_DIRECT_IO check in the read/write fallback is not strictly
needed as the caller already handles it, but including it here makes
the fallback logic self-contained.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/file.c | 7 ++++---
fs/fuse/fuse_i.h | 3 +++
fs/fuse/passthrough.c | 18 ++++++++++++++++++
3 files changed, 25 insertions(+), 3 deletions(-)
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 1173811b2ea7..5b70d8272700 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1781,7 +1781,7 @@ static ssize_t __fuse_direct_read(struct fuse_io_priv *io,
static ssize_t fuse_direct_IO(struct kiocb *iocb, struct iov_iter *iter);
-static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
+ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
ssize_t res;
@@ -1796,7 +1796,7 @@ static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
return res;
}
-static ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
+ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct inode *inode = file_inode(iocb->ki_filp);
ssize_t res;
@@ -1884,7 +1884,8 @@ static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out,
struct fuse_file *ff = out->private_data;
/* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
- if (fuse_file_passthrough(ff) && !(ff->open_flags & FOPEN_DIRECT_IO))
+ if (fuse_passthrough_op(file_inode(out), FUSE_WRITE) &&
+ !(ff->open_flags & FOPEN_DIRECT_IO))
return fuse_passthrough_splice_write(pipe, out, ppos, len, flags);
else
return iter_file_splice_write(pipe, out, ppos, len, flags);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 9e5142a94a09..a1034533ce60 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1261,6 +1261,9 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
void fuse_file_release(struct inode *inode, struct fuse_file *ff,
unsigned int open_flags, fl_owner_t id, bool isdir);
+ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to);
+ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from);
+
/* passthrough.c */
/* READ/WRITE are implied by FOPEN_PASSTHROUGH, but defined for completeness */
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index f2025772c9c1..d62a1c751157 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -44,6 +44,10 @@ ssize_t fuse_passthrough_read_iter(struct kiocb *iocb, struct iov_iter *iter)
if (!count)
return 0;
+ if ((ff->open_flags & FOPEN_DIRECT_IO) ||
+ !fuse_passthrough_op(file_inode(file), FUSE_READ))
+ return fuse_direct_read_iter(iocb, iter);
+
ret = backing_file_read_iter(backing_file, iter, iocb, iocb->ki_flags,
&ctx);
@@ -70,6 +74,10 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
if (!count)
return 0;
+ if ((ff->open_flags & FOPEN_DIRECT_IO) ||
+ !fuse_passthrough_op(inode, FUSE_WRITE))
+ return fuse_direct_write_iter(iocb, iter);
+
inode_lock(inode);
ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
&ctx);
@@ -91,6 +99,9 @@ ssize_t fuse_passthrough_splice_read(struct file *in, loff_t *ppos,
struct kiocb iocb;
ssize_t ret;
+ if (!fuse_passthrough_op(file_inode(in), FUSE_READ))
+ return copy_splice_read(in, ppos, pipe, len, flags);
+
pr_debug("%s: backing_file=0x%p, pos=%lld, len=%zu, flags=0x%x\n", __func__,
backing_file, *ppos, len, flags);
@@ -133,6 +144,9 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
{
struct fuse_file *ff = file->private_data;
struct file *backing_file = fuse_file_passthrough(ff);
+ struct inode *inode = file_inode(file);
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_backing *fb = fuse_inode_backing(fi);
struct backing_file_ctx ctx = {
.cred = ff->cred,
.accessed = fuse_file_accessed,
@@ -141,6 +155,10 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
pr_debug("%s: backing_file=0x%p, start=%lu, end=%lu\n", __func__,
backing_file, vma->vm_start, vma->vm_end);
+ /* mmap requires both read and write passthrough */
+ if ((fb->ops_mask & FUSE_PASSTHROUGH_RW_OPS) != FUSE_PASSTHROUGH_RW_OPS)
+ return -ENODEV;
+
return backing_file_mmap(backing_file, vma, &ctx);
}
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 10/21] fuse: prepare to cache statx attributes from entry replies
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (8 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 11/21] fuse: clean up fuse_dentry_revalidate() Joanne Koong
` (10 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Add an arg for a struct fuse_statx pointer to fuse_iget() and pass it
through to fuse_change_attributes_i() so the statx attributes get
persisted in cache.
All existing callers pass in NULL for the arg. This change is in
preparation for a new struct fuse_entry2_out outarg which will contain
filled out statx information from the server returned on
LOOKUP/MKDIR/MKNOD/etc requests.
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 6 +++---
fs/fuse/fuse_i.h | 7 +++----
fs/fuse/inode.c | 12 ++++++------
fs/fuse/readdir.c | 2 +-
4 files changed, 13 insertions(+), 14 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 9a8e525f4d2b..7d3c9878b833 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -590,7 +590,7 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
}
*inode = fuse_iget(sb, outarg->nodeid, outarg->generation,
- &outarg->attr, ATTR_TIMEOUT(outarg),
+ &outarg->attr, NULL, ATTR_TIMEOUT(outarg),
attr_version, evict_ctr);
err = -ENOMEM;
if (!*inode) {
@@ -890,7 +890,7 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
ff->nodeid = outentry.nodeid;
ff->open_flags = outopenp->open_flags;
inode = fuse_iget(dir->i_sb, outentry.nodeid, outentry.generation,
- &outentry.attr, ATTR_TIMEOUT(&outentry), 0, 0);
+ &outentry.attr, NULL, ATTR_TIMEOUT(&outentry), 0, 0);
if (!inode) {
flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
fuse_sync_release(NULL, ff, flags, false);
@@ -1017,7 +1017,7 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
goto out_put_forget_req;
inode = fuse_iget(dir->i_sb, outarg.nodeid, outarg.generation,
- &outarg.attr, ATTR_TIMEOUT(&outarg), 0, 0);
+ &outarg.attr, NULL, ATTR_TIMEOUT(&outarg), 0, 0);
if (!inode) {
fuse_chan_queue_forget(fm->fc->chan, forget, outarg.nodeid, 1);
return ERR_PTR(-ENOMEM);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index a1034533ce60..1f2c849ea4e3 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -912,10 +912,9 @@ extern const struct dentry_operations fuse_dentry_operations;
/*
* Get a filled in inode
*/
-struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
- int generation, struct fuse_attr *attr,
- u64 attr_valid, u64 attr_version,
- u64 evict_ctr);
+struct inode *fuse_iget(struct super_block *sb, u64 nodeid, int generation,
+ struct fuse_attr *attr, struct fuse_statx *sx,
+ u64 attr_valid, u64 attr_version, u64 evict_ctr);
int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name,
struct fuse_entry_out *outarg, struct inode **inode);
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index 9e9c20e846e7..8757c01e3bb2 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -457,8 +457,8 @@ static int fuse_inode_set(struct inode *inode, void *_nodeidp)
struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
int generation, struct fuse_attr *attr,
- u64 attr_valid, u64 attr_version,
- u64 evict_ctr)
+ struct fuse_statx *sx, u64 attr_valid,
+ u64 attr_version, u64 evict_ctr)
{
struct inode *inode;
struct fuse_inode *fi;
@@ -520,7 +520,7 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
fi->nlookup++;
spin_unlock(&fi->lock);
done:
- fuse_change_attributes_i(inode, attr, NULL, attr_valid, attr_version,
+ fuse_change_attributes_i(inode, attr, sx, attr_valid, attr_version,
evict_ctr);
if (is_new_inode)
unlock_new_inode(inode);
@@ -1049,7 +1049,7 @@ static struct inode *fuse_get_root_inode(struct super_block *sb, unsigned int mo
attr.mode = mode;
attr.ino = FUSE_ROOT_ID;
attr.nlink = 1;
- return fuse_iget(sb, FUSE_ROOT_ID, 0, &attr, 0, 0, 0);
+ return fuse_iget(sb, FUSE_ROOT_ID, 0, &attr, NULL, 0, 0, 0);
}
struct fuse_inode_handle {
@@ -1652,8 +1652,8 @@ static int fuse_fill_super_submount(struct super_block *sb,
return -ENOMEM;
fuse_fill_attr_from_inode(&root_attr, parent_fi);
- root = fuse_iget(sb, parent_fi->nodeid, 0, &root_attr, 0, 0,
- fuse_get_evict_ctr(fm->fc));
+ root = fuse_iget(sb, parent_fi->nodeid, 0, &root_attr, NULL, 0,
+ 0, fuse_get_evict_ctr(fm->fc));
/*
* This inode is just a duplicate, so it is not looked up and
* its nlookup should not be incremented. fuse_iget() does
diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
index 49226f022339..0dcb2d75d50f 100644
--- a/fs/fuse/readdir.c
+++ b/fs/fuse/readdir.c
@@ -234,7 +234,7 @@ static int fuse_direntplus_link(struct file *file,
*/
} else {
inode = fuse_iget(dir->i_sb, o->nodeid, o->generation,
- &o->attr, ATTR_TIMEOUT(o),
+ &o->attr, NULL, ATTR_TIMEOUT(o),
attr_version, evict_ctr);
if (!inode)
inode = ERR_PTR(-ENOMEM);
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 11/21] fuse: clean up fuse_dentry_revalidate()
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (9 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 10/21] fuse: prepare to cache statx attributes from entry replies Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies Joanne Koong
` (9 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Remove gotos with direct returns and flatten nested if/else structure.
No functional changes.
Suggested-by: Amir Goldstein <amir73il@gmail.com>
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 112 +++++++++++++++++++++++---------------------------
1 file changed, 52 insertions(+), 60 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 7d3c9878b833..a338f2a06b50 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -386,72 +386,31 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
struct dentry *entry, unsigned int flags)
{
struct inode *inode;
- struct fuse_mount *fm;
struct fuse_conn *fc;
struct fuse_inode *fi;
+ struct fuse_entry_out outarg;
+ struct fuse_forget_link *forget;
+ FUSE_ARGS(args);
+ u64 attr_version;
+ bool need_reval;
int ret;
fc = get_fuse_conn_super(dir->i_sb);
if (entry->d_time < atomic_read(&fc->epoch))
- goto invalid;
+ return 0;
inode = d_inode_rcu(entry);
if (inode && fuse_is_bad(inode))
- goto invalid;
- else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
- (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {
- struct fuse_entry_out outarg;
- FUSE_ARGS(args);
- struct fuse_forget_link *forget;
- u64 attr_version;
-
- /* For negative dentries, always do a fresh lookup */
- if (!inode)
- goto invalid;
-
- ret = -ECHILD;
- if (flags & LOOKUP_RCU)
- goto out;
-
- fm = get_fuse_mount(inode);
+ return 0;
- forget = fuse_alloc_forget();
- ret = -ENOMEM;
- if (!forget)
- goto out;
+ need_reval = time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
+ (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET));
- attr_version = fuse_get_attr_version(fm->fc);
-
- fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
- ret = fuse_simple_request(fm, &args);
- /* Zero nodeid is same as -ENOENT */
- if (!ret && !outarg.nodeid)
- ret = -ENOENT;
- if (!ret) {
- fi = get_fuse_inode(inode);
- if (outarg.nodeid != get_node_id(inode) ||
- (bool) IS_AUTOMOUNT(inode) != (bool) (outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
- fuse_chan_queue_forget(fm->fc->chan, forget,
- outarg.nodeid, 1);
- goto invalid;
- }
- spin_lock(&fi->lock);
- fi->nlookup++;
- spin_unlock(&fi->lock);
- }
- kfree(forget);
- if (ret == -ENOMEM || ret == -EINTR)
- goto out;
- if (ret || fuse_invalid_attr(&outarg.attr) ||
- fuse_stale_inode(inode, outarg.generation, &outarg.attr))
- goto invalid;
+ /* For negative dentries that need revalidation, always do a fresh lookup */
+ if (!inode)
+ return need_reval ? 0 : 1;
- forget_all_cached_acls(inode);
- fuse_change_attributes(inode, &outarg.attr, NULL,
- ATTR_TIMEOUT(&outarg),
- attr_version);
- fuse_change_entry_timeout(entry, &outarg);
- } else if (inode) {
+ if (!need_reval) {
fi = get_fuse_inode(inode);
if (flags & LOOKUP_RCU) {
if (test_bit(FUSE_I_INIT_RDPLUS, &fi->state))
@@ -459,14 +418,47 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
} else if (test_and_clear_bit(FUSE_I_INIT_RDPLUS, &fi->state)) {
fuse_advise_use_readdirplus(dir);
}
+
+ return 1;
}
- ret = 1;
-out:
- return ret;
-invalid:
- ret = 0;
- goto out;
+ if (flags & LOOKUP_RCU)
+ return -ECHILD;
+
+ forget = fuse_alloc_forget();
+ if (!forget)
+ return -ENOMEM;
+
+ attr_version = fuse_get_attr_version(fc);
+
+ fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
+ ret = fuse_simple_request(get_fuse_mount(inode), &args);
+ if (ret || !outarg.nodeid) {
+ kfree(forget);
+ return (ret == -ENOMEM || ret == -EINTR) ? ret : 0;
+ }
+
+ if (outarg.nodeid != get_node_id(inode) ||
+ !!IS_AUTOMOUNT(inode) != !!(outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
+ fuse_chan_queue_forget(fc->chan, forget, outarg.nodeid, 1);
+ return 0;
+ }
+
+ kfree(forget);
+ fi = get_fuse_inode(inode);
+ spin_lock(&fi->lock);
+ fi->nlookup++;
+ spin_unlock(&fi->lock);
+
+ if (fuse_invalid_attr(&outarg.attr) ||
+ fuse_stale_inode(inode, outarg.generation, &outarg.attr))
+ return 0;
+
+ forget_all_cached_acls(inode);
+ fuse_change_attributes(inode, &outarg.attr, NULL, ATTR_TIMEOUT(&outarg),
+ attr_version);
+ fuse_change_entry_timeout(entry, &outarg);
+ return 1;
}
static int fuse_dentry_init(struct dentry *dentry)
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (10 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 11/21] fuse: clean up fuse_dentry_revalidate() Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 13/21] fuse: add passthrough lookup Joanne Koong
` (8 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Add struct fuse_entry2_out, which is a new extended entry reply struct
that carries a backing_id and statx attributes. This will be necessary
for setting fuse passthrough on inodes.
Add helpers that subsequent commits will use to process fuse_entry2_out
for passthrough support for lookup, revalidate, and create.
fuse_statx_to_attr() is also moved to earlier in the file to avoid
forward declaring.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 116 +++++++++++++++++++++++++++++---------
fs/fuse/fuse_i.h | 5 ++
include/uapi/linux/fuse.h | 14 +++++
3 files changed, 107 insertions(+), 28 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index a338f2a06b50..b7a9d2b0476a 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -355,11 +355,18 @@ static void fuse_invalidate_entry(struct dentry *entry)
fuse_invalidate_entry_cache(entry);
}
-static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
- const struct qstr *name,
- struct fuse_entry_out *outarg)
+static void fuse_lookup_init(struct fuse_conn *fc, struct fuse_args *args,
+ u64 nodeid, const struct qstr *name,
+ struct fuse_entry_out *outarg,
+ struct fuse_entry2_out *outarg2)
{
- memset(outarg, 0, sizeof(struct fuse_entry_out));
+ bool use_entry2 = fuse_use_entry2(fc);
+
+ if (use_entry2)
+ memset(outarg2, 0, sizeof(struct fuse_entry2_out));
+ else
+ memset(outarg, 0, sizeof(struct fuse_entry_out));
+
args->opcode = FUSE_LOOKUP;
args->nodeid = nodeid;
args->in_numargs = 3;
@@ -369,8 +376,79 @@ static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
args->in_args[2].size = 1;
args->in_args[2].value = "";
args->out_numargs = 1;
- args->out_args[0].size = sizeof(struct fuse_entry_out);
- args->out_args[0].value = outarg;
+
+ if (use_entry2) {
+ args->out_args[0].size = sizeof(struct fuse_entry2_out);
+ args->out_args[0].value = outarg2;
+ } else {
+ args->out_args[0].size = sizeof(struct fuse_entry_out);
+ args->out_args[0].value = outarg;
+ }
+}
+
+static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr)
+{
+ memset(attr, 0, sizeof(*attr));
+ attr->ino = sx->ino;
+ attr->size = sx->size;
+ attr->blocks = sx->blocks;
+ attr->atime = sx->atime.tv_sec;
+ attr->mtime = sx->mtime.tv_sec;
+ attr->ctime = sx->ctime.tv_sec;
+ attr->atimensec = sx->atime.tv_nsec;
+ attr->mtimensec = sx->mtime.tv_nsec;
+ attr->ctimensec = sx->ctime.tv_nsec;
+ attr->mode = sx->mode;
+ attr->nlink = sx->nlink;
+ attr->uid = sx->uid;
+ attr->gid = sx->gid;
+ attr->rdev = new_encode_dev(MKDEV(sx->rdev_major, sx->rdev_minor));
+ attr->blksize = sx->blksize;
+}
+
+static void fuse_entry2_to_entry(struct fuse_entry2_out *outarg2,
+ struct fuse_entry_out *outarg)
+{
+ memset(outarg, 0, sizeof(struct fuse_entry_out));
+ outarg->nodeid = outarg2->nodeid;
+ outarg->generation = outarg2->generation;
+ outarg->entry_valid = outarg2->entry_valid;
+ outarg->attr_valid = outarg2->attr_valid;
+ outarg->entry_valid_nsec = outarg2->entry_valid_nsec;
+ outarg->attr_valid_nsec = outarg2->attr_valid_nsec;
+ fuse_statx_to_attr(&outarg2->statx, &outarg->attr);
+ outarg->attr.flags = outarg2->flags;
+}
+
+static __maybe_unused int fuse_process_entry2(struct fuse_conn *fc,
+ struct fuse_entry2_out *outarg2,
+ struct fuse_entry_out *outarg,
+ struct fuse_statx **sxp)
+{
+ if (!fuse_use_entry2(fc))
+ return 0;
+
+ /*
+ * Convert entry2 format to entry format before error checking since the
+ * caller needs outarg->nodeid field even on error
+ */
+ fuse_entry2_to_entry(outarg2, outarg);
+
+ if (outarg2->reserved)
+ return -EINVAL;
+
+ /* error */
+ if (outarg2->backing_id < 0)
+ return outarg2->backing_id;
+
+ /*
+ * If passthrough is enabled (backing_id > 0), statx attributes are not
+ * cached because passthrough getattr fetches them directly from the
+ * backing inode
+ */
+ if (!outarg2->backing_id)
+ *sxp = &outarg2->statx;
+ return outarg2->backing_id;
}
/*
@@ -389,6 +467,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
struct fuse_conn *fc;
struct fuse_inode *fi;
struct fuse_entry_out outarg;
+ struct fuse_entry2_out outarg2;
struct fuse_forget_link *forget;
FUSE_ARGS(args);
u64 attr_version;
@@ -431,7 +510,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
attr_version = fuse_get_attr_version(fc);
- fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
+ fuse_lookup_init(fc, &args, get_node_id(dir), name, &outarg, &outarg2);
ret = fuse_simple_request(get_fuse_mount(inode), &args);
if (ret || !outarg.nodeid) {
kfree(forget);
@@ -551,6 +630,7 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
FUSE_ARGS(args);
struct fuse_forget_link *forget;
u64 attr_version, evict_ctr;
+ struct fuse_entry2_out outarg2;
int err;
*inode = NULL;
@@ -567,7 +647,7 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
attr_version = fuse_get_attr_version(fm->fc);
evict_ctr = fuse_get_evict_ctr(fm->fc);
- fuse_lookup_init(&args, nodeid, name, outarg);
+ fuse_lookup_init(fm->fc, &args, nodeid, name, outarg, &outarg2);
err = fuse_simple_request(fm, &args);
/* Zero nodeid is same as -ENOENT, but with valid timeout */
if (err || !outarg->nodeid)
@@ -1386,26 +1466,6 @@ static void fuse_fillattr(struct mnt_idmap *idmap, struct inode *inode,
stat->blksize = 1 << blkbits;
}
-static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr)
-{
- memset(attr, 0, sizeof(*attr));
- attr->ino = sx->ino;
- attr->size = sx->size;
- attr->blocks = sx->blocks;
- attr->atime = sx->atime.tv_sec;
- attr->mtime = sx->mtime.tv_sec;
- attr->ctime = sx->ctime.tv_sec;
- attr->atimensec = sx->atime.tv_nsec;
- attr->mtimensec = sx->mtime.tv_nsec;
- attr->ctimensec = sx->ctime.tv_nsec;
- attr->mode = sx->mode;
- attr->nlink = sx->nlink;
- attr->uid = sx->uid;
- attr->gid = sx->gid;
- attr->rdev = new_encode_dev(MKDEV(sx->rdev_major, sx->rdev_minor));
- attr->blksize = sx->blksize;
-}
-
void fuse_kstat_to_attr(struct fuse_conn *fc, const struct kstat *stat,
struct fuse_attr *attr)
{
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 1f2c849ea4e3..89c9333e9702 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1368,6 +1368,11 @@ static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
u32 request_mask, unsigned int flags);
+static inline bool fuse_use_entry2(struct fuse_conn *fc)
+{
+ return fc->passthrough_ino;
+}
+
#ifdef CONFIG_SYSCTL
extern int fuse_sysctl_register(void);
extern void fuse_sysctl_unregister(void);
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 6404ed95c758..3963631558f9 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -244,6 +244,7 @@
* 7.46
* - add FUSE_PASSTHROUGH_INO
* - add ops_mask field to struct fuse_backing_map
+ * - add fuse_entry2_out
*/
#ifndef _LINUX_FUSE_H
@@ -705,6 +706,19 @@ struct fuse_entry_out {
struct fuse_attr attr;
};
+struct fuse_entry2_out {
+ uint64_t nodeid;
+ uint64_t generation;
+ uint64_t entry_valid;
+ uint64_t attr_valid;
+ uint32_t entry_valid_nsec;
+ uint32_t attr_valid_nsec;
+ int32_t backing_id;
+ uint32_t flags;
+ uint64_t reserved;
+ struct fuse_statx statx;
+};
+
struct fuse_forget_in {
uint64_t nlookup;
};
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 13/21] fuse: add passthrough lookup
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (11 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 14/21] fuse: add passthrough support for entry creation Joanne Koong
` (7 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Use the new/extended struct fuse_entry2_out for lookups. If a backing id
is set, associate the fuse inode with the backing inode that should be
used for passthrough operations.
If no backing id was set, cache the statx attributes from the reply.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 66 ++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 52 insertions(+), 14 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index b7a9d2b0476a..3a6adae530da 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -420,10 +420,10 @@ static void fuse_entry2_to_entry(struct fuse_entry2_out *outarg2,
outarg->attr.flags = outarg2->flags;
}
-static __maybe_unused int fuse_process_entry2(struct fuse_conn *fc,
- struct fuse_entry2_out *outarg2,
- struct fuse_entry_out *outarg,
- struct fuse_statx **sxp)
+static int fuse_process_entry2(struct fuse_conn *fc,
+ struct fuse_entry2_out *outarg2,
+ struct fuse_entry_out *outarg,
+ struct fuse_statx **sxp)
{
if (!fuse_use_entry2(fc))
return 0;
@@ -469,7 +469,9 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
struct fuse_entry_out outarg;
struct fuse_entry2_out outarg2;
struct fuse_forget_link *forget;
+ struct fuse_statx *sx = NULL;
FUSE_ARGS(args);
+ int backing_id = 0;
u64 attr_version;
bool need_reval;
int ret;
@@ -512,15 +514,27 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
fuse_lookup_init(fc, &args, get_node_id(dir), name, &outarg, &outarg2);
ret = fuse_simple_request(get_fuse_mount(inode), &args);
+ if (!ret)
+ backing_id = fuse_process_entry2(fc, &outarg2, &outarg, &sx);
if (ret || !outarg.nodeid) {
kfree(forget);
return (ret == -ENOMEM || ret == -EINTR) ? ret : 0;
}
+ if (backing_id < 0) {
+ ret = backing_id;
+ goto forget;
+ }
+
+ ret = 0;
if (outarg.nodeid != get_node_id(inode) ||
- !!IS_AUTOMOUNT(inode) != !!(outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
- fuse_chan_queue_forget(fc->chan, forget, outarg.nodeid, 1);
- return 0;
+ !!IS_AUTOMOUNT(inode) != !!(outarg.attr.flags & FUSE_ATTR_SUBMOUNT))
+ goto forget;
+
+ if (backing_id) {
+ ret = fuse_inode_set_passthrough(inode, backing_id);
+ if (ret)
+ goto forget;
}
kfree(forget);
@@ -534,10 +548,14 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
return 0;
forget_all_cached_acls(inode);
- fuse_change_attributes(inode, &outarg.attr, NULL, ATTR_TIMEOUT(&outarg),
+ fuse_change_attributes(inode, &outarg.attr, sx, ATTR_TIMEOUT(&outarg),
attr_version);
fuse_change_entry_timeout(entry, &outarg);
return 1;
+
+forget:
+ fuse_chan_queue_forget(fc->chan, forget, outarg.nodeid, 1);
+ return ret;
}
static int fuse_dentry_init(struct dentry *dentry)
@@ -631,6 +649,8 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
struct fuse_forget_link *forget;
u64 attr_version, evict_ctr;
struct fuse_entry2_out outarg2;
+ struct fuse_statx *sx = NULL;
+ int backing_id;
int err;
*inode = NULL;
@@ -649,9 +669,18 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
fuse_lookup_init(fm->fc, &args, nodeid, name, outarg, &outarg2);
err = fuse_simple_request(fm, &args);
+ if (err)
+ goto out_put_forget;
+
+ backing_id = fuse_process_entry2(fm->fc, &outarg2, outarg, &sx);
+
/* Zero nodeid is same as -ENOENT, but with valid timeout */
- if (err || !outarg->nodeid)
+ if (!outarg->nodeid)
goto out_put_forget;
+ if (backing_id < 0) {
+ err = backing_id;
+ goto out_send_forget;
+ }
err = -EIO;
if (fuse_invalid_attr(&outarg->attr))
@@ -662,19 +691,28 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
}
*inode = fuse_iget(sb, outarg->nodeid, outarg->generation,
- &outarg->attr, NULL, ATTR_TIMEOUT(outarg),
+ &outarg->attr, sx, ATTR_TIMEOUT(outarg),
attr_version, evict_ctr);
err = -ENOMEM;
- if (!*inode) {
- fuse_chan_queue_forget(fm->fc->chan, forget, outarg->nodeid, 1);
- goto out;
- }
+ if (!*inode)
+ goto out_send_forget;
+
err = 0;
+ if (backing_id) {
+ err = fuse_inode_set_passthrough(*inode, backing_id);
+ if (err) {
+ iput(*inode);
+ *inode = NULL;
+ }
+ }
out_put_forget:
kfree(forget);
out:
return err;
+ out_send_forget:
+ fuse_chan_queue_forget(fm->fc->chan, forget, outarg->nodeid, 1);
+ return err;
}
static struct dentry *fuse_lookup(struct inode *dir, struct dentry *entry,
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 14/21] fuse: add passthrough support for entry creation
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (12 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 13/21] fuse: add passthrough lookup Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 15/21] fuse: add passthrough support for create+open Joanne Koong
` (6 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Use the new extended fuse_entry2_out reply for entry creation operations
(mknod, mkdir, symlink, link) when FUSE_PASSTHROUGH_INO is enabled.
If the server returns a backing id, the newly created inode will be
automatically associated with the backing file for passthrough
operations. If no backing id is returned (no passthrough), the statx
attributes are cached.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 32 ++++++++++++++++++++++++++++----
1 file changed, 28 insertions(+), 4 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 3a6adae530da..a11f9e4c1999 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -1088,9 +1088,12 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
struct dentry *entry, umode_t mode)
{
struct fuse_entry_out outarg;
+ struct fuse_entry2_out outarg2;
struct inode *inode;
struct dentry *d;
struct fuse_forget_link *forget;
+ struct fuse_statx *sx = NULL;
+ int backing_id;
int epoch, err;
if (fuse_is_bad(dir))
@@ -1102,11 +1105,18 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
if (!forget)
return ERR_PTR(-ENOMEM);
- memset(&outarg, 0, sizeof(outarg));
args->nodeid = get_node_id(dir);
args->out_numargs = 1;
- args->out_args[0].size = sizeof(outarg);
- args->out_args[0].value = &outarg;
+
+ if (fuse_use_entry2(fm->fc)) {
+ memset(&outarg2, 0, sizeof(outarg2));
+ args->out_args[0].size = sizeof(outarg2);
+ args->out_args[0].value = &outarg2;
+ } else {
+ memset(&outarg, 0, sizeof(outarg));
+ args->out_args[0].size = sizeof(outarg);
+ args->out_args[0].value = &outarg;
+ }
if (args->opcode != FUSE_LINK) {
err = get_create_ext(idmap, args, dir, entry, mode);
@@ -1119,21 +1129,35 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
if (err)
goto out_put_forget_req;
+ backing_id = fuse_process_entry2(fm->fc, &outarg2, &outarg, &sx);
+
err = -EIO;
if (invalid_nodeid(outarg.nodeid) || fuse_invalid_attr(&outarg.attr))
goto out_put_forget_req;
+ if (backing_id < 0) {
+ fuse_chan_queue_forget(fm->fc->chan, forget, outarg.nodeid, 1);
+ return ERR_PTR(backing_id);
+ }
if ((outarg.attr.mode ^ mode) & S_IFMT)
goto out_put_forget_req;
inode = fuse_iget(dir->i_sb, outarg.nodeid, outarg.generation,
- &outarg.attr, NULL, ATTR_TIMEOUT(&outarg), 0, 0);
+ &outarg.attr, sx, ATTR_TIMEOUT(&outarg), 0, 0);
if (!inode) {
fuse_chan_queue_forget(fm->fc->chan, forget, outarg.nodeid, 1);
return ERR_PTR(-ENOMEM);
}
kfree(forget);
+ if (backing_id) {
+ err = fuse_inode_set_passthrough(inode, backing_id);
+ if (err) {
+ iput(inode);
+ return ERR_PTR(err);
+ }
+ }
+
d_drop(entry);
d = d_splice_alias(inode, entry);
if (IS_ERR(d))
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 15/21] fuse: add passthrough support for create+open
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (13 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 14/21] fuse: add passthrough support for entry creation Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 16/21] fuse: allow backing_id=0 in open to inherit inode's backing file Joanne Koong
` (5 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Use the extended fuse_entry2_out reply for FUSE_CREATE and FUSE_TMPFILE
when FUSE_PASSTHROUGH_INO is enabled. If the server returns a backing
id, the newly created inode is associated with the backing file for
passthrough operations before the dentry is instantiated.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 43 +++++++++++++++++++++++++++++++++++--------
1 file changed, 35 insertions(+), 8 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index a11f9e4c1999..4c7e3e1604af 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -934,9 +934,12 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
struct fuse_create_in inarg;
struct fuse_open_out *outopenp;
struct fuse_entry_out outentry;
+ struct fuse_entry2_out outentry2;
+ struct fuse_statx *sx = NULL;
struct fuse_inode *fi;
struct fuse_file *ff;
int epoch, err;
+ int backing_id;
bool trunc = flags & O_TRUNC;
/* Userspace expects S_IFREG in create mode */
@@ -957,7 +960,6 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
flags &= ~O_NOCTTY;
memset(&inarg, 0, sizeof(inarg));
- memset(&outentry, 0, sizeof(outentry));
inarg.flags = flags;
inarg.mode = mode;
inarg.umask = current_umask();
@@ -975,8 +977,15 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
args.in_args[1].size = entry->d_name.len + 1;
args.in_args[1].value = entry->d_name.name;
args.out_numargs = 2;
- args.out_args[0].size = sizeof(outentry);
- args.out_args[0].value = &outentry;
+ if (fuse_use_entry2(fm->fc)) {
+ memset(&outentry2, 0, sizeof(outentry2));
+ args.out_args[0].size = sizeof(outentry2);
+ args.out_args[0].value = &outentry2;
+ } else {
+ memset(&outentry, 0, sizeof(outentry));
+ args.out_args[0].size = sizeof(outentry);
+ args.out_args[0].value = &outentry;
+ }
/* Store outarg for fuse_finish_open() */
outopenp = &ff->args->open_outarg;
args.out_args[1].size = sizeof(*outopenp);
@@ -991,6 +1000,8 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
if (err)
goto out_free_ff;
+ backing_id = fuse_process_entry2(fm->fc, &outentry2, &outentry, &sx);
+
err = -EIO;
if (!S_ISREG(outentry.attr.mode) || invalid_nodeid(outentry.nodeid) ||
fuse_invalid_attr(&outentry.attr))
@@ -999,16 +1010,26 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
ff->fh = outopenp->fh;
ff->nodeid = outentry.nodeid;
ff->open_flags = outopenp->open_flags;
+
+ if (backing_id < 0) {
+ err = backing_id;
+ goto out_queue_forget;
+ }
+
inode = fuse_iget(dir->i_sb, outentry.nodeid, outentry.generation,
- &outentry.attr, NULL, ATTR_TIMEOUT(&outentry), 0, 0);
+ &outentry.attr, sx, ATTR_TIMEOUT(&outentry), 0, 0);
if (!inode) {
- flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
- fuse_sync_release(NULL, ff, flags, false);
- fuse_chan_queue_forget(fm->fc->chan, forget, outentry.nodeid, 1);
err = -ENOMEM;
- goto out_err;
+ goto out_queue_forget;
}
kfree(forget);
+ if (backing_id) {
+ err = fuse_inode_set_passthrough(inode, backing_id);
+ if (err) {
+ iput(inode);
+ goto out_sync_release;
+ }
+ }
d_instantiate(entry, inode);
entry->d_time = epoch;
fuse_change_entry_timeout(entry, &outentry);
@@ -1035,6 +1056,12 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
kfree(forget);
out_err:
return err;
+out_queue_forget:
+ fuse_chan_queue_forget(fm->fc->chan, forget, outentry.nodeid, 1);
+out_sync_release:
+ flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
+ fuse_sync_release(NULL, ff, flags, false);
+ return err;
}
static int fuse_mknod(struct mnt_idmap *, struct inode *, struct dentry *,
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 16/21] fuse: allow backing_id=0 in open to inherit inode's backing file
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (14 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 15/21] fuse: add passthrough support for create+open Joanne Koong
@ 2026-05-16 0:39 ` Joanne Koong
2026-05-16 0:40 ` [PATCH v2 17/21] backing-inode: add backing_inode_copyattr() Joanne Koong
` (4 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:39 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
With FUSE_PASSTHROUGH_INO, the backing file is associated with the inode
at lookup time. Allow the server to pass in backing_id=0 in the open
response. On the kernel side, this uses the backing file already set up
on the inode.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/passthrough.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index d62a1c751157..ae0137caa06d 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -193,13 +193,17 @@ int fuse_passthrough_readdir(struct file *file, struct dir_context *ctx)
*/
struct fuse_backing *fuse_passthrough_open(struct file *file, int backing_id)
{
+ struct fuse_inode *fi = get_fuse_inode(file_inode(file));
struct fuse_file *ff = file->private_data;
struct fuse_conn *fc = ff->fm->fc;
struct fuse_backing *fb = NULL;
struct file *backing_file;
int err;
- fb = fuse_backing_id_get(fc, backing_id);
+ if (!backing_id && fc->passthrough_ino)
+ fb = fuse_backing_get(fuse_inode_backing(fi));
+ else
+ fb = fuse_backing_id_get(fc, backing_id);
if (IS_ERR_OR_NULL(fb)) {
err = fb ? PTR_ERR(fb) : -ENOENT;
fb = NULL;
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 17/21] backing-inode: add backing_inode_copyattr()
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (15 preceding siblings ...)
2026-05-16 0:39 ` [PATCH v2 16/21] fuse: allow backing_id=0 in open to inherit inode's backing file Joanne Koong
@ 2026-05-16 0:40 ` Joanne Koong
2026-05-16 0:40 ` [PATCH v2 18/21] backing-inode: add backing_inode_setattr() Joanne Koong
` (3 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:40 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Move logic in ovl_copyattr() to a generic backing_inode_copyattr()
function in a new fs/backing-inode.c, which other filesystems that use
backing inodes (eg fuse passthrough) will use.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/Makefile | 2 +-
fs/backing-inode.c | 43 +++++++++++++++++++++++++++++++++++
fs/overlayfs/util.c | 33 +++------------------------
include/linux/backing-inode.h | 14 ++++++++++++
4 files changed, 61 insertions(+), 31 deletions(-)
create mode 100644 fs/backing-inode.c
create mode 100644 include/linux/backing-inode.h
diff --git a/fs/Makefile b/fs/Makefile
index cf4a745e9679..3f8a227d4938 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -40,7 +40,7 @@ obj-$(CONFIG_COMPAT_BINFMT_ELF) += compat_binfmt_elf.o
obj-$(CONFIG_BINFMT_ELF_FDPIC) += binfmt_elf_fdpic.o
obj-$(CONFIG_BINFMT_FLAT) += binfmt_flat.o
-obj-$(CONFIG_FS_STACK) += backing-file.o
+obj-$(CONFIG_FS_STACK) += backing-file.o backing-inode.o
obj-$(CONFIG_FS_MBCACHE) += mbcache.o
obj-$(CONFIG_FS_POSIX_ACL) += posix_acl.o
obj-$(CONFIG_NFS_COMMON) += nfs_common/
diff --git a/fs/backing-inode.c b/fs/backing-inode.c
new file mode 100644
index 000000000000..474770a1fa9d
--- /dev/null
+++ b/fs/backing-inode.c
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Common helpers for stackable filesystems and backing inodes.
+ */
+
+#include <linux/backing-inode.h>
+
+/*
+ * backing_inode_copyattr - copy inode attributes from a backing inode
+ *
+ * When a filesystem copies inode information from a backing layer to its own
+ * inode, it applies the idmapping of the backing mount, ensuring that
+ * the inode ownership will correctly reflect the ownership of the idmapped
+ * backing mount. For example, an idmapped backing mount mapping id 1001 to id
+ * 1000 will take care to map any backing inode owned by id 1001 to id 1000.
+ * These mapping helpers are nops when the backing mount isn't idmapped.
+ */
+void backing_inode_copyattr(struct inode *inode,
+ const struct path *backing_path)
+{
+ struct inode *realinode;
+ struct mnt_idmap *real_idmap;
+ vfsuid_t vfsuid;
+ vfsgid_t vfsgid;
+
+ realinode = d_inode_rcu(backing_path->dentry);
+ real_idmap = mnt_idmap(backing_path->mnt);
+
+ spin_lock(&inode->i_lock);
+ vfsuid = i_uid_into_vfsuid(real_idmap, realinode);
+ vfsgid = i_gid_into_vfsgid(real_idmap, realinode);
+
+ inode->i_uid = vfsuid_into_kuid(vfsuid);
+ inode->i_gid = vfsgid_into_kgid(vfsgid);
+ inode->i_mode = realinode->i_mode;
+ inode_set_atime_to_ts(inode, inode_get_atime(realinode));
+ inode_set_mtime_to_ts(inode, inode_get_mtime(realinode));
+ inode_set_ctime_to_ts(inode, inode_get_ctime(realinode));
+ i_size_write(inode, i_size_read(realinode));
+ spin_unlock(&inode->i_lock);
+}
+EXPORT_SYMBOL_GPL(backing_inode_copyattr);
+
diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c
index 3f1b763a8bb4..3e7d66c26c10 100644
--- a/fs/overlayfs/util.c
+++ b/fs/overlayfs/util.c
@@ -16,6 +16,7 @@
#include <linux/namei.h>
#include <linux/ratelimit.h>
#include <linux/overflow.h>
+#include <linux/backing-inode.h>
#include "overlayfs.h"
/* Get write access to upper mnt - may fail if upper sb was remounted ro */
@@ -1503,38 +1504,10 @@ int ovl_sync_status(struct ovl_fs *ofs)
return errseq_check(&mnt->mnt_sb->s_wb_err, ofs->errseq);
}
-/*
- * ovl_copyattr() - copy inode attributes from layer to ovl inode
- *
- * When overlay copies inode information from an upper or lower layer to the
- * relevant overlay inode it will apply the idmapping of the upper or lower
- * layer when doing so ensuring that the ovl inode ownership will correctly
- * reflect the ownership of the idmapped upper or lower layer. For example, an
- * idmapped upper or lower layer mapping id 1001 to id 1000 will take care to
- * map any lower or upper inode owned by id 1001 to id 1000. These mapping
- * helpers are nops when the relevant layer isn't idmapped.
- */
void ovl_copyattr(struct inode *inode)
{
struct path realpath;
- struct inode *realinode;
- struct mnt_idmap *real_idmap;
- vfsuid_t vfsuid;
- vfsgid_t vfsgid;
- realinode = ovl_i_path_real(inode, &realpath);
- real_idmap = mnt_idmap(realpath.mnt);
-
- spin_lock(&inode->i_lock);
- vfsuid = i_uid_into_vfsuid(real_idmap, realinode);
- vfsgid = i_gid_into_vfsgid(real_idmap, realinode);
-
- inode->i_uid = vfsuid_into_kuid(vfsuid);
- inode->i_gid = vfsgid_into_kgid(vfsgid);
- inode->i_mode = realinode->i_mode;
- inode_set_atime_to_ts(inode, inode_get_atime(realinode));
- inode_set_mtime_to_ts(inode, inode_get_mtime(realinode));
- inode_set_ctime_to_ts(inode, inode_get_ctime(realinode));
- i_size_write(inode, i_size_read(realinode));
- spin_unlock(&inode->i_lock);
+ ovl_i_path_real(inode, &realpath);
+ backing_inode_copyattr(inode, &realpath);
}
diff --git a/include/linux/backing-inode.h b/include/linux/backing-inode.h
new file mode 100644
index 000000000000..6b43cba9fabd
--- /dev/null
+++ b/include/linux/backing-inode.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Common helpers for stackable filesystems and backing inodes.
+ */
+
+#ifndef _LINUX_BACKING_INODE_H
+#define _LINUX_BACKING_INODE_H
+
+#include <linux/fs.h>
+
+void backing_inode_copyattr(struct inode *inode,
+ const struct path *backing_path);
+
+#endif /* _LINUX_BACKING_INODE_H */
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 18/21] backing-inode: add backing_inode_setattr()
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (16 preceding siblings ...)
2026-05-16 0:40 ` [PATCH v2 17/21] backing-inode: add backing_inode_copyattr() Joanne Koong
@ 2026-05-16 0:40 ` Joanne Koong
2026-05-16 0:40 ` [PATCH v2 19/21] fuse: add passthrough setattr Joanne Koong
` (2 subsequent siblings)
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:40 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Move logic in ovl_setattr() to a generic backing_inode_setattr() function
in fs/backing-inode.c, which other filesystems that use backing inodes
(eg fuse passthrough) will use.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/backing-inode.c | 54 ++++++++++++++++++++++++++++
fs/overlayfs/inode.c | 66 ++++++++++-------------------------
include/linux/backing-inode.h | 4 +++
3 files changed, 76 insertions(+), 48 deletions(-)
diff --git a/fs/backing-inode.c b/fs/backing-inode.c
index 474770a1fa9d..e72f278789d8 100644
--- a/fs/backing-inode.c
+++ b/fs/backing-inode.c
@@ -41,3 +41,57 @@ void backing_inode_copyattr(struct inode *inode,
}
EXPORT_SYMBOL_GPL(backing_inode_copyattr);
+int backing_inode_setattr(struct dentry *dentry,
+ const struct path *backing_path,
+ struct iattr *attr, const struct cred *cred)
+{
+ struct dentry *backing_dentry = backing_path->dentry;
+ struct inode *winode = NULL;
+ int err;
+
+ if (attr->ia_valid & ATTR_SIZE) {
+ winode = d_inode(backing_dentry);
+ err = get_write_access(winode);
+ if (err)
+ return err;
+ }
+
+ /*
+ * Clear ATTR_MODE to avoid BUG_ON in notify_change(), which does not
+ * allow ATTR_MODE together with ATTR_KILL_SUID/SGID (the first
+ * notify_change() converted ATTR_KILL_SUID/SGID into ATTR_MODE using
+ * @dentry's inode mode).
+ *
+ * This also ensures notify_change() recomputes the mode from the
+ * backing inode's current mode, instead of a potentially stale value
+ * from @dentry's inode.
+ */
+ if (attr->ia_valid & (ATTR_KILL_SUID|ATTR_KILL_SGID))
+ attr->ia_valid &= ~ATTR_MODE;
+
+ /*
+ * The filesystem's file is not meaningful to the backing filesystem.
+ * Clear ATTR_FILE so the backing filesystem does not try to use it.
+ */
+ attr->ia_valid &= ~ATTR_FILE;
+
+ err = mnt_want_write(backing_path->mnt);
+ if (err)
+ goto out_put_write;
+
+ inode_lock(backing_dentry->d_inode);
+ scoped_with_creds(cred)
+ err = notify_change(mnt_idmap(backing_path->mnt),
+ backing_dentry, attr, NULL);
+ if (!err)
+ backing_inode_copyattr(dentry->d_inode, backing_path);
+ inode_unlock(backing_dentry->d_inode);
+ mnt_drop_write(backing_path->mnt);
+
+out_put_write:
+ if (winode)
+ put_write_access(winode);
+
+ return err;
+}
+EXPORT_SYMBOL_GPL(backing_inode_setattr);
diff --git a/fs/overlayfs/inode.c b/fs/overlayfs/inode.c
index 00c69707bda9..f1cd67e6e9c8 100644
--- a/fs/overlayfs/inode.c
+++ b/fs/overlayfs/inode.c
@@ -15,16 +15,17 @@
#include <linux/namei.h>
#include <linux/posix_acl.h>
#include <linux/posix_acl_xattr.h>
+#include <linux/backing-inode.h>
#include "overlayfs.h"
int ovl_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
struct iattr *attr)
{
+ struct path backing_path;
int err;
struct ovl_fs *ofs = OVL_FS(dentry->d_sb);
bool full_copy_up = false;
- struct dentry *upperdentry;
err = setattr_prepare(&nop_mnt_idmap, dentry, attr);
if (err)
@@ -39,57 +40,26 @@ int ovl_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
err = ovl_copy_up(dentry);
else
err = ovl_copy_up_with_data(dentry);
- if (!err) {
- struct inode *winode = NULL;
-
- upperdentry = ovl_dentry_upper(dentry);
-
- if (attr->ia_valid & ATTR_SIZE) {
- winode = d_inode(upperdentry);
- err = get_write_access(winode);
- if (err)
- goto out;
- }
-
- if (attr->ia_valid & (ATTR_KILL_SUID|ATTR_KILL_SGID))
- attr->ia_valid &= ~ATTR_MODE;
- /*
- * We might have to translate ovl file into real file object
- * once use cases emerge. For now, simply don't let underlying
- * filesystem rely on attr->ia_file
- */
- attr->ia_valid &= ~ATTR_FILE;
-
- /*
- * If open(O_TRUNC) is done, VFS calls ->setattr with ATTR_OPEN
- * set. Overlayfs does not pass O_TRUNC flag to underlying
- * filesystem during open -> do not pass ATTR_OPEN. This
- * disables optimization in fuse which assumes open(O_TRUNC)
- * already set file size to 0. But we never passed O_TRUNC to
- * fuse. So by clearing ATTR_OPEN, fuse will be forced to send
- * setattr request to server.
- */
- attr->ia_valid &= ~ATTR_OPEN;
+ if (err)
+ return err;
- err = ovl_want_write(dentry);
- if (err)
- goto out_put_write;
+ /*
+ * If open(O_TRUNC) is done, VFS calls ->setattr with ATTR_OPEN
+ * set. Overlayfs does not pass O_TRUNC flag to underlying
+ * filesystem during open -> do not pass ATTR_OPEN. This
+ * disables optimization in fuse which assumes open(O_TRUNC)
+ * already set file size to 0. But we never passed O_TRUNC to
+ * fuse. So by clearing ATTR_OPEN, fuse will be forced to send
+ * setattr request to server.
+ */
+ attr->ia_valid &= ~ATTR_OPEN;
- inode_lock(upperdentry->d_inode);
- with_ovl_creds(dentry->d_sb)
- err = ovl_do_notify_change(ofs, upperdentry, attr);
- if (!err)
- ovl_copyattr(dentry->d_inode);
- inode_unlock(upperdentry->d_inode);
- ovl_drop_write(dentry);
+ backing_path.dentry = ovl_dentry_upper(dentry);
+ backing_path.mnt = ovl_upper_mnt(ofs);
-out_put_write:
- if (winode)
- put_write_access(winode);
- }
-out:
- return err;
+ return backing_inode_setattr(dentry, &backing_path, attr,
+ ovl_creds(dentry->d_sb));
}
static void ovl_map_dev_ino(struct dentry *dentry, struct kstat *stat, int fsid)
diff --git a/include/linux/backing-inode.h b/include/linux/backing-inode.h
index 6b43cba9fabd..fd02c87e0f99 100644
--- a/include/linux/backing-inode.h
+++ b/include/linux/backing-inode.h
@@ -11,4 +11,8 @@
void backing_inode_copyattr(struct inode *inode,
const struct path *backing_path);
+int backing_inode_setattr(struct dentry *dentry,
+ const struct path *backing_path,
+ struct iattr *attr, const struct cred *cred);
+
#endif /* _LINUX_BACKING_INODE_H */
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 19/21] fuse: add passthrough setattr
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (17 preceding siblings ...)
2026-05-16 0:40 ` [PATCH v2 18/21] backing-inode: add backing_inode_setattr() Joanne Koong
@ 2026-05-16 0:40 ` Joanne Koong
2026-05-16 1:04 ` Joanne Koong
2026-05-16 0:40 ` [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling Joanne Koong
2026-05-16 0:40 ` [PATCH v2 21/21] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO) Joanne Koong
20 siblings, 1 reply; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:40 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Add passthrough setattr which sets attributes directly on the backing
inode through backing_inode_setattr() instead of sending FUSE_SETATTR to
the server.
Passthrough setattr is checked before the
handle_killpriv/handle_killpriv_v2 suid/sgid stripping because the
stripping is handled natively by notify_change() on the backing inode.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 3 +++
fs/fuse/fuse_i.h | 3 ++-
fs/fuse/passthrough.c | 28 ++++++++++++++++++++++++++++
include/uapi/linux/fuse.h | 1 +
4 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 4c7e3e1604af..b67b3b334e69 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -2506,6 +2506,9 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
if (!fuse_allow_current_process(get_fuse_conn(inode)))
return -EACCES;
+ if (fuse_passthrough_op(inode, FUSE_SETATTR))
+ return fuse_passthrough_setattr(entry, attr);
+
if (attr->ia_valid & (ATTR_KILL_SUID | ATTR_KILL_SGID)) {
attr->ia_valid &= ~(ATTR_KILL_SUID | ATTR_KILL_SGID |
ATTR_MODE);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 89c9333e9702..0d978a9837a0 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1279,7 +1279,7 @@ ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from);
/* Inode passthrough operations for backing file attached to inode */
#define FUSE_PASSTHROUGH_INODE_OPS \
- (FUSE_PASSTHROUGH_OP_GETATTR)
+ (FUSE_PASSTHROUGH_OP_GETATTR | FUSE_PASSTHROUGH_OP_SETATTR)
#define FUSE_BACKING_MAP_OP(map, op) \
((map)->ops_mask & FUSE_PASSTHROUGH_OP(op))
@@ -1367,6 +1367,7 @@ static inline bool fuse_passthrough_op(struct inode *inode, enum fuse_opcode op)
int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
u32 request_mask, unsigned int flags);
+int fuse_passthrough_setattr(struct dentry *entry, struct iattr *attr);
static inline bool fuse_use_entry2(struct fuse_conn *fc)
{
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index ae0137caa06d..c083ab68537e 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -9,6 +9,8 @@
#include <linux/file.h>
#include <linux/backing-file.h>
+#include <linux/backing-inode.h>
+#include <linux/posix_acl.h>
#include <linux/splice.h>
static void fuse_file_accessed(struct file *file)
@@ -275,3 +277,29 @@ int fuse_passthrough_getattr(struct inode *inode, struct kstat *stat,
return 0;
}
+
+int fuse_passthrough_setattr(struct dentry *entry, struct iattr *attr)
+{
+ struct inode *inode = d_inode(entry);
+ struct fuse_conn *fc = get_fuse_conn(inode);
+ struct fuse_inode *fi = get_fuse_inode(inode);
+ struct fuse_backing *fb = fuse_inode_backing(fi);
+ struct path path;
+ int err;
+
+ err = setattr_prepare(&nop_mnt_idmap, entry, attr);
+ if (err)
+ return err;
+
+ path.mnt = fb->file->f_path.mnt;
+ path.dentry = fb->file->f_path.dentry;
+
+ err = backing_inode_setattr(entry, &path, attr, fb->cred);
+ if (err)
+ return err;
+
+ if (fc->posix_acl)
+ forget_all_cached_acls(inode);
+
+ return 0;
+}
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 3963631558f9..040fee549bb9 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -1158,6 +1158,7 @@ struct fuse_backing_map {
#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
#define FUSE_PASSTHROUGH_OP_READDIR FUSE_PASSTHROUGH_OP(FUSE_READDIR)
#define FUSE_PASSTHROUGH_OP_GETATTR FUSE_PASSTHROUGH_OP(FUSE_GETATTR)
+#define FUSE_PASSTHROUGH_OP_SETATTR FUSE_PASSTHROUGH_OP(FUSE_SETATTR)
/* Device ioctls: */
#define FUSE_DEV_IOC_MAGIC 229
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (18 preceding siblings ...)
2026-05-16 0:40 ` [PATCH v2 19/21] fuse: add passthrough setattr Joanne Koong
@ 2026-05-16 0:40 ` Joanne Koong
2026-05-16 1:20 ` Joanne Koong
2026-05-16 0:40 ` [PATCH v2 21/21] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO) Joanne Koong
20 siblings, 1 reply; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:40 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
When refreshing i_mode for suid/sgid kill during setattr, use
passthrough getattr if the inode has that enabled.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
fs/fuse/dir.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index b67b3b334e69..fd1b3fd86968 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -2524,7 +2524,11 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
* ia_mode calculation may have used stale i_mode.
* Refresh and recalculate.
*/
- ret = fuse_do_getattr(idmap, inode, NULL, file);
+ if (fuse_passthrough_op(inode, FUSE_GETATTR))
+ ret = fuse_passthrough_getattr(inode, NULL,
+ STATX_MODE, 0);
+ else
+ ret = fuse_do_getattr(idmap, inode, NULL, file);
if (ret)
return ret;
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 21/21] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO)
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
` (19 preceding siblings ...)
2026-05-16 0:40 ` [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling Joanne Koong
@ 2026-05-16 0:40 ` Joanne Koong
20 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 0:40 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
Add section about extended passthrough (FUSE_PASSTHROUGH_INO) mode.
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
.../filesystems/fuse/fuse-passthrough.rst | 126 ++++++++++++++++++
1 file changed, 126 insertions(+)
diff --git a/Documentation/filesystems/fuse/fuse-passthrough.rst b/Documentation/filesystems/fuse/fuse-passthrough.rst
index 2b0e7c2da54a..751d27c6fc5c 100644
--- a/Documentation/filesystems/fuse/fuse-passthrough.rst
+++ b/Documentation/filesystems/fuse/fuse-passthrough.rst
@@ -25,6 +25,11 @@ operations.
Currently, passthrough is supported for operations like ``read(2)``/``write(2)``
(via ``read_iter``/``write_iter``), ``splice(2)``, and ``mmap(2)``.
+With the extended ``FUSE_PASSTHROUGH_INO`` mode, passthrough is also supported
+for inode operations (getattr, setattr) and directory operations (readdir).
+In this mode, a backing file is attached to a fuse inode for its entire
+lifetime.
+
Enabling Passthrough
====================
@@ -46,6 +51,127 @@ To use FUSE passthrough:
the ``backing_id`` to release the kernel's reference to the backing file
when it's no longer needed for passthrough setups.
+Extended Passthrough (FUSE_PASSTHROUGH_INO)
+============================================
+
+``FUSE_PASSTHROUGH_INO`` is a stricter variant of ``FUSE_PASSTHROUGH`` in
+which the backing file inode number must match the fuse inode number, enforcing
+a one-to-one mapping. The kernel offers this flag during ``FUSE_INIT`` if
+``CONFIG_FUSE_PASSTHROUGH`` is enabled and the architecture has 64-bit
+``ino_t``. The daemon accepts by returning it back in the init reply.
+
+Enabling Extended Passthrough
+-----------------------------
+
+To use extended passthrough:
+
+ 1. Follow steps 1-2 from `Enabling Passthrough`_ above. The daemon must
+ also negotiate the ``FUSE_PASSTHROUGH_INO`` capability during
+ ``FUSE_INIT``.
+ 2. When registering a backing file via ``FUSE_DEV_IOC_BACKING_OPEN``, set
+ the ``ops_mask`` field in ``struct fuse_backing_map`` to declare which
+ operations should be passed through. At minimum,
+ ``FUSE_PASSTHROUGH_OP_GETATTR`` must be set for any inode-level
+ passthrough.
+ 3. When handling a ``LOOKUP``, ``CREATE``, ``MKNOD``, ``MKDIR``,
+ ``SYMLINK``, or ``LINK`` request, the daemon responds with a
+ ``fuse_entry2_out`` (instead of ``fuse_entry_out``). To enable
+ passthrough on the inode, set ``backing_id`` to the id returned by
+ ``FUSE_DEV_IOC_BACKING_OPEN``. Set ``backing_id`` to 0 for inodes
+ that should not use passthrough. The ``nodeid`` in the response must
+ be the backing file's inode number (``i_ino``). If they don't match,
+ the kernel rejects the passthrough setup with ``-EIO``.
+ 4. When handling an ``OPEN`` request for a FUSE file, the daemon
+ replies with the ``FOPEN_PASSTHROUGH`` flag set in
+ ``fuse_open_out::open_flags`` and provides the corresponding ``backing_id``
+ in ``fuse_open_out::backing_id`` or leaves ``fuse_open_out::backing_id``
+ blank. If the daemon would like to opt out of passthrough when the inode
+ is already in passthrough mode, it may additionally set
+ ``FOPEN_DIRECT_IO``, which will forward read/write operations directly to
+ the daemon.
+ 5. The FUSE daemon should eventually call ``FUSE_DEV_IOC_BACKING_CLOSE`` with
+ the ``backing_id`` to release the kernel's reference to the backing file
+ when it's no longer needed for passthrough setups.
+
+Passthrough Operations Mask
+---------------------------
+
+When registering a backing file via ``FUSE_DEV_IOC_BACKING_OPEN``, the daemon
+sets ``ops_mask`` in ``struct fuse_backing_map`` to declare which operations
+should be passed through::
+
+ FUSE_PASSTHROUGH_OP_READ
+ FUSE_PASSTHROUGH_OP_WRITE
+ FUSE_PASSTHROUGH_OP_READDIR
+ FUSE_PASSTHROUGH_OP_GETATTR
+ FUSE_PASSTHROUGH_OP_SETATTR
+
+Operations fall into two categories, which can be combined:
+
+**Inode operations** (getattr, setattr): Activated on lookup when the daemon
+returns a ``backing_id`` in the ``fuse_entry2_out`` response. The backing
+file reference persists for the lifetime of the fuse inode. Getattr is the
+minimum required inode operation.
+
+**File operations** (read, write, readdir): Require the file to be opened with
+``FOPEN_PASSTHROUGH`` in the daemon's open response. Read and write can be set
+independently for partial passthrough. If only one direction is set, the other
+falls back to direct IO and mmap is disabled.
+
+Extended Entry Reply
+--------------------
+
+When ``FUSE_PASSTHROUGH_INO`` is negotiated, the kernel uses
+``fuse_entry2_out`` instead of ``fuse_entry_out`` for entry responses.
+This struct carries a ``backing_id`` and ``fuse_statx`` attributes instead
+of ``fuse_attr``.
+
+When ``backing_id > 0``, the kernel associates the inode with the backing file
+for passthrough inode operations. Statx attributes are not cached because
+passthrough getattr fetches them directly from the backing inode.
+
+When ``backing_id == 0`` (no passthrough), the statx attributes from the reply
+are cached normally.
+
+A negative ``backing_id`` is treated as an error. The kernel sends
+``FUSE_FORGET`` for the returned nodeid and fails the operation.
+
+IO Mode State Machine
+---------------------
+
+The ``iocachectr`` field in ``struct fuse_inode`` prevents conflicting access
+modes on the same inode (page-cache I/O and passthrough I/O cannot coexist)::
+
+ iocachectr > 0 Cached mode
+ iocachectr == 0 Idle. No files open, no passthrough
+ iocachectr < 0 Uncached/passthrough mode
+
+Each open file in passthrough mode holds one reference (``iocachectr--``).
+The inode-level passthrough setup holds one additional long-lived reference
+if the backing has inode ops (getattr/setattr). This long-lived reference is
+released on inode eviction.
+
+Cached mode and passthrough mode are mutually exclusive. Attempting either
+while the other is active returns ``-ETXTBSY``.
+
+For directories, the same mechanism arbitrates between cached readdir
+(``FOPEN_CACHE_DIR``) and passthrough readdir. A directory opened without
+``FOPEN_CACHE_DIR`` and without ``FOPEN_PASSTHROUGH`` is treated as direct I/O
+and does not affect io mode.
+
+Things to note
+--------------
+
+- ``FUSE_PASSTHROUGH_INO`` requires 64-bit ``ino_t``.
+- Readdirplus does not set up inode passthrough. Inodes created via readdirplus
+ use normal FUSE operations until a fresh lookup occurs.
+- An inode's backing association is set once and cannot be changed.
+- Passthrough and cached I/O cannot coexist on the same inode.
+- If any inode operations are passed through, this means all opened files need
+ to set the ``FOPEN_PASSTHROUGH`` flag in the open response, even if reads
+ and writes are not passed through. If reads and writes are not passed
+ through, they will go directly to the daemon.
+
Privilege Requirements
======================
--
2.52.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* Re: [PATCH v2 19/21] fuse: add passthrough setattr
2026-05-16 0:40 ` [PATCH v2 19/21] fuse: add passthrough setattr Joanne Koong
@ 2026-05-16 1:04 ` Joanne Koong
0 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 1:04 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
On Fri, May 15, 2026 at 5:53 PM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Add passthrough setattr which sets attributes directly on the backing
> inode through backing_inode_setattr() instead of sending FUSE_SETATTR to
> the server.
>
> Passthrough setattr is checked before the
> handle_killpriv/handle_killpriv_v2 suid/sgid stripping because the
> stripping is handled natively by notify_change() on the backing inode.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
> fs/fuse/dir.c | 3 +++
> fs/fuse/fuse_i.h | 3 ++-
> fs/fuse/passthrough.c | 28 ++++++++++++++++++++++++++++
> include/uapi/linux/fuse.h | 1 +
> 4 files changed, 34 insertions(+), 1 deletion(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index 4c7e3e1604af..b67b3b334e69 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -2506,6 +2506,9 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
> if (!fuse_allow_current_process(get_fuse_conn(inode)))
> return -EACCES;
>
> + if (fuse_passthrough_op(inode, FUSE_SETATTR))
> + return fuse_passthrough_setattr(entry, attr);
In the v1 discussion [1], there was a suggestion about gating
FUSE_PASSTHROUGH_INO on FUSE_HANDLE_KILLPRIV_V2 to avoid the
non-atomic killpriv path, but I think we were only encountering the
non-atomic killpriv path because I was calling
fuse_passthrough_setattr() in the wrong place. I think we can avoid
this altogether by just having the backing filesystem handle all of
the suid/sgid stripping atomically through its own notify_change()
path, which will let us skip all the fuse killpriv handling logic in
this function.
Thanks,
Joanne
[1] https://lore.kernel.org/fuse-devel/CAOQ4uxipQJt5zskb_THxueGi_MXpFdywFiGpd_nmWeY_sMHwzQ@mail.gmail.com/
> +
> if (attr->ia_valid & (ATTR_KILL_SUID | ATTR_KILL_SGID)) {
> attr->ia_valid &= ~(ATTR_KILL_SUID | ATTR_KILL_SGID |
> ATTR_MODE);
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling
2026-05-16 0:40 ` [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling Joanne Koong
@ 2026-05-16 1:20 ` Joanne Koong
0 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 1:20 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
On Fri, May 15, 2026 at 5:53 PM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> When refreshing i_mode for suid/sgid kill during setattr, use
> passthrough getattr if the inode has that enabled.
>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
> fs/fuse/dir.c | 6 +++++-
> 1 file changed, 5 insertions(+), 1 deletion(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index b67b3b334e69..fd1b3fd86968 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -2524,7 +2524,11 @@ static int fuse_setattr(struct mnt_idmap *idmap, struct dentry *entry,
> * ia_mode calculation may have used stale i_mode.
> * Refresh and recalculate.
> */
> - ret = fuse_do_getattr(idmap, inode, NULL, file);
> + if (fuse_passthrough_op(inode, FUSE_GETATTR))
> + ret = fuse_passthrough_getattr(inode, NULL,
> + STATX_MODE, 0);
> + else
> + ret = fuse_do_getattr(idmap, inode, NULL, file);
I left this part untouched from v1 but there was a discussion [1]
about whether to enforce that setattr must be passed through if
getattr is passed through [1]. I think there might be some use cases
though where having only getattr passed through and not setattr is
useful (eg server wants fast stat() but needs to intercept attribute
changes for access control or policy enforcement). I'm not sure if you
still feel it's better to keep it simple with enforcing that
getattr+setattr rmust always be passed through together, Amir, but if
so I'm happy to add that in.
Thanks,
Joanne
[1] https://lore.kernel.org/fuse-devel/CAOQ4uxg7jaA0FdLW6uzZBRX=m1Xw4v-k7zeQx7kYcpFwFDpf7A@mail.gmail.com/
> if (ret)
> return ret;
>
> --
> 2.52.0
>
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH v2 02/21] fuse: prepare for passthrough of inode operations
2026-05-16 0:39 ` [PATCH v2 02/21] fuse: prepare for passthrough of inode operations Joanne Koong
@ 2026-05-16 1:34 ` Joanne Koong
0 siblings, 0 replies; 25+ messages in thread
From: Joanne Koong @ 2026-05-16 1:34 UTC (permalink / raw)
To: amir73il, miklos; +Cc: fuse-devel, linux-unionfs
On Fri, May 15, 2026 at 5:52 PM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> From: Amir Goldstein <amir73il@gmail.com>
>
> So far, fuse passthrough was implemented for read/write/splice/mmap
> operations for regular files opened with FOPEN_PASSTHROUGH.
>
> A backing file is attached to a fuse inode, but only for as long as
> there are FOPEN_PASSTHROUGH files opened on this inode.
>
> We would like to attach a backing file to fuse inode also without an
> open file to allow passthrough of some inode operations.
>
> Add field ops_mask to the input argument of FUSE_DEV_IOC_BACKING_OPEN
> ioctl to declare the operations that would passthrough to the backing
> file once it has been attached to the fuse inode on lookup.
>
> Setting the FUSE_READ/FUSE_WRITE operations in the ops_mask is not
> required because those operations are implied by FOPEN_PASSTHROUGH.
>
> When setting operations other than FUSE_READ/FUSE_WRITE in ops_mask,
> non-regular backing files are allowed, so we need to verify when
> attaching a backing file to a fuse inode, that their file types match.
>
> For simplification of inode attribute caching, for now, require a
> filesystem with FUSE_PASSTHROUGH_INO (one-to-one mapping from fuse inode
> to backing inode) for setting up passthrough of any inode operations.
> We may consider relaxing this requirement for some inode operations
> in the future.
>
> Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
> Signed-off-by: Amir Goldstein <amir73il@gmail.com>
> ---
> fs/fuse/backing.c | 13 ++++++++++---
> fs/fuse/fuse_i.h | 30 ++++++++++++++++++++++++++++++
> fs/fuse/iomode.c | 16 ++++++++++++++++
> include/uapi/linux/fuse.h | 9 ++++++++-
> 4 files changed, 64 insertions(+), 4 deletions(-)
>
> diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
> index 4be9ccc5b3ff..0f1e1c1ec367 100644
> --- a/include/uapi/linux/fuse.h
> +++ b/include/uapi/linux/fuse.h
> @@ -243,6 +243,7 @@
>
> +#define FUSE_PASSTHROUGH_OP(op) (1ULL << ((op) - 1))
> +
> +/* op bits for fuse_backing_map ops_mask */
> +#define FUSE_PASSTHROUGH_OP_READ FUSE_PASSTHROUGH_OP(FUSE_READ)
> +#define FUSE_PASSTHROUGH_OP_WRITE FUSE_PASSTHROUGH_OP(FUSE_WRITE)
Do you think we should couple the passthrough op code to the fuse
opcode so closely, instead of defining the passthrough ops separately,
eg doing something like this?:
#define FUSE_PASSTHROUGH_READ (1 << 0)
#define FUSE_PASSTHROUGH_WRITE (1 << 1)
In the (far) future when some more advanced passthrough features get
added (eg full subtree passthrough), it seems like we'd want to add a
passthrough op for that, but that wouldn't map to a fuse op. I think
there are also some fuse ops we might skip defining as passthrough ops
but are implicitly passed through (eg FUSE_STATX which is covered by
FUSE_GETATTR), so it seems more intuitive to define the passthrough
ops as describing capabilities rather than implying that it describes
what specific opcodes get passed through?
Thanks,
Joanne
^ permalink raw reply [flat|nested] 25+ messages in thread
end of thread, other threads:[~2026-05-16 1:34 UTC | newest]
Thread overview: 25+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-16 0:39 [PATCH v2 00/21] fuse: extend passthrough to inode operations Joanne Koong
2026-05-16 0:39 ` [PATCH v2 01/21] fuse: introduce FUSE_PASSTHROUGH_INO mode Joanne Koong
2026-05-16 0:39 ` [PATCH v2 02/21] fuse: prepare for passthrough of inode operations Joanne Koong
2026-05-16 1:34 ` Joanne Koong
2026-05-16 0:39 ` [PATCH v2 03/21] fuse: prepare for readdir passthrough on directories Joanne Koong
2026-05-16 0:39 ` [PATCH v2 04/21] fuse: implement passthrough for readdir Joanne Koong
2026-05-16 0:39 ` [PATCH v2 05/21] fuse: prepare for long lived reference on backing file Joanne Koong
2026-05-16 0:39 ` [PATCH v2 06/21] fuse: implement passthrough for getattr/statx Joanne Koong
2026-05-16 0:39 ` [PATCH v2 07/21] fuse: prepare to setup backing inode passthrough on lookup Joanne Koong
2026-05-16 0:39 ` [PATCH v2 08/21] fuse: handle zero ops_mask in FUSE_DEV_IOC_BACKING_OPEN Joanne Koong
2026-05-16 0:39 ` [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap Joanne Koong
2026-05-16 0:39 ` [PATCH v2 10/21] fuse: prepare to cache statx attributes from entry replies Joanne Koong
2026-05-16 0:39 ` [PATCH v2 11/21] fuse: clean up fuse_dentry_revalidate() Joanne Koong
2026-05-16 0:39 ` [PATCH v2 12/21] fuse: add struct fuse_entry2_out and helpers for extended entry replies Joanne Koong
2026-05-16 0:39 ` [PATCH v2 13/21] fuse: add passthrough lookup Joanne Koong
2026-05-16 0:39 ` [PATCH v2 14/21] fuse: add passthrough support for entry creation Joanne Koong
2026-05-16 0:39 ` [PATCH v2 15/21] fuse: add passthrough support for create+open Joanne Koong
2026-05-16 0:39 ` [PATCH v2 16/21] fuse: allow backing_id=0 in open to inherit inode's backing file Joanne Koong
2026-05-16 0:40 ` [PATCH v2 17/21] backing-inode: add backing_inode_copyattr() Joanne Koong
2026-05-16 0:40 ` [PATCH v2 18/21] backing-inode: add backing_inode_setattr() Joanne Koong
2026-05-16 0:40 ` [PATCH v2 19/21] fuse: add passthrough setattr Joanne Koong
2026-05-16 1:04 ` Joanne Koong
2026-05-16 0:40 ` [PATCH v2 20/21] fuse: use passthrough getattr in setattr suid/sgid handling Joanne Koong
2026-05-16 1:20 ` Joanne Koong
2026-05-16 0:40 ` [PATCH v2 21/21] docs: fuse: document extended passthrough (FUSE_PASSTHROUGH_INO) Joanne Koong
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox