Skip to content

Commit 26484bf

Browse files
committed
Heap overflow in FUSE_LISTXATTR
CVE-2026-45252 If a malicious daemon sends a non-NUL-terminated list, the fusefs kernel module may read beyond the end of one heap-allocated buffer and potentially write beyond the end of a second buffer. A malicious daemon could disclose up to 253 bytes of kernel heap memory, or it could inject up to 250 attacker-controlled bytes into unallocated kernel heap space. Obtained from: FreeBSD 14.3
1 parent a82a5c6 commit 26484bf

3 files changed

Lines changed: 85 additions & 6 deletions

File tree

sys/fs/fuse/fuse_ipc.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ struct fuse_data {
238238
#define FSESS_WARN_WB_CACHE_INCOHERENT 0x400000 /* WB cache incoherent */
239239
#define FSESS_WARN_ILLEGAL_INODE 0x800000 /* Illegal inode for new file */
240240
#define FSESS_WARN_READLINK_EMBEDDED_NUL 0x1000000 /* corrupt READLINK output */
241+
#define FSESS_WARN_LSEXTATTR_NUL 0x20000000 /* Non nul-terminated xattr list */
241242
#define FSESS_MNTOPTS_MASK ( \
242243
FSESS_DAEMON_CAN_SPY | FSESS_PUSH_SYMLINKS_IN | \
243244
FSESS_DEFAULT_PERMISSIONS | FSESS_INTR)

