Talos Vulnerability Report

TALOS-2026-2442

mackron dr_libs dr_wav drwav__read_cue_to_metadata_obj out-of-bounds write vulnerability

November 10, 2026

CVE Number

CVE-2026-11316

Summary

A out-of-bounds write vulnerability exists in the dr_wav drwav__read_cue_to_metadata_obj functionality of dr_libs (version(s): 0.14.5). A specially crafted .wav file can lead to an out-of-bounds write. An attacker can provide a malicious file to trigger this vulnerability.

Confirmed Vulnerable Versions

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

dr_libs (version(s): 0.14.5)

Product URLs

dr_libs - https://github.com/mackron/dr_libs

CVSSv3 Score

8.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-787 - Out-of-bounds Write

Details

dr_libs is a collection of high-quality, single-file audio decoding libraries for C and C++, developed by David Reid (mackron). It is popular in the game development and embedded systems communities because it is extremely easy to integrate.

dr_wav is the WAV decoder and encoder from that collection. It is a single header that drops into any project needing WAV support without extra build steps and is widely used in games, audio tools and embedded software.

WAV files use the RIFF container format: a top-level RIFF chunk wraps a WAVE identifier and a sequence of typed sub-chunks. dr_wav supports metadata chunks including smpl, cue, bext, LIST and others and processes them in two passes. Pass 1 (drwav__metadata_parser_stage_count) walks the file to count recognized chunks and request the extra memory each variable-length field needs. Pass 2 (drwav__metadata_parser_stage_read) allocates a single contiguous block and reads each chunk into pParser->pMetadata[pParser->metadataCursor].

For smpl chunks, pass 1 computes calculatedLoopCount from pChunkHeader->sizeInBytes and reserves a slot only when it equals the declared loopCount at [1]. Pass 2 calls drwav__read_smpl_to_metadata_obj regardless; that function returns early on the same mismatch but returns totalBytesRead == pChunkHeader->sizeInBytes, which the caller accepts as success and uses to advance metadataCursor at [2] even though pass 1 reserved no slot:

