From fde104475bd0070604a0b4bc5a8927bb273a2855 Mon Sep 17 00:00:00 2001 From: Higgs32584 Date: Tue, 27 Oct 2026 21:30:33 -0400 Subject: [PATCH] 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 --- crates/cos/src/document/reader.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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);