git: 9e682c426093 - main - fusefs: don't fake the mountpoint's stat info before FUSE_INIT completes

From: Alan Somers <asomers_at_FreeBSD.org>
Date: Tue, 05 Aug 2025 22:53:28 UTC
The branch main has been updated by asomers:

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

commit 9e682c426093a3afc0f609a1f04048b0beb021ec
Author:     Alan Somers <asomers@FreeBSD.org>
AuthorDate: 2025-06-11 21:26:48 +0000
Commit:     Alan Somers <asomers@FreeBSD.org>
CommitDate: 2025-08-05 22:53:05 +0000

    fusefs: don't fake the mountpoint's stat info before FUSE_INIT completes
    
    Ever since the first GSoC contribution, fusefs has had a curious
    behavior.  If the daemon hasn't finished responding to FUSE_INIT,
    fuse_vnop_getattr would reply to VOP_GETATTR requests for the mountpoint
    by returning all zeros.  I don't know why.  It isn't necessary for
    unmounting, even if the daemon is dead.
    
    Delete that behavior.  Now VOP_GETATTR for the mountpoint will wait for
    the daemon to be ready, just like it will for any other vnode.
    
    Reported by:    Vassili Tchersky
    Sponsored by:   ConnectWise
    Differential Revision: https://reviews.freebsd.org/D50800
---
 sys/fs/fuse/fuse_vnops.c        | 34 ++++++-------------
 tests/sys/fs/fusefs/pre-init.cc | 72 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+), 25 deletions(-)

diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c
index 32872e8f3f3a..b90ce60ec664 100644
--- a/sys/fs/fuse/fuse_vnops.c
+++ b/sys/fs/fuse/fuse_vnops.c
@@ -1219,36 +1219,20 @@ fuse_vnop_getattr(struct vop_getattr_args *ap)
 	struct vattr *vap = ap->a_vap;
 	struct ucred *cred = ap->a_cred;
 	struct thread *td = curthread;
-
 	int err = 0;
-	int dataflags;
-
-	dataflags = fuse_get_mpdata(vnode_mount(vp))->dataflags;
-
-	/* Note that we are not bailing out on a dead file system just yet. */
 
-	if (!(dataflags & FSESS_INITED)) {
-		if (!vnode_isvroot(vp)) {
-			fdata_set_dead(fuse_get_mpdata(vnode_mount(vp)));
-			return (EXTERROR(ENOTCONN, "FUSE daemon is not "
-			    "initialized"));
-		} else {
-			goto fake;
-		}
-	}
 	err = fuse_internal_getattr(vp, vap, cred, td);
 	if (err == ENOTCONN && vnode_isvroot(vp)) {
-		/* see comment in fuse_vfsop_statfs() */
-		goto fake;
-	} else {
-		return err;
+		/*
+		 * We want to seem a legitimate fs even if the daemon is dead,
+		 * so that, eg., we can still do path based unmounting after
+		 * the daemon dies.
+		 */
+		err = 0;
+		bzero(vap, sizeof(*vap));
+		vap->va_type = vnode_vtype(vp);
 	}
-
-fake:
-	bzero(vap, sizeof(*vap));
-	vap->va_type = vnode_vtype(vp);
-
-	return 0;
+	return err;
 }
 
 /*
diff --git a/tests/sys/fs/fusefs/pre-init.cc b/tests/sys/fs/fusefs/pre-init.cc
index e990d3cafffa..2d3257500304 100644
--- a/tests/sys/fs/fusefs/pre-init.cc
+++ b/tests/sys/fs/fusefs/pre-init.cc
@@ -44,12 +44,26 @@ using namespace testing;
 
 /* Tests for behavior that happens before the server responds to FUSE_INIT */
 class PreInit: public FuseTest {
+public:
 void SetUp() {
 	m_no_auto_init = true;
 	FuseTest::SetUp();
 }
 };
 
+/*
+ * Tests for behavior that happens before the server responds to FUSE_INIT,
+ * parameterized on default_permissions
+ */
+class PreInitP: public PreInit,
+	        public WithParamInterface<bool>
+{
+void SetUp() {
+	m_default_permissions = GetParam();
+	PreInit::SetUp();
+}
+};
+
 static void* unmount1(void* arg __unused) {
 	ssize_t r;
 
@@ -152,3 +166,61 @@ TEST_F(PreInit, signal_during_unmount_before_init)
 	sem_post(&sem0);
 	m_mock->join_daemon();
 }
+
+/*
+ * If some process attempts VOP_GETATTR for the mountpoint before init is
+ * complete, fusefs should wait, just like it does for other VOPs.
+ *
+ * To verify that fuse_vnop_getattr does indeed wait for FUSE_INIT to complete,
+ * invoke the test like this:
+ *
+> sudo cpuset -c -l 0 dtrace -i 'fbt:fusefs:fuse_internal_init_callback:' -i 'fbt:fusefs:fuse_vnop_getattr:' -c "./pre-init --gtest_filter=PI/PreInitP.getattr_before_init/0"
+...
+dtrace: pid 4224 has exited
+CPU     ID                    FUNCTION:NAME
+  0  68670          fuse_vnop_getattr:entry
+  0  68893 fuse_internal_init_callback:entry
+  0  68894 fuse_internal_init_callback:return
+  0  68671         fuse_vnop_getattr:return
+ *
+ * Note that fuse_vnop_getattr was entered first, but exitted last.
+ */
+TEST_P(PreInitP, getattr_before_init)
+{
+	struct stat sb;
+	nlink_t nlink = 12345;
+
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_INIT);
+		}, Eq(true)),
+		_)
+	).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+		SET_OUT_HEADER_LEN(out, init);
+		out.body.init.major = FUSE_KERNEL_VERSION;
+		out.body.init.minor = FUSE_KERNEL_MINOR_VERSION;
+		out.body.init.flags = in.body.init.flags & m_init_flags;
+		out.body.init.max_write = m_maxwrite;
+		out.body.init.max_readahead = m_maxreadahead;
+		out.body.init.time_gran = m_time_gran;
+		nap();	/* Allow stat() to run first */
+	})));
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_GETATTR &&
+				in.header.nodeid == FUSE_ROOT_ID);
+		}, Eq(true)),
+		_)
+	).WillOnce(Invoke(ReturnImmediate([=](auto& in, auto& out) {
+		SET_OUT_HEADER_LEN(out, attr);
+		out.body.attr.attr.ino = in.header.nodeid;
+		out.body.attr.attr.mode = S_IFDIR | 0644;
+		out.body.attr.attr.nlink = nlink;
+		out.body.attr.attr_valid = UINT64_MAX;
+	})));
+
+	EXPECT_EQ(0, stat("mountpoint", &sb));
+	EXPECT_EQ(nlink, sb.st_nlink);
+}
+
+INSTANTIATE_TEST_SUITE_P(PI, PreInitP, Bool());