git: 3152a2f5dd04 - stable/14 - fusefs: fix a kernel panic regarding SCM_RIGHTS

From: Alan Somers <asomers_at_FreeBSD.org>
Date: Sat, 27 Sep 2025 14:36:23 UTC
The branch stable/14 has been updated by asomers:

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

commit 3152a2f5dd0447daba80466f70c66ea794400015
Author:     Alan Somers <asomers@FreeBSD.org>
AuthorDate: 2025-09-19 16:02:25 +0000
Commit:     Alan Somers <asomers@FreeBSD.org>
CommitDate: 2025-09-27 14:30:13 +0000

    fusefs: fix a kernel panic regarding SCM_RIGHTS
    
    If the last copy of an open file resides within the socket buffer of a
    unix-domain socket, then VOP_CLOSE will be called with no thread
    information.  Fix fusefs to handle that case, and add a regression test.
    
    Also add a test case for writes to a file that lies within a sockbuf.
    Along with close, a write from the writeback cache is the only other
    operation I can think of that might apply to a file residing in a
    sockbuf.
    
    PR:             289686
    Reported by:    iron.udjin@gmail.com
    Sponsored by:   ConnectWise
    Reviewed by:    glebius, markj
    Differential Revision: https://reviews.freebsd.org/D52625
    
    (cherry picked from commit e043af9ca59608309cac2fd222c17f989ba0d35e)
---
 sys/fs/fuse/fuse_vnops.c       | 10 ++++--
 tests/sys/fs/fusefs/release.cc | 54 +++++++++++++++++++++++++++++++
 tests/sys/fs/fusefs/write.cc   | 73 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 134 insertions(+), 3 deletions(-)

diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c
index 265d1604e796..28dca332dfd3 100644
--- a/sys/fs/fuse/fuse_vnops.c
+++ b/sys/fs/fuse/fuse_vnops.c
@@ -785,11 +785,15 @@ fuse_vnop_close(struct vop_close_args *ap)
 	struct mount *mp = vnode_mount(vp);
 	struct ucred *cred = ap->a_cred;
 	int fflag = ap->a_fflag;
-	struct thread *td = ap->a_td;
-	pid_t pid = td->td_proc->p_pid;
+	struct thread *td;
 	struct fuse_vnode_data *fvdat = VTOFUD(vp);
+	pid_t pid;
 	int err = 0;
 
+	/* NB: a_td will be NULL from some async kernel contexts */
+	td = ap->a_td ? ap->a_td : curthread;
+	pid = td->td_proc->p_pid;
+
 	if (fuse_isdeadfs(vp))
 		return 0;
 	if (vnode_isdir(vp))
@@ -828,7 +832,7 @@ fuse_vnop_close(struct vop_close_args *ap)
 	}
 	/* TODO: close the file handle, if we're sure it's no longer used */
 	if ((fvdat->flag & FN_SIZECHANGE) != 0) {
-		fuse_vnode_savesize(vp, cred, td->td_proc->p_pid);
+		fuse_vnode_savesize(vp, cred, pid);
 	}
 	return err;
 }
diff --git a/tests/sys/fs/fusefs/release.cc b/tests/sys/fs/fusefs/release.cc
index b664ba512b64..9df236bfbaf7 100644
--- a/tests/sys/fs/fusefs/release.cc
+++ b/tests/sys/fs/fusefs/release.cc
@@ -29,6 +29,9 @@
  */
 
 extern "C" {
+#include <sys/socket.h>
+#include <sys/un.h>
+
 #include <fcntl.h>
 #include <unistd.h>
 }
@@ -188,6 +191,57 @@ TEST_F(Release, ok)
 	ASSERT_EQ(0, close(fd)) << strerror(errno);
 }
 
