mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH] fuse: add inode generation number support
@ 2026-09-27 14:14 Russell Harmon
  2026-09-28  1:43 ` [PATCH v2] " Russell Harmon
  0 siblings, 1 reply; 7+ messages in thread
From: Russell Harmon @ 2026-09-27 14:14 UTC (permalink / raw)
  To: miklos
  Cc: corbet, skhan, rdunlap, fuse-devel, linux-doc, linux-kernel,
	Russell Harmon

This patch adds support for propagating the inode generation number from
the FUSE server to the kernel. This is useful for exporting FUSE
filesystems over NFS, where the generation number is used to detect
stale file handles (ESTALE) when inodes are recycled.

Key changes:
- Bump FUSE protocol version to 7.47.
- Repurpose the unused `dummy` field in `struct fuse_attr_out` as
  `generation`.
- Add a `FUSE_ATTR_GENERATION` INIT flag with which the filesystem opts
  into the kernel consuming that field. Gating on the protocol minor
  version alone would break existing filesystems: the minor version only
  reflects the library, not whether the individual filesystem fills the
  field, and a zero there would look like a generation change for any
  filesystem that reports nonzero generations in LOOKUP.
- Update `fuse_change_attributes` and related functions to accept and set
  `inode->i_generation`.
- Populate `i_generation` from `LOOKUP`, `GETATTR`, and `READDIRPLUS`
  responses.
- Detect nodeid recycling on `GETATTR` and `SETATTR` responses via
  `fuse_stale_inode()`, the same check already used by the `LOOKUP` and
  `READDIRPLUS` paths, and mark the inode bad (EIO) instead of merging
  the recycled file's attributes into the existing, possibly still-open,
  inode.
- Update `fuse_get_dentry` to validate the generation number against the
  file handle, returning ESTALE on mismatch.
- Maintain backward compatibility: without `FUSE_ATTR_GENERATION` the
  generation field in attr replies is ignored.

Verification:
Tested with a QEMU harness in fuse-generation-qemu against a patched
libfuse (FUSE_CAP_ATTR_GENERATION, fuse_reply_attr_with_generation) and
its passthrough_ll example reporting real backing-filesystem generation
numbers. The suite verifies that:
1.  The generation from `LOOKUP` reaches `name_to_handle_at()` file
    handles and matches the backing filesystem's FS_IOC_GETVERSION.
2.  `open_by_handle_at()` succeeds for a valid handle and fails with
    ESTALE for a handle whose generation does not match, both while the
    inode is cached and after cache eviction.
3.  When a `GETATTR` reply reports a new generation for a cached inode
    (inode recycling), the kernel marks the inode bad: fstat() on an
    open fd fails with EIO, while a fresh path lookup recovers and
    pre-recycling file handles fail with ESTALE.

Signed-off-by: Russell Harmon <russ@har.mn>
Assisted-by: Gemini:gemini-3.1
---
 Documentation/filesystems/fuse/fuse.rst | 32 +++++++++++++++++++++++++
 fs/fuse/dir.c                           | 21 +++++++++++-----
 fs/fuse/fuse_i.h                        | 10 ++++++--
 fs/fuse/inode.c                         | 23 ++++++++++++------
 fs/fuse/readdir.c                       |  2 +-
 include/uapi/linux/fuse.h               |  9 ++++++-
 6 files changed, 80 insertions(+), 17 deletions(-)

diff --git a/Documentation/filesystems/fuse/fuse.rst b/Documentation/filesystems/fuse/fuse.rst
index 0fbd5a03fdc9..f67bc9fc6316 100644
--- a/Documentation/filesystems/fuse/fuse.rst
+++ b/Documentation/filesystems/fuse/fuse.rst
@@ -49,6 +49,38 @@ using the sftp protocol.
 The userspace library and utilities are available from the
 `FUSE homepage: <https://github.com/libfuse/>`_
 
+NFS export support
+==================
+
+FUSE filesystems can be exported via NFS if the filesystem daemon supports it.
+For reliable NFS export, the filesystem should provide a unique inode
+generation number for each inode. This generation number is used by the
+NFS server to distinguish between different file instances that may
+share the same inode number (e.g. after an inode number is reused).
+
+The inode generation number is provided by the filesystem daemon in the
+following messages:
+
+- `FUSE_LOOKUP`
+- `FUSE_GETATTR` (see below)
+- `FUSE_SETATTR` (see below)
+- `FUSE_READDIRPLUS`
+- `FUSE_CREATE` / `FUSE_TMPFILE` / `FUSE_MKNOD` / `FUSE_MKDIR` / `FUSE_SYMLINK` / `FUSE_LINK`
+
+A daemon that keeps the generation number in its `FUSE_GETATTR` and
+`FUSE_SETATTR` replies (the `generation` field of `fuse_attr_out`,
+protocol 7.46) must announce this by setting `FUSE_ATTR_GENERATION` in
+its `FUSE_INIT` reply flags. When the flag is negotiated, the kernel
+compares the generation in every getattr/setattr reply against the
+cached inode: a mismatch means the daemon has reused the node ID for a
+different file, and the cached inode is marked bad (subsequent
+operations on it fail with EIO). Without the flag, the field is ignored
+and the generation is only taken from lookup-type replies, preserving
+the behavior of existing filesystems.
+
+If the filesystem daemon does not provide a generation number, the kernel
+will use a default value of 0.
+
 Filesystem type
 ===============
 
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index e49b4e874b15..8f0822847337 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -456,7 +456,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
 		forget_all_cached_acls(inode);
 		fuse_change_attributes(inode, &outarg.attr, NULL,
 				       ATTR_TIMEOUT(&outarg),
-				       attr_version);
+				       attr_version, outarg.generation);
 		fuse_change_entry_timeout(entry, &outarg);
 	} else if (inode) {
 		fi = get_fuse_inode(inode);
@@ -1476,7 +1476,8 @@ static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode,
 	fuse_statx_to_attr(&outarg.stat, &attr);
 	if ((sx->mask & STATX_BASIC_STATS) == STATX_BASIC_STATS) {
 		fuse_change_attributes(inode, &attr, &outarg.stat,
-				       ATTR_TIMEOUT(&outarg), attr_version);
+				       ATTR_TIMEOUT(&outarg), attr_version,
+				       inode->i_generation);
 	}
 
 	if (stat) {
@@ -1521,14 +1522,17 @@ static int fuse_do_getattr(struct mnt_idmap *idmap, struct inode *inode,
 	args.out_args[0].value = &outarg;
 	err = fuse_simple_request(fm, &args);
 	if (!err) {
+		u64 generation = fm->fc->attr_generation ?
+				  outarg.generation : inode->i_generation;
+
 		if (fuse_invalid_attr(&outarg.attr) ||
-		    inode_wrong_type(inode, outarg.attr.mode)) {
+		    fuse_stale_inode(inode, generation, &outarg.attr)) {
 			fuse_make_bad(inode);
 			err = -EIO;
 		} else {
 			fuse_change_attributes(inode, &outarg.attr, NULL,
 					       ATTR_TIMEOUT(&outarg),
-					       attr_version);
+					       attr_version, generation);
 			if (stat)
 				fuse_fillattr(idmap, inode, &outarg.attr, stat);
 		}
@@ -2160,6 +2164,7 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 	bool trust_local_cmtime = is_wb;
 	bool fault_blocked = false;
 	u64 attr_version;
+	u64 generation;
 
 	if (!fc->default_permissions)
 		attr->ia_valid |= ATTR_FORCE;
@@ -2252,8 +2257,11 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 		goto error;
 	}
 
+	generation = fc->attr_generation ? outarg.generation :
+					   inode->i_generation;
+
 	if (fuse_invalid_attr(&outarg.attr) ||
-	    inode_wrong_type(inode, outarg.attr.mode)) {
+	    fuse_stale_inode(inode, generation, &outarg.attr)) {
 		fuse_make_bad(inode);
 		err = -EIO;
 		goto error;
@@ -2279,7 +2287,8 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 
 	fuse_change_attributes_common(inode, &outarg.attr, NULL,
 				      ATTR_TIMEOUT(&outarg),
-				      fuse_get_cache_mask(inode), 0);
+				      fuse_get_cache_mask(inode), 0,
+				      generation);
 	oldsize = inode->i_size;
 	/* see the comment in fuse_change_attributes() */
 	if (!is_wb || is_truncate)
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index c8d4c5f3af7e..cfeb98c217a2 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -514,6 +514,12 @@ struct fuse_conn {
 	 */
 	unsigned export_support:1;
 
+	/**
+	 * @attr_generation: Filesystem fills the generation field of
+	 * fuse_attr_out in GETATTR and SETATTR replies. Only set in INIT
+	 */
+	unsigned attr_generation:1;
+
 	/** @writeback_cache: write-back cache policy (default is write-through) */
 	unsigned writeback_cache:1;
 
@@ -988,12 +994,12 @@ void fuse_init_symlink(struct inode *inode);
  */
 void fuse_change_attributes(struct inode *inode, struct fuse_attr *attr,
 			    struct fuse_statx *sx,
-			    u64 attr_valid, u64 attr_version);
+			    u64 attr_valid, u64 attr_version, u64 generation);
 
 void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr,
 				   struct fuse_statx *sx,
 				   u64 attr_valid, u32 cache_mask,
-				   u64 evict_ctr);
+				   u64 evict_ctr, u64 generation);
 
 u32 fuse_get_cache_mask(struct inode *inode);
 
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index e9552be3637b..584dce2b21fd 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -210,7 +210,7 @@ static ino_t fuse_squash_ino(u64 ino64)
 void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr,
 				   struct fuse_statx *sx,
 				   u64 attr_valid, u32 cache_mask,
