The branch main has been updated by asomers:

URL: 
https://cgit.FreeBSD.org/src/commit/?id=f1ec3bc06ed276e6e24996515bdce729d51e11d8

commit f1ec3bc06ed276e6e24996515bdce729d51e11d8
Author:     Alan Somers <asom...@freebsd.org>
AuthorDate: 2025-01-15 19:25:34 +0000
Commit:     Alan Somers <asom...@freebsd.org>
CommitDate: 2025-06-13 21:00:25 +0000

    fusefs: add more checks for buggy FUSE servers
    
    * If a FUSE file system is NFS-exported (either by a kernel or userspace
      NFS server), then it must support FUSE_LOOKUP operations for ".".  But
      if the response reports a different nodeid than the request, that's
      very bad.  Fail the operation and warn the operator.
    
    * In general, a FUSE file may have a distinct "nodeid" and "inode
      number".  But it the file system is NFS-exported (either by a kernel
      or userspace NFS server), then those two must match, because the NFS
      server will do VFS_VGET operations using the inode number.  If they
      don't match, warn the operator.
    
    MFC after:      2 weeks
    Sponsored by:   ConnectWise
    Differential Revision: https://reviews.freebsd.org/D48471
---
 sys/fs/fuse/fuse_ipc.h                   |   2 +
 sys/fs/fuse/fuse_node.c                  |  19 ++++++
 sys/fs/fuse/fuse_vfsops.c                |  13 ++++
 tests/sys/fs/fusefs/last_local_modify.cc |   5 +-
 tests/sys/fs/fusefs/lookup.cc            |   7 ++
 tests/sys/fs/fusefs/nfs.cc               | 106 +++++++++++++++++++++++++++++++
 6 files changed, 149 insertions(+), 3 deletions(-)

diff --git a/sys/fs/fuse/fuse_ipc.h b/sys/fs/fuse/fuse_ipc.h
index 5648624f4c63..3bfc859dbac9 100644
--- a/sys/fs/fuse/fuse_ipc.h
+++ b/sys/fs/fuse/fuse_ipc.h
@@ -238,6 +238,8 @@ struct fuse_data {
 #define FSESS_WARN_WB_CACHE_INCOHERENT 0x400000        /* WB cache incoherent 
*/
 #define        FSESS_WARN_ILLEGAL_INODE  0x800000 /* Illegal inode for new 
file */
 #define FSESS_WARN_READLINK_EMBEDDED_NUL 0x1000000 /* corrupt READLINK output 
*/
+#define FSESS_WARN_DOT_LOOKUP    0x2000000 /* Inconsistent . LOOKUP response */
+#define FSESS_WARN_INODE_MISMATCH 0x4000000 /* ino != nodeid */
 #define FSESS_MNTOPTS_MASK     ( \
        FSESS_DAEMON_CAN_SPY | FSESS_PUSH_SYMLINKS_IN | \
        FSESS_DEFAULT_PERMISSIONS | FSESS_INTR)
