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.
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, ¤tPos)) {
+ 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;
}
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);
}
}
}
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/lablmetadata subchunk and the declared byte count of an ADTLltxtlabelled 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
A local MP3-like file with a valid
APETAGEXfooter and a large apparent stream length can makedrmp3_init_file_with_metadata()request a large buffer before any metadata body is read, which can deny service to applications.PoC
poc.zip
Uploading a zip with poc. It consists of just C files for each findings. They can be run with:
for the last one, you need a crafted mp3:
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.