mirror of
https://github.com/taglib/taglib.git
synced 2026-10-23 16:38:49 -04:00
Limit Matroska EBML element allocations (#1456)
MasterElement permits up to one million elements per read. Segment metadata sections each start a fresh counter, allowing small files with empty elements to exhaust memory during parsing. Share one count across segment sections and seek-head lookups. Bound the count by file size, with a minimum for small valid files.
This commit is contained in:
@@ -104,6 +104,11 @@ bool EBML::MasterElement::read(File &file, int depth)
|
||||
return read(file, depth, elementCount);
|
||||
}
|
||||
|
||||
bool EBML::MasterElement::read(File &file, unsigned int &elementCount)
|
||||
{
|
||||
return read(file, 0, elementCount);
|
||||
}
|
||||
|
||||
bool EBML::MasterElement::read(File &file, int depth, unsigned int &elementCount)
|
||||
{
|
||||
static constexpr int MAX_EBML_DEPTH = 64;
|
||||
@@ -112,6 +117,17 @@ bool EBML::MasterElement::read(File &file, int depth, unsigned int &elementCount
|
||||
// elements, so a 50000 whole-file budget rejects ordinary media outright.
|
||||
static constexpr int MAX_EBML_ELEMENT_COUNT = 1000000;
|
||||
static constexpr int MAX_EBML_ELEMENT_COUNT_PER_LEVEL = 50000;
|
||||
static constexpr offset_t MIN_EBML_ELEMENT_COUNT = 1024;
|
||||
static constexpr offset_t BYTES_PER_EBML_ELEMENT = 32;
|
||||
// A zero-length element can occupy only two bytes on disk but takes a heap
|
||||
// allocation and a list node in memory. Bound the element budget by the
|
||||
// input length, with a minimum for small but valid media files.
|
||||
const offset_t fileLength = file.length();
|
||||
if(fileLength < 0)
|
||||
return false;
|
||||
const auto maxElementCount = static_cast<unsigned int>(
|
||||
std::min<offset_t>(MAX_EBML_ELEMENT_COUNT,
|
||||
std::max(MIN_EBML_ELEMENT_COUNT, fileLength / BYTES_PER_EBML_ELEMENT)));
|
||||
if(depth > MAX_EBML_DEPTH) {
|
||||
debug("EBML: Maximum nesting depth exceeded");
|
||||
return false;
|
||||
@@ -119,7 +135,7 @@ bool EBML::MasterElement::read(File &file, int depth, unsigned int &elementCount
|
||||
const offset_t maxOffset = file.tell() + dataSize;
|
||||
std::unique_ptr<Element> element;
|
||||
while((element = findNextElement(file, maxOffset))) {
|
||||
if(elementCount >= MAX_EBML_ELEMENT_COUNT ||
|
||||
if(elementCount >= maxElementCount ||
|
||||
elements.size() >= MAX_EBML_ELEMENT_COUNT_PER_LEVEL) {
|
||||
debug("EBML: Maximum element count exceeded");
|
||||
return false;
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace TagLib
|
||||
|
||||
offset_t getOffset() const;
|
||||
bool read(File &file) override;
|
||||
bool read(File &file, unsigned int &elementCount);
|
||||
ByteVector render() override;
|
||||
void appendElement(std::unique_ptr<Element> &&element);
|
||||
std::list<std::unique_ptr<Element>>::iterator begin();
|
||||
|
||||
@@ -38,7 +38,8 @@ namespace {
|
||||
template <EBML::Element::Id Id, typename ElementType>
|
||||
std::unique_ptr<ElementType> readElementAt(File &file,
|
||||
offset_t offset,
|
||||
offset_t maxOffset)
|
||||
offset_t maxOffset,
|
||||
unsigned int &elementCount)
|
||||
{
|
||||
if(offset < 0 || offset >= maxOffset) {
|
||||
return nullptr;
|
||||
@@ -51,7 +52,7 @@ std::unique_ptr<ElementType> readElementAt(File &file,
|
||||
}
|
||||
|
||||
auto typed = EBML::element_cast<Id>(std::move(element));
|
||||
if(!typed || !typed->read(file)) {
|
||||
if(!typed || !typed->read(file, elementCount)) {
|
||||
return nullptr;
|
||||
}
|
||||
return typed;
|
||||
@@ -94,11 +95,12 @@ bool EBML::MkSegment::readLimited(File &file, offset_t scanLimit)
|
||||
const bool skipCues = file.readOnly() && scanLimit < dataSize;
|
||||
MasterElement *pendingPaddingTarget = nullptr;
|
||||
offset_t accumulatedPadding = 0;
|
||||
unsigned int elementCount = 0;
|
||||
std::unique_ptr<Element> element;
|
||||
while((element = findNextElement(file, maxOffset, maxScanOffset))) {
|
||||
if(const Id id = element->getId(); id == Id::MkSeekHead) {
|
||||
seekHead = element_cast<Id::MkSeekHead>(std::move(element));
|
||||
if(!seekHead->read(file))
|
||||
if(!seekHead->read(file, elementCount))
|
||||
return false;
|
||||
// We have a seek head, let's use it for faster access to the other elements
|
||||
if(const auto elementAfterSeekHead = findNextElement(file, maxOffset, maxScanOffset);
|
||||
@@ -138,7 +140,7 @@ bool EBML::MkSegment::readLimited(File &file, offset_t scanLimit)
|
||||
if(chainedSeekHeadsFollowed++ >= MAX_CHAINED_SEEKHEADS)
|
||||
break;
|
||||
auto chained = readElementAt<Id::MkSeekHead, MkSeekHead>(
|
||||
file, absoluteOffset, maxOffset);
|
||||
file, absoluteOffset, maxOffset, elementCount);
|
||||
if(!chained)
|
||||
break;
|
||||
if(const auto parsed = chained->parse(segDataOffset)) {
|
||||
@@ -150,35 +152,35 @@ bool EBML::MkSegment::readLimited(File &file, offset_t scanLimit)
|
||||
case Id::MkCues:
|
||||
if(!skipCues) {
|
||||
if(!((cues = readElementAt<Id::MkCues, MkCues>(
|
||||
file, absoluteOffset, maxOffset))))
|
||||
file, absoluteOffset, maxOffset, elementCount))))
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case Id::MkInfo:
|
||||
if(!((info = readElementAt<Id::MkInfo, MkInfo>(
|
||||
file, absoluteOffset, maxOffset))))
|
||||
file, absoluteOffset, maxOffset, elementCount))))
|
||||
return false;
|
||||
break;
|
||||
case Id::MkTracks:
|
||||
if(!((tracks = readElementAt<Id::MkTracks, MkTracks>(
|
||||
file, absoluteOffset, maxOffset))))
|
||||
file, absoluteOffset, maxOffset, elementCount))))
|
||||
return false;
|
||||
break;
|
||||
case Id::MkTags:
|
||||
if(!((tags = readElementAt<Id::MkTags, MkTags>(
|
||||
file, absoluteOffset, maxOffset))))
|
||||
file, absoluteOffset, maxOffset, elementCount))))
|
||||
return false;
|
||||
accumulateVoidPadding(tags.get());
|
||||
break;
|
||||
case Id::MkAttachments:
|
||||
if(!((attachments = readElementAt<Id::MkAttachments, MkAttachments>(
|
||||
file, absoluteOffset, maxOffset))))
|
||||
file, absoluteOffset, maxOffset, elementCount))))
|
||||
return false;
|
||||
accumulateVoidPadding(attachments.get());
|
||||
break;
|
||||
case Id::MkChapters:
|
||||
if(!((chapters = readElementAt<Id::MkChapters, MkChapters>(
|
||||
file, absoluteOffset, maxOffset))))
|
||||
file, absoluteOffset, maxOffset, elementCount))))
|
||||
return false;
|
||||
accumulateVoidPadding(chapters.get());
|
||||
break;
|
||||
@@ -200,7 +202,7 @@ bool EBML::MkSegment::readLimited(File &file, offset_t scanLimit)
|
||||
accumulatedPadding = 0;
|
||||
if(!skipCues) {
|
||||
cues = element_cast<Id::MkCues>(std::move(element));
|
||||
if(!cues->read(file))
|
||||
if(!cues->read(file, elementCount))
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
@@ -211,21 +213,21 @@ bool EBML::MkSegment::readLimited(File &file, offset_t scanLimit)
|
||||
pendingPaddingTarget = nullptr;
|
||||
accumulatedPadding = 0;
|
||||
info = element_cast<Id::MkInfo>(std::move(element));
|
||||
if(!info->read(file))
|
||||
if(!info->read(file, elementCount))
|
||||
return false;
|
||||
}
|
||||
else if(id == Id::MkTracks) {
|
||||
pendingPaddingTarget = nullptr;
|
||||
accumulatedPadding = 0;
|
||||
tracks = element_cast<Id::MkTracks>(std::move(element));
|
||||
if(!tracks->read(file))
|
||||
if(!tracks->read(file, elementCount))
|
||||
return false;
|
||||
}
|
||||
else if(id == Id::MkTags) {
|
||||
pendingPaddingTarget = nullptr;
|
||||
accumulatedPadding = 0;
|
||||
tags = element_cast<Id::MkTags>(std::move(element));
|
||||
if(!tags->read(file))
|
||||
if(!tags->read(file, elementCount))
|
||||
return false;
|
||||
pendingPaddingTarget = tags.get();
|
||||
}
|
||||
@@ -233,7 +235,7 @@ bool EBML::MkSegment::readLimited(File &file, offset_t scanLimit)
|
||||
pendingPaddingTarget = nullptr;
|
||||
accumulatedPadding = 0;
|
||||
attachments = element_cast<Id::MkAttachments>(std::move(element));
|
||||
if(!attachments->read(file))
|
||||
if(!attachments->read(file, elementCount))
|
||||
return false;
|
||||
pendingPaddingTarget = attachments.get();
|
||||
}
|
||||
@@ -241,7 +243,7 @@ bool EBML::MkSegment::readLimited(File &file, offset_t scanLimit)
|
||||
pendingPaddingTarget = nullptr;
|
||||
accumulatedPadding = 0;
|
||||
chapters = element_cast<Id::MkChapters>(std::move(element));
|
||||
if(!chapters->read(file))
|
||||
if(!chapters->read(file, elementCount))
|
||||
return false;
|
||||
pendingPaddingTarget = chapters.get();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user