fix(underlay): malformed DWF and DGN files no longer crash or hang the reader

- W2D: a stream cut off right after an extended opcode's '(' sliced its
  body backwards and panicked.
- DGN V8: the compound-file header was indexed before its length was
  known (a short file with the signature panicked), the sector shift was
  taken on trust (0 or 1 underflowed `sector / 4 - 1` into an endless
  loop), and the DIFAT chain count was unbounded. The header must now be
  complete, the shift 9 or 12, and the chain no longer than the file.

Found fuzzing the #1514 readers with truncated and random input.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-09-28 17:06:14 +03:00
1 parent e7aee3baa9
commit e26e0352b7
2 files changed
+14 -4

No files matched your search

+11 -3
View File
@@ -39,16 +39,24 @@ fn u32_at(b: &[u8], at: usize) -> Option<u32> {
impl<'a> Cfb<'a> {
fn open(b: &'a [u8]) -> Option<Self> {
if !b.starts_with(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) {
// The 512-byte header, with the only two sector sizes the format
// has (512 and 4096 bytes); anything else is not a compound file.
if b.len() < 512 || !b.starts_with(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) {
return None;
}
let sector = 1usize << u16::from_le_bytes([b[30], b[31]]);
let sector_shift = u16::from_le_bytes([b[30], b[31]]);
if sector_shift != 9 && sector_shift != 12 {
return None;
}
let sector = 1usize << sector_shift;
let mini_sector_shift = u16::from_le_bytes([b[32], b[33]]);
if mini_sector_shift != 6 {
return None;
}
let mut difat: Vec<u32> = (0..109).filter_map(|i| u32_at(b, 76 + i * 4)).filter(|&v| v < END).collect();
let (mut next, mut count) = (u32_at(b, 68)?, u32_at(b, 72)?);
// The DIFAT chain cannot hold more sectors than the file does, so a
// looping chain stops there.
let (mut next, mut count) = (u32_at(b, 68)?, u32_at(b, 72)?.min((b.len() / sector) as u32));
while count > 0 && next < END {
let at = (next as usize + 1) * sector;
for i in 0..sector / 4 - 1 {
+3 -1
View File
@@ -270,7 +270,9 @@ mod w2d {
_ => {}
}
}
let body = &self.b[start + 1..self.i.saturating_sub(1)];
// A stream cut off right after its '(' has no body at all.
let end = self.i.saturating_sub(1).max(start + 1);
let body = self.b.get(start + 1..end)?;
let name_len = body.iter().position(|c| c.is_ascii_whitespace() || *c == b'(').unwrap_or(body.len());
Some((String::from_utf8_lossy(&body[..name_len]).to_string(), &body[name_len..]))
}