if (drwav__chunk_matches(allowedMetadataTypes, pChunkID, drwav_metadata_type_smpl, "smpl")) {
    if (pChunkHeader->sizeInBytes >= DRWAV_SMPL_BYTES) {
        if (pParser->stage == drwav__metadata_parser_stage_count) {
            drwav_uint8 buffer[4];
            size_t bytesJustRead;

            if (!pParser->onSeek(pParser->pReadSeekUserData, 28, DRWAV_SEEK_CUR)) {
                return bytesRead;
            }
            bytesRead += 28;

            bytesJustRead = drwav__metadata_parser_read(pParser, buffer, sizeof(buffer), &bytesRead);
            if (bytesJustRead == sizeof(buffer)) {
                drwav_uint32 loopCount = drwav_bytes_to_u32(buffer);
                drwav_uint64 calculatedLoopCount;

                /* The loop count must be validated against the size of the chunk. */
                calculatedLoopCount = (pChunkHeader->sizeInBytes - DRWAV_SMPL_BYTES) / DRWAV_SMPL_LOOP_BYTES;
                if (calculatedLoopCount == loopCount) {
                    bytesJustRead = drwav__metadata_parser_read(pParser, buffer, sizeof(buffer), &bytesRead);
                    if (bytesJustRead == sizeof(buffer)) {
                        drwav_uint32 samplerSpecificDataSizeInBytes = drwav_bytes_to_u32(buffer);

                        pParser->metadataCount += 1; [1]
                        drwav__metadata_request_extra_memory_for_stage_2(pParser, sizeof(drwav_smpl_loop) * loopCount, DRWAV_METADATA_ALIGNMENT);
                        drwav__metadata_request_extra_memory_for_stage_2(pParser, samplerSpecificDataSizeInBytes, 1);
                    }
                } else {
                    /* Loop count in header does not match the size of the chunk. */
                }
            }
        } else {
            bytesRead = drwav__read_smpl_to_metadata_obj(pParser, pChunkHeader, &pParser->pMetadata[pParser->metadataCursor]);
            if (bytesRead == pChunkHeader->sizeInBytes) {
                pParser->metadataCursor += 1; [2]
            } else {
                /* Failed to parse. */
            }
        }

All metadata storage is handed out by the bump allocator drwav__metadata_get_memory at [3]. It returns the current pDataCursor and advances it unconditionally; its own comment states that out-of-memory checks “should have been done at an earlier stage”, so it trusts the caller to have pre-accounted for all capacity:

DRWAV_PRIVATE drwav_uint8* drwav__metadata_get_memory(drwav__metadata_parser* pParser, size_t size, size_t align)
{
    drwav_uint8* pResult;

    if (align) {
        drwav_uintptr modulo = (drwav_uintptr)pParser->pDataCursor % align;
        if (modulo != 0) {
            pParser->pDataCursor += align - modulo;
        }
    }

    pResult = pParser->pDataCursor; [3]

    /*
    Getting to the point where this function is called means there should always be memory
    available. Out of memory checks should have been done at an earlier stage.
    */
    DRWAV_ASSERT((pResult + size) <= (pParser->pData + drwav__metadata_memory_capacity(pParser)));

    pParser->pDataCursor += size;
    return pResult;
}

Because the smpl chunk advanced metadataCursor past the counted total, the following cue chunk is processed at an out-of-bounds slot of the metadata array. drwav__read_cue_to_metadata_obj requests storage for its cue points through drwav__metadata_get_memory at [4], which returns the now-corrupted pDataCursor and the per-cue-point loop writes file-controlled bytes through that pointer at [5]:

if (pMetadata->data.cue.cuePointCount == (pChunkHeader->sizeInBytes - DRWAV_CUE_BYTES) / DRWAV_CUE_POINT_BYTES) {
    pMetadata->data.cue.pCuePoints = (drwav_cue_point*)drwav__metadata_get_memory(pParser, sizeof(drwav_cue_point) * pMetadata->data.cue.cuePointCount, DRWAV_METADATA_ALIGNMENT); [4]
    DRWAV_ASSERT(pMetadata->data.cue.pCuePoints != NULL);

    if (pMetadata->data.cue.cuePointCount > 0) {
        drwav_uint32 iCuePoint;

        for (iCuePoint = 0; iCuePoint < pMetadata->data.cue.cuePointCount; ++iCuePoint) {
            drwav_uint8 cuePointData[DRWAV_CUE_POINT_BYTES];
            bytesJustRead = drwav__metadata_parser_read(pParser, cuePointData, sizeof(cuePointData), &totalBytesRead);

            if (bytesJustRead == sizeof(cuePointData)) {
                pMetadata->data.cue.pCuePoints[iCuePoint].id = drwav_bytes_to_u32(cuePointData + 0);
                pMetadata->data.cue.pCuePoints[iCuePoint].playOrderPosition = drwav_bytes_to_u32(cuePointData + 4); [5]
                pMetadata->data.cue.pCuePoints[iCuePoint].dataChunkId[0] = cuePointData[8];
                pMetadata->data.cue.pCuePoints[iCuePoint].dataChunkId[1] = cuePointData[9];
                pMetadata->data.cue.pCuePoints[iCuePoint].dataChunkId[2] = cuePointData[10];
                pMetadata->data.cue.pCuePoints[iCuePoint].dataChunkId[3] = cuePointData[11];
                pMetadata->data.cue.pCuePoints[iCuePoint].chunkStart = drwav_bytes_to_u32(cuePointData + 12);
                pMetadata->data.cue.pCuePoints[iCuePoint].blockStart = drwav_bytes_to_u32(cuePointData + 16);
                pMetadata->data.cue.pCuePoints[iCuePoint].sampleOffset = drwav_bytes_to_u32(cuePointData + 20);
            } else {
                break;
            }
        }

AddressSanitizer and UndefinedBehaviorSanitizer confirm the write-what-where. The fault is the store at [5] through pCuePoints = 0x7d1f41414141, whose low 32 bits (0x41414141) are the id marker bytes from the crafted cue point, confirming attacker control over the destination address. Register r15 = 0x42424242 is the playOrderPosition value drawn straight from the file, confirming attacker control over the value written. Both the address and the value are file-controlled.

The root cause is the divergence between pass 1 and pass 2 of drwav__metadata_process_chunk: when the smpl chunk body’s loopCount does not match calculatedLoopCount derived from pChunkHeader->sizeInBytes, pass 1 withholds the allocation while pass 2 still increments metadataCursor, placing subsequent chunk metadata in unaccounted heap space. The bump allocator advances pDataCursor unconditionally on the assumption that capacity was pre-checked, so the corrupted allocator state yields a heap-derived write pointer whose low 32 bits are attacker-controlled. Any application calling drwav_init_with_metadata, drwav_init_file_with_metadata, or drwav_init_memory_with_metadata on an untrusted WAV file is affected.

This desync is a single root cause with several reachable sinks: whichever metadata chunk follows the mismatched smpl chunk takes the out-of-bounds write. Three sinks were demonstrated against dr_libs 0.14.5. A following cue chunk escalates to the write-what-where reported here (drwav__read_cue_to_metadata_obj, at [5]): AddressSanitizer confirms a store through a pointer whose low 32 bits (0x41414141) and the written value (0x42424242) are both file-controlled. A following unknown chunk produces an 8-byte out-of-bounds write in drwav__metadata_process_unknown_chunk. A second smpl chunk produces a 4-byte out-of-bounds write in drwav__read_smpl_to_metadata_obj. All three are the same vulnerability and are eliminated by a single correction to the pass-2 metadataCursor increment at [2]; the cue path is reported here because it is the most severe.

Crash Information

../src/dr_wav.h:2299:25: runtime error: member access within misaligned address 0x7d1f41414141 for type 'drwav_cue_point', which requires 4 byte alignment
0x7d1f41414141: note: pointer points here
<memory cannot be printed>
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../src/dr_wav.h:2299:25
../src/dr_wav.h:2299:67: runtime error: store to misaligned address 0x7d1f41414145 for type 'drwav_uint32' (aka 'unsigned int'), which requires 4 byte alignment
0x7d1f41414145: note: pointer points here
<memory cannot be printed>
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../src/dr_wav.h:2299:67
AddressSanitizer:DEADLYSIGNAL
=================================================================
==568138==ERROR: AddressSanitizer: SEGV on unknown address 0x7d1f41414145 (pc 0x000000521df5 bp 0x7fffffffb550 sp 0x7fffffffb460 T0)
==568138==The signal is caused by a WRITE memory access.
    #0 0x000000521df5 in drwav__read_cue_to_metadata_obj /run/media/shiv/1tb/projects/vulnresearch/campaigns/dr-libs-20260327/dr-wav/./../src/dr_wav.h:2299:85
    #1 0x00000051cd77 in drwav__metadata_process_chunk /run/media/shiv/1tb/projects/vulnresearch/campaigns/dr-libs-20260327/dr-wav/./../src/dr_wav.h:2825:29
    #2 0x0000004ef798 in drwav_init__internal /run/media/shiv/1tb/projects/vulnresearch/campaigns/dr-libs-20260327/dr-wav/./../src/dr_wav.h:3724:33
    #3 0x000000519a6e in main /run/media/shiv/1tb/projects/vulnresearch/campaigns/dr-libs-20260327/dr-wav/harness_replay.c:44:7
    #4 0x7ffff7c785b4 in __libc_start_call_main (/lib64/libc.so.6+0x35b4) (BuildId: ff0267465bc3d76e21003b3bc5598fd5ee63e261)
    #5 0x7ffff7c78667 in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x3667) (BuildId: ff0267465bc3d76e21003b3bc5598fd5ee63e261)
    #6 0x000000400704 in _start (/run/media/shiv/1tb/projects/vulnresearch/campaigns/dr-libs-20260327/dr-wav/build/target.asan+0x400704) (BuildId: 432025a58bda4f195bc805802473bf127b752d2e)

==568138==Register values:
rax = 0x0000000000000000 rbx = 0x00007fffffffb460 rcx = 0x0000000000000000 rdx = 0x00007d1ff6de0128
rdi = 0x00000f7ffeb60000 rsi = 0x00007d1ff6de0120 rbp = 0x00007fffffffb550 rsp = 0x00007fffffffb460
 r8 = 0x00007d1ff6de0130 r9 = 0x00000fa3fedbc026 r10 = 0x00007bfff5b00050 r11 = 0x0000000000000246
r12 = 0x00007d1f41414145 r13 = 0x0000000000000000 r14 = 0x00007d1f41414141 r15 = 0x0000000042424242
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /run/media/shiv/1tb/projects/vulnresearch/campaigns/dr-libs-20260327/dr-wav/./../src/dr_wav.h:2299:85 in drwav__read_cue_to_metadata_obj
==568138==ABORTING

Vendor Response (CVE-2026-11316)

Vendor Link: https://github.com/mackron/dr_libs

Vendor Notes: Vendor had not replied. Please check their GitHub repository for latest release and information regarding the product.

Timeline

2026-07-02 - Cisco - Vendor Notification
2026-07-16 - Cisco - Follow-up sent
2026-09-15 - Cisco - Follow-up sent
2026-11-10 - Cisco - Published

Credit

Ankur Tyagi of Cisco Talos