-				   u64 evict_ctr)
+				   u64 evict_ctr, u64 generation)
 {
 	struct fuse_conn *fc = get_fuse_conn(inode);
 	struct fuse_inode *fi = get_fuse_inode(inode);
@@ -240,6 +240,7 @@ void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr,
 	inode->i_uid     = make_kuid(fc->user_ns, attr->uid);
 	inode->i_gid     = make_kgid(fc->user_ns, attr->gid);
 	inode->i_blocks  = attr->blocks;
+	inode->i_generation = generation;
 
 	/* Sanitize nsecs */
 	attr->atimensec = min_t(u32, attr->atimensec, NSEC_PER_SEC - 1);
@@ -313,7 +314,8 @@ u32 fuse_get_cache_mask(struct inode *inode)
 
 static void fuse_change_attributes_i(struct inode *inode, struct fuse_attr *attr,
 				     struct fuse_statx *sx, u64 attr_valid,
-				     u64 attr_version, u64 evict_ctr)
+				     u64 attr_version, u64 evict_ctr,
+				     u64 generation)
 {
 	struct fuse_conn *fc = get_fuse_conn(inode);
 	struct fuse_inode *fi = get_fuse_inode(inode);
@@ -348,7 +350,7 @@ static void fuse_change_attributes_i(struct inode *inode, struct fuse_attr *attr
 
 	old_mtime = inode_get_mtime(inode);
 	fuse_change_attributes_common(inode, attr, sx, attr_valid, cache_mask,
-				      evict_ctr);
+				      evict_ctr, generation);
 
 	oldsize = inode->i_size;
 	/*
@@ -391,9 +393,10 @@ static void fuse_change_attributes_i(struct inode *inode, 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)
+			    u64 attr_version, u64 generation)
 {
-	fuse_change_attributes_i(inode, attr, sx, attr_valid, attr_version, 0);
+	fuse_change_attributes_i(inode, attr, sx, attr_valid, attr_version, 0,
+				 generation);
 }
 
 static void fuse_init_submount_lookup(struct fuse_submount_lookup *sl,
@@ -514,7 +517,7 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
 	spin_unlock(&fi->lock);
 done:
 	fuse_change_attributes_i(inode, attr, NULL, attr_valid, attr_version,
-				 evict_ctr);
+				 evict_ctr, generation);
 	if (is_new_inode)
 		unlock_new_inode(inode);
 	return inode;
@@ -1063,6 +1066,10 @@ static struct dentry *fuse_get_dentry(struct super_block *sb,
 		goto out_err;
 
 	inode = ilookup5(sb, handle->nodeid, fuse_inode_eq, &handle->nodeid);
+	if (inode && inode->i_generation != handle->generation) {
+		iput(inode);
+		inode = NULL;
+	}
 	if (!inode) {
 		struct fuse_entry_out outarg;
 
@@ -1399,6 +1406,8 @@ static void process_init_reply(struct fuse_args *args, int error)
 			}
 			if (flags & FUSE_NO_EXPORT_SUPPORT)
 				fm->sb->s_export_op = &fuse_export_fid_operations;
+			if (flags & FUSE_ATTR_GENERATION)
+				fc->attr_generation = 1;
 			if (flags & FUSE_ALLOW_IDMAP) {
 				if (fc->default_permissions)
 					fm->sb->s_iflags &= ~SB_I_NOIDMAP;
@@ -1468,7 +1477,7 @@ static struct fuse_init_args *fuse_new_init(struct fuse_mount *fm)
 		FUSE_SECURITY_CTX | FUSE_CREATE_SUPP_GROUP |
 		FUSE_HAS_EXPIRE_ONLY | FUSE_DIRECT_IO_ALLOW_MMAP |
 		FUSE_NO_EXPORT_SUPPORT | FUSE_HAS_RESEND | FUSE_ALLOW_IDMAP |
-		FUSE_REQUEST_TIMEOUT;
+		FUSE_REQUEST_TIMEOUT | FUSE_ATTR_GENERATION;
 #ifdef CONFIG_FUSE_DAX
 	if (fm->fc->dax)
 		flags |= FUSE_MAP_ALIGNMENT;
diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
index d2599043f7ec..40781f365fc5 100644
--- a/fs/fuse/readdir.c
+++ b/fs/fuse/readdir.c
@@ -229,7 +229,7 @@ static int fuse_direntplus_link(struct file *file,
 		forget_all_cached_acls(inode);
 		fuse_change_attributes(inode, &o->attr, NULL,
 				       ATTR_TIMEOUT(o),
-				       attr_version);
+				       attr_version, o->generation);
 		/*
 		 * The other branch comes via fuse_iget()
 		 * which bumps nlookup inside
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 7435e09c87fe..143560dc029f 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -248,6 +248,10 @@
  *  - add bufpool offset field to fuse_uring_ent_in_out struct
  *  - add FUSE_URING_ZERO_COPY, FUSE_URING_ENT_ZERO_COPY, and
  *    FOPEN_IO_URING_ZERO_COPY flag
+ *
+ *  7.47
+ *  - add generation to fuse_attr_out
+ *  - add FUSE_ATTR_GENERATION
  */
 
 #ifndef _LINUX_FUSE_H
@@ -464,6 +468,8 @@ struct fuse_file_lock {
  * FUSE_REQUEST_TIMEOUT: kernel supports timing out requests.
  *			 init_out.request_timeout contains the timeout (in secs)
  * FUSE_HAS_IO_URING_BUFPOOL: kernel supports io-uring buffer pools
+ * FUSE_ATTR_GENERATION: filesystem fills the generation field of
+ *			 fuse_attr_out in GETATTR and SETATTR replies
  */
 #define FUSE_ASYNC_READ		(1 << 0)
 #define FUSE_POSIX_LOCKS	(1 << 1)
@@ -512,6 +518,7 @@ struct fuse_file_lock {
 #define FUSE_OVER_IO_URING	(1ULL << 41)
 #define FUSE_REQUEST_TIMEOUT	(1ULL << 42)
 #define FUSE_HAS_IO_URING_BUFPOOL (1ULL << 43)
+#define FUSE_ATTR_GENERATION	(1ULL << 44)
 
 /**
  * CUSE INIT request/reply flags
@@ -742,7 +749,7 @@ struct fuse_getattr_in {
 struct fuse_attr_out {
 	uint64_t	attr_valid;	/* Cache timeout for the attributes */
 	uint32_t	attr_valid_nsec;
-	uint32_t	dummy;
+	uint32_t	generation;
 	struct fuse_attr attr;
 };
 
-- 
2.43.0


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

* [PATCH v2] fuse: add inode generation number support
  2026-09-27 14:14 [PATCH] fuse: add inode generation number support Russell Harmon
@ 2026-09-28  1:43 ` Russell Harmon
  2026-09-28  9:38   ` Amir Goldstein
  0 siblings, 1 reply; 7+ messages in thread
From: Russell Harmon @ 2026-09-28  1:43 UTC (permalink / raw)
  To: miklos
  Cc: corbet, skhan, rdunlap, fuse-devel, linux-doc, linux-kernel,
	Russell Harmon

This patch adds support for propagating the inode generation number from
the FUSE server to the kernel. This is useful for exporting FUSE
filesystems over NFS, where the generation number is used to detect
stale file handles (ESTALE) when inodes are recycled.

Key changes:
- Bump FUSE protocol version to 7.47.
- Repurpose the unused `dummy` field in `struct fuse_attr_out` as
  `generation`.
- Add a `FUSE_ATTR_GENERATION` INIT flag with which the filesystem opts
  into the kernel consuming that field. Gating on the protocol minor
  version alone would break existing filesystems: the minor version only
  reflects the library, not whether the individual filesystem fills the
  field, and a zero there would look like a generation change for any
  filesystem that reports nonzero generations in LOOKUP.
- Update `fuse_change_attributes` and related functions to accept and set
  `inode->i_generation`.
- Populate `i_generation` from `LOOKUP`, `GETATTR`, and `READDIRPLUS`
  responses.
- Detect nodeid recycling on `GETATTR` and `SETATTR` responses via
  `fuse_stale_inode()`, the same check already used by the `LOOKUP` and
  `READDIRPLUS` paths, and mark the inode bad (EIO) instead of merging
  the recycled file's attributes into the existing, possibly still-open,
  inode.
- Update `fuse_get_dentry` to validate the generation number against the
  file handle, returning ESTALE on mismatch.
- Maintain backward compatibility: without `FUSE_ATTR_GENERATION` the
  generation field in attr replies is ignored.

Verification:
Tested with a QEMU harness in fuse-generation-qemu against a patched
libfuse (FUSE_CAP_ATTR_GENERATION, fuse_reply_attr_with_generation) and
its passthrough_ll example reporting real backing-filesystem generation
numbers. The suite verifies that:
1.  The generation from `LOOKUP` reaches `name_to_handle_at()` file
    handles and matches the backing filesystem's FS_IOC_GETVERSION.
2.  `open_by_handle_at()` succeeds for a valid handle and fails with
    ESTALE for a handle whose generation does not match, both while the
    inode is cached and after cache eviction.
3.  When a `GETATTR` reply reports a new generation for a cached inode
    (inode recycling), the kernel marks the inode bad: fstat() on an
    open fd fails with EIO, while a fresh path lookup recovers and
    pre-recycling file handles fail with ESTALE.

Signed-off-by: Russell Harmon <russ@har.mn>
Assisted-by: Gemini:gemini-3.1
---
v2:
- Bump FUSE_KERNEL_MINOR_VERSION to 47 to match the new 7.47 changelog
  entry (v1 added the entry but left the minor at 46).

v1: https://lore.kernel.org/all/20260927141437.1432584-1-russ@har.mn/

 Documentation/filesystems/fuse/fuse.rst | 32 +++++++++++++++++++++++++
 fs/fuse/dir.c                           | 21 +++++++++++-----
 fs/fuse/fuse_i.h                        | 10 ++++++--
 fs/fuse/inode.c                         | 23 ++++++++++++------
 fs/fuse/readdir.c                       |  2 +-
 include/uapi/linux/fuse.h               | 11 +++++++--
 6 files changed, 81 insertions(+), 18 deletions(-)

diff --git a/Documentation/filesystems/fuse/fuse.rst b/Documentation/filesystems/fuse/fuse.rst
index 0fbd5a03fdc9..f67bc9fc6316 100644
--- a/Documentation/filesystems/fuse/fuse.rst
+++ b/Documentation/filesystems/fuse/fuse.rst
@@ -49,6 +49,38 @@ using the sftp protocol.
 The userspace library and utilities are available from the
 `FUSE homepage: <https://github.com/libfuse/>`_
 
+NFS export support
+==================
+
+FUSE filesystems can be exported via NFS if the filesystem daemon supports it.
+For reliable NFS export, the filesystem should provide a unique inode
+generation number for each inode. This generation number is used by the
+NFS server to distinguish between different file instances that may
+share the same inode number (e.g. after an inode number is reused).
+
+The inode generation number is provided by the filesystem daemon in the
+following messages:
+
+- `FUSE_LOOKUP`
+- `FUSE_GETATTR` (see below)
+- `FUSE_SETATTR` (see below)
+- `FUSE_READDIRPLUS`
+- `FUSE_CREATE` / `FUSE_TMPFILE` / `FUSE_MKNOD` / `FUSE_MKDIR` / `FUSE_SYMLINK` / `FUSE_LINK`
+
+A daemon that keeps the generation number in its `FUSE_GETATTR` and
+`FUSE_SETATTR` replies (the `generation` field of `fuse_attr_out`,
+protocol 7.46) must announce this by setting `FUSE_ATTR_GENERATION` in
+its `FUSE_INIT` reply flags. When the flag is negotiated, the kernel
+compares the generation in every getattr/setattr reply against the
+cached inode: a mismatch means the daemon has reused the node ID for a
+different file, and the cached inode is marked bad (subsequent
+operations on it fail with EIO). Without the flag, the field is ignored
+and the generation is only taken from lookup-type replies, preserving
+the behavior of existing filesystems.
+
+If the filesystem daemon does not provide a generation number, the kernel
+will use a default value of 0.
+
 Filesystem type
 ===============
 
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index e49b4e874b15..8f0822847337 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -456,7 +456,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
 		forget_all_cached_acls(inode);
 		fuse_change_attributes(inode, &outarg.attr, NULL,
 				       ATTR_TIMEOUT(&outarg),
-				       attr_version);
+				       attr_version, outarg.generation);
 		fuse_change_entry_timeout(entry, &outarg);
 	} else if (inode) {
 		fi = get_fuse_inode(inode);
@@ -1476,7 +1476,8 @@ static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode,
 	fuse_statx_to_attr(&outarg.stat, &attr);
 	if ((sx->mask & STATX_BASIC_STATS) == STATX_BASIC_STATS) {
 		fuse_change_attributes(inode, &attr, &outarg.stat,
-				       ATTR_TIMEOUT(&outarg), attr_version);
+				       ATTR_TIMEOUT(&outarg), attr_version,
+				       inode->i_generation);
 	}
 
 	if (stat) {
@@ -1521,14 +1522,17 @@ static int fuse_do_getattr(struct mnt_idmap *idmap, struct inode *inode,
 	args.out_args[0].value = &outarg;
 	err = fuse_simple_request(fm, &args);
 	if (!err) {
+		u64 generation = fm->fc->attr_generation ?
+				  outarg.generation : inode->i_generation;
+
 		if (fuse_invalid_attr(&outarg.attr) ||
-		    inode_wrong_type(inode, outarg.attr.mode)) {
+		    fuse_stale_inode(inode, generation, &outarg.attr)) {
 			fuse_make_bad(inode);
 			err = -EIO;
 		} else {
 			fuse_change_attributes(inode, &outarg.attr, NULL,
 					       ATTR_TIMEOUT(&outarg),
-					       attr_version);
+					       attr_version, generation);
 			if (stat)
 				fuse_fillattr(idmap, inode, &outarg.attr, stat);
 		}
@@ -2160,6 +2164,7 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 	bool trust_local_cmtime = is_wb;
 	bool fault_blocked = false;
 	u64 attr_version;
+	u64 generation;
 
 	if (!fc->default_permissions)
 		attr->ia_valid |= ATTR_FORCE;
@@ -2252,8 +2257,11 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 		goto error;
 	}
 
+	generation = fc->attr_generation ? outarg.generation :
+					   inode->i_generation;
+
 	if (fuse_invalid_attr(&outarg.attr) ||
-	    inode_wrong_type(inode, outarg.attr.mode)) {
+	    fuse_stale_inode(inode, generation, &outarg.attr)) {
 		fuse_make_bad(inode);
 		err = -EIO;
 		goto error;
@@ -2279,7 +2287,8 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 
 	fuse_change_attributes_common(inode, &outarg.attr, NULL,
 				      ATTR_TIMEOUT(&outarg),
-				      fuse_get_cache_mask(inode), 0);
+				      fuse_get_cache_mask(inode), 0,
+				      generation);
 	oldsize = inode->i_size;
 	/* see the comment in fuse_change_attributes() */
 	if (!is_wb || is_truncate)
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index c8d4c5f3af7e..cfeb98c217a2 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -514,6 +514,12 @@ struct fuse_conn {
 	 */
 	unsigned export_support:1;
 
+	/**
+	 * @attr_generation: Filesystem fills the generation field of
+	 * fuse_attr_out in GETATTR and SETATTR replies. Only set in INIT
+	 */
+	unsigned attr_generation:1;
+
 	/** @writeback_cache: write-back cache policy (default is write-through) */
 	unsigned writeback_cache:1;
 
@@ -988,12 +994,12 @@ void fuse_init_symlink(struct inode *inode);
  */
 void fuse_change_attributes(struct inode *inode, struct fuse_attr *attr,
 			    struct fuse_statx *sx,
-			    u64 attr_valid, u64 attr_version);
+			    u64 attr_valid, u64 attr_version, u64 generation);
 
 void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr,
 				   struct fuse_statx *sx,
 				   u64 attr_valid, u32 cache_mask,
-				   u64 evict_ctr);
+				   u64 evict_ctr, u64 generation);
 
 u32 fuse_get_cache_mask(struct inode *inode);
 
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index e9552be3637b..584dce2b21fd 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -210,7 +210,7 @@ static ino_t fuse_squash_ino(u64 ino64)
 void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr,
 				   struct fuse_statx *sx,
 				   u64 attr_valid, u32 cache_mask,
