git: 5a8d8ffd7c8d - stable/14 - fusefs: add more checks for buggy FUSE servers
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Wed, 23 Jul 2025 23:29:32 UTC
The branch stable/14 has been updated by asomers:
URL: https://cgit.FreeBSD.org/src/commit/?id=5a8d8ffd7c8d3f7f09f18fc53e7c74ec6ef6698b
commit 5a8d8ffd7c8d3f7f09f18fc53e7c74ec6ef6698b
Author: Alan Somers <asomers@FreeBSD.org>
AuthorDate: 2025-01-15 19:25:34 +0000
Commit: Alan Somers <asomers@FreeBSD.org>
CommitDate: 2025-07-23 23:28:54 +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.
Sponsored by: ConnectWise
Differential Revision: https://reviews.freebsd.org/D48471
(cherry picked from commit f1ec3bc06ed276e6e24996515bdce729d51e11d8)
---
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 297ea4a1fad2..c0ec6b202c6e 100644
--- a/sys/fs/fuse/fuse_node.c
+++ b/sys/fs/fuse/fuse_node.c
@@ -298,6 +298,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
@@ -310,6 +312,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 ce9c1d7fd946..7d58348349c7 100644
--- a/sys/fs/fuse/fuse_vfsops.c
+++ b/sys/fs/fuse/fuse_vfsops.c
@@ -569,12 +569,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;