git: 290fd77a0a14 - releng/15.1 - fusefs: Handle buggy servers' LISTXATTR response
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Wed, 20 May 2026 19:38:53 UTC
The branch releng/15.1 has been updated by markj:
URL: https://cgit.FreeBSD.org/src/commit/?id=290fd77a0a1454ce051ebb290f8d2abd34d3a736
commit 290fd77a0a1454ce051ebb290f8d2abd34d3a736
Author: Alan Somers <asomers@FreeBSD.org>
AuthorDate: 2026-05-04 19:35:11 +0000
Commit: Mark Johnston <markj@FreeBSD.org>
CommitDate: 2026-05-20 13:51:59 +0000
fusefs: Handle buggy servers' LISTXATTR response
The fuse protocol requires server to respond to LISTXATTR with a
NUL-terminated string. If they don't, report an error rather than
attempt to scan through uninitialized memory for a NUL.
Approved by: re
Approved by: so
Security: FreeBSD-SA-26:20.fusefs
Security: CVE-2026-45252
admbugs: 1039
Reported by: Joshua Rogers
Sponsored by: ConnectWise
---
sys/fs/fuse/fuse_ipc.h | 1 +
sys/fs/fuse/fuse_vnops.c | 18 +++++++----
tests/sys/fs/fusefs/xattr.cc | 73 ++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 86 insertions(+), 6 deletions(-)
diff --git a/sys/fs/fuse/fuse_ipc.h b/sys/fs/fuse/fuse_ipc.h
index 374d0891617d..69501adf4e64 100644
--- a/sys/fs/fuse/fuse_ipc.h
+++ b/sys/fs/fuse/fuse_ipc.h
@@ -238,6 +238,7 @@ struct fuse_data {
#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_WARN_LSEXTATTR_NUL 0x20000000 /* Non nul-terminated xattr list */
#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 43a0d2de0d1a..1923325bba19 100644
--- a/sys/fs/fuse/fuse_vnops.c
+++ b/sys/fs/fuse/fuse_vnops.c
@@ -2895,8 +2895,8 @@ out:
* bsd_list, bsd_list_len - output list compatible with bsd vfs
*/
static int
-fuse_xattrlist_convert(char *prefix, const char *list, int list_len,
- char *bsd_list, int *bsd_list_len)
+fuse_xattrlist_convert(struct fuse_data *data, char *prefix, const char *list,
+ int list_len, char *bsd_list, int *bsd_list_len)
{
int len, pos, dist_to_next, prefix_len;
@@ -2905,7 +2905,14 @@ fuse_xattrlist_convert(char *prefix, const char *list, int list_len,
prefix_len = strlen(prefix);
while (pos < list_len && list[pos] != '\0') {
- dist_to_next = strlen(&list[pos]) + 1;
+ dist_to_next = strnlen(&list[pos], list_len - pos - 1) + 1;
+ if (list[pos + dist_to_next - 1] != '\0') {
+ fuse_warn(data, FSESS_WARN_LSEXTATTR_NUL,
+ "The FUSE server returned a non nul-terminated "
+ "LISTXATTR response.");
+ return (EXTERROR(EIO,
+ "The FUSE server returned a malformed list"));
+ }
if (bcmp(&list[pos], prefix, prefix_len) == 0 &&
list[pos + prefix_len] == extattr_namespace_separator) {
len = dist_to_next -
@@ -2961,6 +2968,7 @@ fuse_vnop_listextattr(struct vop_listextattr_args *ap)
struct fuse_listxattr_in *list_xattr_in;
struct fuse_listxattr_out *list_xattr_out;
struct mount *mp = vnode_mount(vp);
+ struct fuse_data *data = fuse_get_mpdata(mp);
struct thread *td = ap->a_td;
struct ucred *cred = ap->a_cred;
char *prefix;
@@ -3041,8 +3049,6 @@ fuse_vnop_listextattr(struct vop_listextattr_args *ap)
linux_list = fdi.answ;
/* FUSE doesn't allow the server to return more data than requested */
if (fdi.iosize > linux_list_len) {
- struct fuse_data *data = fuse_get_mpdata(mp);
-
fuse_warn(data, FSESS_WARN_LSEXTATTR_LONG,
"server returned "
"more extended attribute data than requested; "
@@ -3059,7 +3065,7 @@ fuse_vnop_listextattr(struct vop_listextattr_args *ap)
* FreeBSD's format before giving it to the user.
*/
bsd_list = malloc(linux_list_len, M_TEMP, M_WAITOK);
- err = fuse_xattrlist_convert(prefix, linux_list, linux_list_len,
+ err = fuse_xattrlist_convert(data, prefix, linux_list, linux_list_len,
bsd_list, &bsd_list_len);
if (err != 0)
goto out;
diff --git a/tests/sys/fs/fusefs/xattr.cc b/tests/sys/fs/fusefs/xattr.cc
index afeacd4a249e..6dfda55079eb 100644
--- a/tests/sys/fs/fusefs/xattr.cc
+++ b/tests/sys/fs/fusefs/xattr.cc
@@ -492,6 +492,79 @@ TEST_F(ListxattrSig, erange_forever)
ASSERT_TRUE(WIFSIGNALED(status));
}
+/*
+ * A buggy or malicious server returns a list that isn't nul-terminated. The
+ * kernel should handle it gracefully.
+ */
+TEST_F(Listxattr, not_nul_terminated)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+ char *data;
+ const char expected[4] = {3, 'f', 'o', 'o'};
+ const char first[255] = "user.foo\0system.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
+ const uint8_t badlist[9] = {'u', 's', 'e', 'r', '.', 'f', 'o', 'o', 'd'};
+ Sequence seq;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ /*
+ * On the first LISTXATTRS call, return a big attribute just to fill
+ * the heap with non-NUL data.
+ */
+ expect_listxattr(ino, 0,
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ out.body.listxattr.size = sizeof(first);
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }), &seq
+ );
+ expect_listxattr(ino, sizeof(first),
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ memcpy((void*)out.body.bytes, first, sizeof(first));
+ out.header.len = sizeof(fuse_out_header) + sizeof(first);
+ }), &seq
+ );
+ /*
+ * On the second LISTXATTRS call, return a malformed list with no NUL
+ * termination. The heap might still be full of the data from the
+ * first call.
+ */
+ expect_listxattr(ino, 0,
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ out.body.listxattr.size = sizeof(badlist);
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }), &seq
+ );
+ expect_listxattr(ino, sizeof(badlist),
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ memset((void*)out.body.bytes, 'x', sizeof(first));
+ memcpy((void*)out.body.bytes, badlist, sizeof(badlist));
+ out.header.len = sizeof(fuse_out_header) + sizeof(badlist);
+ }), &seq
+ );
+
+ data = new char[1024];
+
+ ASSERT_EQ(static_cast<ssize_t>(sizeof(expected)),
+ extattr_list_file(FULLPATH, ns, data, sizeof(data)))
+ << strerror(errno);
+ /*
+ * Receiving this malformed list, the kernel should log it to dmesg and
+ * report an IO error to the caller.
+ */
+ ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, data, sizeof(data)));
+ EXPECT_EQ(EIO, errno);
+}
+
/*
* Get the size of the list that it would take to list no extended attributes
*/