-				   u64 evict_ctr)
+				   u64 evict_ctr, u64 generation)
 {
 	struct fuse_conn *fc = get_fuse_conn(inode);
 	struct fuse_inode *fi = get_fuse_inode(inode);
@@ -240,6 +240,7 @@ void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr,
 	inode->i_uid     = make_kuid(fc->user_ns, attr->uid);
 	inode->i_gid     = make_kgid(fc->user_ns, attr->gid);
 	inode->i_blocks  = attr->blocks;
+	inode->i_generation = generation;
 
 	/* Sanitize nsecs */
 	attr->atimensec = min_t(u32, attr->atimensec, NSEC_PER_SEC - 1);
@@ -313,7 +314,8 @@ u32 fuse_get_cache_mask(struct inode *inode)
 
 static void fuse_change_attributes_i(struct inode *inode, struct fuse_attr *attr,
 				     struct fuse_statx *sx, u64 attr_valid,
-				     u64 attr_version, u64 evict_ctr)
+				     u64 attr_version, u64 evict_ctr,
+				     u64 generation)
 {
 	struct fuse_conn *fc = get_fuse_conn(inode);
 	struct fuse_inode *fi = get_fuse_inode(inode);
@@ -348,7 +350,7 @@ static void fuse_change_attributes_i(struct inode *inode, struct fuse_attr *attr
 
 	old_mtime = inode_get_mtime(inode);
 	fuse_change_attributes_common(inode, attr, sx, attr_valid, cache_mask,
-				      evict_ctr);
+				      evict_ctr, generation);
 
 	oldsize = inode->i_size;
 	/*
@@ -391,9 +393,10 @@ static void fuse_change_attributes_i(struct inode *inode, 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)
+			    u64 attr_version, u64 generation)
 {
-	fuse_change_attributes_i(inode, attr, sx, attr_valid, attr_version, 0);
+	fuse_change_attributes_i(inode, attr, sx, attr_valid, attr_version, 0,
+				 generation);
 }
 
 static void fuse_init_submount_lookup(struct fuse_submount_lookup *sl,
@@ -514,7 +517,7 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
 	spin_unlock(&fi->lock);
 done:
 	fuse_change_attributes_i(inode, attr, NULL, attr_valid, attr_version,
-				 evict_ctr);
+				 evict_ctr, generation);
 	if (is_new_inode)
 		unlock_new_inode(inode);
 	return inode;
@@ -1063,6 +1066,10 @@ static struct dentry *fuse_get_dentry(struct super_block *sb,
 		goto out_err;
 
 	inode = ilookup5(sb, handle->nodeid, fuse_inode_eq, &handle->nodeid);
+	if (inode && inode->i_generation != handle->generation) {
+		iput(inode);
+		inode = NULL;
+	}
 	if (!inode) {
 		struct fuse_entry_out outarg;
 
@@ -1399,6 +1406,8 @@ static void process_init_reply(struct fuse_args *args, int error)
 			}
 			if (flags & FUSE_NO_EXPORT_SUPPORT)
 				fm->sb->s_export_op = &fuse_export_fid_operations;
+			if (flags & FUSE_ATTR_GENERATION)
+				fc->attr_generation = 1;
 			if (flags & FUSE_ALLOW_IDMAP) {
 				if (fc->default_permissions)
 					fm->sb->s_iflags &= ~SB_I_NOIDMAP;
@@ -1468,7 +1477,7 @@ static struct fuse_init_args *fuse_new_init(struct fuse_mount *fm)
 		FUSE_SECURITY_CTX | FUSE_CREATE_SUPP_GROUP |
 		FUSE_HAS_EXPIRE_ONLY | FUSE_DIRECT_IO_ALLOW_MMAP |
 		FUSE_NO_EXPORT_SUPPORT | FUSE_HAS_RESEND | FUSE_ALLOW_IDMAP |
-		FUSE_REQUEST_TIMEOUT;
+		FUSE_REQUEST_TIMEOUT | FUSE_ATTR_GENERATION;
 #ifdef CONFIG_FUSE_DAX
 	if (fm->fc->dax)
 		flags |= FUSE_MAP_ALIGNMENT;
diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
index d2599043f7ec..40781f365fc5 100644
--- a/fs/fuse/readdir.c
+++ b/fs/fuse/readdir.c
@@ -229,7 +229,7 @@ static int fuse_direntplus_link(struct file *file,
 		forget_all_cached_acls(inode);
 		fuse_change_attributes(inode, &o->attr, NULL,
 				       ATTR_TIMEOUT(o),
-				       attr_version);
+				       attr_version, o->generation);
 		/*
 		 * The other branch comes via fuse_iget()
 		 * which bumps nlookup inside
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 7435e09c87fe..a7da139e2897 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -248,6 +248,10 @@
  *  - add bufpool offset field to fuse_uring_ent_in_out struct
  *  - add FUSE_URING_ZERO_COPY, FUSE_URING_ENT_ZERO_COPY, and
  *    FOPEN_IO_URING_ZERO_COPY flag
+ *
+ *  7.47
+ *  - add generation to fuse_attr_out
+ *  - add FUSE_ATTR_GENERATION
  */
 
 #ifndef _LINUX_FUSE_H
@@ -283,7 +287,7 @@
 #define FUSE_KERNEL_VERSION 7
 
 /** Minor version number of this interface */
-#define FUSE_KERNEL_MINOR_VERSION 46
+#define FUSE_KERNEL_MINOR_VERSION 47
 
 /** The node ID of the root inode */
 #define FUSE_ROOT_ID 1
@@ -464,6 +468,8 @@ struct fuse_file_lock {
  * FUSE_REQUEST_TIMEOUT: kernel supports timing out requests.
  *			 init_out.request_timeout contains the timeout (in secs)
  * FUSE_HAS_IO_URING_BUFPOOL: kernel supports io-uring buffer pools
+ * FUSE_ATTR_GENERATION: filesystem fills the generation field of
+ *			 fuse_attr_out in GETATTR and SETATTR replies
  */
 #define FUSE_ASYNC_READ		(1 << 0)
 #define FUSE_POSIX_LOCKS	(1 << 1)
@@ -512,6 +518,7 @@ struct fuse_file_lock {
 #define FUSE_OVER_IO_URING	(1ULL << 41)
 #define FUSE_REQUEST_TIMEOUT	(1ULL << 42)
 #define FUSE_HAS_IO_URING_BUFPOOL (1ULL << 43)
+#define FUSE_ATTR_GENERATION	(1ULL << 44)
 
 /**
  * CUSE INIT request/reply flags
@@ -742,7 +749,7 @@ struct fuse_getattr_in {
 struct fuse_attr_out {
 	uint64_t	attr_valid;	/* Cache timeout for the attributes */
 	uint32_t	attr_valid_nsec;
-	uint32_t	dummy;
+	uint32_t	generation;
 	struct fuse_attr attr;
 };
 
-- 
2.43.0


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

* Re: [PATCH v2] fuse: add inode generation number support
  2026-09-28  1:43 ` [PATCH v2] " Russell Harmon
@ 2026-09-28  9:38   ` Amir Goldstein
  2026-09-28 16:03     ` Russell Harmon
  2026-09-29 13:07     ` Luis Henriques
  0 siblings, 2 replies; 7+ messages in thread
From: Amir Goldstein @ 2026-09-28  9:38 UTC (permalink / raw)
  To: Russell Harmon
  Cc: miklos, corbet, skhan, rdunlap, fuse-devel, linux-doc,
	linux-kernel, Bernd Schubert, Luis Henriques

On Mon, Sep 28, 2026 at 3:44 AM Russell Harmon <russ@har.mn> wrote:
>
> This patch adds support for propagating the inode generation number from
> the FUSE server to the kernel.

Incomplete statement.
Without the mention of GETATTR/SETATTR this is a misleading statement.

> This is useful for exporting FUSE
> filesystems over NFS, where the generation number is used to detect
> stale file handles (ESTALE) when inodes are recycled.

How exactly does it help?
I am not trying to troll you, I am really curious. how?

Context: I have been trying to improve FUSE NFS export support for a while
I have built a library that provides reliable NFS export for FUSE passthrough fs
for specific backing file system types [1].

[1] https://github.com/amir73il/libfuse/tree/libfuse_passthrough/passthrough

It is broadly understood that real NFS export support requires extending the
FUSE protocol to identify objects using file handles and Luis has
already started
with this work [2]

[2] https://lore.kernel.org/linux-fsdevel/20260225112439.27276-1-luis@igalia.com/

So my question is, what does adding generation id to GETATTR/SETATTR
improve for FUSE filesystem writers that wish to export their filesystem to NFS?

>
> Key changes:
> - Bump FUSE protocol version to 7.47.
> - Repurpose the unused `dummy` field in `struct fuse_attr_out` as
>   `generation`.
> - Add a `FUSE_ATTR_GENERATION` INIT flag with which the filesystem opts
>   into the kernel consuming that field. Gating on the protocol minor
>   version alone would break existing filesystems: the minor version only
>   reflects the library, not whether the individual filesystem fills the
>   field, and a zero there would look like a generation change for any
>   filesystem that reports nonzero generations in LOOKUP.
> - Update `fuse_change_attributes` and related functions to accept and set
>   `inode->i_generation`.
> - Populate `i_generation` from `LOOKUP`, `GETATTR`, and `READDIRPLUS`
>   responses.
> - Detect nodeid recycling on `GETATTR` and `SETATTR` responses via
>   `fuse_stale_inode()`, the same check already used by the `LOOKUP` and
>   `READDIRPLUS` paths, and mark the inode bad (EIO) instead of merging
>   the recycled file's attributes into the existing, possibly still-open,
>   inode.
> - Update `fuse_get_dentry` to validate the generation number against the
>   file handle, returning ESTALE on mismatch.
> - Maintain backward compatibility: without `FUSE_ATTR_GENERATION` the
>   generation field in attr replies is ignored.
>
> Verification:
> Tested with a QEMU harness in fuse-generation-qemu against a patched
> libfuse (FUSE_CAP_ATTR_GENERATION, fuse_reply_attr_with_generation) and
> its passthrough_ll example reporting real backing-filesystem generation
> numbers. The suite verifies that:
> 1.  The generation from `LOOKUP` reaches `name_to_handle_at()` file
>     handles and matches the backing filesystem's FS_IOC_GETVERSION.
> 2.  `open_by_handle_at()` succeeds for a valid handle and fails with
>     ESTALE for a handle whose generation does not match, both while the
>     inode is cached and after cache eviction.

1 and 2 should work on upstream FUSE right?
This is something worth mentioning.

> 3.  When a `GETATTR` reply reports a new generation for a cached inode
>     (inode recycling), the kernel marks the inode bad: fstat() on an
>     open fd fails with EIO, while a fresh path lookup recovers and
>     pre-recycling file handles fail with ESTALE.

How did the inode recycle happen with passtrhough_ll which keeps
open fds for fuse inodes?
Something is missing from this test report.
If you just used a mock filesystem which makes no sense in the real world
then the value of this change is questionable.

>
> Signed-off-by: Russell Harmon <russ@har.mn>
> Assisted-by: Gemini:gemini-3.1
> ---
> v2:
> - Bump FUSE_KERNEL_MINOR_VERSION to 47 to match the new 7.47 changelog
>   entry (v1 added the entry but left the minor at 46).
>
> v1: https://lore.kernel.org/all/20260927141437.1432584-1-russ@har.mn/
>
>  Documentation/filesystems/fuse/fuse.rst | 32 +++++++++++++++++++++++++
>  fs/fuse/dir.c                           | 21 +++++++++++-----
>  fs/fuse/fuse_i.h                        | 10 ++++++--
>  fs/fuse/inode.c                         | 23 ++++++++++++------
>  fs/fuse/readdir.c                       |  2 +-
>  include/uapi/linux/fuse.h               | 11 +++++++--
>  6 files changed, 81 insertions(+), 18 deletions(-)
>
> diff --git a/Documentation/filesystems/fuse/fuse.rst b/Documentation/filesystems/fuse/fuse.rst
> index 0fbd5a03fdc9..f67bc9fc6316 100644
> --- a/Documentation/filesystems/fuse/fuse.rst
> +++ b/Documentation/filesystems/fuse/fuse.rst
> @@ -49,6 +49,38 @@ using the sftp protocol.
>  The userspace library and utilities are available from the
>  `FUSE homepage: <https://github.com/libfuse/>`_
>
> +NFS export support
> +==================
> +
> +FUSE filesystems can be exported via NFS if the filesystem daemon supports it.
> +For reliable NFS export, the filesystem should provide a unique inode
> +generation number for each inode. This generation number is used by the
> +NFS server to distinguish between different file instances that may
> +share the same inode number (e.g. after an inode number is reused).
> +
> +The inode generation number is provided by the filesystem daemon in the
> +following messages:
> +
> +- `FUSE_LOOKUP`
> +- `FUSE_GETATTR` (see below)
> +- `FUSE_SETATTR` (see below)
> +- `FUSE_READDIRPLUS`
> +- `FUSE_CREATE` / `FUSE_TMPFILE` / `FUSE_MKNOD` / `FUSE_MKDIR` / `FUSE_SYMLINK` / `FUSE_LINK`
> +
> +A daemon that keeps the generation number in its `FUSE_GETATTR` and
> +`FUSE_SETATTR` replies (the `generation` field of `fuse_attr_out`,
> +protocol 7.46) must announce this by setting `FUSE_ATTR_GENERATION` in
> +its `FUSE_INIT` reply flags. When the flag is negotiated, the kernel
> +compares the generation in every getattr/setattr reply against the
> +cached inode: a mismatch means the daemon has reused the node ID for a
> +different file, and the cached inode is marked bad (subsequent
> +operations on it fail with EIO). Without the flag, the field is ignored
> +and the generation is only taken from lookup-type replies, preserving
> +the behavior of existing filesystems.
> +
> +If the filesystem daemon does not provide a generation number, the kernel
> +will use a default value of 0.
> +

On the one hand, I still need to understand the value of reporting generation
in GETATTR/SETATTR.

On the other hand, I do see the value in the server negotiating at init time the
fact that "Generation values are reliable".

What happens today is that NFS exporting is allowed for all FUSE filesystems
regardless of the reliability of generation id, so after inode evict
and recycle,
an NFSv3 client  that had access to inode X.Y may get access to a completely
different file or even a directory with inode X.Y, where X is the recycle nodeid
and Y is an unreliable generation provided by the server.

The problem is that FUSE does not require opt-in for NFS export, it only
allows servers to opt-out of NFS export (FUSE_NO_EXPORT_SUPPORT).

So what can be done given a declaration of the server that generation
is reliable?

One option is to set a non-zero uuid/fsid to the fuse filesystem.
This will allow exporting the fuse filesystem without the opt-in uuid/fsid=
in /etc/exports.
This will also allow setting fanotify FAN_MARK_FILESYSTEM watches
on this fuse filesystem, whose file handles could be trusted to be a genuine
unique identity of the filesystem objects.

But if we take this route, it is better to take it one step further
and allow the
server to determine the filesystem uuid/fsid during negotiation.

In any case, I am not convinced there is value in doing all this
without extending
the protocol with LOOKUP_HANDLE/LOOKUPX lookup by file handle, so if you
have compelling use cases, please spell them out.

Thanks,
Amir.

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

* Re: [PATCH v2] fuse: add inode generation number support
  2026-09-28  9:38   ` Amir Goldstein
@ 2026-09-28 16:03     ` Russell Harmon
  2026-09-28 16:54       ` Amir Goldstein
  2026-09-29 13:07     ` Luis Henriques
  1 sibling, 1 reply; 7+ messages in thread
From: Russell Harmon @ 2026-09-28 16:03 UTC (permalink / raw)
  To: Amir Goldstein
  Cc: miklos, corbet, skhan, rdunlap, fuse-devel, linux-doc,
	linux-kernel, Bernd Schubert, Luis Henriques

(re-sending in plaintext)

Thanks for the review! Comments inline.

On Mon, Sep 28, 2026 at 2:38 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Mon, Sep 28, 2026 at 3:44 AM Russell Harmon <russ@har.mn> wrote:
> >
> > This patch adds support for propagating the inode generation number from
> > the FUSE server to the kernel.
>
> Incomplete statement.
> Without the mention of GETATTR/SETATTR this is a misleading statement.
>
> > This is useful for exporting FUSE
> > filesystems over NFS, where the generation number is used to detect
> > stale file handles (ESTALE) when inodes are recycled.
>
> How exactly does it help?
> I am not trying to troll you, I am really curious. how?
>
> Context: I have been trying to improve FUSE NFS export support for a while
> I have built a library that provides reliable NFS export for FUSE passthrough fs
> for specific backing file system types [1].
>
> [1] https://github.com/amir73il/libfuse/tree/libfuse_passthrough/passthrough
>
> It is broadly understood that real NFS export support requires extending the
> FUSE protocol to identify objects using file handles and Luis has
> already started
> with this work [2]
>
> [2] https://lore.kernel.org/linux-fsdevel/20260225112439.27276-1-luis@igalia.com/
>
> So my question is, what does adding generation id to GETATTR/SETATTR
> improve for FUSE filesystem writers that wish to export their filesystem to NFS?

Long answer is https://russ.har.mn/blog/2026-04-09/fuse-loopback-is-incomplete

Short answer is that inode on its own doesn't uniquely identify a
file. The unique identifier is inode + generation. Reason being that a
user can delete a file and create a new one, and the filesystem is
allowed to reuse the inode (but if it does it must use a different
generation).

As for why it needs to be a part of GETATTR/SETATTR, consider that,
given the above, without a generation number such calls are actually
ambiguous. They don't uniquely identify a filesystem object. e.g.
"what are you asked to GETATTR/SETATTR _on_?"

> >
> > Key changes:
> > - Bump FUSE protocol version to 7.47.
> > - Repurpose the unused `dummy` field in `struct fuse_attr_out` as
> >   `generation`.
> > - Add a `FUSE_ATTR_GENERATION` INIT flag with which the filesystem opts
> >   into the kernel consuming that field. Gating on the protocol minor
> >   version alone would break existing filesystems: the minor version only
> >   reflects the library, not whether the individual filesystem fills the
> >   field, and a zero there would look like a generation change for any
> >   filesystem that reports nonzero generations in LOOKUP.
> > - Update `fuse_change_attributes` and related functions to accept and set
> >   `inode->i_generation`.
> > - Populate `i_generation` from `LOOKUP`, `GETATTR`, and `READDIRPLUS`
> >   responses.
> > - Detect nodeid recycling on `GETATTR` and `SETATTR` responses via
> >   `fuse_stale_inode()`, the same check already used by the `LOOKUP` and
> >   `READDIRPLUS` paths, and mark the inode bad (EIO) instead of merging
> >   the recycled file's attributes into the existing, possibly still-open,
> >   inode.
> > - Update `fuse_get_dentry` to validate the generation number against the
> >   file handle, returning ESTALE on mismatch.
> > - Maintain backward compatibility: without `FUSE_ATTR_GENERATION` the
> >   generation field in attr replies is ignored.
> >
> > Verification:
> > Tested with a QEMU harness in fuse-generation-qemu against a patched
> > libfuse (FUSE_CAP_ATTR_GENERATION, fuse_reply_attr_with_generation) and
> > its passthrough_ll example reporting real backing-filesystem generation
> > numbers. The suite verifies that:
> > 1.  The generation from `LOOKUP` reaches `name_to_handle_at()` file
> >     handles and matches the backing filesystem's FS_IOC_GETVERSION.
> > 2.  `open_by_handle_at()` succeeds for a valid handle and fails with
> >     ESTALE for a handle whose generation does not match, both while the
> >     inode is cached and after cache eviction.
>
> 1 and 2 should work on upstream FUSE right?
> This is something worth mentioning.
>
> > 3.  When a `GETATTR` reply reports a new generation for a cached inode
> >     (inode recycling), the kernel marks the inode bad: fstat() on an
> >     open fd fails with EIO, while a fresh path lookup recovers and
> >     pre-recycling file handles fail with ESTALE.
>
> How did the inode recycle happen with passtrhough_ll which keeps
> open fds for fuse inodes?
> Something is missing from this test report.
> If you just used a mock filesystem which makes no sense in the real world
> then the value of this change is questionable.

Ack, I'll dig more into this. Sending responses to your other
questions now and will reply again once I've got an answer.


> > Signed-off-by: Russell Harmon <russ@har.mn>
> > Assisted-by: Gemini:gemini-3.1
> > ---
> > v2:
> > - Bump FUSE_KERNEL_MINOR_VERSION to 47 to match the new 7.47 changelog
> >   entry (v1 added the entry but left the minor at 46).
> >
> > v1: https://lore.kernel.org/all/20260927141437.1432584-1-russ@har.mn/
> >
> >  Documentation/filesystems/fuse/fuse.rst | 32 +++++++++++++++++++++++++
> >  fs/fuse/dir.c                           | 21 +++++++++++-----
> >  fs/fuse/fuse_i.h                        | 10 ++++++--
> >  fs/fuse/inode.c                         | 23 ++++++++++++------
> >  fs/fuse/readdir.c                       |  2 +-
> >  include/uapi/linux/fuse.h               | 11 +++++++--
> >  6 files changed, 81 insertions(+), 18 deletions(-)
> >
> > diff --git a/Documentation/filesystems/fuse/fuse.rst b/Documentation/filesystems/fuse/fuse.rst
> > index 0fbd5a03fdc9..f67bc9fc6316 100644
> > --- a/Documentation/filesystems/fuse/fuse.rst
> > +++ b/Documentation/filesystems/fuse/fuse.rst
> > @@ -49,6 +49,38 @@ using the sftp protocol.
> >  The userspace library and utilities are available from the
> >  `FUSE homepage: <https://github.com/libfuse/>`_
> >
> > +NFS export support
> > +==================
> > +
> > +FUSE filesystems can be exported via NFS if the filesystem daemon supports it.
> > +For reliable NFS export, the filesystem should provide a unique inode
> > +generation number for each inode. This generation number is used by the
> > +NFS server to distinguish between different file instances that may
> > +share the same inode number (e.g. after an inode number is reused).
> > +
> > +The inode generation number is provided by the filesystem daemon in the
> > +following messages:
> > +
> > +- `FUSE_LOOKUP`
> > +- `FUSE_GETATTR` (see below)
> > +- `FUSE_SETATTR` (see below)
> > +- `FUSE_READDIRPLUS`
> > +- `FUSE_CREATE` / `FUSE_TMPFILE` / `FUSE_MKNOD` / `FUSE_MKDIR` / `FUSE_SYMLINK` / `FUSE_LINK`
> > +
> > +A daemon that keeps the generation number in its `FUSE_GETATTR` and
> > +`FUSE_SETATTR` replies (the `generation` field of `fuse_attr_out`,
> > +protocol 7.46) must announce this by setting `FUSE_ATTR_GENERATION` in
> > +its `FUSE_INIT` reply flags. When the flag is negotiated, the kernel
> > +compares the generation in every getattr/setattr reply against the
> > +cached inode: a mismatch means the daemon has reused the node ID for a
> > +different file, and the cached inode is marked bad (subsequent
> > +operations on it fail with EIO). Without the flag, the field is ignored
> > +and the generation is only taken from lookup-type replies, preserving
> > +the behavior of existing filesystems.
> > +
> > +If the filesystem daemon does not provide a generation number, the kernel
> > +will use a default value of 0.
> > +
>
> On the one hand, I still need to understand the value of reporting generation
> in GETATTR/SETATTR.
>
> On the other hand, I do see the value in the server negotiating at init time the
> fact that "Generation values are reliable".
>
> What happens today is that NFS exporting is allowed for all FUSE filesystems
> regardless of the reliability of generation id, so after inode evict
> and recycle,
> an NFSv3 client  that had access to inode X.Y may get access to a completely
> different file or even a directory with inode X.Y, where X is the recycle nodeid
> and Y is an unreliable generation provided by the server.
>
> The problem is that FUSE does not require opt-in for NFS export, it only
> allows servers to opt-out of NFS export (FUSE_NO_EXPORT_SUPPORT).
>
> So what can be done given a declaration of the server that generation
> is reliable?
>
> One option is to set a non-zero uuid/fsid to the fuse filesystem.
> This will allow exporting the fuse filesystem without the opt-in uuid/fsid=
> in /etc/exports.
> This will also allow setting fanotify FAN_MARK_FILESYSTEM watches
> on this fuse filesystem, whose file handles could be trusted to be a genuine
> unique identity of the filesystem objects.
>
> But if we take this route, it is better to take it one step further
> and allow the
> server to determine the filesystem uuid/fsid during negotiation.
>
> In any case, I am not convinced there is value in doing all this
> without extending
> the protocol with LOOKUP_HANDLE/LOOKUPX lookup by file handle, so if you
> have compelling use cases, please spell them out.

I'm trying to implement a caching filesystem which uses a sqlite
database (stored on an SSD) to cache all file and directory metadata,
ultimately to avoid HDD spinups (and the associated electricity cost)
for operations other than file reads. I'm also exporting this
filesystem over NFS, hence the need for NFS support.

If I remember correctly, name_to_handle_at on a FUSE filesystem
contains just the inode+generation, and open_by_handle_at results in a
LOOKUP of that inode (and now inode+generation). But I'll
double-check. Assuming my memory is correct, I think that's
sufficient, assuming that we consider inode+generation as a unique
file object identifier.

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

* Re: [PATCH v2] fuse: add inode generation number support
  2026-09-28 16:03     ` Russell Harmon
@ 2026-09-28 16:54       ` Amir Goldstein
  2026-09-29 20:55         ` Russell Harmon
  0 siblings, 1 reply; 7+ messages in thread
From: Amir Goldstein @ 2026-09-28 16:54 UTC (permalink / raw)
  To: Russell Harmon
  Cc: miklos, corbet, skhan, rdunlap, fuse-devel, linux-doc,
	linux-kernel, Bernd Schubert, Luis Henriques

On Mon, Sep 28, 2026 at 6:04 PM Russell Harmon <russ@har.mn> wrote:
>
> (re-sending in plaintext)
>
> Thanks for the review! Comments inline.
>
> On Mon, Sep 28, 2026 at 2:38 AM Amir Goldstein <amir73il@gmail.com> wrote:
> >
> > On Mon, Sep 28, 2026 at 3:44 AM Russell Harmon <russ@har.mn> wrote:
> > >
> > > This patch adds support for propagating the inode generation number from
> > > the FUSE server to the kernel.
> >
> > Incomplete statement.
> > Without the mention of GETATTR/SETATTR this is a misleading statement.
> >
> > > This is useful for exporting FUSE
> > > filesystems over NFS, where the generation number is used to detect
> > > stale file handles (ESTALE) when inodes are recycled.
> >
> > How exactly does it help?
> > I am not trying to troll you, I am really curious. how?
> >
> > Context: I have been trying to improve FUSE NFS export support for a while
> > I have built a library that provides reliable NFS export for FUSE passthrough fs
> > for specific backing file system types [1].
> >
> > [1] https://github.com/amir73il/libfuse/tree/libfuse_passthrough/passthrough
> >
> > It is broadly understood that real NFS export support requires extending the
> > FUSE protocol to identify objects using file handles and Luis has
> > already started
> > with this work [2]
> >
> > [2] https://lore.kernel.org/linux-fsdevel/20260225112439.27276-1-luis@igalia.com/
> >
> > So my question is, what does adding generation id to GETATTR/SETATTR
> > improve for FUSE filesystem writers that wish to export their filesystem to NFS?
>
> Long answer is https://russ.har.mn/blog/2026-04-09/fuse-loopback-is-incomplete
>
> Short answer is that inode on its own doesn't uniquely identify a
> file. The unique identifier is inode + generation. Reason being that a
> user can delete a file and create a new one, and the filesystem is
> allowed to reuse the inode (but if it does it must use a different
> generation).

This is obvious. That's the reason LOOKUP already returns a generation.

>
> As for why it needs to be a part of GETATTR/SETATTR, consider that,
> given the above, without a generation number such calls are actually
> ambiguous. They don't uniquely identify a filesystem object. e.g.
> "what are you asked to GETATTR/SETATTR _on_?"
>

You phrased the question correctly, but the same question
applies to any other command, i.e.
"What are you asked to OPEN/TRUNCATE _on_?"

So you see, what's needed is to change the input argument
of ALL the fuse requests from nodeid to nodeid+generation, or
in the more generic form of NFS handles, a variable size object id blob.

This is from the latest proposal for FUSEX protocol [3]

struct fusex_id {
    u64 nodeid;
    /* will extend with file handle */
};

[3] https://lore.kernel.org/fuse-devel/20260429102058.1362965-1-mszeredi@redhat.com/

> > >
> > > Key changes:
> > > - Bump FUSE protocol version to 7.47.
> > > - Repurpose the unused `dummy` field in `struct fuse_attr_out` as
> > >   `generation`.
> > > - Add a `FUSE_ATTR_GENERATION` INIT flag with which the filesystem opts
> > >   into the kernel consuming that field. Gating on the protocol minor
> > >   version alone would break existing filesystems: the minor version only
> > >   reflects the library, not whether the individual filesystem fills the
> > >   field, and a zero there would look like a generation change for any
> > >   filesystem that reports nonzero generations in LOOKUP.
> > > - Update `fuse_change_attributes` and related functions to accept and set
> > >   `inode->i_generation`.
> > > - Populate `i_generation` from `LOOKUP`, `GETATTR`, and `READDIRPLUS`
> > >   responses.
> > > - Detect nodeid recycling on `GETATTR` and `SETATTR` responses via
> > >   `fuse_stale_inode()`, the same check already used by the `LOOKUP` and
> > >   `READDIRPLUS` paths, and mark the inode bad (EIO) instead of merging
> > >   the recycled file's attributes into the existing, possibly still-open,
> > >   inode.
> > > - Update `fuse_get_dentry` to validate the generation number against the
> > >   file handle, returning ESTALE on mismatch.
> > > - Maintain backward compatibility: without `FUSE_ATTR_GENERATION` the
> > >   generation field in attr replies is ignored.
> > >
> > > Verification:
> > > Tested with a QEMU harness in fuse-generation-qemu against a patched
> > > libfuse (FUSE_CAP_ATTR_GENERATION, fuse_reply_attr_with_generation) and
> > > its passthrough_ll example reporting real backing-filesystem generation
> > > numbers. The suite verifies that:
> > > 1.  The generation from `LOOKUP` reaches `name_to_handle_at()` file
> > >     handles and matches the backing filesystem's FS_IOC_GETVERSION.
> > > 2.  `open_by_handle_at()` succeeds for a valid handle and fails with
> > >     ESTALE for a handle whose generation does not match, both while the
> > >     inode is cached and after cache eviction.
> >
> > 1 and 2 should work on upstream FUSE right?
> > This is something worth mentioning.
> >
> > > 3.  When a `GETATTR` reply reports a new generation for a cached inode
> > >     (inode recycling), the kernel marks the inode bad: fstat() on an
> > >     open fd fails with EIO, while a fresh path lookup recovers and
> > >     pre-recycling file handles fail with ESTALE.
> >
> > How did the inode recycle happen with passtrhough_ll which keeps
> > open fds for fuse inodes?
> > Something is missing from this test report.
> > If you just used a mock filesystem which makes no sense in the real world
> > then the value of this change is questionable.
>
> Ack, I'll dig more into this. Sending responses to your other
> questions now and will reply again once I've got an answer.
>
>
> > > Signed-off-by: Russell Harmon <russ@har.mn>
> > > Assisted-by: Gemini:gemini-3.1
> > > ---
> > > v2:
> > > - Bump FUSE_KERNEL_MINOR_VERSION to 47 to match the new 7.47 changelog
> > >   entry (v1 added the entry but left the minor at 46).
> > >
> > > v1: https://lore.kernel.org/all/20260927141437.1432584-1-russ@har.mn/
> > >
> > >  Documentation/filesystems/fuse/fuse.rst | 32 +++++++++++++++++++++++++
> > >  fs/fuse/dir.c                           | 21 +++++++++++-----
> > >  fs/fuse/fuse_i.h                        | 10 ++++++--
> > >  fs/fuse/inode.c                         | 23 ++++++++++++------
> > >  fs/fuse/readdir.c                       |  2 +-
> > >  include/uapi/linux/fuse.h               | 11 +++++++--
> > >  6 files changed, 81 insertions(+), 18 deletions(-)
> > >
> > > diff --git a/Documentation/filesystems/fuse/fuse.rst b/Documentation/filesystems/fuse/fuse.rst
> > > index 0fbd5a03fdc9..f67bc9fc6316 100644
> > > --- a/Documentation/filesystems/fuse/fuse.rst
> > > +++ b/Documentation/filesystems/fuse/fuse.rst
> > > @@ -49,6 +49,38 @@ using the sftp protocol.
> > >  The userspace library and utilities are available from the
> > >  `FUSE homepage: <https://github.com/libfuse/>`_
> > >
> > > +NFS export support
> > > +==================
> > > +
> > > +FUSE filesystems can be exported via NFS if the filesystem daemon supports it.
> > > +For reliable NFS export, the filesystem should provide a unique inode
> > > +generation number for each inode. This generation number is used by the
> > > +NFS server to distinguish between different file instances that may
> > > +share the same inode number (e.g. after an inode number is reused).
> > > +
> > > +The inode generation number is provided by the filesystem daemon in the
> > > +following messages:
> > > +
> > > +- `FUSE_LOOKUP`
> > > +- `FUSE_GETATTR` (see below)
> > > +- `FUSE_SETATTR` (see below)
> > > +- `FUSE_READDIRPLUS`
> > > +- `FUSE_CREATE` / `FUSE_TMPFILE` / `FUSE_MKNOD` / `FUSE_MKDIR` / `FUSE_SYMLINK` / `FUSE_LINK`
> > > +
> > > +A daemon that keeps the generation number in its `FUSE_GETATTR` and
> > > +`FUSE_SETATTR` replies (the `generation` field of `fuse_attr_out`,
> > > +protocol 7.46) must announce this by setting `FUSE_ATTR_GENERATION` in
> > > +its `FUSE_INIT` reply flags. When the flag is negotiated, the kernel
> > > +compares the generation in every getattr/setattr reply against the
> > > +cached inode: a mismatch means the daemon has reused the node ID for a
> > > +different file, and the cached inode is marked bad (subsequent
> > > +operations on it fail with EIO). Without the flag, the field is ignored
> > > +and the generation is only taken from lookup-type replies, preserving
> > > +the behavior of existing filesystems.
> > > +
> > > +If the filesystem daemon does not provide a generation number, the kernel
> > > +will use a default value of 0.
> > > +
> >
> > On the one hand, I still need to understand the value of reporting generation
> > in GETATTR/SETATTR.
> >
> > On the other hand, I do see the value in the server negotiating at init time the
> > fact that "Generation values are reliable".
> >
> > What happens today is that NFS exporting is allowed for all FUSE filesystems
> > regardless of the reliability of generation id, so after inode evict
> > and recycle,
> > an NFSv3 client  that had access to inode X.Y may get access to a completely
> > different file or even a directory with inode X.Y, where X is the recycle nodeid
> > and Y is an unreliable generation provided by the server.
> >
> > The problem is that FUSE does not require opt-in for NFS export, it only
> > allows servers to opt-out of NFS export (FUSE_NO_EXPORT_SUPPORT).
> >
> > So what can be done given a declaration of the server that generation
> > is reliable?
> >
> > One option is to set a non-zero uuid/fsid to the fuse filesystem.
> > This will allow exporting the fuse filesystem without the opt-in uuid/fsid=
> > in /etc/exports.
> > This will also allow setting fanotify FAN_MARK_FILESYSTEM watches
> > on this fuse filesystem, whose file handles could be trusted to be a genuine
> > unique identity of the filesystem objects.
> >
> > But if we take this route, it is better to take it one step further
> > and allow the
> > server to determine the filesystem uuid/fsid during negotiation.
> >
> > In any case, I am not convinced there is value in doing all this
> > without extending
> > the protocol with LOOKUP_HANDLE/LOOKUPX lookup by file handle, so if you
> > have compelling use cases, please spell them out.
>
> I'm trying to implement a caching filesystem which uses a sqlite
> database (stored on an SSD) to cache all file and directory metadata,
> ultimately to avoid HDD spinups (and the associated electricity cost)
> for operations other than file reads. I'm also exporting this
> filesystem over NFS, hence the need for NFS support.

OK, so you are implementing a passthrough filesystem (to HDD)
with a fast caching layer, I assume to cache results of GETATTR/
LOOKUP/READDIR/READDIRPLUS? Anything else?
Are you caching a specific type of backing filesystem?

I advise you to look at libfuse_passthrough [1].
You can either see if it fits you or learn from my experience with NFS
export of a fuse passthrough filesystem.
I am giving a talk on the subject in Linux Plumbers next week [4].

>
> If I remember correctly, name_to_handle_at on a FUSE filesystem
> contains just the inode+generation, and open_by_handle_at results in a
> LOOKUP of that inode (and now inode+generation). But I'll
> double-check. Assuming my memory is correct, I think that's
> sufficient, assuming that we consider inode+generation as a unique
> file object identifier.

The problem is, that what you assume is not an API definition, this is
a specific
filesystem implementation, which happens to be correct for some commonly
used filesystems (e.g. ext4, xfs), but is not true in general.
In general the API {name_to,open_by}_handle_at() the file handle is an
opaque blob.

The way that libfuse_passthrough gates against recycled inode numbers
is that the file handle is stored in the server's inode table at lookup time
(generation is recorded) and all the commands that follow, lookup in the
server's inode table (could be in sqlite in your case) and use
open_by_handle_at() with the recorded file handle from lookup time
to get an O_PATH fd to the object on the backing filesystem (on HDD).

This is called InodeRef in libfuse_passthrough and any request will return
ESTALE to the kernel if it cannot get an InodeRef.

Thanks,
Amir.

[4] https://lpc.events/event/20/contributions/2367

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

* Re: [PATCH v2] fuse: add inode generation number support
  2026-09-28  9:38   ` Amir Goldstein
  2026-09-28 16:03     ` Russell Harmon
@ 2026-09-29 13:07     ` Luis Henriques
  1 sibling, 0 replies; 7+ messages in thread
From: Luis Henriques @ 2026-09-29 13:07 UTC (permalink / raw)
  To: Amir Goldstein
  Cc: Russell Harmon, miklos, corbet, skhan, rdunlap, fuse-devel,
	linux-doc, linux-kernel, Bernd Schubert

On Mon, Sep 28 2026, Amir Goldstein wrote:

> On Mon, Sep 28, 2026 at 3:44 AM Russell Harmon <russ@har.mn> wrote:
>>
>> This patch adds support for propagating the inode generation number from
>> the FUSE server to the kernel.
>
> Incomplete statement.
> Without the mention of GETATTR/SETATTR this is a misleading statement.
>
>> This is useful for exporting FUSE
>> filesystems over NFS, where the generation number is used to detect
>> stale file handles (ESTALE) when inodes are recycled.
>
> How exactly does it help?
> I am not trying to troll you, I am really curious. how?
>
> Context: I have been trying to improve FUSE NFS export support for a while
> I have built a library that provides reliable NFS export for FUSE passthrough fs
> for specific backing file system types [1].
>
> [1] https://github.com/amir73il/libfuse/tree/libfuse_passthrough/passthrough
>
> It is broadly understood that real NFS export support requires extending the
> FUSE protocol to identify objects using file handles and Luis has
> already started
> with this work [2]

I just want to add that this work is mostly stale at the moment.  It was
decided that it needs to be done on top of fusex, the next major protocol
version bump.  Also note that the initial draft of fusex lacked a bunch of
features (e.g., it was available for local-filesystems only), so it may
take a while before a LOOKUP_HANDLE operation is available.

Cheers,
-- 
Luís

> [2] https://lore.kernel.org/linux-fsdevel/20260225112439.27276-1-luis@igalia.com/
>
> So my question is, what does adding generation id to GETATTR/SETATTR
> improve for FUSE filesystem writers that wish to export their filesystem to NFS?
>
>>
>> Key changes:
>> - Bump FUSE protocol version to 7.47.
>> - Repurpose the unused `dummy` field in `struct fuse_attr_out` as
>>   `generation`.
>> - Add a `FUSE_ATTR_GENERATION` INIT flag with which the filesystem opts
>>   into the kernel consuming that field. Gating on the protocol minor
>>   version alone would break existing filesystems: the minor version only
>>   reflects the library, not whether the individual filesystem fills the
>>   field, and a zero there would look like a generation change for any
>>   filesystem that reports nonzero generations in LOOKUP.
>> - Update `fuse_change_attributes` and related functions to accept and set
>>   `inode->i_generation`.
>> - Populate `i_generation` from `LOOKUP`, `GETATTR`, and `READDIRPLUS`
>>   responses.
>> - Detect nodeid recycling on `GETATTR` and `SETATTR` responses via
>>   `fuse_stale_inode()`, the same check already used by the `LOOKUP` and
>>   `READDIRPLUS` paths, and mark the inode bad (EIO) instead of merging
>>   the recycled file's attributes into the existing, possibly still-open,
>>   inode.
>> - Update `fuse_get_dentry` to validate the generation number against the
>>   file handle, returning ESTALE on mismatch.
>> - Maintain backward compatibility: without `FUSE_ATTR_GENERATION` the
>>   generation field in attr replies is ignored.
>>
>> Verification:
>> Tested with a QEMU harness in fuse-generation-qemu against a patched
>> libfuse (FUSE_CAP_ATTR_GENERATION, fuse_reply_attr_with_generation) and
>> its passthrough_ll example reporting real backing-filesystem generation
>> numbers. The suite verifies that:
>> 1.  The generation from `LOOKUP` reaches `name_to_handle_at()` file
>>     handles and matches the backing filesystem's FS_IOC_GETVERSION.
>> 2.  `open_by_handle_at()` succeeds for a valid handle and fails with
>>     ESTALE for a handle whose generation does not match, both while the
>>     inode is cached and after cache eviction.
>
> 1 and 2 should work on upstream FUSE right?
> This is something worth mentioning.
>
>> 3.  When a `GETATTR` reply reports a new generation for a cached inode
>>     (inode recycling), the kernel marks the inode bad: fstat() on an
>>     open fd fails with EIO, while a fresh path lookup recovers and
>>     pre-recycling file handles fail with ESTALE.
>
> How did the inode recycle happen with passtrhough_ll which keeps
> open fds for fuse inodes?
> Something is missing from this test report.
> If you just used a mock filesystem which makes no sense in the real world
> then the value of this change is questionable.
>
>>
>> Signed-off-by: Russell Harmon <russ@har.mn>
>> Assisted-by: Gemini:gemini-3.1
>> ---
>> v2:
>> - Bump FUSE_KERNEL_MINOR_VERSION to 47 to match the new 7.47 changelog
>>   entry (v1 added the entry but left the minor at 46).
>>
>> v1: https://lore.kernel.org/all/20260927141437.1432584-1-russ@har.mn/
>>
>>  Documentation/filesystems/fuse/fuse.rst | 32 +++++++++++++++++++++++++
>>  fs/fuse/dir.c                           | 21 +++++++++++-----
>>  fs/fuse/fuse_i.h                        | 10 ++++++--
>>  fs/fuse/inode.c                         | 23 ++++++++++++------
>>  fs/fuse/readdir.c                       |  2 +-
>>  include/uapi/linux/fuse.h               | 11 +++++++--
>>  6 files changed, 81 insertions(+), 18 deletions(-)
>>
>> diff --git a/Documentation/filesystems/fuse/fuse.rst b/Documentation/filesystems/fuse/fuse.rst
>> index 0fbd5a03fdc9..f67bc9fc6316 100644
>> --- a/Documentation/filesystems/fuse/fuse.rst
>> +++ b/Documentation/filesystems/fuse/fuse.rst
>> @@ -49,6 +49,38 @@ using the sftp protocol.
>>  The userspace library and utilities are available from the
>>  `FUSE homepage: <https://github.com/libfuse/>`_
>>
>> +NFS export support
>> +==================
>> +
>> +FUSE filesystems can be exported via NFS if the filesystem daemon supports it.
>> +For reliable NFS export, the filesystem should provide a unique inode
>> +generation number for each inode. This generation number is used by the
>> +NFS server to distinguish between different file instances that may
>> +share the same inode number (e.g. after an inode number is reused).
>> +
>> +The inode generation number is provided by the filesystem daemon in the
>> +following messages:
>> +
>> +- `FUSE_LOOKUP`
>> +- `FUSE_GETATTR` (see below)
>> +- `FUSE_SETATTR` (see below)
>> +- `FUSE_READDIRPLUS`
>> +- `FUSE_CREATE` / `FUSE_TMPFILE` / `FUSE_MKNOD` / `FUSE_MKDIR` / `FUSE_SYMLINK` / `FUSE_LINK`
>> +
>> +A daemon that keeps the generation number in its `FUSE_GETATTR` and
>> +`FUSE_SETATTR` replies (the `generation` field of `fuse_attr_out`,
>> +protocol 7.46) must announce this by setting `FUSE_ATTR_GENERATION` in
>> +its `FUSE_INIT` reply flags. When the flag is negotiated, the kernel
>> +compares the generation in every getattr/setattr reply against the
>> +cached inode: a mismatch means the daemon has reused the node ID for a
>> +different file, and the cached inode is marked bad (subsequent
>> +operations on it fail with EIO). Without the flag, the field is ignored
>> +and the generation is only taken from lookup-type replies, preserving
>> +the behavior of existing filesystems.
>> +
>> +If the filesystem daemon does not provide a generation number, the kernel
>> +will use a default value of 0.
>> +
>
> On the one hand, I still need to understand the value of reporting generation
> in GETATTR/SETATTR.
>
> On the other hand, I do see the value in the server negotiating at init time the
> fact that "Generation values are reliable".
>
> What happens today is that NFS exporting is allowed for all FUSE filesystems
> regardless of the reliability of generation id, so after inode evict
> and recycle,
> an NFSv3 client  that had access to inode X.Y may get access to a completely
> different file or even a directory with inode X.Y, where X is the recycle nodeid
> and Y is an unreliable generation provided by the server.
>
> The problem is that FUSE does not require opt-in for NFS export, it only
> allows servers to opt-out of NFS export (FUSE_NO_EXPORT_SUPPORT).
>
> So what can be done given a declaration of the server that generation
> is reliable?
>
> One option is to set a non-zero uuid/fsid to the fuse filesystem.
> This will allow exporting the fuse filesystem without the opt-in uuid/fsid=
> in /etc/exports.
> This will also allow setting fanotify FAN_MARK_FILESYSTEM watches
> on this fuse filesystem, whose file handles could be trusted to be a genuine
> unique identity of the filesystem objects.
>
> But if we take this route, it is better to take it one step further
> and allow the
> server to determine the filesystem uuid/fsid during negotiation.
>
> In any case, I am not convinced there is value in doing all this
> without extending
> the protocol with LOOKUP_HANDLE/LOOKUPX lookup by file handle, so if you
> have compelling use cases, please spell them out.
>
> Thanks,
> Amir.


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

* Re: [PATCH v2] fuse: add inode generation number support
  2026-09-28 16:54       ` Amir Goldstein
@ 2026-09-29 20:55         ` Russell Harmon
  0 siblings, 0 replies; 7+ messages in thread
From: Russell Harmon @ 2026-09-29 20:55 UTC (permalink / raw)
  To: Amir Goldstein
  Cc: miklos, corbet, skhan, rdunlap, fuse-devel, linux-doc,
	linux-kernel, Bernd Schubert, Luis Henriques

On Mon, Sep 28, 2026 at 9:54 AM Amir Goldstein <amir73il@gmail.com> wrote:
>
> On Mon, Sep 28, 2026 at 6:04 PM Russell Harmon <russ@har.mn> wrote:
> >
> > (re-sending in plaintext)
> >
> > Thanks for the review! Comments inline.
> >
> > On Mon, Sep 28, 2026 at 2:38 AM Amir Goldstein <amir73il@gmail.com> wrote:
> > >
> > > On Mon, Sep 28, 2026 at 3:44 AM Russell Harmon <russ@har.mn> wrote:
> > > >
> > > > This patch adds support for propagating the inode generation number from
> > > > the FUSE server to the kernel.
> > >
> > > Incomplete statement.
> > > Without the mention of GETATTR/SETATTR this is a misleading statement.
> > >
> > > > This is useful for exporting FUSE
> > > > filesystems over NFS, where the generation number is used to detect
> > > > stale file handles (ESTALE) when inodes are recycled.
> > >
> > > How exactly does it help?
> > > I am not trying to troll you, I am really curious. how?
> > >
> > > Context: I have been trying to improve FUSE NFS export support for a while
> > > I have built a library that provides reliable NFS export for FUSE passthrough fs
> > > for specific backing file system types [1].
> > >
> > > [1] https://github.com/amir73il/libfuse/tree/libfuse_passthrough/passthrough
> > >
> > > It is broadly understood that real NFS export support requires extending the
> > > FUSE protocol to identify objects using file handles and Luis has
> > > already started
> > > with this work [2]
> > >
> > > [2] https://lore.kernel.org/linux-fsdevel/20260225112439.27276-1-luis@igalia.com/
> > >
> > > So my question is, what does adding generation id to GETATTR/SETATTR
> > > improve for FUSE filesystem writers that wish to export their filesystem to NFS?
> >
> > Long answer is https://russ.har.mn/blog/2026-04-09/fuse-loopback-is-incomplete
> >
> > Short answer is that inode on its own doesn't uniquely identify a
> > file. The unique identifier is inode + generation. Reason being that a
> > user can delete a file and create a new one, and the filesystem is
> > allowed to reuse the inode (but if it does it must use a different
> > generation).
>
> This is obvious. That's the reason LOOKUP already returns a generation.
>
> >
> > As for why it needs to be a part of GETATTR/SETATTR, consider that,
> > given the above, without a generation number such calls are actually
> > ambiguous. They don't uniquely identify a filesystem object. e.g.
> > "what are you asked to GETATTR/SETATTR _on_?"
> >
>
> You phrased the question correctly, but the same question
> applies to any other command, i.e.
> "What are you asked to OPEN/TRUNCATE _on_?"
>
> So you see, what's needed is to change the input argument
> of ALL the fuse requests from nodeid to nodeid+generation, or
> in the more generic form of NFS handles, a variable size object id blob.

Yes, fair, we'll need to change all the fuse requests.

> This is from the latest proposal for FUSEX protocol [3]
>
> struct fusex_id {
>     u64 nodeid;
>     /* will extend with file handle */
> };
>
> [3] https://lore.kernel.org/fuse-devel/20260429102058.1362965-1-mszeredi@redhat.com/
>
> > > >
> > > > Key changes:
> > > > - Bump FUSE protocol version to 7.47.
> > > > - Repurpose the unused `dummy` field in `struct fuse_attr_out` as
> > > >   `generation`.
> > > > - Add a `FUSE_ATTR_GENERATION` INIT flag with which the filesystem opts
> > > >   into the kernel consuming that field. Gating on the protocol minor
> > > >   version alone would break existing filesystems: the minor version only
> > > >   reflects the library, not whether the individual filesystem fills the
> > > >   field, and a zero there would look like a generation change for any
> > > >   filesystem that reports nonzero generations in LOOKUP.
> > > > - Update `fuse_change_attributes` and related functions to accept and set
> > > >   `inode->i_generation`.
> > > > - Populate `i_generation` from `LOOKUP`, `GETATTR`, and `READDIRPLUS`
> > > >   responses.
> > > > - Detect nodeid recycling on `GETATTR` and `SETATTR` responses via
> > > >   `fuse_stale_inode()`, the same check already used by the `LOOKUP` and
> > > >   `READDIRPLUS` paths, and mark the inode bad (EIO) instead of merging
> > > >   the recycled file's attributes into the existing, possibly still-open,
> > > >   inode.
> > > > - Update `fuse_get_dentry` to validate the generation number against the
> > > >   file handle, returning ESTALE on mismatch.
> > > > - Maintain backward compatibility: without `FUSE_ATTR_GENERATION` the
> > > >   generation field in attr replies is ignored.
> > > >
> > > > Verification:
> > > > Tested with a QEMU harness in fuse-generation-qemu against a patched
> > > > libfuse (FUSE_CAP_ATTR_GENERATION, fuse_reply_attr_with_generation) and
> > > > its passthrough_ll example reporting real backing-filesystem generation
> > > > numbers. The suite verifies that:
> > > > 1.  The generation from `LOOKUP` reaches `name_to_handle_at()` file
> > > >     handles and matches the backing filesystem's FS_IOC_GETVERSION.
> > > > 2.  `open_by_handle_at()` succeeds for a valid handle and fails with
> > > >     ESTALE for a handle whose generation does not match, both while the
> > > >     inode is cached and after cache eviction.
> > >
> > > 1 and 2 should work on upstream FUSE right?
> > > This is something worth mentioning.
> > >
> > > > 3.  When a `GETATTR` reply reports a new generation for a cached inode
> > > >     (inode recycling), the kernel marks the inode bad: fstat() on an
> > > >     open fd fails with EIO, while a fresh path lookup recovers and
> > > >     pre-recycling file handles fail with ESTALE.
> > >
> > > How did the inode recycle happen with passtrhough_ll which keeps
> > > open fds for fuse inodes?
> > > Something is missing from this test report.
> > > If you just used a mock filesystem which makes no sense in the real world
> > > then the value of this change is questionable.
> >
> > Ack, I'll dig more into this. Sending responses to your other
> > questions now and will reply again once I've got an answer.
> >
> >
> > > > Signed-off-by: Russell Harmon <russ@har.mn>
> > > > Assisted-by: Gemini:gemini-3.1
> > > > ---
> > > > v2:
> > > > - Bump FUSE_KERNEL_MINOR_VERSION to 47 to match the new 7.47 changelog
> > > >   entry (v1 added the entry but left the minor at 46).
> > > >
> > > > v1: https://lore.kernel.org/all/20260927141437.1432584-1-russ@har.mn/
> > > >
> > > >  Documentation/filesystems/fuse/fuse.rst | 32 +++++++++++++++++++++++++
> > > >  fs/fuse/dir.c                           | 21 +++++++++++-----
> > > >  fs/fuse/fuse_i.h                        | 10 ++++++--
> > > >  fs/fuse/inode.c                         | 23 ++++++++++++------
> > > >  fs/fuse/readdir.c                       |  2 +-
> > > >  include/uapi/linux/fuse.h               | 11 +++++++--
> > > >  6 files changed, 81 insertions(+), 18 deletions(-)
> > > >
> > > > diff --git a/Documentation/filesystems/fuse/fuse.rst b/Documentation/filesystems/fuse/fuse.rst
> > > > index 0fbd5a03fdc9..f67bc9fc6316 100644
> > > > --- a/Documentation/filesystems/fuse/fuse.rst
> > > > +++ b/Documentation/filesystems/fuse/fuse.rst
> > > > @@ -49,6 +49,38 @@ using the sftp protocol.
> > > >  The userspace library and utilities are available from the
> > > >  `FUSE homepage: <https://github.com/libfuse/>`_
> > > >
> > > > +NFS export support
> > > > +==================
> > > > +
> > > > +FUSE filesystems can be exported via NFS if the filesystem daemon supports it.
> > > > +For reliable NFS export, the filesystem should provide a unique inode
> > > > +generation number for each inode. This generation number is used by the
> > > > +NFS server to distinguish between different file instances that may
> > > > +share the same inode number (e.g. after an inode number is reused).
> > > > +
> > > > +The inode generation number is provided by the filesystem daemon in the
> > > > +following messages:
> > > > +
> > > > +- `FUSE_LOOKUP`
> > > > +- `FUSE_GETATTR` (see below)
> > > > +- `FUSE_SETATTR` (see below)
> > > > +- `FUSE_READDIRPLUS`
> > > > +- `FUSE_CREATE` / `FUSE_TMPFILE` / `FUSE_MKNOD` / `FUSE_MKDIR` / `FUSE_SYMLINK` / `FUSE_LINK`
> > > > +
> > > > +A daemon that keeps the generation number in its `FUSE_GETATTR` and
> > > > +`FUSE_SETATTR` replies (the `generation` field of `fuse_attr_out`,
> > > > +protocol 7.46) must announce this by setting `FUSE_ATTR_GENERATION` in
> > > > +its `FUSE_INIT` reply flags. When the flag is negotiated, the kernel
> > > > +compares the generation in every getattr/setattr reply against the
> > > > +cached inode: a mismatch means the daemon has reused the node ID for a
> > > > +different file, and the cached inode is marked bad (subsequent
> > > > +operations on it fail with EIO). Without the flag, the field is ignored
> > > > +and the generation is only taken from lookup-type replies, preserving
> > > > +the behavior of existing filesystems.
> > > > +
> > > > +If the filesystem daemon does not provide a generation number, the kernel
> > > > +will use a default value of 0.
> > > > +
> > >
> > > On the one hand, I still need to understand the value of reporting generation
> > > in GETATTR/SETATTR.
> > >
> > > On the other hand, I do see the value in the server negotiating at init time the
> > > fact that "Generation values are reliable".
> > >
> > > What happens today is that NFS exporting is allowed for all FUSE filesystems
> > > regardless of the reliability of generation id, so after inode evict
> > > and recycle,
> > > an NFSv3 client  that had access to inode X.Y may get access to a completely
> > > different file or even a directory with inode X.Y, where X is the recycle nodeid
> > > and Y is an unreliable generation provided by the server.
> > >
> > > The problem is that FUSE does not require opt-in for NFS export, it only
> > > allows servers to opt-out of NFS export (FUSE_NO_EXPORT_SUPPORT).
> > >
> > > So what can be done given a declaration of the server that generation
> > > is reliable?
> > >
> > > One option is to set a non-zero uuid/fsid to the fuse filesystem.
> > > This will allow exporting the fuse filesystem without the opt-in uuid/fsid=
> > > in /etc/exports.
> > > This will also allow setting fanotify FAN_MARK_FILESYSTEM watches
> > > on this fuse filesystem, whose file handles could be trusted to be a genuine
> > > unique identity of the filesystem objects.
> > >
> > > But if we take this route, it is better to take it one step further
> > > and allow the
> > > server to determine the filesystem uuid/fsid during negotiation.
> > >
> > > In any case, I am not convinced there is value in doing all this
> > > without extending
> > > the protocol with LOOKUP_HANDLE/LOOKUPX lookup by file handle, so if you
> > > have compelling use cases, please spell them out.
> >
> > I'm trying to implement a caching filesystem which uses a sqlite
> > database (stored on an SSD) to cache all file and directory metadata,
> > ultimately to avoid HDD spinups (and the associated electricity cost)
> > for operations other than file reads. I'm also exporting this
> > filesystem over NFS, hence the need for NFS support.
>
> OK, so you are implementing a passthrough filesystem (to HDD)
> with a fast caching layer, I assume to cache results of GETATTR/
> LOOKUP/READDIR/READDIRPLUS? Anything else?
> Are you caching a specific type of backing filesystem?
>
> I advise you to look at libfuse_passthrough [1].
> You can either see if it fits you or learn from my experience with NFS
> export of a fuse passthrough filesystem.
> I am giving a talk on the subject in Linux Plumbers next week [4].
>
> >
> > If I remember correctly, name_to_handle_at on a FUSE filesystem
> > contains just the inode+generation, and open_by_handle_at results in a
> > LOOKUP of that inode (and now inode+generation). But I'll
> > double-check. Assuming my memory is correct, I think that's
> > sufficient, assuming that we consider inode+generation as a unique
> > file object identifier.
>
> The problem is, that what you assume is not an API definition, this is
> a specific
> filesystem implementation, which happens to be correct for some commonly
> used filesystems (e.g. ext4, xfs), but is not true in general.
> In general the API {name_to,open_by}_handle_at() the file handle is an
> opaque blob.

Maybe I'm thinking about this backwards, but we're not talking about
"any" filesystem here. We're talking about the FUSE filesystem where
the definition of a handle _is_ inode+generation. All I'm proposing we
do is to expose that fact to the FUSE userspace daemon. That daemon
can then turn around and call open on another filesystem, or make a
network call to AWS or whatever. In my case I want to
open_by_handle_at on an underlying filesystem, but I can solve that by
storing a mapping from fuse_ino+fuse_generation_num to
underlying_fs_file_handle in my database.


> The way that libfuse_passthrough gates against recycled inode numbers
> is that the file handle is stored in the server's inode table at lookup time
> (generation is recorded) and all the commands that follow, lookup in the
> server's inode table (could be in sqlite in your case) and use
> open_by_handle_at() with the recorded file handle from lookup time
> to get an O_PATH fd to the object on the backing filesystem (on HDD).
>
> This is called InodeRef in libfuse_passthrough and any request will return
> ESTALE to the kernel if it cannot get an InodeRef.
>
> Thanks,
> Amir.
>
> [4] https://lpc.events/event/20/contributions/2367

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

end of thread, other threads:[~2026-09-29 20:55 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-27 14:14 [PATCH] fuse: add inode generation number support Russell Harmon
2026-09-28  1:43 ` [PATCH v2] " Russell Harmon
2026-09-28  9:38   ` Amir Goldstein
2026-09-28 16:03     ` Russell Harmon
2026-09-28 16:54       ` Amir Goldstein
2026-09-29 20:55         ` Russell Harmon
2026-09-29 13:07     ` Luis Henriques

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

all inboxes | Powered by JetHome®