diff --git a/sys/fs/fuse/fuse_node.c b/sys/fs/fuse/fuse_node.c
index 0a24d0da4fac..742dc66bcafc 100644
--- a/sys/fs/fuse/fuse_node.c
+++ b/sys/fs/fuse/fuse_node.c
@@ -297,6 +297,8 @@ fuse_vnode_get(struct mount *mp,
     __enum_uint8(vtype) vtyp)
 {
        struct thread *td = curthread;
+       bool exportable = fuse_get_mpdata(mp)->dataflags & FSESS_EXPORT_SUPPORT;
+
        /* 
         * feo should only be NULL for the root directory, which (when libfuse
         * is used) always has generation 0
@@ -309,6 +311,23 @@ fuse_vnode_get(struct mount *mp,
                        "Assigned same inode to both parent and child.");
                return EIO;
        }
+       if (feo && feo->nodeid != feo->attr.ino && exportable) {
+               /*
+                * NFS servers (both kernelspace and userspace) rely on
+                * VFS_VGET to lookup inodes.  But that's only possible if the
+                * file's inode number matches its nodeid, which isn't
+                * necessarily the case for FUSE.  If they don't match, then we
+                * can complete the current operation, but future VFS_VGET
+                * operations will almost certainly return spurious results.
+                * Warn the operator.
+                *
+                * But only warn the operator if the file system reports
+                * NFS-compatibility, because that's the only time that this
+                * matters, and dumb fuse servers abound.
+                */
+               fuse_warn(fuse_get_mpdata(mp), FSESS_WARN_INODE_MISMATCH,
+                   "file has different inode number and nodeid.");
+       }
 
        err = fuse_vnode_alloc(mp, td, nodeid, vtyp, vpp);
        if (err) {
diff --git a/sys/fs/fuse/fuse_vfsops.c b/sys/fs/fuse/fuse_vfsops.c
index 0da51b865873..48b84d3c75af 100644
--- a/sys/fs/fuse/fuse_vfsops.c
+++ b/sys/fs/fuse/fuse_vfsops.c
@@ -568,12 +568,25 @@ fuse_vfsop_vget(struct mount *mp, ino_t ino, int flags, 
struct vnode **vpp)
                goto out;
 
        feo = (struct fuse_entry_out *)fdi.answ;
+
        if (feo->nodeid == 0) {
                /* zero nodeid means ENOENT and cache it */
                error = ENOENT;
                goto out;
        }
 
+       if (feo->nodeid != nodeid) {
+               /*
+                * Something is very wrong with the server if "foo/." has a
+                * different inode number than "foo".
+                */
+               fuse_warn(data, FSESS_WARN_DOT_LOOKUP,
+                   "Inconsistent LOOKUP response: \"FILE/.\" has a different "
+                   "inode number than \"FILE\".");
+               error = EIO;
+               goto out;
+       }
+
        vtyp = IFTOVT(feo->attr.mode);
        error = fuse_vnode_get(mp, feo, nodeid, NULL, vpp, NULL, vtyp);
        if (error)
diff --git a/tests/sys/fs/fusefs/last_local_modify.cc 
b/tests/sys/fs/fusefs/last_local_modify.cc
index 495bfd8aa959..5fcd3c36c892 100644
--- a/tests/sys/fs/fusefs/last_local_modify.cc
+++ b/tests/sys/fs/fusefs/last_local_modify.cc
@@ -233,7 +233,6 @@ TEST_P(LastLocalModify, lookup)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.nodeid = ino;
                out.body.entry.attr.size = oldsize;
-               out.body.entry.nodeid = ino;
                out.body.entry.attr_valid_nsec = NAP_NS / 2;
                out.body.entry.attr.ino = ino;
                out.body.entry.attr.mode = S_IFREG | 0644;
@@ -277,6 +276,7 @@ TEST_P(LastLocalModify, lookup)
                SET_OUT_HEADER_LEN(*out0, entry);
                out0->body.entry.attr.mode = S_IFREG | 0644;
                out0->body.entry.nodeid = ino;
+               out0->body.entry.attr.ino = ino;
                out0->body.entry.entry_valid = UINT64_MAX;
                out0->body.entry.attr_valid = UINT64_MAX;
                out0->body.entry.attr.size = oldsize;
@@ -392,7 +392,6 @@ TEST_P(LastLocalModify, vfs_vget)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.nodeid = ino;
                out.body.entry.attr.size = oldsize;
-               out.body.entry.nodeid = ino;
                out.body.entry.attr_valid_nsec = NAP_NS / 2;
                out.body.entry.attr.ino = ino;
                out.body.entry.attr.mode = S_IFREG | 0644;
@@ -414,7 +413,6 @@ TEST_P(LastLocalModify, vfs_vget)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.nodeid = ino;
                out.body.entry.attr.size = oldsize;
-               out.body.entry.nodeid = ino;
                out.body.entry.attr_valid_nsec = NAP_NS / 2;
                out.body.entry.attr.ino = ino;
                out.body.entry.attr.mode = S_IFREG | 0644;
@@ -439,6 +437,7 @@ TEST_P(LastLocalModify, vfs_vget)
                SET_OUT_HEADER_LEN(*out0, entry);
                out0->body.entry.attr.mode = S_IFREG | 0644;
                out0->body.entry.nodeid = ino;
+               out0->body.entry.attr.ino = ino;
                out0->body.entry.entry_valid = UINT64_MAX;
                out0->body.entry.attr_valid = UINT64_MAX;
                out0->body.entry.attr.size = oldsize;
diff --git a/tests/sys/fs/fusefs/lookup.cc b/tests/sys/fs/fusefs/lookup.cc
index 6d506c1ab700..2cfe888b6b08 100644
--- a/tests/sys/fs/fusefs/lookup.cc
+++ b/tests/sys/fs/fusefs/lookup.cc
@@ -560,6 +560,7 @@ TEST_F(LookupExportable, dotdot_entry_cache_timeout)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFDIR | 0755;
                out.body.entry.nodeid = foo_ino;
+               out.body.entry.attr.ino = foo_ino;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0; // immediate timeout
        })));