+/*
+ * Nothing bad should happen when closing a Unix-domain named socket that
+ * contains a fusefs file descriptor within its receive buffer.
+ * Regression test for
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=289686
+ */
+TEST_F(Release, scm_rights)
+{
+	const char FULLPATH[] = "mountpoint/some_file.txt";
+	const char RELPATH[] = "some_file.txt";
+	struct msghdr msg;
+	struct iovec iov;
+	char message[CMSG_SPACE(sizeof(int))];
+	uint64_t ino = 42;
+	int fd;
+	int s[2];
+	union {
+		char buf[CMSG_SPACE(sizeof(fd))];
+		struct cmsghdr align;
+	} u;
+
+	expect_lookup(RELPATH, ino, 1);
+	expect_open(ino, 0, 1);
+	expect_flush(ino, 1, ReturnErrno(0));
+	expect_release(ino, getpid(), O_RDONLY, 0);
+
+	ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, s)) << strerror(errno);
+
+	fd = open(FULLPATH, O_RDONLY);
+	ASSERT_LE(0, fd) << strerror(errno);
+
+	memset(&message, 0, sizeof(message));
+	memset(&msg, 0, sizeof(msg));
+	iov.iov_base = NULL;
+	iov.iov_len = 0;
+	msg.msg_iov = &iov;
+	msg.msg_iovlen = 1;
+	msg.msg_control = u.buf,
+	msg.msg_controllen = sizeof(u.buf);
+	struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+	cmsg->cmsg_level = SOL_SOCKET;
+	cmsg->cmsg_type = SCM_RIGHTS;
+	cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
+	memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
+	ASSERT_GE(sendmsg(s[0], &msg, 0), 0) << strerror(errno);
+
+	close(fd);	// Close fd within our process
+	close(s[0]);
+	close(s[1]);	// The last copy of fd is within this socket's rcvbuf
+}
+
 /* When closing a file with a POSIX file lock, release should release the lock*/
 TEST_F(ReleaseWithLocks, unlock_on_close)
 {
diff --git a/tests/sys/fs/fusefs/write.cc b/tests/sys/fs/fusefs/write.cc
index 1fe2e3cc522d..f5573a865a04 100644
--- a/tests/sys/fs/fusefs/write.cc
+++ b/tests/sys/fs/fusefs/write.cc
@@ -32,9 +32,11 @@ extern "C" {
 #include <sys/param.h>
 #include <sys/mman.h>
 #include <sys/resource.h>
+#include <sys/socket.h>
 #include <sys/stat.h>
 #include <sys/time.h>
 #include <sys/uio.h>
+#include <sys/un.h>
 
 #include <aio.h>
 #include <fcntl.h>
@@ -1398,6 +1400,77 @@ TEST_F(WriteBackAsync, eof)
 	leak(fd);
 }
 
+/*
+ * Nothing bad should happen if a file with a dirty writeback cache is closed
+ * while the last copy lies in some socket's socket buffer.  Inspired by bug
+ * 289686 .
+ */
+TEST_F(WriteBackAsync, scm_rights)
+{
+	const char FULLPATH[] = "mountpoint/some_file.txt";
+	const char RELPATH[] = "some_file.txt";
+	const char *CONTENTS = "abcdefgh";
+	uint64_t ino = 42;
+	int fd;
+	ssize_t bufsize = strlen(CONTENTS);
+	int s[2];
+	struct msghdr msg;
+	struct iovec iov;
+	char message[CMSG_SPACE(sizeof(int))];
+	union {
+		char buf[CMSG_SPACE(sizeof(fd))];
+		struct cmsghdr align;
+	} u;
+
+	expect_lookup(RELPATH, ino, 0);
+	expect_open(ino, 0, 1);
+	/* VOP_SETATTR will try to set timestamps during flush */
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_SETATTR &&
+				in.header.nodeid == ino);
+		}, Eq(true)),
+		_)
+	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+		SET_OUT_HEADER_LEN(out, attr);
+		out.body.attr.attr.ino = ino;
+		out.body.attr.attr.mode = S_IFREG | 0644;
+		out.body.attr.attr.size = bufsize;
+	})));
+
+	expect_write(ino, 0, bufsize, bufsize, CONTENTS);
+	expect_flush(ino, 1, ReturnErrno(0));
+	expect_release(ino, ReturnErrno(0));
+
+	/* Open a file on the fusefs file system */
+	fd = open(FULLPATH, O_RDWR);
+	ASSERT_LE(0, fd) << strerror(errno);
+
+	/* Write to the file to dirty its writeback cache */
+	ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+
+	/* Send the file into a socket */
+	ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, s)) << strerror(errno);
+	memset(&message, 0, sizeof(message));
+	memset(&msg, 0, sizeof(msg));
+	iov.iov_base = NULL;
+	iov.iov_len = 0;
+	msg.msg_iov = &iov;
+	msg.msg_iovlen = 1;
+	msg.msg_control = u.buf,
+	msg.msg_controllen = sizeof(u.buf);
+	struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+	cmsg->cmsg_level = SOL_SOCKET;
+	cmsg->cmsg_type = SCM_RIGHTS;
+	cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
+	memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
+	ASSERT_GE(sendmsg(s[0], &msg, 0), 0) << strerror(errno);
+
+	close(fd);	// Close fd within our process
+	close(s[0]);
+	close(s[1]);	// The last copy of fd is within this socket's rcvbuf
+}
+
 /* 
  * When a file has dirty writes that haven't been flushed, the server's notion
  * of its mtime and ctime will be wrong.  The kernel should ignore those if it