Skip to content

Potential Variants of a previous bug discovered #312

Description

@Alexender676

I am writing this under the security policy of opening security risk bugs via issue. Under analyzing your code and the previous bug CVE-2026-32836 that occurred in this project, I checked if any other properties show similar patterns, and have found some with the same potential risk. I believe the risk and impact is the exact same as the original CVE.

Summary

dir_wav

These are all related to callsites of drwav__metadata_request_extra_memory_for_stage_2()

  • size of a RIFF/WAV LIST/adtl/labl metadata subchunk and ADTL labelled cue region
    The declared size of a RIFF/WAV LIST/adtl/labl metadata subchunk and the declared byte count of an ADTL ltxt labelled cue region are trusted.

  • size of BEXT coding history
    BEXT chunk's declared coding-history tail length is allocated before verifying that those bytes are present.

  • text size of LIST INFO
    An attacker-declared LIST/INFO text subchunk size is parsed before proving that the text payload exists.

  • size of unknown metadata chunk
    In unknown RIFF/WAV chunk path, the count pass adds the attacker-declared chunk size to the second-pass metadata allocation budget before proving that the chunk payload bytes are present

For these cases, a tiny RIFF/WAV passed through the metadata-enabled in-memory API can make the parser request a multi-gigabyte metadata allocation, leading to deny service in applications.

dir_mp3

  • tag size of dr_mp3 APE
    A local MP3-like file with a valid APETAGEX footer and a large apparent stream length can make drmp3_init_file_with_metadata() request a large buffer before any metadata body is read, which can deny service to applications.
size_t apeTagSize = (size_t)tagSize + 32;   /* tagSize = footer[12..15] uint32 LE */
drmp3_uint8* pTagData = (drmp3_uint8*)drmp3_malloc(apeTagSize, pAllocationCallbacks);

PoC

poc.zip

Uploading a zip with poc. It consists of just C files for each findings. They can be run with:

git clone https://github.com/mackron/dr_libs source
git -C source checkout 47a4f08e777faddf59a8955c4ea84f69f41020d5

for src in drlibs_adtl_label_size_oom.c \
           drlibs_adtl_ltxt_size_oom.c \
           drlibs_bext_coding_history_oom.c \
           drlibs_list_info_size_oom.c \
           drlibs_unknown_chunk_oom.c \
           drlibs_ape_tag_size_oom.c; do
    cc -O2 -g -Isource "$src" -lm -o "${src%.c}"
done

./drlibs_adtl_label_size_oom
./drlibs_adtl_ltxt_size_oom
./drlibs_bext_coding_history_oom
./drlibs_list_info_size_oom
./drlibs_unknown_chunk_oom
./drlibs_ape_tag_size_oom ape-declared-size.mp3

for the last one, you need a crafted mp3:

out=ape-declared-size.mp3
sz=$((0x40000000))                            # declared APE tag size = 1 GiB
truncate -s $((sz + 64)) "$out"               # sparse zeros throughout

printf 'APETAGEX'         | dd of="$out" bs=1 seek=$((sz + 32)) conv=notrunc 2>/dev/null
printf '\x00\x00\x00\x40' | dd of="$out" bs=1 seek=$((sz + 56)) conv=notrunc 2>/dev/null

ls -lh "$out"; du -h "$out"  

Suggested fix

I tried to make a check on every allocation site as a patch to resolve the symptoms. They were made in the help of LLM.

  • dir_wav
