Skip to content

Robustness fixes for malformed ID3v2 tags - #2

Open
avwarez wants to merge 3 commits into
irwir:v3.9.1from
avwarez:v3.9.2
Open

Robustness fixes for malformed ID3v2 tags#2
avwarez wants to merge 3 commits into
irwir:v3.9.1from
avwarez:v3.9.2

Conversation

@avwarez

@avwarez avwarez commented Aug 30, 2026

Copy link
Copy Markdown

Three robustness fixes in the v2 parser, found while feeding id3lib deliberately malformed tags. Each one concerns a length or a nesting level that is taken from the file and used without a sanity check. None of them changes how a well-formed tag is parsed.

Branched off v3.9.1 (48c49ba). All three cases are present in master as well, so they come from id3lib-devel rather than from anything you changed.

1. Extended header flag bytes — src/header_tag.cpp

ParseExtended() reads the number of extended flag bytes from the file and loops that many times filling an ID3_Flags*[1]. ID3v2.4 defines exactly one flag byte, and only that one is ever examined afterwards, but the counter is a full byte, so a tag can declare up to 255 and the loop writes one pointer per declared byte into the one-element array. The array is replaced by a single ID3_Flags value; any further declared bytes are consumed and discarded. This also removes the allocations that were never freed.

2. Nested compressed frames — src/tag_parse.cpp

An ID3v2.2.1 CDM frame contains frames, and parseFrames() recurses into it. Nothing forbids those inner frames from being CDM frames in turn, so the nesting depth is whatever the file says it is, and each level costs a decompression buffer and a stack frame. A depth limit of 16 is added; past it the frame is skipped with a warning instead of being decompressed and descended into. Real tags do not nest these at all.

3. Declared uncompressed size — src/io_decorators.cpp

CompressedReader allocates the uncompressed size the frame declares — four bytes straight from the file — before looking at how much compressed data is actually available, so the declared size can be arbitrarily larger than anything those bytes could produce. Deflate cannot expand by more than 1032:1, so the declared size is now clamped to that ceiling for the compressed bytes at hand; the arithmetic is done wider and narrowed afterwards, since uLong is 32-bit on Windows. The return value of uncompress() is also checked, and the buffer is sized to what it actually produced, so a truncated stream no longer leaves an uninitialised tail for the parser to read.

Built and tested on Windows with MSVC (x64, Win32, ARM64) as part of an eMule build; ordinary tags round-trip unchanged.

I've kept the branch on my side named v3.9.2, but the PR necessarily targets v3.9.1 — feel free to merge wherever suits you, or to put it on a new branch if you'd rather leave v3.9.1 as published. Happy to rebase or split it differently if that helps review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant