git: 2da045abb9f0 - stable/15 - fusefs: fix error handling when reading a directory's sticky bit

From: Alan Somers <asomers_at_FreeBSD.org>
Date: Wed, 08 Jul 2026 17:57:32 UTC
The branch stable/15 has been updated by asomers:

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

commit 2da045abb9f0e36531910f5c80c7293be1d0771c
Author:     Alan Somers <asomers@FreeBSD.org>
AuthorDate: 2026-06-14 17:37:15 +0000
Commit:     Alan Somers <asomers@FreeBSD.org>
CommitDate: 2026-07-08 17:56:50 +0000

    fusefs: fix error handling when reading a directory's sticky bit
    
    When trying to delete or rename a file, fuse_vnop_lookup must check
    whether its parent directory's sticky bit is set.  Realistically, the
    parent directory's attributes will almost always be cached.  But it's
    possible that they won't be, and in that case we must send a new
    FUSE_GETATTR request to the server.  If that request fails for some
    reason, then we must fail the lookup.  Prior to this change fusefs would
    ignore failure of that request.
    
    Reported by:    Yuxiang Yang, Yizhou Zhao, Ao Wang, Xuewei Feng, Qi Li,
                    and Ke Xu of Tsinghua University
    Reviewed by:    markj
    Differential Revision: https://reviews.freebsd.org/D57588
    
    (cherry picked from commit b4af6a4cccc3b4b0ea461463196c258eb92ad2e5)
---
 sys/fs/fuse/fuse_vnops.c                   | 10 +++---
 tests/sys/fs/fusefs/default_permissions.cc | 53 ++++++++++++++++++++++++++++++
 2 files changed, 59 insertions(+), 4 deletions(-)

diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c
index d5404a8b9805..b5f65decc399 100644
--- a/sys/fs/fuse/fuse_vnops.c
+++ b/sys/fs/fuse/fuse_vnops.c
@@ -1660,14 +1660,16 @@ fuse_vnop_lookup(struct vop_lookup_args *ap)
 				 * Need to figure out the vnode locking to make
 				 * this work.
 				 */
-				fuse_internal_getattr(dvp, &dvattr, cred, td);
-				if ((dvattr.va_mode & S_ISTXT) &&
+				err = fuse_internal_getattr(dvp, &dvattr, cred,
+						td);
+				if (err == 0 &&
+					(dvattr.va_mode & S_ISTXT) &&
 					fuse_internal_access(dvp, VADMIN, td,
 						cred) &&
 					fuse_internal_access(*vpp, VADMIN, td,
-						cred)) {
+						cred))
+				{
 					err = EPERM;
-					goto out;
 				}
 			}
 		}
diff --git a/tests/sys/fs/fusefs/default_permissions.cc b/tests/sys/fs/fusefs/default_permissions.cc
index 4b04297d97ac..f9a37f904d30 100644
--- a/tests/sys/fs/fusefs/default_permissions.cc
+++ b/tests/sys/fs/fusefs/default_permissions.cc
@@ -1562,6 +1562,59 @@ TEST_F(Unlink, sticky_directory)
 	ASSERT_EQ(EPERM, errno);
 }
 
+/*
+ * When trying to delete or rename a file, we must check its parent directory's
+ * sticky bit.  That may entail a FUSE_GETATTR.  If that operation fails, then
+ * we must fail the rename or delete.
+ */
+TEST_F(Unlink, sticky_directory_io_during_getattr)
+{
+	const char FULLPATH[] = "mountpoint/some_file.txt";
+	const char RELPATH[] = "some_file.txt";
+	Sequence seq;
+	uint64_t ino = 42;
+
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_GETATTR &&
+				in.header.nodeid == FUSE_ROOT_ID);
+		}, Eq(true)),
+		_)
+	).Times(2)
+	.InSequence(seq)
+	.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+		SET_OUT_HEADER_LEN(out, attr);
+		out.body.attr.attr.ino = FUSE_ROOT_ID;
+		out.body.attr.attr.mode = S_IFDIR | 0177;
+		/*
+		 * Realistically, the parent directory's attributes will almost
+		 * always be cached when we try to lookup the sticky bit.  For
+		 * this test case, set attr_valid to 0 so that won't be the
+		 * case.
+		 */
+		out.body.attr.attr_valid = 0;
+	})));
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_GETATTR &&
+				in.header.nodeid == FUSE_ROOT_ID);
+		}, Eq(true)),
+		_)
+	).Times(1)
+	.InSequence(seq)
+	.WillOnce(Invoke(ReturnErrno(EIO)));
+	expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_UNLINK);
+		}, Eq(true)),
+		_)
+	).Times(0);
+
+	ASSERT_EQ(-1, unlink(FULLPATH));
+	ASSERT_EQ(EIO, errno);
+}
+
 /* A write by a non-owner should clear a file's SUID bit */
 TEST_F(Write, clear_suid)
 {