Skip to content

[Bug]signed left-shift overflow in mp3dec_skip_id3v1 when parsing the APEv2 tag-size field (CWE-190) #151

Description

@1820893135-pixel

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] = 233233 << 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

  1. 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];
  1. 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.

  1. Also validate the magic/version per the APEv2 spec and bounds-check tag_size against the remaining buffer before using it.

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