sys/fs/fuse/fuse_vnops.c

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2825,8 +2825,8 @@ fuse_vnop_setextattr(struct vop_setextattr_args *ap)
28252825
* bsd_list, bsd_list_len - output list compatible with bsd vfs
28262826
*/
28272827
static int
2828-
fuse_xattrlist_convert(char *prefix, const char *list, int list_len,
2829-
char *bsd_list, int *bsd_list_len)
2828+
fuse_xattrlist_convert(struct fuse_data *data, char *prefix, const char *list,
2829+
int list_len, char *bsd_list, int *bsd_list_len)
28302830
{
28312831
int len, pos, dist_to_next, prefix_len;
28322832

@@ -2835,7 +2835,13 @@ fuse_xattrlist_convert(char *prefix, const char *list, int list_len,
28352835
prefix_len = strlen(prefix);
28362836

28372837
while (pos < list_len && list[pos] != '\0') {
2838-
dist_to_next = strlen(&list[pos]) + 1;
2838+
dist_to_next = strnlen(&list[pos], list_len - pos - 1) + 1;
2839+
if (list[pos + dist_to_next - 1] != '\0') {
2840+
fuse_warn(data, FSESS_WARN_LSEXTATTR_NUL,
2841+
"The FUSE server returned a non nul-terminated "
2842+
"LISTXATTR response.");
2843+
return (EIO);
2844+
}
28392845
if (bcmp(&list[pos], prefix, prefix_len) == 0 &&
28402846
list[pos + prefix_len] == extattr_namespace_separator) {
28412847
len = dist_to_next -
@@ -2891,6 +2897,7 @@ fuse_vnop_listextattr(struct vop_listextattr_args *ap)
28912897
struct fuse_listxattr_in *list_xattr_in;
28922898
struct fuse_listxattr_out *list_xattr_out;
28932899
struct mount *mp = vnode_mount(vp);
2900+
struct fuse_data *data = fuse_get_mpdata(mp);
28942901
struct thread *td = ap->a_td;
28952902
struct ucred *cred = ap->a_cred;
28962903
char *prefix;
@@ -2968,8 +2975,6 @@ fuse_vnop_listextattr(struct vop_listextattr_args *ap)
29682975
linux_list = fdi.answ;
29692976
/* FUSE doesn't allow the server to return more data than requested */
29702977
if (fdi.iosize > linux_list_len) {
2971-
struct fuse_data *data = fuse_get_mpdata(mp);
2972-
29732978
fuse_warn(data, FSESS_WARN_LSEXTATTR_LONG,
29742979
"server returned "
29752980
"more extended attribute data than requested; "
@@ -2986,7 +2991,7 @@ fuse_vnop_listextattr(struct vop_listextattr_args *ap)
29862991
* FreeBSD's format before giving it to the user.
29872992
*/
29882993
bsd_list = malloc(linux_list_len, M_TEMP, M_WAITOK);
2989-
err = fuse_xattrlist_convert(prefix, linux_list, linux_list_len,
2994+
err = fuse_xattrlist_convert(data, prefix, linux_list, linux_list_len,
29902995
bsd_list, &bsd_list_len);
29912996
if (err != 0)
29922997
goto out;

tests/sys/fs/fusefs/xattr.cc

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,79 @@ TEST_F(ListxattrSig, erange_forever)
448448
ASSERT_TRUE(WIFSIGNALED(status));
449449
}
450450

451+
/*
452+
* A buggy or malicious server returns a list that isn't nul-terminated. The
453+
* kernel should handle it gracefully.
454+
*/
455+
TEST_F(Listxattr, not_nul_terminated)
456+
{
457+
uint64_t ino = 42;
458+
int ns = EXTATTR_NAMESPACE_USER;
459+
char *data;
460+
const char expected[4] = {3, 'f', 'o', 'o'};
461+
const char first[255] = "user.foo\0system.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
462+
const uint8_t badlist[9] = {'u', 's', 'e', 'r', '.', 'f', 'o', 'o', 'd'};
463+
Sequence seq;
464+
465+
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
466+
.WillRepeatedly(Invoke(
467+
ReturnImmediate([=](auto in __unused, auto& out) {
468+
SET_OUT_HEADER_LEN(out, entry);
469+
out.body.entry.attr.mode = S_IFREG | 0644;
470+
out.body.entry.nodeid = ino;
471+
out.body.entry.attr.nlink = 1;
472+
out.body.entry.attr_valid = UINT64_MAX;
473+
out.body.entry.entry_valid = UINT64_MAX;
474+
})));
475+
476+
/*
477+
* On the first LISTXATTRS call, return a big attribute just to fill
478+
* the heap with non-NUL data.
479+
*/
480+
expect_listxattr(ino, 0,
481+
ReturnImmediate([&](auto in __unused, auto& out) {
482+
out.body.listxattr.size = sizeof(first);
483+
SET_OUT_HEADER_LEN(out, listxattr);
484+
}), &seq
485+
);
486+
expect_listxattr(ino, sizeof(first),
487+
ReturnImmediate([&](auto in __unused, auto& out) {
488+
memcpy((void*)out.body.bytes, first, sizeof(first));
489+
out.header.len = sizeof(fuse_out_header) + sizeof(first);
490+
}), &seq
491+
);
492+
/*
493+
* On the second LISTXATTRS call, return a malformed list with no NUL
494+
* termination. The heap might still be full of the data from the
495+
* first call.
496+
*/
497+
expect_listxattr(ino, 0,
498+
ReturnImmediate([&](auto in __unused, auto& out) {
499+
out.body.listxattr.size = sizeof(badlist);
500+
SET_OUT_HEADER_LEN(out, listxattr);
501+
}), &seq
502+
);
503+
expect_listxattr(ino, sizeof(badlist),
504+
ReturnImmediate([&](auto in __unused, auto& out) {
505+
memset((void*)out.body.bytes, 'x', sizeof(first));
506+
memcpy((void*)out.body.bytes, badlist, sizeof(badlist));
507+
out.header.len = sizeof(fuse_out_header) + sizeof(badlist);
508+
}), &seq
509+
);
510+
511+
data = new char[1024];
512+
513+
ASSERT_EQ(static_cast<ssize_t>(sizeof(expected)),
514+
extattr_list_file(FULLPATH, ns, data, sizeof(data)))
515+
<< strerror(errno);
516+
/*
517+
* Receiving this malformed list, the kernel should log it to dmesg and
518+
* report an IO error to the caller.
519+
*/
520+
ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, data, sizeof(data)));
521+
EXPECT_EQ(EIO, errno);
522+
}
523+
451524
/*
452525
* Get the size of the list that it would take to list no extended attributes
453526
*/

0 commit comments

Comments
 (0)