Exploit-DB #47479
Description: XNU - Remote Double-Free via Data Race in IPComp Input Path
Type: dos
Platform: macos
Date: 2019-10-09
Author: Google Security Research

Exploit:
=== Summary ===
This report describes a bug in the XNU implementation of the IPComp protocol
(https://tools.ietf.org/html/rfc3173). This bug can be remotely triggered by an
attacker who is able to send traffic to a macOS system (iOS AFAIK isn't
affected) *over two network interfaces at the same time*.


=== Some basics to provide context ===
IPComp is a protocol for compressing the payload of IP packets.

The XNU implementation of IPComp is (going by the last public XNU release)
enabled only on X86-64; ARM64 doesn't seem to have the feature enabled at all
(look for ipcomp_zlib in config/MASTER.x86_64 and config/MASTER.arm64). In other
words, it's enabled on macOS and disabled on iOS.

While IPComp is related to IPsec, the IPComp input path processes input even
when the user has not configured any IPsec stuff on the system.

zlib requires fairly large buffers for decompression and especially for
compression. In order to avoid allocating such buffers for each packet, IPComp
uses two global z_stream instances "deflate_stream" and "inflate_stream".
If IPComp isn't used, the buffer pointers in these z_stream instances remain
NULL; only when IPComp is actually used, the kernel will attempt to initialize
the buffer pointers.

As far as I can tell, the IPComp implementation of XNU has been completely
broken for years, which makes it impossible to actually reach the decompression
code. ipcomp_algorithm_lookup() is responsible for allocating global buffers for
the compression and decompression code; however, all of these allocations go
through deflate_alloc(), which (since xnu-1228, which corresponds to macOS 10.5
from 2007) calls _MALLOC() with M_NOWAIT. _MALLOC() leads to kalloc_canblock(),
which, if the M_NOWAIT flag was set and the allocation is too big for a kalloc
zone (size >= kalloc_max_prerounded), immediately returns NULL. On X86-64,
kalloc_max_prerounded is 0x2001; both deflateInit2() and inflateInit2() attempt
allocations bigger than that, causing them to fail with Z_MEM_ERROR, as is
visible with dtrace when observing the system's reaction to a single incoming
IPComp packet [empty lines removed]:

```
bash-3.2# ./inflate_test.dtrace
dtrace: script './inflate_test.dtrace' matched 11 probes
CPU     ID                    FUNCTION:NAME
  0 243037              deflateInit2_:entry deflate init (thread=ffffff802db84a40)
  0 224285            kalloc_canblock:entry kalloc_canblock(size=0x1738, canblock=0, site=ffffff8018e787e8)
  0 224286           kalloc_canblock:return kalloc_canblock()=0xffffff80496b9800
  0 224285            kalloc_canblock:entry kalloc_canblock(size=0x2000, canblock=0, site=ffffff8018e787e8)
  0 224286           kalloc_canblock:return kalloc_canblock()=0xffffff802f42f000
  0 224285            kalloc_canblock:entry kalloc_canblock(size=0x2000, canblock=0, site=ffffff8018e787e8)
  0 224286           kalloc_canblock:return kalloc_canblock()=0x0
  0 224285            kalloc_canblock:entry kalloc_canblock(size=0x20000, canblock=0, site=ffffff8018e787e8)
  0 224286           kalloc_canblock:return kalloc_canblock()=0x0
  0 224285            kalloc_canblock:entry kalloc_canblock(size=0x20000, canblock=0, site=ffffff8018e787e8)
  0 224286           kalloc_canblock:return kalloc_canblock()=0x0
  0 243038             deflateInit2_:return rval=0xfffffffc
  0 243073              inflateInit2_:entry inflate init (thread=ffffff802db84a40)
  0 224285            kalloc_canblock:entry kalloc_canblock(size=0x2550, canblock=0, site=ffffff8018e787e8)
  0 224286           kalloc_canblock:return kalloc_canblock()=0x0
  0 243074             inflateInit2_:return rval=0xfffffffc
```

(On iOS, the kalloc() limit seems to be higher, so if IPComp was built there,
the input path might actually work?)


=== main bug description ===
IPComp uses a single global `static z_stream inflate_stream` for decompressing
all incoming packets. This global is used without any locking. While processing
of packets from a single interface seems to be single-threaded, packets arriving
on multiple ethernet interfaces at the same time (or on an ethernet interface
and a non-ethernet interface) can be processed in parallel (see
dlil_create_input_thread() and its caller for the precise threading rules).
Since zlib isn't designed for concurrent use of a z_stream, this leads to memory
corruption.

If IPComp actually worked, I believe that this bug would lead to
things like out-of-bounds reads, out-of-bounds writes and use-after-frees.
However, since IPComp never actually manages to set up the compression and
decompression state, the bug instead manifests in the code that, for every
incoming IPComp packet, attempts to set up the deflate buffers and tears down
the successfully allocated buffers because some of the allocations failed:

```
int ZEXPORT
deflateInit2_(z_streamp strm, int  level, int  method, int  windowBits,
              int  memLevel, int  strategy, const char *version,
              int stream_size)
{
    [...]
    if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || method != Z_DEFLATED ||
        windowBits < 8 || windowBits > 15 || level < 0 || level > 9 ||
        strategy < 0 || strategy > Z_FIXED) {
        return Z_STREAM_ERROR;
    }
    if (windowBits == 8) windowBits = 9;  /* until 256-byte window bug fixed */
    s = (deflate_state *) ZALLOC(strm, 1, sizeof(deflate_state));
    if (s == Z_NULL) return Z_MEM_ERROR;
    strm->state = (struct internal_state FAR *)s;
    [...]
    s->window = (Bytef *) ZALLOC(strm, s->w_size, 2*sizeof(Byte));
    s->prev   = (Posf *)  ZALLOC(strm, s->w_size, sizeof(Pos));
    s->head   = (Posf *)  ZALLOC(strm, s->hash_size, sizeof(Pos));

    s->lit_bufsize = 1 << (memLevel + 6); /* 16K elements by default */

    overlay = (ushf *) ZALLOC(strm, s->lit_bufsize, sizeof(ush)+2);
    s->pending_buf = (uchf *) overlay;
    [...]
    if (s->window == Z_NULL || s->prev == Z_NULL