Summary
To skip a trailing APEv2 tag, mp3dec_skip_id3v1 inspects the last 32 bytes of the input: after verifying the 8-byte APETAGEX magic it reads the 4-byte tag-size field and reconstructs the 32-bit size as (uint32_t)(tag[3] << 24) | (tag[2] << 16) | (tag[1] << 8) | tag[0]. The tag[] bytes are uint8_t, which C promotes to signed int before the shift. When the most-significant byte tag[3] is >= 0x80 (here 0xE9 = 233), tag[3] << 24 yields 0xE9000000, above INT_MAX (0x7FFFFFFF), which is undefined behavior in C. UBSan reports left shift of 233 by 24 places cannot be represented in type 'int'. Because this build uses -fno-sanitize-recover=undefined, the process aborts immediately — a single crafted MP3 file becomes a denial of service.
- Affected versions: minimp3
ea99364f61c14656440e8d77e9c233ccf3124633 (single-header minimp3_ex.h)
- Severity: Medium (unauthenticated DoS in non-recovering UBSan builds; data corruption in recovering builds; no OOB memory access observed)
- CWE: CWE-190 (Integer Overflow)
Detail
Affected code
/* minimp3_ex.h:153:47 (excerpt from mp3dec_skip_id3v1) */
if (buf_size >= 32 && memcmp(buf + buf_size - 32, "APETAGEX", 8) == 0) {
const uint8_t *tag = buf + buf_size - 24; /* 4-byte tag-size field */
uint32_t tag_size = (uint32_t)(tag[3] << 24) | /* <-- tag[3] promoted to signed int */
(tag[2] << 16) |
(tag[1] << 8) |
tag[0];
...
}
Root cause: tag[] is uint8_t. In C, each element is integer-promoted to int before the shift. When tag[3] >= 0x80 (the high bit of the APEv2 little-endian size field is set), tag[3] << 24 produces a value above INT_MAX, which is undefined behavior. The outer (uint32_t) cast happens after the UB has already occurred.
Call chain:
mp3dec_skip_id3v1 (minimp3_ex.h:153, left-shift UB)
└─ mp3dec_skip_id3 (minimp3_ex.h:188)
└─ mp3dec_load_cb (minimp3_ex.h:355)
└─ mp3dec_load_buf (minimp3_ex.h:310)
└─ fuzz_load_buf (harness.c:92)
POC (tested on the unmodified source)
Attached minimp3_poc_apev2_shift.mp3 (307 bytes) — valid MPEG-1 Layer-3 audio data (frame sync 0xFF 0xFB at offset 0x00) followed by a 32-byte APEv2 tag footer at the very end of the file.
APEv2 footer structure (offsets 0x113-0x132, last 32 bytes):
| Offset |
Bytes |
Field |
| 0x113..0x11A |
APETAGEX |
8-byte magic |
| 0x11B..0x11E |
D0 07 00 00 |
version = 2000 (APEv2) |
| 0x11F..0x122 |
A7 E6 CC E9 |
tag-size field → tag[3] = 0xE9 = 233 |
| 0x123..0x126 |
34 FA 0D 99 |
item count |
| 0x127..0x12A |
D2 6B 9B 76 |
flags |
| 0x12B..0x132 |
... |
reserved |
tag[3] = 233 → 233 << 24 = 0xE9000000 > INT_MAX → UBSan trigger.
ASAN_OPTIONS=detect_leaks=0 ./minimp3_fuzzer -runs=0 minimp3_poc_apev2_shift.mp3
Trigger result
/src/minimp3_ex.h:153:47: runtime error: left shift of 233 by 24 places cannot be represented in type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /src/minimp3_ex.h:153:47
[process exited with code 1]
Symbolized stack:
#0 mp3dec_skip_id3v1 /src/minimp3_ex.h:153:47
#1 mp3dec_skip_id3 /src/minimp3_ex.h:188:5
#2 mp3dec_load_cb /src/minimp3_ex.h:355:9
#3 mp3dec_load_buf /src/minimp3_ex.h:310:12
#4 fuzz_load_buf /src/harness.c:92:11
#5 LLVMFuzzerTestOneInput /src/harness.c:134:5
- Replay exit code:
1 (UBSan abort)
- Deterministic: yes — reproduces 3/3 in the vul container
- No ASan out-of-bounds access was observed; this is arithmetic UB only (signed left-shift overflow)
Suggested fix
- Minimal fix: promote each byte to an unsigned type before shifting:
uint32_t tag_size = (uint32_t)tag[3] << 24 |
(uint32_t)tag[2] << 16 |
(uint32_t)tag[1] << 8 |
(uint32_t)tag[0];
- Cleaner: read the 4 bytes as little-endian directly (the APEv2 size field is already little-endian):
uint32_t tag_size;
memcpy(&tag_size, tag, 4);
This removes the shifts entirely.
- Also validate the magic/version per the APEv2 spec and bounds-check
tag_size against the remaining buffer before using it.
Summary
To skip a trailing APEv2 tag,
mp3dec_skip_id3v1inspects the last 32 bytes of the input: after verifying the 8-byteAPETAGEXmagic it reads the 4-byte tag-size field and reconstructs the 32-bit size as(uint32_t)(tag[3] << 24) | (tag[2] << 16) | (tag[1] << 8) | tag[0]. Thetag[]bytes areuint8_t, which C promotes to signedintbefore the shift. When the most-significant bytetag[3]is>= 0x80(here0xE9= 233),tag[3] << 24yields0xE9000000, aboveINT_MAX(0x7FFFFFFF), which is undefined behavior in C. UBSan reportsleft shift of 233 by 24 places cannot be represented in type 'int'. Because this build uses-fno-sanitize-recover=undefined, the process aborts immediately — a single crafted MP3 file becomes a denial of service.ea99364f61c14656440e8d77e9c233ccf3124633(single-headerminimp3_ex.h)Detail
Affected code
Root cause:
tag[]isuint8_t. In C, each element is integer-promoted tointbefore the shift. Whentag[3] >= 0x80(the high bit of the APEv2 little-endian size field is set),tag[3] << 24produces a value aboveINT_MAX, which is undefined behavior. The outer(uint32_t)cast happens after the UB has already occurred.Call chain:
POC (tested on the unmodified source)
Attached
minimp3_poc_apev2_shift.mp3(307 bytes) — valid MPEG-1 Layer-3 audio data (frame sync0xFF 0xFBat offset 0x00) followed by a 32-byte APEv2 tag footer at the very end of the file.APEv2 footer structure (offsets 0x113-0x132, last 32 bytes):
tag[3] = 233→233 << 24 = 0xE9000000 > INT_MAX→ UBSan trigger.Trigger result
Symbolized stack:
1(UBSan abort)Suggested fix
This removes the shifts entirely.
tag_sizeagainst the remaining buffer before using it.