CVE-2021-30660 - XNU Kernel Memory Disclosure

Alex Plaskett · June 1, 2021

The msgrcv_nocancel syscall could disclose uninitialized memory from kernel space into userspace. This is due to an incorrect calculation being performed when copying the memory.

The vulnerability was patched in the following releases:

Vulnerability Details (sysv_msg.c)

The msgrcv_nocancel syscall takes as part of its arguments the message size from userspace as msgsz

int
msgrcv_nocancel(struct proc *p, struct msgrcv_nocancel_args *uap, user_ssize_t *retval)
{
	int msqid = uap->msqid;
	user_addr_t user_msgp = uap->msgp;
	size_t msgsz = (size_t)uap->msgsz;      /* limit to 4G */

If the size requested by userspace is greater than the size sent to the kernel initially (using the msgsnd function) then msgsz is truncated to the sender size.

	if (msgsz > msghdr->msg_ts) {
		msgsz = msghdr->msg_ts;
	}

A loop is then used to copyout this data from the kernel msgpool into userspace:

	for (len = 0; len < msgsz; len += msginfo.msgssz) {
		size_t tlen;

		/* compare input (size_t) value against restrict (int) value */
		if (msgsz > (size_t)msginfo.msgssz) {
			tlen = msginfo.msgssz;
		} else {
			tlen = msgsz;
		}
		if (next <= -1) {
			panic("next too low #3");
		}
		if (next >= msginfo.msgseg) {
			panic("next out of range #3");
		}
		SYSV_MSG_SUBSYS_UNLOCK();
		eval = copyout(&msgpool[next * msginfo.msgssz],
		    user_msgp, tlen);

msginfo.msgsize is #define MSGSSZ 8 /* Each segment must be 2^N long */

If we examine the check in more detail we can see that if msgsz > (size_t)msginfo.msgssz i.e if the sender message size is greater than 8, then tlen will be set to 8. 8 bytes will then by copied out from kernel space to userspace and the len count incremented by 8 bytes.

As this size is controlled as a userspace argument to msgsnd, there is a problem here if the msgsnd SYS_msgsnd_nocancel is not a multiple of 8.

For example, if we use SYS_msgsnd_nocancel of 9 bytes which will set the msghdr->msg_ts to 9. Therefore tlen will be set to 8 bytes on each iteration of the loop. Therefore the first loop will copyout 8 bytes, then second loop will copy out 8 bytes and the loop will terminate. This will lead to 7 bytes being leaked. This primitive gives an attacker the ability to leak out between 1 and 7 bytes of uninitialized data following the sent message.

As the message pool memory is MALLOC’d without the M_ZERO flag being set:

msgpool = (char *)_MALLOC(msginfo.msgmax, M_SHM, M_WAITOK);

As an example we can set msgsnd msgsz as 17, this will copy 2 segments each of 8 bytes (starting at 0x41), then a segment containing 1 byte of sent data and the final 7 bytes leaked data.

We can see this has been initialized with the fill pattern of 0xbe under the KSAN kernel, demonstrating disclosure of the uninitialized kernel memory following the sent message:

msgid is 65536
Vuln:
  0000  03 00 00 00 00 00 00 00 41 41 41 41 41 41 41 41  ........AAAAAAAA
  0010  42 0b 85 ea fe 7f 00 00 32 be be be be be be be  B.......2.......
  0020  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  0030  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  0040  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  0050  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  0060  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  0070  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  0080  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  0090  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00a0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00b0  00 00 00 00                                      ....
Leaked address: 0xbebebebebebebe32

Under KASAN:

panic(cpu 0 caller 0xffffff8001b05179): "KASan: invalid 7-byte leak from 0xffffff803891d001 [VALID]
 Shadow             0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
 fffff7f0071239b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 
 fffff7f0071239c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 
 fffff7f0071239d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 
 fffff7f0071239e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 
 fffff7f0071239f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
 fffff7f007123a00:[00]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
 fffff7f007123a10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
 fffff7f007123a20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
 fffff7f007123a30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
 fffff7f007123a40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
 fffff7f007123a50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

"@/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/xnu_kasan/xnu-6153.141.1/san/kasan.c:509
Backtrace (CPU 0), Frame : Return Address
0xffffff8866ccf440 : 0xffffff8000473134 mach_kernel : _handle_debugger_trap + 0x384
0xffffff8866ccf490 : 0xffffff800081047c mach_kernel : _kdp_i386_trap + 0x15c
0xffffff8866ccf4d0 : 0xffffff80007fc537 mach_kernel : _kernel_trap + 0xa87
0xffffff8866ccf560 : 0xffffff80008171d0 mach_kernel : trap_from_kernel + 0x26
0xffffff8866ccf580 : 0xffffff8000472a2e mach_kernel : _DebuggerTrapWithState + 0x4e
0xffffff8866ccf6a0 : 0xffffff8001af5646 mach_kernel : _panic_trap_to_debugger.cold.1 + 0xa6
0xffffff8866ccf6f0 : 0xffffff8000473636 mach_kernel : _panic_trap_to_debugger + 0x156
0xffffff8866ccf740 : 0xffffff8001af5294 mach_kernel : _panic + 0x54
0xffffff8866ccf7b0 : 0xffffff8001b05179 mach_kernel : _kasan_report_internal.cold.1 + 0x19
0xffffff8866ccf7c0 : 0xffffff8001aea3cd mach_kernel : _kasan_report_internal + 0x34d
0xffffff8866ccf840 : 0xffffff8001ae7f13 mach_kernel : _kasan_crash_report + 0x33
0xffffff8866ccf870 : 0xffffff8001ae7a17 mach_kernel : _kasan_violation + 0x277
0xffffff8866ccfa30 : 0xffffff8001ae7bd1 mach_kernel : _kasan_check_uninitialized + 0x161
0xffffff8866ccfc30 : 0xffffff80007dc2be mach_kernel : _copyio + 0x27e
0xffffff8866ccfd50 : 0xffffff8001558383 mach_kernel : _msgrcv_nocancel + 0x9f3
0xffffff8866ccfee0 : 0xffffff800178fc20 mach_kernel : _unix_syscall64 + 0x890
0xffffff8866ccffa0 : 0xffffff8000817996 mach_kernel : _hndl_unix_scall64 + 0x16

In the fix we can see the following change was made to copy a full segment or less if at the end of the message:

	for (len = 0; len < msgsz; len += msginfo.msgssz) {
		size_t tlen;

		/*
		 * copy the full segment, or less if we're at the end
		 * of the message
		 */
		tlen = MIN(msgsz - len, (size_t)msginfo.msgssz);
		if (next <= -1) {
			panic("next too low #3");
		}
		if (next >= msginfo.msgseg) {
			panic("next out of range #3");
		}
		SYSV_MSG_SUBSYS_UNLOCK();
		eval = copyout(&msgpool[next * msginfo.msgssz],
		    user_msgp, tlen);

Twitter, Facebook