diff --git a/crates/cos/src/document/reader.rs b/crates/cos/src/document/reader.rs index 66129c1423..0bd4c980cd 100644 --- a/crates/cos/src/document/reader.rs +++ b/crates/cos/src/document/reader.rs @@ -343,6 +343,7 @@ impl Document { // At most MAX_XREF_OBJECTS entries are kept in all; a stream past that is read again. let mut hybrids: HashMap>> = HashMap::new(); let (mut hybrid_rows, mut noted) = (0usize, HashSet::new()); + let mut merged_rows = 0usize; let mut next = Some(first); while let Some(off) = next.take() { if !visited.insert(off) || visited.len() > 10_000 { @@ -413,6 +414,19 @@ impl Document { hybrids.insert(x, read); } } + // Each section's rows are capped, so the chain is too: at most MAX_XREF_OBJECTS rows + // merged in all, or the file is rebuilt from object headers, as when one section's + // stream decodes past that. This counts rows merged, not the map's size, since a chain + // that relists the same objects costs as much to merge. A section's rows (its hybrid + // stream's included) never outnumber the rows the compact reader counts for it, and + // that reader stops at about a quarter of this cap: no chain within its limit is refused. + let Some(total) = merged_rows.checked_add(sect.len()).filter(|total| *total <= MAX_XREF_OBJECTS) else { + return Err(CosError::Syntax { + offset: usize::try_from(off).unwrap_or(usize::MAX), + detail: format!("cross-reference chain lists more than {MAX_XREF_OBJECTS} entries"), + }); + }; + merged_rows = total; oddities.extend(odd.into_iter().map(|num| (off, num))); for (num, e) in sect { entries.entry(num).or_insert(e);