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)
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)
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)
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)
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)
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)
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)
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)
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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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]>
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.
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]>
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
In matroska an element data size of a VINT with all bits 1 means the
data size is unknown. Unknown data size can only apply to Master
Elements.
Unknown sized elements are described in
https://datatracker.ietf.org/doc/rfc8794/ section 6.2
It gives the following 5 conditions for detecting the end of an
unknown sized element:
* Any EBML Element that is a valid Parent Element of the Unknown-
Sized Element according to the EBML Schema, Global Elements
excluded.
* Any valid EBML Element according to the EBML Schema, Global
Elements excluded, that is not a Descendant Element of the
Unknown-Sized Element but shares a common direct parent, such as a
Top-Level Element.
* Any EBML Element that is a valid Root Element according to the
EBML Schema, Global Elements excluded.
* The end of the Parent Element with a known size has been reached.
* The end of the EBML Document, either when reaching the end of the
file or because a new EBML Header started.
In this patch we use the higher level maxOffset to determine
the maximum data size for the element, which matches the fourth
condition, but is incomplete without the other four methods.
As only Segment and Cluster elements of Matroska files are allowed
to use unknown size length and TagLib does not process Cluster
elements, this should be sufficient.
---------
Signed-off-by: Anthony Brandon <[email protected]>
Co-authored-by: Urs Fleisch <[email protected]>
Some files might have some invalid elements, but most elements might be
correct. In this case it would be useful to still parse the correct
elements and skip the invalid ones.
When an invalid element is encountered we can set the seek position in
the file to the (known) end of that Element. Then further elements can
be read starting from that position.
Signed-off-by: Anthony Brandon <[email protected]>
By checking the Element size against the maximum offset we can find
out of bounds elements early and not try to read them at all.
This is useful for the next patch.
Signed-off-by: Anthony Brandon <[email protected]>
Each time save is called registerSizeListener will add
new size listeners. If save is called multiple times this causes the
same delta being add/subtracted multiple times, resulting in incorrect
element sizes.
This can be observed in TestMatroska::testRepeatedSave.
Although the test passes, some sizes are incorrect resulting in the
following messages being printed during the test:
TagLib: Failed to read VINT size
TagLib: Failed to parse EMBL ElementID
Signed-off-by: Anthony Brandon <[email protected]>