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:
- The AIFF specification defines
numMarkersas an unsigned 16-bit integer. - Real-world files usually contain no more than a few dozen markers, but the parser enforces no upper bound.
int64_t AIFFAudioFile::GetMarkerList(uint32_t*, AudioFileMarkerList*, bool) is the back end of the public CoreAudio property kAudioFilePropertyMarkerList. The expected contract for AudioFileGetProperty(...) is:
- On entry,
*ioSizecontains the byte capacity of the caller’soutListbuffer. - The function fills
outListwith at most*ioSize / sizeof(AudioFileMarker) = *ioSize / 40markers. - On return,
*ioSizecontains the number of bytes actually written.
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:
- Reads 2 bytes for
id, 4 bytes forposition, 1 byte fornameLen, and thennameLenbytes for the name. - Writes 40 bytes to the caller’s buffer at
dst: aFloat64, aCFStringRefcreated from the marker name, aSInt32marker ID, and several zero-valued fields; then advancesdst += 0x28. - Increments
*ioSizeby0x28. - Decrements
loopCountand exits when it reaches zero.
The loop has only two termination conditions:
- A file read returns a non-zero status, such as EOF or an I/O error.
loopCountreaches 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:
- Audio editors enumerating cue points or regions.
- AVFoundation or AVAsset metadata extractors.
- Music-library applications importing AIFF files.
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 size | Maximum iterations | Maximum 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:
- A
Float64mFramePosition, derived from the marker’s file-controlledpositionfield and converted to a double. - A
CFStringRefmName, pointing to a newly allocated CoreFoundation string created from the marker name. - A
SInt32mMarkerID, derived from the file-controlled markerid. - Zero-valued type, reserved, and channel fields.
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:
- There is no bound on
numMarkersduring parsing. - There is no
NULLor size guard for the destination buffer. - The clamp selects the wrong branch precisely when the input is hostile.
- The correctly shifted value is computed but never used.
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:
Heap grooming and object-shaping through
CFStringallocations. On every iteration, the parser calls into CoreFoundation to allocate a freshCFStringReffrom 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
CFStringheap 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.
Resource exhaustion as a parallel DoS vector. A
numMarkersvalue with its high bit set can drive approximately 4.29 billion iterations. Each iteration allocates aCFStringthat 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
kAudioFilePropertyMarkerListon untrusted input, including background indexers such asmdimport.
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
GetMarkerListSize → malloc(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!
GetMarkerListdoes not depend onGetMarkerListSize.GetMarkerListis 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
- I found CVE-2026-64725 in
AudioToolboxCoreon macOS 26.4.1, build 25E253, running Darwin 25.4.0 (xnu-12377.101.15~1) on Apple Silicon, specifically an M1/T8103 system, while testing HOBO BN MCP. - I reported the vulnerability on May 11, 2026.
- Apple released the fix on July 27, 2026, in macOS, iOS, and iPadOS 26.6. The entire process took 77 days.
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.
ALTV!ST