@@ -568,6 +569,7 @@ TEST_F(LookupExportable, dotdot_entry_cache_timeout)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFDIR | 0755;
                out.body.entry.nodeid = bar_ino;
+               out.body.entry.attr.ino = bar_ino;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = UINT64_MAX;
        })));
@@ -577,6 +579,7 @@ TEST_F(LookupExportable, dotdot_entry_cache_timeout)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFDIR | 0755;
                out.body.entry.nodeid = FUSE_ROOT_ID;
+               out.body.entry.attr.ino = FUSE_ROOT_ID;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = UINT64_MAX;
        })));
@@ -607,6 +610,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFDIR | 0755;
                out.body.entry.nodeid = foo_ino;
+               out.body.entry.attr.ino = foo_ino;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = UINT64_MAX;
        })));
@@ -615,6 +619,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFDIR | 0755;
                out.body.entry.nodeid = bar_ino;
+               out.body.entry.attr.ino = bar_ino;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = UINT64_MAX;
        })));
@@ -632,6 +637,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFDIR | 0755;
                out.body.entry.nodeid = foo_ino;
+               out.body.entry.attr.ino = foo_ino;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = UINT64_MAX;
        })));
@@ -640,6 +646,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFDIR | 0755;
                out.body.entry.nodeid = FUSE_ROOT_ID;
+               out.body.entry.attr.ino = FUSE_ROOT_ID;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = UINT64_MAX;
        })));
diff --git a/tests/sys/fs/fusefs/nfs.cc b/tests/sys/fs/fusefs/nfs.cc
index 27ffc8f5cbc1..2fa2b290f383 100644
--- a/tests/sys/fs/fusefs/nfs.cc
+++ b/tests/sys/fs/fusefs/nfs.cc
@@ -84,6 +84,7 @@ TEST_F(Fhstat, estale)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
@@ -95,6 +96,7 @@ TEST_F(Fhstat, estale)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = 2;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
@@ -121,6 +123,7 @@ TEST_F(Fhstat, lookup_dot)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.uid = uid;
                out.body.entry.attr_valid = UINT64_MAX;
@@ -132,6 +135,7 @@ TEST_F(Fhstat, lookup_dot)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.uid = uid;
                out.body.entry.attr_valid = UINT64_MAX;
@@ -160,6 +164,7 @@ TEST_F(Fhstat, lookup_dot_error)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.uid = uid;
                out.body.entry.attr_valid = UINT64_MAX;
@@ -189,6 +194,7 @@ TEST_F(Fhstat, cached)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.ino = ino;
                out.body.entry.attr_valid = UINT64_MAX;
@@ -215,6 +221,7 @@ TEST_F(Fhstat, cache_expired)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.ino = ino;
                out.body.entry.attr_valid = UINT64_MAX;
@@ -226,6 +233,7 @@ TEST_F(Fhstat, cache_expired)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.ino = ino;
                out.body.entry.attr_valid = UINT64_MAX;
@@ -243,6 +251,99 @@ TEST_F(Fhstat, cache_expired)
        EXPECT_EQ(ino, sb.st_ino);
 }
 
