fix(cos): cap the total entries a materialized xref-stream chain merges (#5082)

read_xref_chain_materialized capped each section's decode and the chain length but merged
every row of every section into one map with no total. It now counts the rows each section
merges and, past MAX_XREF_OBJECTS, returns a syntax error so the open path logs it and
rebuilds from object headers, as it already does when a single section's stream decodes past
that limit. The count is rows merged rather than map size, so relisted objects are bounded too,
and it never exceeds what the compact reader counts, whose limit is about a quarter of this one.

Fixes #5082

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Higgs32584andClaude Opus 5.5 committed 2026-10-27 21:30:33 -04:00
1 parent 3932ee0639
commit fde104475b
1 file changed
+14
+14
View File
@@ -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<i64, Option<BTreeMap<u32, XrefEntry>>> = 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);