libfreemkv: v1.0 hardening — codec/EBML/TS robustness + DTS parser fixes

Audit-driven fixes (rounds 1–3):
- hevc: correct hvcC profile/level SPS offsets (HEVC has a 2-byte NAL header)
- mkv: map all DTS variants to the registered A_DTS codec id; force a new
  cluster before the i16 cluster-relative timestamp can overflow
- ebml/mkvstream: bound untrusted EBML sizes (no multi-GB allocs); reject
  uint>8 (was an OOB panic) and non-{0,4,8} float widths (were a desync)
- ts: skip PES-header bytes that span a TS packet boundary; add the PMT
  section_len/prog_info_len bounds the PAT parser already had
- ac3: preserve a 0x0B77 syncword split across a PES boundary; cap buffer
- dts: validate each next-core boundary by decoded core size (a 0x7FFE8001
  pattern inside XLL payload no longer false-splits/drops the lossless
  extension); reject sub-minimum core frames; fix forced-emit PTS base
- lpcm: DVD program-stream PCM no longer double-strips the BD LPCM header
- vc1/mpeg2: do not emit a parameter-set-only PES as a standalone frame
- pgs/truehd: cap the pending reassembly buffer (parity with ac3/dts)
- aacs: ts_syncs_intact uses the exact packet count
- prefetched: capacity-guard the recycled-buffer set_len
- Cargo.toml: exclude project docs from the published crate

Convergence: a third independent audit pass found no remaining material
(CRITICAL/HIGH/MEDIUM) issues. Full precommit (fmt + clippy -D + tests,
Rust 1.86) green.
This commit is contained in:
MattJackson
2026-06-05 16:23:39 -07:00
parent e2aa9abd6d
commit 6be5198886
18 changed files with 1201 additions and 119 deletions
+44 -13
View File
@@ -264,6 +264,13 @@ pub fn read_element_header(r: &mut impl Read) -> io::Result<(u32, u64, usize)> {
/// Read an unsigned integer value of `len` bytes.
pub fn read_uint_val(r: &mut impl Read, len: usize) -> io::Result<u64> {
// An EBML unsigned integer is at most 8 bytes. A malformed element
// claiming `len > 8` would index past this stack buffer and panic
// (DoS on untrusted input) — reject it at the source so every caller
// is safe, not just the ones that pre-check.
if len > 8 {
return Err(crate::error::Error::MkvInvalid.into());
}
let mut buf = [0u8; 8];
r.read_exact(&mut buf[..len])?;
let mut val = 0u64;
@@ -273,23 +280,33 @@ pub fn read_uint_val(r: &mut impl Read, len: usize) -> io::Result<u64> {
Ok(val)
}
/// Read a float value (4 or 8 bytes).
/// Read a float value. EBML floats are exactly 0, 4, or 8 bytes.
///
/// The previous `else` branch read a fixed 8 bytes for ANY non-4 length,
/// so a malformed element with `len > 8` left `len - 8` unconsumed bytes
/// (mis-read as the next EBML header → desync of the rest of the parent
/// element) and `len < 4` over-read. Consume exactly `len` bytes and
/// reject anything that isn't a valid float width.
pub fn read_float_val(r: &mut impl Read, len: usize) -> io::Result<f64> {
if len == 4 {
let mut buf = [0u8; 4];
r.read_exact(&mut buf)?;
Ok(f32::from_be_bytes(buf) as f64)
} else {
let mut buf = [0u8; 8];
r.read_exact(&mut buf)?;
Ok(f64::from_be_bytes(buf))
match len {
0 => Ok(0.0),
4 => {
let mut buf = [0u8; 4];
r.read_exact(&mut buf)?;
Ok(f32::from_be_bytes(buf) as f64)
}
8 => {
let mut buf = [0u8; 8];
r.read_exact(&mut buf)?;
Ok(f64::from_be_bytes(buf))
}
_ => Err(crate::error::Error::MkvInvalid.into()),
}
}
/// Read a UTF-8 string value of `len` bytes.
pub fn read_string_val(r: &mut impl Read, len: usize) -> io::Result<String> {
let mut buf = vec![0u8; len];
r.read_exact(&mut buf)?;
let mut buf = read_exact_bounded(r, len)?;
// Strip trailing nulls
while buf.last() == Some(&0) {
buf.pop();
@@ -299,8 +316,22 @@ pub fn read_string_val(r: &mut impl Read, len: usize) -> io::Result<String> {
/// Read binary data of `len` bytes.
pub fn read_binary_val(r: &mut impl Read, len: usize) -> io::Result<Vec<u8>> {
let mut buf = vec![0u8; len];
r.read_exact(&mut buf)?;
read_exact_bounded(r, len)
}
/// Read exactly `len` bytes WITHOUT trusting `len` to size the allocation.
///
/// `vec![0u8; len]` on an attacker-controlled EBML size would allocate
/// gigabytes before the read fails. Instead we cap the reader to `len`
/// and grow the buffer as bytes actually arrive: a malformed element that
/// claims a huge length but supplies few bytes allocates only what it
/// delivers, then errors on the short read.
fn read_exact_bounded(r: &mut impl Read, len: usize) -> io::Result<Vec<u8>> {
let mut buf = Vec::new();
let got = r.take(len as u64).read_to_end(&mut buf)?;
if got != len {
return Err(io::ErrorKind::UnexpectedEof.into());
}
Ok(buf)
}