diff --git a/dr_wav.h b/dr_wav.h
index a18bc04..ab8c470 100644
--- a/dr_wav.h
+++ b/dr_wav.h
@@ -2098,6 +2098,7 @@ typedef struct
 {
     drwav_read_proc onRead;
     drwav_seek_proc onSeek;
+    drwav_tell_proc onTell;
     void *pReadSeekUserData;
     drwav__metadata_parser_stage stage;
     drwav_metadata *pMetadata;
@@ -2147,6 +2148,45 @@ DRWAV_PRIVATE void drwav__metadata_request_extra_memory_for_stage_2(drwav__metad
     pParser->extraCapacity += extra;
 }
 
+/* Verify that `bytesRequired` bytes remain ahead of the current read
+ * cursor before we trust an attacker-supplied length field as an
+ * allocation size. Uses the optional onTell + onSeek to consult the
+ * stream end position; when onTell is NULL we return TRUE so callers
+ * that never wired a tell callback keep the legacy behaviour. */
+DRWAV_PRIVATE drwav_bool32 drwav__metadata_parser_has_bytes_remaining(drwav__metadata_parser* pParser, drwav_uint64 bytesRequired)
+{
+    drwav_int64 currentPos;
+    drwav_int64 endPos;
+
+    if (pParser->onTell == NULL) {
+        return DRWAV_TRUE;
+    }
+
+    if (!pParser->onTell(pParser->pReadSeekUserData, &currentPos)) {
+        return DRWAV_FALSE;
+    }
+    if (currentPos < 0) {
+        return DRWAV_FALSE;
+    }
+
+    if (!pParser->onSeek(pParser->pReadSeekUserData, 0, DRWAV_SEEK_END)) {
+        return DRWAV_FALSE;
+    }
+    if (!pParser->onTell(pParser->pReadSeekUserData, &endPos)) {
+        drwav__seek_from_start(pParser->onSeek, (drwav_uint64)currentPos, pParser->pReadSeekUserData);
+        return DRWAV_FALSE;
+    }
+    if (!drwav__seek_from_start(pParser->onSeek, (drwav_uint64)currentPos, pParser->pReadSeekUserData)) {
+        return DRWAV_FALSE;
+    }
+
+    if (endPos < currentPos) {
+        return DRWAV_FALSE;
+    }
+
+    return bytesRequired <= (drwav_uint64)(endPos - currentPos);
+}
+
 DRWAV_PRIVATE drwav_result drwav__metadata_alloc(drwav__metadata_parser* pParser, drwav_allocation_callbacks* pAllocationCallbacks)
 {
     if (pParser->extraCapacity != 0 || pParser->metadataCount != 0) {
@@ -2583,14 +2623,35 @@ DRWAV_PRIVATE drwav_uint64 drwav__read_bext_to_metadata_obj(drwav__metadata_pars
 
             DRWAV_ASSERT((drwav_offset_ptr(drwav_buffer_reader_ptr(&reader), DRWAV_BEXT_RESERVED_BYTES)) == (bextData + DRWAV_BEXT_BYTES));
 
-            extraBytes = (size_t)(chunkSize - DRWAV_BEXT_BYTES);
+            {
+                /* The stage-1 count-stage helper may already have
+                 * capped the declared coding-history length, but be
+                 * defensive here too: never copy more bytes than the
+                 * pre-allocated metadata buffer can hold. */
+                drwav_uint64 declaredExtraBytes = chunkSize - DRWAV_BEXT_BYTES;
+                size_t bytesAvailable = 0;
+                size_t bytesUsed = (size_t)(pParser->pDataCursor - pParser->pData);
+                size_t capacity = drwav__metadata_memory_capacity(pParser);
+
+                if (bytesUsed < capacity) {
+                    bytesAvailable = capacity - bytesUsed;
+                }
+                if (bytesAvailable > 0) {
+                    bytesAvailable -= 1;    /* Leave room for the null terminator. */
+                }
+
+                extraBytes = bytesAvailable;
+                if (declaredExtraBytes < (drwav_uint64)extraBytes) {
+                    extraBytes = (size_t)declaredExtraBytes;
+                }
+            }
             if (extraBytes > 0) {
                 pMetadata->data.bext.pCodingHistory = (char*)drwav__metadata_get_memory(pParser, extraBytes + 1, 1);
                 DRWAV_ASSERT(pMetadata->data.bext.pCodingHistory != NULL);
 
                 pMetadata->data.bext.codingHistorySize = (drwav_uint32)drwav__metadata_parser_read(pParser, pMetadata->data.bext.pCodingHistory, extraBytes, NULL);
                 pMetadata->data.bext.pCodingHistory[pMetadata->data.bext.codingHistorySize] = '\0'; /* <-- Explicit null terminator in case of a badly formed file. */
-                
+                DRWAV_ASSERT(pMetadata->data.bext.codingHistorySize <= extraBytes);
                 bytesRead += pMetadata->data.bext.codingHistorySize;
             } else {
                 pMetadata->data.bext.pCodingHistory    = NULL;
@@ -2677,6 +2738,12 @@ DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_info_text_chunk(drwav__metada
     drwav_uint32 stringSizeWithNullTerminator = (drwav_uint32)chunkSize;
 
     if (pParser->stage == drwav__metadata_parser_stage_count) {
+        /* Don't charge the second-pass allocation for bytes that
+         * aren't actually in the stream; protects against a crafted
+         * LIST/INFO subchunk that declares a multi-GB text size. */
+        if (!drwav__metadata_parser_has_bytes_remaining(pParser, chunkSize)) {
+            return 0;
+        }
         pParser->metadataCount += 1;
         drwav__metadata_request_extra_memory_for_stage_2(pParser, stringSizeWithNullTerminator, 1);
     } else {
@@ -2716,6 +2783,9 @@ DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_unknown_chunk(drwav__metadata
     }
 
     if (pParser->stage == drwav__metadata_parser_stage_count) {
+        if (!drwav__metadata_parser_has_bytes_remaining(pParser, chunkSize)) {
+            return 0;
+        }
         pParser->metadataCount += 1;
         drwav__metadata_request_extra_memory_for_stage_2(pParser, (size_t)chunkSize, 1);
     } else {
@@ -2871,7 +2941,12 @@ DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_chunk(drwav__metadata_parser*
                 }
                 allocSizeNeeded += drwav__strlen(buffer) + 1;
 
-                /* Coding history. */
+                /* Coding history — only charge against extraCapacity
+                 * if those bytes really exist; otherwise a crafted
+                 * BEXT chunkSize triggers a multi-GB malloc later. */
+                if (!drwav__metadata_parser_has_bytes_remaining(pParser, pChunkHeader->sizeInBytes - DRWAV_BEXT_BYTES)) {
+                    return bytesRead;
+                }
                 allocSizeNeeded += (size_t)pChunkHeader->sizeInBytes - DRWAV_BEXT_BYTES + 1;
 
                 drwav__metadata_request_extra_memory_for_stage_2(pParser, allocSizeNeeded, 1);
@@ -2924,6 +2999,13 @@ DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_chunk(drwav__metadata_parser*
                 break;
             }
             subchunkDataSize = drwav_bytes_to_u32(subchunkSizeBuffer);
+            /* Reject a subchunk whose declared size exceeds the bytes
+             * remaining in the enclosing LIST chunk. Catches the
+             * labl/note/ltxt/INFO accumulation OOM in one place. */
+            if (bytesRead > pChunkHeader->sizeInBytes ||
+                subchunkDataSize > (pChunkHeader->sizeInBytes - bytesRead)) {
+                break;
+            }
 
             if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_label, "labl") || drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_note, "note")) {
                 if (subchunkDataSize >= DRWAV_LIST_LABEL_OR_NOTE_BYTES) {
@@ -3285,6 +3367,7 @@ DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc on
     if (isProcessingMetadata) {
         metadataParser.onRead = pWav->onRead;
         metadataParser.onSeek = pWav->onSeek;
+        metadataParser.onTell = pWav->onTell;
         metadataParser.pReadSeekUserData = pWav->pUserData;
         metadataParser.stage  = drwav__metadata_parser_stage_count;
     }
  • dir_mp3
diff --git a/dr_mp3.h b/dr_mp3.h
index a458d86..2e0b196 100644
--- a/dr_mp3.h
+++ b/dr_mp3.h
diff --git a/dr_mp3.h b/dr_mp3.h
index a458d86..2e0b196 100644
--- a/dr_mp3.h
+++ b/dr_mp3.h
@@ -3029,13 +3029,18 @@ static drmp3_bool32 drmp3_init_internal(drmp3* pMP3, drmp3_read_proc onRead, drm
                                     /* We first need to seek to the start of the APE tag. */
                                     if (onSeek(pUserData, streamEndOffset, DRMP3_SEEK_END)) {
                                         size_t apeTagSize = (size_t)tagSize + 32;
-                                        drmp3_uint8* pTagData = (drmp3_uint8*)drmp3_malloc(apeTagSize, pAllocationCallbacks);
-                                        if (pTagData != NULL) {
-                                            if (onRead(pUserData, pTagData, apeTagSize) == apeTagSize) {
-                                                drmp3__on_meta(pMP3, DRMP3_METADATA_TYPE_APE, pTagData, apeTagSize);
+                                        /* Bound APE-tag allocation: legitimate
+                                         * tags are kilobytes; a multi-GB tagSize
+                                         * is a crafted DoS input. */
+                                        if (apeTagSize <= 16777216) {
+                                            drmp3_uint8* pTagData = (drmp3_uint8*)drmp3_malloc(apeTagSize, pAllocationCallbacks);
+                                            if (pTagData != NULL) {
+                                                if (onRead(pUserData, pTagData, apeTagSize) == apeTagSize) {
+                                                    drmp3__on_meta(pMP3, DRMP3_METADATA_TYPE_APE, pTagData, apeTagSize);
+                                                }
+
+                                                drmp3_free(pTagData, pAllocationCallbacks);
                                             }
-
-                                            drmp3_free(pTagData, pAllocationCallbacks);
                                         }
                                     }
                                 }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions