Commit Graph
2739 Commits
Author SHA1 Message Date
Acts1631andGitHub 8a78c5e5bd Guard file-derived audio property conversions (#1421)
Crafted duration and sampling-rate fields can produce values outside the int properties API. Leave those properties at their defaults instead of invoking undefined floating-point conversions, and promote Shorten bitrate operands before multiplication.
2026-08-17 18:46:22 +02:00
manuaudioandGitHub ee0ab489e0 Validate enumerations decoded from file bytes before casting to them (#1420)
* ID3v2: validate the text encoding byte before casting it

The first byte of most ID3v2 frames selects the text encoding and is
cast straight to String::Type. It comes from the file, so it can be any
value, and String::Type enumerates 0..4 — loading an enumeration object
whose value is outside the enumeration's range is undefined:

  runtime error: load of value 127, which is not a valid value for type
  'String::Type'
  runtime error: load of value 4294967295, which is not a valid value
  for type 'String::Type'

The second value is 0xFF read through a plain signed char.

It matters beyond the sanitizer: String::data() switches on the type
with no default case, so an unrecognised encoding silently falls
through and returns an empty ByteVector on the render path.

Add Utils::textEncodingFromByte(), which maps the byte to its
String::Type or falls back to Latin1 — the encoding these frames
already declare as their default — and use it at the eleven sites that
read the byte from a file.

Note that a byte of 5, 6 or 7 is *not* undefined, because the range of
an enumeration is the bit width spanned by its enumerators rather than
the enumerators themselves. It is still not a valid encoding, and the
same helper rejects it.

Assisted-By: Claude Code (Claude Opus 5)

* Validate the picture type byte before casting it

The picture type enumeration is declared by DECLARE_PICTURE_TYPE_ENUM
and shared by three classes, and all three cast a file-supplied value
to it without checking. The enumerators run 0x00..0x14, so the range of
the enumeration is 0..31 and a load outside that is undefined:

  taglib/mpeg/id3v2/frames/attachedpictureframe.cpp:96:13: runtime
  error: load of value 4294967295, which is not a valid value for type
  'AttachedPictureFrame::Type'
  taglib/flac/flacpicture.cpp:129:13: runtime error: load of value
  65536, which is not a valid value for type 'Type'

FLAC is the widest of the three: it casts a whole 32 bit field, so no
truncation to a byte limits it. ASF and ID3v2 read a plain signed char,
which reaches the same place through sign extension.

This is visible to callers, not only to a sanitizer. Before the change
AttachedPictureFrame::type() and FLAC::Picture::type() return -1 and
65536 for the files above; after it they return Other.

Since the enumeration comes from a macro, add one typeFromByte() to the
macro backed by Utils::pictureTypeFromByte(), rather than three copies
of the same check.

Six reports before the change, none after.

Assisted-By: Claude Code (Claude Opus 5)

* ID3v2: validate the RVA2 channel byte before casting it

Each channel record in a relative volume frame starts with a channel
type byte that is cast straight to ChannelType. The enumerators run
0x00..0x08, so the range of the enumeration is 0..15 and the byte from
the file can leave it:

  runtime error: load of value 127, which is not a valid value for type
  'RelativeVolumeFrame::ChannelType'
  runtime error: load of value 4294967295, which is not a valid value
  for type 'RelativeVolumeFrame::ChannelType'

The value is also used as a map key, so the frame ends up holding a
channel that channels() then reports back.

Map an unrecognised byte to Other instead.

Two reports before the change, none after. The 123 files in tests/data
produce identical channel output either way.

Assisted-By: Claude Code (Claude Opus 5)

* ID3v2: validate the SYLT and ETCO enum bytes before casting them

Synchronised lyrics carry a timestamp format byte and a content type
byte, and event timing codes carry a timestamp format byte. All three
are cast without checking. TimestampFormat enumerates 0..2, so its
range is only 0..3:

  runtime error: load of value 127, which is not a valid value for type
  'SynchronizedLyricsFrame::TimestampFormat'
  runtime error: load of value 127, which is not a valid value for type
  'SynchronizedLyricsFrame::Type'
  runtime error: load of value 127, which is not a valid value for type
  'EventTimingCodesFrame::TimestampFormat'

with 4294967295 in place of 127 when the byte is 0xFF. Before the
change timestampFormat() and type() return -1 for such a file.

Map an unrecognised byte to Unknown and Other respectively.

Worth noting what is *not* changed: the event type byte on the line
below the ETCO cast is already written
static_cast<EventType>(static_cast<unsigned char>(...)), and EventType
enumerates up to 0xFE, so an unsigned char cannot leave its range. It
reads like the same defect and is not one.

Three reports before the change, none after. The 123 files in
tests/data produce identical output either way.

Assisted-By: Claude Code (Claude Opus 5)

* MP4: validate the atom data type before casting it

The type field of an iTunes metadata atom is a 32 bit value read from
the file and cast straight to AtomDataType, whose enumerators run
0..255. Values above 255 are outside the range of the enumeration:

  taglib/mp4/mp4atom.h:85:36: runtime error: load of value 65536, which
  is not a valid value for type 'AtomDataType'
  taglib/mp4/mp4atom.h:85:36: runtime error: load of value 4294967295,
  which is not a valid value for type 'AtomDataType'

Both cast sites are reachable: parseFreeForm calls parseData2 with
expectedFlags = -1, so the flags == expectedFlags test never
constrains the value, and the mean/name branch casts unconditionally. A
'----' atom with an arbitrary flags field reaches both.

Map anything outside the range to TypeUndefined, which the enumeration
already provides for exactly this.

Four reports before the change, none after. The 123 files in tests/data
produce identical item output either way.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-16 12:30:31 +02:00
manuaudioandUrs Fleisch d841424d48 MP4: do not convert an out of range length to int
The mdhd duration is a signed 64 bit field in version 1 and the
timescale sitting beside it is a 32 bit field that may be 1, so a
4 KB file can declare 2^62 units of a one-hertz clock. The millisecond
length is then ~4.6e21 and the conversion to int is undefined:

  taglib/mp4/mp4properties.cpp:207:34: runtime error: 4.61169e+21 is
  outside the range of representable values of type 'int'

The version 0 path reaches the same line with a 32 bit duration:

  taglib/mp4/mp4properties.cpp:207:34: runtime error: 4.29497e+12 is
  outside the range of representable values of type 'int'

The mvhd fallback a few lines above feeds the same expression, so it is
covered by the same guard.

Leave the field at its default rather than converting.

For the record, the other conversions in this file were checked and are
not affected. The esds and alac nominal bitrates divide a 32 bit value
by 1000.0, which cannot leave int's range, and the three
calculateMdatLength() estimates are integer arithmetic on a long long
rather than a double conversion.

One report per file before the change, none after. The 18 MP4 files in
tests/data report identical channels, sample rate, bitrate and length
before and after, and the suite runs 576 tests either way.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-16 08:19:59 +02:00
manuaudioandUrs Fleisch be424957da MPEG: do not convert out of range properties to int
The Xing header supplies both the frame count and the byte count as raw
32 bit values, and TagLib multiplies the frame count by the per-frame
duration without checking the result. At MPEG 2.5 Layer III / 8 kHz a
frame is 72 ms, so 2^32-1 declared frames puts the length at ~3.1e11 ms
and the conversion to int is undefined:

  taglib/mpeg/mpegproperties.cpp:166:35: runtime error: 3.09238e+11 is
  outside the range of representable values of type 'int'

The bitrate on the next line is reachable the other way round. One
declared frame of MPEG 1 Layer I at 48 kHz is 8 ms, and the declared
size is not the real file size, so a 1 KB file can claim 2^32-1 bytes:

  taglib/mpeg/mpegproperties.cpp:167:35: runtime error: 4.29497e+09 is
  outside the range of representable values of type 'int'

The length computed on the non-Xing path has the same shape but is
bounded by the real stream length rather than a declared count, so it
needs a file of a couple of gigabytes rather than a crafted header. I
have not built one; that guard is there for consistency, not on
demonstrated evidence.

Leave the field at its default rather than converting.

Two reports before the change, none after. The 23 MPEG files in
tests/data report identical channels, sample rate, bitrate and length
before and after, and the suite runs 576 tests either way.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-16 08:16:52 +02:00
manuaudioandUrs Fleisch c3d4eef597 Speex: do not convert out of range properties to int
Same shape as Vorbis. The length comes from the difference between two
64 bit granule positions over a sample rate read from the Speex header,
so a file can declare 2^62 samples at 1 Hz. The millisecond length is
then ~4.6e21 and the conversion to int is undefined:

  taglib/ogg/speex/speexproperties.cpp:165:39: runtime error: 4.61169e+21
  is outside the range of representable values of type 'int'

The bitrate on the next line is reachable the other way round, with one
sample at a high declared rate making the divisor tiny:

  taglib/ogg/speex/speexproperties.cpp:166:39: runtime error: 3.61877e+13
  is outside the range of representable values of type 'int'

Leave the field at its default rather than converting.

Two reports before the change, none after. The 23 FLAC, APE, Ogg
Vorbis, Opus and Speex files in tests/data report identical channels,
sample rate, bitrate and length before and after, and the suite runs
576 tests either way.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-15 18:18:23 +02:00
manuaudioandUrs Fleisch 83e5427fe0 Opus: do not convert out of range properties to int
Opus is the one worth reading twice: its granule clock is fixed at
48 kHz, so unlike the other formats no absurd declared sample rate is
needed. A 64 bit granule position alone is enough. At 2^62 samples the
millisecond length is ~9.6e16 and the conversion to int is undefined:

  taglib/ogg/opus/opusproperties.cpp:156:39: runtime error: 9.60768e+16
  is outside the range of representable values of type 'int'

The bitrate on the next line has the same shape as the other formats,
but because the divisor cannot be made arbitrarily small at a fixed
48 kHz it needs a stream of several megabytes to overflow, so I have
not built a test file for it. It is guarded for consistency rather than
on demonstrated evidence.

Leave the field at its default rather than converting.

One report before the change, none after. The 23 FLAC, APE, Ogg
Vorbis, Opus and Speex files in tests/data report identical channels,
sample rate, bitrate and length before and after, and the suite runs
576 tests either way.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-15 18:18:23 +02:00
manuaudioandUrs Fleisch 16d2585f84 Vorbis: do not convert out of range properties to int
The length comes from the difference between two 64 bit granule
positions over a sample rate read from the identification header, so a
file can declare 2^62 samples at 1 Hz. The millisecond length is then
~4.6e21 and the conversion to int is undefined:

  taglib/ogg/vorbis/vorbisproperties.cpp:168:39: runtime error:
  4.61169e+21 is outside the range of representable values of type 'int'

The bitrate on the next line is reachable the other way round, with one
sample at a high declared rate making the divisor tiny:

  taglib/ogg/vorbis/vorbisproperties.cpp:169:39: runtime error:
  3.51569e+13 is outside the range of representable values of type 'int'

Leave the field at its default rather than converting.

Two reports before the change, none after. The 23 FLAC, APE, Ogg
Vorbis, Opus and Speex files in tests/data report identical channels,
sample rate, bitrate and length before and after, and the suite runs
576 tests either way.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-15 18:18:23 +02:00
manuaudioandUrs Fleisch b4a1f3d7d1 APE: do not convert out of range properties to int
The frame count is assembled from three 32 bit header fields and the
sample rate is read straight from the header, so a file can declare
~4.3e9 frames at 1 Hz. The millisecond length is then ~4.3e12 and the
conversion to int is undefined:

  taglib/ape/apeproperties.cpp:141:35: runtime error: 4.29497e+12 is
  outside the range of representable values of type 'int'

The bitrate on the next line is reachable the other way round, with one
frame at a high declared rate making the divisor tiny:

  taglib/ape/apeproperties.cpp:142:35: runtime error: 3.60301e+13 is
  outside the range of representable values of type 'int'

Leave the field at its default rather than converting.

Two reports before the change, none after. The 23 FLAC, APE, Ogg
Vorbis, Opus and Speex files in tests/data report identical channels,
sample rate, bitrate and length before and after, and the suite runs
576 tests either way.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-15 18:18:23 +02:00
manuaudioandUrs Fleisch 4c3e3cf1b8 FLAC: do not convert out of range properties to int
STREAMINFO carries the total sample count in a 36 bit field and the
sample rate in a 20 bit one, so a structurally valid file can declare
2^36-1 frames at 1 Hz. The millisecond length is then ~6.9e13 and the
conversion to int is undefined:

  taglib/flac/flacproperties.cpp:136:35: runtime error: 6.87195e+13 is
  outside the range of representable values of type 'int'

The bitrate on the next line is reachable the other way round, with a
short stream at a high declared rate making the divisor tiny:

  taglib/flac/flacproperties.cpp:137:35: runtime error: 1.75922e+10 is
  outside the range of representable values of type 'int'

Leave the field at its default rather than converting.

Two reports before the change, none after. The 23 FLAC, APE, Ogg
Vorbis, Opus and Speex files in tests/data report identical channels,
sample rate, bitrate and length before and after, and the suite runs
576 tests either way.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-15 18:18:23 +02:00
manuaudioandUrs Fleisch df6c8e15d1 MPC: do not convert an out of range length to int
Fifth instance of the same shape, in readSV8:

    const auto length = static_cast<double>(frameCount) * 1000.0 / d->sampleRate;
    d->length  = static_cast<int>(length + 0.5);
    d->bitrate = static_cast<int>(static_cast<double>(streamLength) * 8.0 / length + 0.5);

frameCount is sampleFrames minus begSilence, both read from the file, so
the millisecond figure can land outside int:

  taglib/mpc/mpcproperties.cpp:244:39: runtime error: 4.18294e+17 is
  outside the range of representable values of type 'int'

The bitrate on the next line goes the same way when length is small, so
it is guarded too. One report before, none after, and the .mpc files in
tests/data report identical properties.

Turned up independently from two different seed files, sv8_header.mpc
and zerodiv.mpc.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-15 17:52:56 +02:00
manuaudioandUrs Fleisch c767a55744 AIFF: do not convert out of range properties to int
Fourth instance of the same shape. The sample rate is an 80 bit float
read from the file and the frame count is a 32 bit count from it, so all
three conversions here can be handed a value int cannot represent:

  taglib/riff/aiff/aiffproperties.cpp:145:38: runtime error: 6.29416e+49
  is outside the range of representable values of type 'int'
  taglib/riff/aiff/aiffproperties.cpp:150:35: runtime error: 1.00707e+48
  is outside the range of representable values of type 'int'

Line 149 is the same shape and reachable the other way round, with a
large sampleFrames over a small sample rate, so it is guarded too.

Leave the field at its default rather than converting. All seven AIFF
files in tests/data report identical channels, sample rate, bitrate and
length before and after.

Two reports before the change, none after.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-15 17:52:56 +02:00
manuaudioandUrs Fleisch 9e3285c988 ASF: do not convert an out of range length to int
Third instance of the same shape as the DSF and DSDIFF changes:

    static_cast<int>(static_cast<double>(duration) / 10000.0
                     - static_cast<double>(preroll) + 0.5)

duration and preroll are both long long values read from the file
properties object, so the result can land outside int, and converting a
double the destination type cannot represent is undefined:

  taglib/asf/asffile.cpp:240:22: runtime error: -2.80375e+14 is outside
  the range of representable values of type 'int'

Only skip the conversion when the value will not fit. Values that do
fit, negative ones included, are set exactly as before, so the three
.wma files in tests/data still report lengthMs 3549, 96502 and 3712.

Found by the same fuzzing that turned up the DSF case.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-15 17:52:56 +02:00
manuaudioandUrs Fleisch c275891a77 DSDIFF: do not convert an out of range length to int
Same shape as the DSF change: sampleCount comes from a chunk size in the
file and sampleRate is read from it, so

    static_cast<int>(static_cast<double>(d->sampleCount) * 1000.0
                     / d->sampleRate + 0.5)

can be handed a value int cannot represent, which is undefined.

In the PR I said I had not demonstrated this one, because chunkFits
bounds the sample count by the file length so a few mutated bytes will
not do it. Constructing the file it does need - a 700 kB DSD chunk with
a sample rate of 1 - reaches it:

  taglib/dsdiff/dsdiffproperties.cpp:61:24: runtime error: 2.8e+09 is
  outside the range of representable values of type 'int'

Two reports before the change, none after. tests/data/empty10ms.dff is
unchanged at lengthMs=10 bitrate=5644 rate=2822400 ch=2.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-15 17:52:56 +02:00
manuaudioandUrs Fleisch b136be8c3c DSF: do not convert an out of range length to unsigned int
DSF::Properties::read computes

    d->length = d->samplingFrequency > 0
        ? static_cast<unsigned int>(static_cast<double>(d->sampleCount)
                                    * 1000.0 / d->samplingFrequency + 0.5)
        : 0;

sampleCount is a long long taken straight from the file and
samplingFrequency is an unsigned int from the file, so the millisecond
count can land well outside unsigned int. Converting a floating point
value the destination type cannot represent is undefined:

  taglib/dsf/dsfproperties.cpp:132:35: runtime error: 8.41595e+09 is
  outside the range of representable values of type 'unsigned int'
    #0 TagLib::DSF::Properties::read(TagLib::ByteVector const&)
    #1 TagLib::DSF::Properties::Properties(...)
    #10 TagLib::FileRef::FileRef(char const*, bool, ...)

A negative sampleCount converts just as badly, so guard that too.

Report an unknown length instead of converting, matching what a zero
sampling frequency already does. Nothing changes for a valid file:
tests/data/empty10ms.dsf reads lengthMs=10 bitrate=5645 rate=2822400
ch=2 both before and after.

Found by mutating the files in tests/data and running them through a
parse, read properties and save round trip under UBSan.

Assisted-By: Claude Code (Claude Opus 5)
2026-08-15 17:52:56 +02:00
Acts1631andGitHub 3b6da94771 MP4: bound QT chapter track parsing (#1415)
Reject truncated QT chapter atoms before reading fixed fields and bound manual tref parsing to the containing atom. Verify ordinary 32-bit parent sizes before removing a chapter reference so malformed files cannot trigger out-of-bounds reads or corrupt trailing data.
2026-08-15 16:33:35 +02:00
Acts1631andGitHub 38a78d56a2 RIFF: validate RF64 data chunk sizes (#1414)
RF64 ds64 stores unsigned 64-bit sizes, but RIFF uses signed offsets.
Convert and clamp the size before doing chunk extent arithmetic so
malformed files cannot overflow offsets or corrupt metadata on save.
2026-08-15 08:15:20 +02:00
Ryan FrancesconiandGitHub 3ace0483c0 RIFF: support RF64 and BW64 (#1412)
RF64 and BW64 are the long forms of WAVE, used past 4 GB: each 32-bit
size field holds a 0xffffffff sentinel and the real sizes live in a
leading ds64 chunk. RIFF::WAV::File::isSupported() rejected them, but
FileRef reaches the class by extension for any .wav and RIFF::File::read()
never inspected the magic, so these files opened as valid.

updateGlobalSize() then wrote a real 32-bit total over the sentinel at
offset 4. Readers stop consulting ds64 once that field holds a number, so
a 4.8 GB file measured 0.005958 sec and 1144 audio bytes after a tag save
that returned true. Below 4 GB the tags were lost instead: the appended
LIST landed inside the region read() had clamped the sentinel data chunk
to, and a re-read found no properties.

Worse, the append offset is last.offset + last.size with size truncated to
0xffffffff, so on a long file the new chunk was spliced into the middle of
the audio — measured at offset 4294971392 on that same file, 4 GiB past
the data chunk's start, displacing everything after it.

Accept both magics, take the riff and data sizes from ds64, and write the
sentinel back on save. Writing it unconditionally is what the format
requires and also repairs a file an earlier version damaged: the same
4.8 GB file, clobbered and then saved through this path, read back at
25000.000000 sec and 4,800,000,000 bytes.

The ds64 table of additional oversized chunks is not parsed — the data
chunk has its own dedicated field and is the only one that is ever large —
so any chunk listed there stays on the clamping path added in #1329, which
this leaves untouched.

Chunk::size becomes offset_t so the append offset is computed correctly.
The struct is file-scope in rifffile.cpp and FilePrivate is only
forward-declared, so no protected signature changes and no ABI break.
chunkDataSize() still returns unsigned int, saturating rather than
truncating; a new chunkDataSize64() carries the real value to
WAV::Properties, which otherwise reports 0 s for a long file.

AIFF is big-endian and has no long form, so RIFF::File's only other
subclass cannot reach the new branch.

tests/data/rf64.wav is 9,680 bytes and built by construction, not by an
encoder; Core Audio reads it as RF64 at 0.050000 sec. The sentinels behave
identically at any size, so a small fixture covers the detection failure,
the sentinel overwrite and the repair.
2026-08-14 06:47:05 +02:00
Acts1631andGitHub 766471a5a4 APE: limit parsed tag item count (#1411)
An APEv2 footer controls the number of parsed items without a bound.
A small crafted tag can therefore allocate a large item map and terminate
a memory-constrained application.

Stop parsing after 50,000 items. This matches existing parser count limits
and preserves entries parsed before the limit.
2026-08-14 06:07:47 +02:00
Acts1631andGitHub 77f5477609 Ogg FLAC: limit metadata packet size (#1407)
Reject metadata packets larger than FLAC's 24-bit payload limit.
Continued Ogg pages could otherwise make TagLib allocate arbitrary
amounts of memory before validating the FLAC block length.

Add bounded Ogg packet reassembly for Ogg FLAC metadata parsing.
2026-08-13 06:40:24 +02:00
Ryan FrancesconiandGitHub b2e27c3a45 Matroska: read the segment title without audio properties (#1409)
Matroska::Tag::title() falls back to \Segment\Info\Title when a file has
no TITLE simple tag. That title was only extracted inside read()'s
readProperties branch, because Info is also where the audio properties
come from, so opening a file with readAudioProperties = false left the
tag with an empty title:

  FileRef(path, true ).tag()->title();  // "handbrake"
  FileRef(path, false).tag()->title();  // ""

Reading tags without audio properties is the documented fast path for
scanning a library, so this silently emptied the title for exactly the
callers who opted into it, and it made tag content depend on an
audio-properties flag in a way no other format does.

Store the segment title on the file itself, read regardless of
readProperties, and use it from both read() and tag(). The Info element
is already resident by then, so no additional I/O is involved and
audioProperties() still returns null when properties were not requested.
2026-08-13 06:28:38 +02:00
Ryan FrancesconiandGitHub e37ee8498f MP4: merge the QT chapter reference into an existing tref (#1408)
A trak may hold at most one tref. setQtChapters() appended a second one for
the chap reference instead of joining the atom already present, which happens
whenever the audio track references another track -- a timecode track, as in
camera recordings.

Parsers differ on the result: the lenient keep reading the track, the strict
discard it entirely. A file written this way therefore still reads correctly
in TagLib while presenting as having no audio elsewhere, and anything that
remuxes it from a stricter parser's track list writes an audioless copy back
to disk.

Removal follows the same rule inverted: only the chap box is taken out when
the tref is shared, and the tref itself goes only when chap was its sole
child. It also now looks for the tref that actually contains chap rather than
the first one on the track, so a file already carrying two is repaired rather
than stripped of the wrong reference.
2026-08-13 06:20:16 +02:00
Acts1631andGitHub 6d9429b121 ID3v2: limit parsed frame count (#1406)
Bound top-level ID3v2 parsing to 50000 frames. Crafted tags with
many small frames could otherwise consume excessive memory and crash
applications.

Stop parsing further frames after the limit while retaining the
successfully parsed tag data.
2026-08-10 18:19:52 +02:00
Urs FleischandGitHub d6b32d8a1d ID3: Accept iTunes ID3 frames with space padded ID3v2.2 ID (#1403) (#1404)
iTunes 12 writes ID3v2.2 three-character sort frames (TSA, TSP, TST,
TS2, TSC) inside ID3v2.3 tags, padded to four bytes with 0x20 (space)
rather than 0x00, which is tolerated by '#ifndef NO_ITUNES_HACKS'
code. Enhance that code to also tolerate padding with a space.
2026-08-09 17:52:33 +02:00
8e6fdb4295 feat(matroska): load attachment data on demand (#1402)
Reading the tags of a file materialised every attachment, so a fast read of
a file with cover art cost as much as the art is large, even though the
caller may never ask for it. Measured with a counting IOStream over
tests/data/no-tags.mka with an attachment behind its seek head, read with
AudioProperties::Fast:

  attachment | read before | read now
  ----------------------------------
  128 KiB    |    131350 B |    278 B
  512 KiB    |    524544 B |    256 B
  4 MiB      |   4194563 B |    259 B

The number of read calls is unchanged (183, 164, 164 before; 182, 163, 163
now), so this is volume, not round trips, and what remains does not grow
with the payload.

MkAttachedFileData is now a DeferredBinaryElement, which registers the offset
of its data and skips over it. Matroska::File::attachments() reads the data
before handing the attachments out, so callers see no difference.

save() renders the attachments from the attached files, which would write an
empty attachment for data that was never requested. It therefore loads the
data before writing.

Co-authored-by: Claude Opus 5 <[email protected]>
2026-08-09 17:51:49 +02:00
Acts1631andGitHub 2cace8e7aa ID3v2: limit embedded frame nesting (#1405)
Limit recursive CHAP and CTOC embedded frame parsing to 64 levels.
This prevents crafted ID3v2 tags from exhausting the parser stack.

Use one thread-local guard shared by both paths so mixed CHAP and
CTOC nesting is bounded as well.
2026-08-09 16:14:41 +02:00
0836f213e7 ASF: Improve PropertyMap interface (#1401)
Support more property keys, also with numeric and Guid types.

---------

Co-authored-by: Urs Fleisch <[email protected]>
2026-08-09 15:44:18 +02:00
Urs FleischandGitHub 2a5fc12b68 Support additional MP4 codecs (#1293, #1338)
Adds MP4::Properties::Codec enum values AC3, EAC3, FLAC, DTS, Opus and a
MP4::Properties::codecId() method.
Parse high-res FLAC bitrate from MP4 dfLa box.
Parse E-AC-3 bitrate from MP4 dec3 box.

The test files were generated using ffmpeg:

ffmpeg -hide_banner -y -f lavfi \
  -i "sine=frequency=440:sample_rate=48000:duration=1" \
  -c:a flac -ac 2 -flags +bitexact -fflags +bitexact \
  -metadata:s:a:0 encoder= -metadata encoder= \
  -f mp4 tests/data/flac.m4a

ffmpeg -hide_banner -y -f lavfi \
  -i "sine=frequency=440:sample_rate=48000:duration=1" \
  -c:a libopus -ac 2 -flags +bitexact -fflags +bitexact \
  -metadata:s:a:0 encoder= -metadata encoder= \
  -f mp4 tests/data/opus.m4a

ffmpeg -hide_banner -y -f lavfi \
  -i "sine=frequency=440:sample_rate=48000:duration=1" \
  -c:a ac3 -b:a 128k -ac 2 -strict experimental \
  -flags +bitexact -fflags +bitexact \
  -metadata:s:a:0 encoder= -metadata encoder= \
  -f mp4 tests/data/ac3.m4a

ffmpeg -hide_banner -y -f lavfi \
  -i "sine=frequency=440:sample_rate=48000:duration=1" \
  -c:a eac3 -b:a 128k -ac 2 -strict experimental \
  -flags +bitexact -fflags +bitexact \
  -metadata:s:a:0 encoder= -metadata encoder= \
  -f mp4 tests/data/eac3.m4a

ffmpeg -hide_banner -y -f lavfi \
  -i "sine=frequency=440:sample_rate=96000:duration=1" \
  -c:a flac -sample_fmt s32 -ac 2 -flags +bitexact -fflags +bitexact \
  -metadata:s:a:0 encoder= -metadata encoder= \
  -f mp4 tests/data/flac96.m4a
2026-08-07 14:54:50 +02:00
ce3b45f186 fix(matroska): find Segments past the fast scan limit (#1398)
Element::factory() rejects any element whose declared size runs past the bound it is given, and
read() passes the Fast scan limit as that bound. A Segment spans practically the whole file, so
under ReadStyle::Fast every Matroska over 512 KiB is rejected and no tags are read:

  EBML: datasize too great: 1003369 > (524288 - 52)
  Failed to find Matroska segment

The limit is readLimited()'s, which already applies it to skip Cues and to bound the walk over a
segment with no usable SeekHead. The lookup only needs the file length.

Correctly handle offsets and scan limits:

- maxOffset: Maximum offset from the beginning of the file; the end of
  the element must be before this offset.
- scanLimit: Offset from the current file position until which scanning
  for elements is allowed. Normally, elements are scanned up to the end
  of the enclosing master element or the end of the file, but in Fast
  reading mode, it is limited to FAST_SCAN_LIMIT, which is 512 kB.
- maxScanOffset: scanLimit from the current file position

---------

Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Urs Fleisch <[email protected]>
2026-08-07 14:51:22 +02:00
Frederik SeiffertandGitHub 819bfce872 Fix data races in lazily initialized shared caches (#1400)
Also added thread safety test.
2026-08-07 14:48:50 +02:00
Urs FleischandGitHub e547578ae8 Support Vorbis comments from a multiplexed Ogg stream (#1370)
Ogg::File::readPages() read pages from all logical bitstreams and
indexed their packets into one global list. In a multiplexed file the
Theora and Vorbis packets got interleaved, so packet 0 was a Theora
header rather than the expected Vorbis type-3 comment header.

Before reading packets, the Vorbis reader now selects the Vorbis
logical bitstream (the one whose first packet is the Vorbis
identification header), so it reads the correct stream regardless of
position. Packet parsing is scoped to the selected bitstream, so
packets from other codecs in the same file are ignored.

tests/data/multiplex.ogg is generated using

ffmpeg -hide_banner -y \
  -f lavfi -i "color=c=navy:s=500x500:r=1:d=2" \
  -f lavfi -i "sine=frequency=440:sample_rate=48000:duration=2" \
  -map 0:v -map 1:a \
  -c:v libtheora -q:v 3 \
  -c:a libvorbis -ac 2 -q:a 2 \
  -flags +bitexact -fflags +bitexact \
  -metadata:s:a:0 TITLE="Paper Lights" \
  -metadata:s:a:0 encoder= -metadata:s:v:0 encoder= \
  -f ogg tests/data/multiplex.ogg
2026-08-05 20:57:29 +02:00
Acts1631andGitHub d781aaed7f Ogg FLAC: limit metadata block count (#1399)
Ogg FLAC scans metadata blocks by repeatedly fetching Ogg packets.
Each packet lookup walks indexed pages from their beginning, so a file
with many small metadata blocks has quadratic parsing time and page
allocation.

Limit the metadata block count to 1024. This keeps the worst-case scan
bounded while allowing more than normal Ogg FLAC files require.
2026-08-05 20:29:11 +02:00
Acts1631andGitHub 2e9cac7307 Xiph: limit parsed comment fields (#1397)
Xiph comment parsing retained an unbounded number of fields. A
crafted comment block with many small fields could consume
disproportionate memory.

Stop parsing comment fields when the parser limit is exceeded.
2026-08-04 06:12:57 +02:00
Acts1631andGitHub eb4ba7e93b FLAC: limit metadata block count (#1396)
FLAC metadata scanning retained an unbounded number of blocks. A
crafted file with many small blocks could consume disproportionate
memory.

Reject files that exceed a maximum metadata block count.
2026-08-04 06:10:22 +02:00
Acts1631andGitHub 2ada48a77f ASF: limit header object count (#1395)
ASF header parsing retained an unbounded number of objects. A crafted
file with many small objects could consume disproportionate memory.

Reject files whose header object count exceeds the parser limit.
2026-08-03 18:46:01 +02:00
Acts1631andGitHub a1d0488dc6 RIFF: limit parsed chunk count (#1394)
RIFF files could contain an unbounded number of small chunks. The
parser retained a descriptor for each chunk, allowing a crafted file
to consume disproportionate memory.

Reject files that exceed a maximum parsed chunk count.
2026-08-03 18:35:36 +02:00
Acts1631andGitHub af2010ff39 Matroska: limit elements in EBML containers (#1393)
EBML containers could contain an unbounded number of small elements.
The parser retained each element, allowing a crafted Matroska file to
consume disproportionate memory.

Reject containers that exceed a per-level element count limit.
2026-08-03 18:30:51 +02:00
Acts1631andGitHub c0f2a939b4 MP4: limit atoms in nested containers (#1392)
Nested MP4 containers did not enforce the atom count limit applied at
the root level. A small file with many child atoms could consume
disproportionate memory while building the atom tree.

Apply the per-level limit to container children and reject files that
exceed it.
2026-08-03 17:46:53 +02:00
Acts1631andGitHub cb92e0aec4 MPC: validate SV8 packet size (#1391)
SV8 packet parsing subtracted its header size from an unchecked
unsigned packet length. An undersized value could wrap and make the
parser allocate the remainder of a large file.

Reject packet lengths smaller than their header or beyond the remaining
file data before reading the payload.
2026-08-03 15:46:46 +02:00
Acts1631andGitHub fd97c86bcb MP4: reject undersized table atoms (#1390)
MP4 chapter parsing subtracted table headers from atom lengths without
checking that the headers were present. A short atom could wrap the
read length and allocate the remainder of a large file.

Validate table sizes before reading their payloads and before updating
chunk offsets while saving.
2026-08-02 16:47:17 +02:00
Acts1631andGitHub 04993427d7 Bound ASF attribute parsing to object data (#1389)
A truncated ASF attribute object could declare a large count and make
the parser create empty attributes after reaching the end of its data.

Validate each attribute object's data extent and stop parsing when an
attribute would exceed it.
2026-08-02 08:23:08 +02:00
Acts1631andGitHub 586a658650 Prevent DSDIFF chunk size wrap (#1388)
A crafted DSDIFF chunk size could wrap the parser boundary check,
seek backwards, and make the parser loop indefinitely.

Compare chunk sizes with remaining bytes before seeking at each DSDIFF
chunk nesting level.
2026-08-02 08:14:43 +02:00
Thomas BergwinklandGitHub 5530420d08 Fix ID3v2 frame data length check for per-frame unsynchronised ID3v2.4 frames (#1385)
Frame::fieldData() (taglib/mpeg/id3v2/id3v2frame.cpp) discarded any
ID3v2.4 frame whose declared size (from the header) no longer matched
its actual buffer size after per-frame unsynchronisation was decoded by
FrameFactory::prepareFrameHeader(), silently emptying frames like
TIT2/TPE1/TALB. Clamp the declared length to what's actually available
instead of discarding the frame, only bailing out if the frame's data
offset itself doesn't fit.

Adds testUnsynchDecodeID3v24Frame() to tests/test_id3v2.cpp, covering a
frame with its own per-frame Unsynchronisation flag (as opposed to the
tag-wide flag already covered by testUnsynchDecode()), using new fixture
tests/data/unsynch24.id3.
2026-08-01 07:53:53 +02:00
MSOB7YandGitHub a100d0b2ec MP4: Allow extracting covr with wrong flags (#1383)
If the covr has invalid flags, detect the image format from the magic bytes
and return the image anyways even if the type is unknown.
2026-08-01 07:32:43 +02:00
Urs FleischandGitHub e8f1e058d6 Use Requires.private for zlib with pkg-config (#1380)
Express dependency on zlib using Requires.private in pkg-config .pc
file instead of adding -lz to the libs.

This will cause the following changes when using TagLib with
pkg-config:
- When using a dynamic library, -lz will no longer be present in
  in linker command, but being linked transitively.
- When using a static library, pkg-config must be used with the
  --static command line argument, then libs will still contain
  -lz, which is needed when linking statically.
- The flags needed for zlib will be provided by pkg-config,
  so if -lz is not appropriate (e.g. with MSVC), correct
  linker flags will be provided.
2026-08-01 06:19:41 +02:00
Acts1631andGitHub 18572e90a6 Limit ID3 compressed-frame expansion (#1382)
A crafted compressed ID3v2 frame can declare an excessive output size
and cause zlib to allocate memory based on attacker-controlled data.

Bound decompression by an absolute 64 MiB limit and a 64:1 expansion
ratio, while retaining normal ID3v2.3 length handling.
2026-08-01 06:18:24 +02:00
Acts1631andGitHub 3aa04e3be4 Fix Shorten AIFF chunk offset wrap (#1381)
A crafted embedded AIFF chunk size could wrap the parser offset and
make it repeatedly process the same chunk, causing a denial of service.

Validate AIFF chunk headers and padded sizes against the remaining
verbatim-header data, and advance every parsed chunk to its checked end.
2026-08-01 06:12:47 +02:00
Urs Fleisch 54ae7d8ac4 Version 2.3.1
Pin submodule utfcpp to tag v4.1.1.
v2.3.1
2026-07-19 19:58:28 +02:00
f7c28ac742 MP4: Fix QT chapters excessive sample allocation with invalid stsc (#1379)
See https://mail.kde.org/pipermail/taglib-devel/2026-July/003122.html

---------

Co-authored-by: Lee, Brian J <[email protected]>
2026-07-17 05:22:50 +02:00
Urs Fleisch 93ebb7fb79 Prepare 2.3.1 release 2026-07-12 20:16:12 +02:00
Urs Fleisch f09a84c4ae Enlarge MP4 atom sibling count at top level limit (#1344)
The MAX_MP4_ATOM_COUNT_PER_LEVEL of 5000 seems to be too restrictive,
a legitimate file with 5390 atoms was reported to have been rejected.
The crafted file from #1344 had 653789 atoms at the top level, which
freezed the read process for 15s on my system. Enlarging the limit
to 50000 should be sufficient and will stop the crafted file after 2s.
2026-07-11 09:05:17 +02:00