git: 8fca98f6881f - stable/14 - fusefs: sanitize FUSE_READLINK results for embedded NULs

From: Alan Somers <asomers_at_FreeBSD.org>
Date: Sat, 14 Oct 2023 17:57:43 UTC
The branch stable/14 has been updated by asomers:

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

commit 8fca98f6881fdd68a786f4366c345159ab0df408
Author:     Alan Somers <asomers@FreeBSD.org>
AuthorDate: 2023-10-04 18:48:01 +0000
Commit:     Alan Somers <asomers@FreeBSD.org>
CommitDate: 2023-10-14 17:57:09 +0000

    fusefs: sanitize FUSE_READLINK results for embedded NULs
    
    If VOP_READLINK returns a path that contains a NUL, it will trigger an
    assertion in vfs_lookup.  Sanitize such paths in fusefs, rejecting any
    and warning the user about the misbehaving server.
    
    PR:             274268
    Sponsored by:   Axcient
    Reviewed by:    mjg, markj
    Differential Revision: https://reviews.freebsd.org/D42081
    
    (cherry picked from commit 662ec2f781521c36b76af748d74bb0a3c2e27a76)
---
 sys/fs/fuse/fuse_ipc.h          |  1 +
 sys/fs/fuse/fuse_vnops.c        |  7 +++++++
 tests/sys/fs/fusefs/readlink.cc | 39 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 47 insertions(+)

diff --git a/sys/fs/fuse/fuse_ipc.h b/sys/fs/fuse/fuse_ipc.h
index 27f3662741c5..0ec556138be0 100644
--- a/sys/fs/fuse/fuse_ipc.h
+++ b/sys/fs/fuse/fuse_ipc.h
@@ -239,6 +239,7 @@ struct fuse_data {
 #define FSESS_WARN_CACHE_INCOHERENT 0x200000	/* Read cache incoherent */
 #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_MNTOPTS_MASK	( \
 	FSESS_DAEMON_CAN_SPY | FSESS_PUSH_SYMLINKS_IN | \
 	FSESS_DEFAULT_PERMISSIONS | FSESS_INTR)
diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c
index 21ee378b24c6..3249e5988801 100644
--- a/sys/fs/fuse/fuse_vnops.c
+++ b/sys/fs/fuse/fuse_vnops.c
@@ -2007,6 +2007,13 @@ fuse_vnop_readlink(struct vop_readlink_args *ap)
 	if (err) {
 		goto out;
 	}
+	if (strnlen(fdi.answ, fdi.iosize) + 1 < fdi.iosize) {
+		struct fuse_data *data = fuse_get_mpdata(vnode_mount(vp));
+		fuse_warn(data, FSESS_WARN_READLINK_EMBEDDED_NUL,
+				"Returned an embedded NUL from FUSE_READLINK.");
+		err = EIO;
+		goto out;
+	}
 	if (((char *)fdi.answ)[0] == '/' &&
 	    fuse_get_mpdata(vnode_mount(vp))->dataflags & FSESS_PUSH_SYMLINKS_IN) {
 		char *mpth = vnode_mount(vp)->mnt_stat.f_mntonname;
diff --git a/tests/sys/fs/fusefs/readlink.cc b/tests/sys/fs/fusefs/readlink.cc
index ff9aa08f6fae..30815f2cd4b6 100644
--- a/tests/sys/fs/fusefs/readlink.cc
+++ b/tests/sys/fs/fusefs/readlink.cc
@@ -79,6 +79,45 @@ TEST_F(Readlink, eloop)
 	EXPECT_EQ(ELOOP, errno);
 }
 
+/*
+ * If a malicious or buggy server returns a NUL in the FUSE_READLINK result, it
+ * should be handled gracefully.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=274268
+ */
+TEST_F(Readlink, embedded_nul)
+{
+	const char FULLPATH[] = "mountpoint/src";
+	const char RELPATH[] = "src";
+	const char dst[] = "dst\0stuff";
+	char buf[80];
+	const uint64_t ino = 42;
+
+	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+		SET_OUT_HEADER_LEN(out, entry);
+		out.body.entry.attr.mode = S_IFLNK | 0777;
+		out.body.entry.nodeid = ino;
+		out.body.entry.attr_valid = UINT64_MAX;
+		out.body.entry.entry_valid = UINT64_MAX;
+	})));
+
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_READLINK &&
+				in.header.nodeid == ino);
+		}, Eq(true)),
+		_)
+	).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+		memcpy(out.body.str, dst, sizeof(dst));
+		out.header.len = sizeof(out.header) + sizeof(dst) + 1;
+	})));
+
+	EXPECT_EQ(-1, readlink(FULLPATH, buf, sizeof(buf)));
+	EXPECT_EQ(EIO, errno);
+	EXPECT_EQ(-1, access(FULLPATH, R_OK));
+	EXPECT_EQ(EIO, errno);
+}
+
 TEST_F(Readlink, ok)
 {
 	const char FULLPATH[] = "mountpoint/src";