The branch main has been updated by markj:

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

commit d91c459a93e5f70c1d3ad3d504bcf64babed8600
Author:     Alan Somers <[email protected]>
AuthorDate: 2026-05-04 19:35:11 +0000
Commit:     Mark Johnston <[email protected]>
CommitDate: 2026-05-20 19:34:50 +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:    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 8ceb6bb1fb1a..7091296bb453 100644
--- a/sys/fs/fuse/fuse_ipc.h
+++ b/sys/fs/fuse/fuse_ipc.h
@@ -240,6 +240,7 @@ struct fuse_data {
 #define FSESS_WARN_INODE_MISMATCH 0x4000000 /* ino != nodeid */
 #define        FSESS_SETXATTR_EXT        0x8000000 /* extended 
fuse_setxattr_in */
 #define FSESS_AUTO_UNMOUNT       0x10000000 /* perform unmount when server 
dies */
+#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 | FSESS_AUTO_UNMOUNT)
diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c
index dd3cc5f16092..d1badd0700f8 100644
--- a/sys/fs/fuse/fuse_vnops.c
+++ b/sys/fs/fuse/fuse_vnops.c
@@ -2978,8 +2978,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;
 
@@ -2988,7 +2988,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 -
@@ -3044,6 +3051,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;
@@ -3124,8 +3132,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; "
@@ -3142,7 +3148,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
  */

Reply via email to