media: cap a Matroska track's content encoding chain (#541)

A track could list any number of ContentEncoding entries, and readers walk
the whole list for every frame they return (frames_readable,
stripped_header). A track with more than 16 entries, far beyond the one or
two a muxer writes, is now treated as having an unsupported compression, so
its frames are returned as stored and the list is never long.
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-03 14:10:50 -05:00
1 parent 6865897f14
commit 32b2fd8adf
2 files changed
+21

No files matched your search

+20
View File
@@ -28,6 +28,11 @@ const MAX_META_DEPTH: usize = 64;
/// produce. Real files strip a few bytes of codec header; a longer one is treated as an
/// unsupported compression and its frames are not reconstructed.
const MAX_STRIPPED_HEADER: usize = 256;
/// Most `ContentEncoding` entries a track may list. A muxer writes one or two (compression,
/// encryption); readers check the whole chain for every frame they return, so a longer chain
/// would multiply that work by the frame count. A track with more is treated as having an
/// unsupported compression, and its frames are returned as stored.
const MAX_CONTENT_ENCODINGS: usize = 16;
/// Largest accepted `TimestampScale` (about 4.3 s per tick). It keeps tick to nanosecond
/// arithmetic on 64-bit timestamps within `i128`.
const MAX_TIMESTAMP_SCALE: u64 = 1 << 32;
@@ -459,6 +464,9 @@ fn parse_audio(d: &[u8]) -> AudioInfo {
}
fn parse_encodings(d: &[u8]) -> Vec<ContentEncoding> {
if Children::new(d).filter(|e| e.id == CONTENT_ENCODING).nth(MAX_CONTENT_ENCODINGS).is_some() {
return vec![ContentEncoding::Compression { algo: u64::MAX, settings: Vec::new(), scope: 1 }];
}
let mut list: Vec<(u64, ContentEncoding)> = Vec::new();
for ce in Children::new(d).filter(|e| e.id == CONTENT_ENCODING) {
let (mut order, mut scope, mut ty) = (0u64, 1u64, 0u64);
@@ -628,6 +636,18 @@ mod metadata_depth_tests {
parsed.remove(0)
}
#[test]
fn a_long_encoding_chain_is_not_rechecked_for_every_frame() {
let strip: (&[u8], u64) = (b"", 2);
let most = track_with_private(b"private", &[strip; MAX_CONTENT_ENCODINGS]);
assert_eq!(most.encodings.len(), MAX_CONTENT_ENCODINGS);
assert!(most.frames_readable());
let many = track_with_private(b"private", &vec![strip; 10_000]);
assert!(matches!(&many.encodings[..], [ContentEncoding::Compression { scope: 1, .. }]), "{:?}", many.encodings.len());
assert!(!many.frames_readable());
assert_eq!(many.codec_private, b"private");
}
#[test]
fn codec_private_stripped_headers_are_restored_in_reverse_order() {
let strips: [(&[u8], u64); 4] = [(b"first", 2), (b"", 2), (b"second", 2), (b"frame", 1)];
+1
View File
@@ -169,6 +169,7 @@ pub enum ContentEncoding {
/// `ContentCompAlgo` 3: the given bytes were stripped from the start of every frame.
HeaderStripping { bytes: Vec<u8>, scope: u64 },
/// Other compression (0 zlib, 1 bzlib, 2 lzo1x): not supported for reading frame data.
/// `algo` is `u64::MAX` for a chain of more encodings than a reader checks.
Compression { algo: u64, settings: Vec<u8>, scope: u64 },
/// Encryption (not supported for reading frame data).
Encryption { scope: u64 },