Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.

Commit d54ee7b

Browse files
cdmurph32polyfloyd
authored andcommitted
fix: prevent panics from malformed id3 metadata
* mllt overflow * invalid frame id length * decode content size underflow * decode MLLT deviation overflow * stream tag frame bytes underflow
1 parent ca57592 commit d54ee7b

File tree

3 files changed

+144
-5
lines changed

3 files changed

+144
-5
lines changed

src/stream/frame/content.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,9 @@ impl<W: io::Write> Encoder<W> {
247247
&mut self,
248248
content: &MpegLocationLookupTable,
249249
) -> crate::Result<()> {
250-
let ref_packed_size = content.bits_for_bytes + content.bits_for_millis;
250+
let ref_packed_size = content
251+
.bits_for_bytes
252+
.saturating_add(content.bits_for_millis);
251253
if ref_packed_size % 4 != 0 {
252254
return Err(Error::new(
253255
ErrorKind::InvalidInput,
@@ -886,7 +888,9 @@ impl<'a> Decoder<'a> {
886888
),
887889
));
888890
}
889-
deviations[i] = u32::try_from(carry >> (64 - bits_us)).unwrap();
891+
deviations[i] = u32::try_from(carry >> (64 - bits_us)).map_err(|_| {
892+
Error::new(ErrorKind::InvalidInput, "MLLT deviation field overflow")
893+
})?;
890894
carry <<= bits_us;
891895
carry_bits -= bits_us;
892896
}
@@ -1039,6 +1043,7 @@ mod tests {
10391043
use crate::frame::Content;
10401044
use crate::frame::{self, Picture, PictureType};
10411045
use std::collections::HashMap;
1046+
use std::io::Cursor;
10421047

10431048
fn bytes_for_encoding(text: &str, encoding: Encoding) -> Vec<u8> {
10441049
encoding.encode(text)
@@ -1835,4 +1840,72 @@ mod tests {
18351840
)
18361841
.is_none());
18371842
}
1843+
1844+
#[test]
1845+
fn test_encode_mllt_overflow() {
1846+
let mllt = Content::MpegLocationLookupTable(MpegLocationLookupTable {
1847+
frames_between_reference: 1,
1848+
bytes_between_reference: 418,
1849+
millis_between_reference: 15,
1850+
bits_for_bytes: 252,
1851+
bits_for_millis: 252, // This would cause an overflow if not for saturating_add
1852+
references: vec![
1853+
MpegLocationLookupTableReference {
1854+
deviate_bytes: 0x1,
1855+
deviate_millis: 0x2,
1856+
},
1857+
MpegLocationLookupTableReference {
1858+
deviate_bytes: 0x3,
1859+
deviate_millis: 0x4,
1860+
},
1861+
MpegLocationLookupTableReference {
1862+
deviate_bytes: 0x5,
1863+
deviate_millis: 0x6,
1864+
},
1865+
],
1866+
});
1867+
1868+
let mut data_out = Vec::new();
1869+
let result = encode(&mut data_out, &mllt, Version::Id3v23, Encoding::UTF8);
1870+
1871+
// Error since 255 is not a multiple of 4, but no panic.
1872+
assert!(result.is_err());
1873+
if let Err(e) = result {
1874+
assert!(matches!(e.kind, ErrorKind::InvalidInput));
1875+
assert_eq!(
1876+
e.description,
1877+
"MLLT bits_for_bytes + bits_for_millis must be a multiple of 4"
1878+
);
1879+
}
1880+
}
1881+
1882+
#[test]
1883+
fn test_decode_mllt_deviation_overflow() {
1884+
// Create a payload with large deviation values that would overflow u32
1885+
let payload = [
1886+
0xFF, 0xFF, // frames_between_reference (u16::MAX)
1887+
0xFF, 0xFF, 0xFF, // bytes_between_reference (u24::MAX)
1888+
0xFF, 0xFF, 0xFF, // millis_between_reference (u24::MAX)
1889+
0x38, // bits_for_bytes (56)
1890+
0x1C, // bits_for_millis (28)
1891+
0xFF, 0xFF, 0xFF, 0xFF, // deviate_bytes (u32::MAX)
1892+
0xFF, 0xFF, 0xFF, 0xFF, // deviate_millis (u32::MAX)
1893+
];
1894+
1895+
// Combine header and payload into a single data stream
1896+
let mut data = Vec::new();
1897+
data.extend_from_slice(&payload);
1898+
1899+
let mut reader = Cursor::new(data);
1900+
1901+
// Attempt to decode the frame
1902+
let result = decode("MLLT", Version::Id3v23, &mut reader);
1903+
1904+
// Ensure that the result is an error due to overflow
1905+
assert!(result.is_err());
1906+
if let Err(e) = result {
1907+
assert!(matches!(e.kind, ErrorKind::InvalidInput));
1908+
assert_eq!(e.description, "MLLT deviation field overflow");
1909+
}
1910+
}
18381911
}

