CVE-2026-64725: write-up and PoC

Jul 28, 2026

In this blog post, we will take a detailed look at CVE-2026-64725.

The AIFF MARK chunk

AIFF (Audio Interchange File Format) is a chunk-based container format built on EA-IFF-85. The MARK chunk stores a list of named marker positions:

"MARK" <ckSize: uint32 BE>
       <numMarkers: uint16 BE>    ; unsigned according to the specification
       Marker[numMarkers] {
           id:       int16 BE
           position: uint32 BE
           name:     PascalString ; uint8 length + chars, padded to an even size
       }

Two facts matter here:

int64_t AIFFAudioFile::GetMarkerList(uint32_t*, AudioFileMarkerList*, bool) is the back end of the public CoreAudio property kAudioFilePropertyMarkerList. The expected contract for AudioFileGetProperty(...) is:

AudioFileMarker is 40 bytes wide, so each loop iteration writes 0x28 bytes.

What AIFFAudioFile::GetMarkerList is trying to do — and what goes wrong

The relevant assembly code is:

0x1831c9024  rev   w9, w9              ; w9 = 32-bit byteswap of numMarkers (left-shifted)
0x1831c9028  lsr   w10, w9, #0x10      ; logical shift, stored as the canonical uint16
...
0x1831c9038  umull x10, w24, w10       ; capacity = arg2 / 40 (unsigned)
0x1831c903c  lsr   x10, x10, #0x25
0x1831c9040  asr   w11, w9, #0x10      ; <-- BUG: arithmetic, sign-extending shift
0x1831c9044  cmp   w10, w9, asr #0x10  ; signed comparison uses the sign-extended value
0x1831c9048  csel  w25, w10, w11, lt   ; w25 = (capacity < signed numMarkers)
                                       ;       ? capacity : signed numMarkers

Here is C-like pseudocode illustrating the logic:

int32_t  capacityBytes = *arg2;                              // supplied by the caller
uint32_t capacity      = capacityBytes / 0x28;               // unsigned division

// Read 2 bytes from the file, then:
int32_t  fileWord      = byteswap32(zx32(numMarkersLE16));
int32_t  numMarkers    = fileWord >> 16;                     // ARITHMETIC shift
uint64_t loopCount;

if (capacity < numMarkers)         // SIGNED comparison
    loopCount = (uint64_t)capacity;
else
    loopCount = (uint64_t)(uint32_t)numMarkers;              // sign bits survive
                                                              // through the cast chain

So when the caller says, “I have room for 25 markers,” and the file claims numMarkers = 0x8000, int64_t AIFFAudioFile::GetMarkerList(uint32_t*, AudioFileMarkerList*, bool) does not clamp the count to 25. It clamps it to approximately 4.29 billion.

That value is then used as the loop counter, and the per-marker loop does the following:

The loop has only two termination conditions:

  1. A file read returns a non-zero status, such as EOF or an I/O error.
  2. loopCount reaches zero.

It does not consult the caller’s original output-buffer capacity. Once the loop passes that capacity, every subsequent iteration writes 40 bytes beyond the caller’s buffer.

Why this is dangerous

Out-of-bounds writes are reachable through a public API

GetMarkerList is the back end of AudioFileGetProperty(kAudioFilePropertyMarkerList, …). This property may be queried, often automatically, by:

The caller provides the destination buffer, while the file controls numMarkers. Triggering the bug requires only setting the high bit of a 16-bit field in the file. No additional malformed structure is necessary.

The OOB region is large and proportional to the file size

Each loop iteration consumes at least 8 bytes of file input—2 + 4 + 1 + at least 1 byte of padding—before the next read failure. The maximum number of iterations is therefore approximately min(0xFFFF8000, file_remaining_after_MARK / 8):

File sizeMaximum iterationsMaximum OOB write at 40 B/iteration
16 KB~2 K~80 KB
1 MB~125 K~5 MB
100 MB~12.5 M~500 MB

In every case, the writes continue beyond the end of the caller’s destination buffer, regardless of whether that buffer resides on the heap, the stack, or in static storage sized for only a few dozen markers.

The written bytes are structured, not opaque

Each 40-byte slot contains the following fields at fixed offsets:

Three of the five values in each slot are derived from attacker-controlled fields in the AIFF file. The specifics of what an attacker might construct from these primitives are outside the scope of this write-up. For triage purposes, what matters is that the write target, the 0x28-byte stride, and three of the four meaningful values in each slot are attacker-influenced.

