diff --git a/crates/aaf/src/aaf/store.rs b/crates/aaf/src/aaf/store.rs index 46ab95dae..69b14fcf2 100644 --- a/crates/aaf/src/aaf/store.rs +++ b/crates/aaf/src/aaf/store.rs @@ -377,6 +377,7 @@ const MAX_PATH_BYTES: usize = 64 << 20; /// Parse the object tree of an AAF compound file (the root object). pub(crate) fn read(bytes: &[u8]) -> Result { let cf = CompoundFile::open(bytes).map_err(|e| e.to_string())?; + cf.validate_root_chain().map_err(|e| e.to_string())?; let mut paths = Vec::new(); if let Ok(rp) = cf.read_path("referenced properties") && rp.len() >= 7 diff --git a/crates/cfb/src/read.rs b/crates/cfb/src/read.rs index decd895da..b0eedde3e 100644 --- a/crates/cfb/src/read.rs +++ b/crates/cfb/src/read.rs @@ -424,6 +424,19 @@ impl<'a> CompoundFile<'a> { self.entries.first().unwrap_or(&EMPTY) } + /// Reject a cycle or invalid link anywhere in the root mini-stream sector chain. + /// Opening remains tolerant so other streams can still be salvaged. + pub fn validate_root_chain(&self) -> Result<()> { + let root = self.root(); + if root.size > 0 { + if root.start == ENDOFCHAIN { + return Err(invalid("nonempty root mini stream has no sector chain")); + } + self.chain_ids(root.start, None)?; + } + Ok(()) + } + pub fn entry(&self, id: usize) -> Option<&Entry> { self.entries.get(id) } diff --git a/crates/cfb/src/tests/ported.rs b/crates/cfb/src/tests/ported.rs index f29eb7ea9..7e9a284df 100644 --- a/crates/cfb/src/tests/ported.rs +++ b/crates/cfb/src/tests/ported.rs @@ -702,3 +702,14 @@ fn special_values_in_every_field_never_panic() { } } } + +#[test] +fn strict_root_validation_rejects_a_loop_beyond_declared_mini_stream() { + let data = pattern(100, 1); + let mut file = foreign_file(&[("Small", &data)]); + let root = dir_at(&file, 0); + let start = u32::from_le_bytes(file[root + 116..root + 120].try_into().unwrap()); + put_u32(&mut file, 512 + start as usize * 4, start); + let cf = CompoundFile::open(&file).unwrap(); + assert!(cf.validate_root_chain().is_err()); +}