src/stream/frame/v4.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub fn decode(mut reader: impl io::Read) -> crate::Result<Option<(usize, Frame)>
4545

4646
let read_size = if flags.contains(Flags::DATA_LENGTH_INDICATOR) {
4747
let _decompressed_size = unsynch::decode_u32(reader.read_u32::<BigEndian>()?);
48-
content_size - 4
48+
content_size.saturating_sub(4)
4949
} else {
5050
content_size
5151
};
@@ -94,7 +94,12 @@ pub fn encode(mut writer: impl io::Write, frame: &Frame, flags: Flags) -> crate:
9494

9595
writer.write_all({
9696
let id = frame.id().as_bytes();
97-
assert_eq!(4, id.len());
97+
if id.len() != 4 {
98+
return Err(Error::new(
99+
ErrorKind::InvalidInput,
100+
"Frame ID must be 4 bytes long",
101+
));
102+
}
98103
id
99104
})?;
100105
writer.write_u32::<BigEndian>(unsynch::encode_u32(
@@ -109,3 +114,51 @@ pub fn encode(mut writer: impl io::Write, frame: &Frame, flags: Flags) -> crate:
109114
writer.write_all(&content_buf)?;
110115
Ok(10 + comp_hint_delta + content_buf.len())
111116
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use super::*;
121+
use crate::frame::Content;
122+
use std::io::Cursor;
123+
124+
#[test]
125+
fn test_encode_with_invalid_frame_id() {
126+
let frame = Frame::with_content("TST", Content::Text("Test content".to_string()));
127+
let flags = Flags::empty();
128+
let mut writer = Cursor::new(Vec::new());
129+
130+
let result = encode(&mut writer, &frame, flags);
131+
132+
assert!(result.is_err());
133+
if let Err(e) = result {
134+
assert!(matches!(e.kind, ErrorKind::InvalidInput));
135+
assert_eq!(e.description, "Frame ID must be 4 bytes long");
136+
}
137+
}
138+
139+
#[test]
140+
fn test_decode_with_underflow() {
141+
// Create a frame header with DATA_LENGTH_INDICATOR flag set and a content size of 3
142+
let frame_header = [
143+
b'T', b'E', b'S', b'T', // Frame ID
144+
0x00, 0x00, 0x00, 0x03, // Content size (3 bytes)
145+
0x00, 0x01, // Flags (DATA_LENGTH_INDICATOR)
146+
];
147+
// Create a reader with the frame header followed by 4 bytes for the decompressed size
148+
let mut data = Vec::new();
149+
data.extend_from_slice(&frame_header);
150+
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x04]); // Decompressed size (4 bytes)
151+
152+
let mut reader = Cursor::new(data);
153+
154+
// Attempt to decode the frame
155+
let result = decode(&mut reader);
156+
157+
// Ensure that the result is an error due to underflow
158+
assert!(result.is_err());
159+
if let Err(e) = result {
160+
assert!(matches!(e.kind, ErrorKind::Parsing));
161+
assert_eq!(e.description, "Insufficient data to decode bytes");
162+
}
163+
}
164+
}

src/stream/tag.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ impl Header {
6565
}
6666

6767
fn frame_bytes(&self) -> u64 {
68-
u64::from(self.tag_size) - u64::from(self.ext_header_size)
68+
u64::from(self.tag_size).saturating_sub(u64::from(self.ext_header_size))
6969
}
7070

7171
fn tag_size(&self) -> u64 {
@@ -1091,4 +1091,17 @@ mod tests {
10911091

10921092
assert_eq!(tag, tag_read);
10931093
}
1094+
1095+
#[test]
1096+
fn test_frame_bytes_underflow() {
1097+
let header = Header {
1098+
version: Version::Id3v24,
1099+
flags: Flags::empty(),
1100+
tag_size: 10,
1101+
ext_header_size: 20,
1102+
};
1103+
1104+
// Without saturating_sub, this would underflow and cause a panic.
1105+
assert_eq!(header.frame_bytes(), 0);
1106+
}
10941107
}

0 commit comments

Comments
 (0)