This is firmly a memory-safety vulnerability, not merely a denial-of-service or silent-corruption issue.

Nothing upstream saves the situation

I checked the code for the most obvious safeguards. The results were not encouraging:

OOB writes into stack buffers

The PoC allocates the output buffer on the heap, but stack-resident destination buffers are a particularly interesting target. A declaration such as AudioFileMarker markers[N]; is a natural pattern for callers reading a bounded number of markers. In that case, the bug becomes a conventional stack-buffer overflow involving attacker-influenced contents.

Additional attack vectors

Beyond the OOB write itself, each loop iteration has two secondary effects worth noting:

  1. Heap grooming and object-shaping through CFString allocations. On every iteration, the parser calls into CoreFoundation to allocate a fresh CFStringRef from the marker’s Pascal-string name. Both the bytes and the length, from 0 to 255, are attacker-controlled. This means:

    • An attacker can force an arbitrary number of CFString heap allocations with attacker-chosen sizes and contents, interleaved with the 40-byte OOB writes. This may be useful for shaping the heap before and during the corruption.

    • The pointers written beyond the caller’s buffer are valid heap pointers to objects whose contents are controlled by the attacker. A victim object corrupted at one of these offsets therefore receives a live pointer to attacker-prepared data rather than random garbage.

  2. Resource exhaustion as a parallel DoS vector. A numMarkers value with its high bit set can drive approximately 4.29 billion iterations. Each iteration allocates a CFString that the caller has no practical way to release, because the pointer is either overwritten in place or written into OOB memory. On a 64-bit host, this exhausts virtual memory or committed memory long before the loop terminates naturally.

    This produces a reliable single-file out-of-memory condition in any process that queries kAudioFilePropertyMarkerList on untrusted input, including background indexers such as mdimport.

This works regardless of whether the output buffer itself resides on the stack, because each CFString is allocated on the heap.

Success rate

The success rate of this primitive is 100%: it always works.

Safe cases

GetMarkerListSizemalloc(size)GetMarkerList

The dangerous scenario reproduced by the PoC is a call to GetMarkerList using a fixed-size buffer. This is not ideal API usage, but it is a common pattern in real-world code.

What happens if the caller invokes GetMarkerListSize first, allocates exactly the reported output-buffer size, and only then calls GetMarkerList?

GetMarkerListSize eventually reaches AIFFAudioFile::GetMarkerListSize. That method reports the number of bytes required for the full marker list—and it is also buggy. It contains the same erroneous asr operation on the same field:

0x1831ca1f4  ldrh w8, [sp, #0xa]
0x1831ca1f8  rev  w8, w8
0x1831ca1fc  asr  w8, w8, #0x10          ; <-- sign-extends numMarkers
0x1831ca200  mov  w9, #0x28
0x1831ca204  mov  w10, #0x8
0x1831ca208  madd w8, w8, w9, w10         ; w8 = numMarkers * 40 + 8 (signed)
0x1831ca20c  str  w8, [x19]               ; *outDataSize = w8

For numMarkers = 0x8000, this produces (-32768) * 40 + 8 = 0xFFEC0008. When stored as a uint32_t, that becomes roughly 4 GB.

A caller that trusts the reported size will therefore usually see malloc or calloc fail before it ever calls GetMarkerList. From a security perspective, this is preferable to memory corruption, but it is still a hard failure.

Important! GetMarkerList does not depend on GetMarkerListSize. GetMarkerList is independently dangerous.

Streaming

The streaming parser is unaffected. AIFFAudioStream::ParseHeader does not dispatch on "MARK", so streaming AIFF consumers such as AudioFileStream do not reach this code path.

The bug is specific to the file-mode AudioFile API.

The same class of problem in the WAV parser

The CoreAudio WAV parser had similar signed-versus-unsigned integer issues. For some reason, Apple declined to treat that case as a vulnerability and quietly fixed it without assigning a CVE or issuing a bounty payment.

Timeline

Why does Apple treat this vulnerability exclusively as a DoS?

I don’t know. I thought that demonstrating a stable, controlled heap/stack OOB write was enough to get RCE. And NIST gives it 7.1/10 (HIGHT)

Turns out it was not enough for Apple 🤷‍♂️

PoC

The PoC is available on GitHub.

This is a personal website, it is not affiliated with any franchise, brand, or organization. All opinions are the author’s own. Information is provided for educational and research purposes only; see the Disclaimer for details. The website uses software, assets, and hosting listed on the Credits page.