+/*
+ * If the server returns a FUSE_LOOKUP response for a nodeid that we didn't
+ * lookup, it's a bug.  But we should handle it gracefully.
+ */
+TEST_F(Fhstat, inconsistent_nodeid)
+{
+       const char FULLPATH[] = "mountpoint/some_dir/.";
+       const char RELDIRPATH[] = "some_dir";
+       fhandle_t fhp;
+       struct stat sb;
+       const uint64_t ino_in = 42;
+       const uint64_t ino_out = 43;
+       const mode_t mode = S_IFDIR | 0755;
+       const uid_t uid = 12345;
+
+       EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+       .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+               SET_OUT_HEADER_LEN(out, entry);
+               out.body.entry.nodeid = ino_in;
+               out.body.entry.attr.ino = ino_in;
+               out.body.entry.attr.mode = mode;
+               out.body.entry.generation = 1;
+               out.body.entry.attr.uid = uid;
+               out.body.entry.attr_valid = UINT64_MAX;
+               out.body.entry.entry_valid = 0;
+       })));
+
+       EXPECT_LOOKUP(ino_in, ".")
+       .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+               SET_OUT_HEADER_LEN(out, entry);
+               out.body.entry.nodeid = ino_out;
+               out.body.entry.attr.ino = ino_out;
+               out.body.entry.attr.mode = mode;
+               out.body.entry.generation = 1;
+               out.body.entry.attr.uid = uid;
+               out.body.entry.attr_valid = UINT64_MAX;
+               out.body.entry.entry_valid = 0;
+       })));
+
+       ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+       EXPECT_NE(0, fhstat(&fhp, &sb)) << strerror(errno);
+       EXPECT_EQ(EIO, errno);
+}
+
+/*
+ * If the server returns a FUSE_LOOKUP response where the nodeid doesn't match
+ * the inode number, and the file system is exported, it's a bug.  But we
+ * should handle it gracefully.
+ */
+TEST_F(Fhstat, inconsistent_ino)
+{
+       const char FULLPATH[] = "mountpoint/some_dir/.";
+       const char RELDIRPATH[] = "some_dir";
+       fhandle_t fhp;
+       struct stat sb;
+       const uint64_t nodeid = 42;
+       const uint64_t ino = 711;       // Could be anything that != nodeid
+       const mode_t mode = S_IFDIR | 0755;
+       const uid_t uid = 12345;
+
+       EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+       .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+               SET_OUT_HEADER_LEN(out, entry);
+               out.body.entry.nodeid = nodeid;
+               out.body.entry.attr.ino = nodeid;
+               out.body.entry.attr.mode = mode;
+               out.body.entry.generation = 1;
+               out.body.entry.attr.uid = uid;
+               out.body.entry.attr_valid = UINT64_MAX;
+               out.body.entry.entry_valid = 0;
+       })));
+
+       EXPECT_LOOKUP(nodeid, ".")
+       .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+               SET_OUT_HEADER_LEN(out, entry);
+               out.body.entry.nodeid = nodeid;
+               out.body.entry.attr.ino = ino;
+               out.body.entry.attr.mode = mode;
+               out.body.entry.generation = 1;
+               out.body.entry.attr.uid = uid;
+               out.body.entry.attr_valid = UINT64_MAX;
+               out.body.entry.entry_valid = 0;
+       })));
+
+       ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+       /*
+        * The fhstat operation will actually succeed.  But future operations
+        * will likely fail.
+        */
+       ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+       EXPECT_EQ(ino, sb.st_ino);
+}
+
 /* 
  * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style
  * lookups
@@ -260,6 +361,7 @@ TEST_F(FhstatNotExportable, lookup_dot)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
@@ -282,6 +384,7 @@ TEST_F(Getfh, eoverflow)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFDIR | 0755;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = (uint64_t)UINT32_MAX + 1;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = UINT64_MAX;
@@ -304,6 +407,7 @@ TEST_F(Getfh, ok)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFDIR | 0755;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = UINT64_MAX;
        })));
@@ -335,6 +439,7 @@ TEST_F(Readdir, getdirentries)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
@@ -345,6 +450,7 @@ TEST_F(Readdir, getdirentries)
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
+               out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;

Reply via email to