Mistreatment by Apple Security is unfortunately something you’re likely to come across on a regular basis. Usually this concerns people that conduct free work for Apple in their spare time by auditing their assets. Despite Apple’s website claiming the opposite, you’ll frequently find things like quiet patching, no credit, no bounties, and an appalling lack of communication.

This is unwise on Apple’s part because it frustrates people who find these bugs and disincentivizes them from sharing them with Apple. Remember, every bug that gets reported to them (instead of being sold to some shady outfit) and that gets fixed subsequently is one that can not be exploited in the future to make your device less safe. The very least they can do to honour the work of independent researchers is to communicate clearly and give them credit where appropriate.

Last year on June 30th 2020, I discovered a vulnerability in Apple’s kernel (darwin-xnu), which I promptly reported. On July 2, 2020, I received notification that my report had been examined and that they were looking into the issue. A few days later, I asked if I could be kept informed of any updates, and they indicated there were none at the time. I never heard from them again after that.

A few weeks ago on 24 June 2021, I found out that this vulnerability had been fixed for a while, unfortunately I have not received any updates or credits. On the basis of this, I sent an e-mail, to which I have not gotten a response to date.

When I discovered that the vulnerability had been resolved due to a new revision of the source code of darwin-xnu being posted on github, my report flashed through my mind, and I went to see if it had been fixed. I discovered using git blame that this vulnerability was already resolved 8 months ago (on January 11, 2021)! (Git blame)

So, let’s have a look at this issue! The mount() syscall for “nfs” filesystems had a double fetch vulnerability.

In theory, a Double Fetch is a conditional competitive vulnerability. It is a battle for data access between kernel mode and user mode. Virtual memory locations are typically partitioned between kernel space and user space in modern operating systems such as Linux and BSD derivatives. More information regarding double fetch vulnerabilities can be found in my post “An introduction to Kernel Exploitation Part 1”.

The first fetch occurs in “this” instance. In this case, the External Data Representation (XDR) variables are parsed in NFS. You’re probably thinking right now What exactly is XDR? Externel Data Representation (XDR) is a data format description language that can only be used to describe data. It isn’t a programming language at all. This language allows you to describe complex data formats in a clear and succinct manner. The XDR “programming” language is similar to the C programming language. NFS and other protocols employ XDR to describe the format of their data.

So, in essence, they are copying the length of all XDR variables from userland, then doing a size check on the argslength variable that was just copied from userland:

argslength = ntohl(argslength);
/* put a reasonable limit on the size of the XDR args */
if (argslength > 16*1024) {
	error = E2BIG;

So obviously argslength cannot be greater than (16*1024 = 16384);. This is fine, it is most likely merely a standard. Afterwards it will round up the argslength and allocate an xdrbuf:

/* allocate xdr buffer */
xdrbuf = xb_malloc(xdr_rndup(argslength));
if (!xdrbuf) {
	error = ENOMEM;

So, after allocating this buffer, they attempt to copy all the XDR parameters from userspace on line 1920:

if (inkernel)
	bcopy(CAST_DOWN(void *, data), xdrbuf, argslength);
	error = copyin(data, xdrbuf, argslength);

This will copy all arguments from data up to argslength to the freshly allocated xdrbuf. When this is done this will be passed to the mountnfs function:

error = mountnfs(xdrbuf, mp, ctx, &vp);

Alright, let’s take a look at the mountnfs function! This accepts our xdrbuf as an argument. Let’s just get to the important things:

/* set up NFS mount with args */
xb_init_buffer(&xb, xdrbuf, 2*XDRWORD);
xb_get_32(error, &xb, val); /* version */
xb_get_32(error, &xb, argslength); /* args length */

In this block, it will initialize xb using the xb_init_buffer function. So, xb is of the type struct xdrbuf. Let’s take a glance at that definition first for context.

struct xdrbuf {
	union {
		struct {
			char *                  xbb_base;       /* base address of buffer */
			size_t                  xbb_size;       /* size of buffer */
			size_t                  xbb_len;        /* length of data in buffer */
		} xb_buffer;
	} xb_u;
	char *          xb_ptr;         /* pointer to current position */
	size_t          xb_left;        /* bytes remaining in current buffer */
	size_t          xb_growsize;    /* bytes to allocate when growing */
	xdrbuf_type     xb_type;        /* type of xdr buffer */
	uint32_t        xb_flags;       /* XB_* (see below) */

This structure, as we can see, will be initialized in xb_init_buffer, so let’s have a look at that as well:

 * initialize a single-buffer xdrbuf
xb_init_buffer(struct xdrbuf *xbp, char *buf, size_t buflen)
	xb_init(xbp, XDRBUF_BUFFER);
	xbp->xb_u.xb_buffer.xbb_base = buf;
	xbp->xb_u.xb_buffer.xbb_size = buflen;
	xbp->xb_u.xb_buffer.xbb_len = buflen;
	xbp->xb_growsize = 512;
	xbp->xb_ptr = buf;
	xbp->xb_left = buflen;
	if (buf) { /* when using an existing buffer, xb code should skip cleanup */
		xbp->xb_flags &= ~XB_CLEANUP;

So this bzero’s the entire xdrbuf structure and sets the type to XDRBUF BUFFER, then it sets the base address of the buffer to the xdrbuf that was copied from userland, sets the size of the buffer and the length of the data to 2 times XDRWORD, which equals 8, and it initializes some pointers to our xdrbuf. Once the xdrbuf struct has been initialized, they will continue to fetch some 32 bit data from the structure’s pointer to our xdrbuf using xb_get_32. They will use this to get the version and the argslength. Notice that this argslength is an XDR variable within the xdrbuf that was copied into kernel space during the second copyin call. This indicates that a malicious thread could manipulate the argslength variable in userspace between the first fetch on line 1902 and the second fetch on line 1920.

After that, they will use xb_init_buffer to reinitialize the xdrbuf structure with the malicious size we changed in userspace, despite the fact that the actual size of the xdrbuf hasn’t changed since the allocation with xb_malloc. A malicious attacker could then exploit this to cause a memory corruption bug.

So, now that we know how the bug operates, how did they resolve it? They’ve checked to see if the argslength from the second fetch is the same as the argslength from the first copy. In this scenario, I believe that is a viable solution.

After notifying Apple about the silent patch without credit, I sent them another email on July 8th, warning them about this article. Because they did not respond, I am making this post public. I have a couple more bugs on the shelf which I am considering to write about for educational purposes.

I truly hope Apple changes the way they handle these kinds of bugs. In my opinion, the way they are managing such issues is a major reason why people contemplate selling vulnerabilities to third-party vendors like Zerodium.

In any case, it was a fun bug to discover and write about, and I hope you enjoyed this blog article. Good-bye!