mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-07 12:13:20 +00:00
hayro-syntax: cap a document's xref map at the PDF object limit
An xref-stream entry can be a single decoded byte, so one xref stream at the 256 MiB decode limit could expand into hundreds of millions of map entries (several gigabytes) while the document was opened. Every insert into the xref map now goes through insert_entry, which refuses a new object past 8,388,607 entries (the PDF 32000-1 Annex C limit on indirect objects). This covers xref tables, xref streams, merges of Prev/XRefStm sections and the repair scan. Replacing an object already in the map is still allowed.
This commit is contained in:
Vendored
+1
-1
@@ -338,7 +338,7 @@
|
||||
"path": "src/xref.rs",
|
||||
"source_sha256": "51ffeeb7d4ac035bae2cd5ba9288c180088ae06cdcefe2b29dfeda560a793aa0",
|
||||
"source_git_blob": "20e33a410489d42e438a3f2b87d70c7bb8d66eae",
|
||||
"sha256": "98fb56eb5c2da8b0c40202ed30b2f464874c1143436e72eda4e49f2333dd7c7f"
|
||||
"sha256": "95261b80fd7bdc946de625325c6c6fc9fd7c0c5b3273ad62b8666dff215e5bd9"
|
||||
},
|
||||
{
|
||||
"path": "src/decode_limits_tests.rs",
|
||||
|
||||
Vendored
+69
-17
@@ -91,8 +91,9 @@ fn fallback_xref_map_inner<'a>(
|
||||
let mut cloned = r.clone();
|
||||
// Check that the object following it is actually valid before inserting it.
|
||||
cloned.skip_white_spaces_and_comments();
|
||||
if cloned.skip::<Object<'_>>(false).is_some() {
|
||||
xref_map.insert(obj_id, EntryType::Normal(cur_pos));
|
||||
if cloned.skip::<Object<'_>>(false).is_some()
|
||||
&& insert_entry(&mut xref_map, obj_id, EntryType::Normal(cur_pos)).is_some()
|
||||
{
|
||||
last_obj_num = Some(obj_id);
|
||||
dummy_ctx.set_obj_number(obj_id);
|
||||
}
|
||||
@@ -139,13 +140,10 @@ fn fallback_xref_map_inner<'a>(
|
||||
.get(&id)
|
||||
.is_none_or(|e| !matches!(e, &EntryType::Normal(_)))
|
||||
{
|
||||
xref_map.insert(
|
||||
id,
|
||||
EntryType::ObjStream(
|
||||
last_obj_num.obj_number as u32,
|
||||
idx as u32,
|
||||
),
|
||||
);
|
||||
let entry = EntryType::ObjStream(last_obj_num.obj_number as u32, idx as u32);
|
||||
if insert_entry(&mut xref_map, id, entry).is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -671,6 +669,33 @@ enum EntryType {
|
||||
|
||||
type XrefMap = FxHashMap<ObjectIdentifier, EntryType>;
|
||||
|
||||
/// PdfCraft patch: the most entries one document's xref map may hold, the PDF 32000-1 (Annex C)
|
||||
/// limit on indirect objects. An xref-stream entry can be one decoded byte, so a stream at the
|
||||
/// decode limit could otherwise become hundreds of millions of map entries (gigabytes of map).
|
||||
const MAX_XREF_ENTRIES: usize = 8_388_607;
|
||||
|
||||
/// PdfCraft patch: insert an entry unless that would take `map` past [`MAX_XREF_ENTRIES`].
|
||||
fn insert_entry(map: &mut XrefMap, id: ObjectIdentifier, entry: EntryType) -> Option<()> {
|
||||
if map.len() >= MAX_XREF_ENTRIES && !map.contains_key(&id) {
|
||||
warn!("xref holds more than {MAX_XREF_ENTRIES} entries");
|
||||
|
||||
return None;
|
||||
}
|
||||
map.insert(id, entry);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// PdfCraft patch: merge a section's entries into `map`, refusing a result past
|
||||
/// [`MAX_XREF_ENTRIES`].
|
||||
fn merge_entries(map: &mut XrefMap, own: XrefMap) -> Option<()> {
|
||||
for (id, entry) in own {
|
||||
insert_entry(map, id, entry)?;
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Representation of a proper xref table.
|
||||
#[derive(Debug)]
|
||||
struct MapRepr {
|
||||
@@ -877,10 +902,11 @@ fn populate_from_xref_table<'a>(
|
||||
// Specification says we should ignore any object number > SIZE, but probably
|
||||
// not important?
|
||||
if entry.used {
|
||||
own.insert(
|
||||
insert_entry(
|
||||
&mut own,
|
||||
ObjectIdentifier::new(obj_number as i32, entry.gen_number),
|
||||
EntryType::Normal(entry.offset),
|
||||
);
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -896,7 +922,7 @@ fn populate_from_xref_table<'a>(
|
||||
populate_xref_impl_inner(data, xref_stm as usize, insert_map, visited, limits)?;
|
||||
}
|
||||
|
||||
insert_map.extend(own);
|
||||
merge_entries(insert_map, own)?;
|
||||
|
||||
Some(trailer.data())
|
||||
}
|
||||
@@ -949,7 +975,7 @@ fn populate_from_xref_stream<'a>(
|
||||
let _ = populate_xref_impl_inner(data, prev, insert_map, visited, limits)?;
|
||||
}
|
||||
|
||||
insert_map.extend(own);
|
||||
merge_entries(insert_map, own)?;
|
||||
|
||||
Some(stream.dict().data())
|
||||
}
|
||||
@@ -1036,10 +1062,11 @@ fn xref_stream_subsection<'a>(
|
||||
0
|
||||
};
|
||||
|
||||
insert_map.insert(
|
||||
insert_entry(
|
||||
insert_map,
|
||||
ObjectIdentifier::new(obj_number, gen_number as i32),
|
||||
EntryType::Normal(offset as usize),
|
||||
);
|
||||
)?;
|
||||
}
|
||||
2 => {
|
||||
let obj_stream_number = {
|
||||
@@ -1054,10 +1081,11 @@ fn xref_stream_subsection<'a>(
|
||||
0
|
||||
};
|
||||
|
||||
insert_map.insert(
|
||||
insert_entry(
|
||||
insert_map,
|
||||
ObjectIdentifier::new(obj_number, gen_number),
|
||||
EntryType::ObjStream(obj_stream_number, index),
|
||||
);
|
||||
)?;
|
||||
}
|
||||
_ => {
|
||||
warn!("xref has unknown field type {f_type}");
|
||||
@@ -1216,6 +1244,30 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xref_entries_stop_at_the_document_limit() {
|
||||
// One-byte entries (type 1, offset and generation zero): the cheapest a stream can encode.
|
||||
let data = vec![1u8; MAX_XREF_ENTRIES + 1];
|
||||
let mut map = XrefMap::default();
|
||||
let mut full = Reader::new(&data[..MAX_XREF_ENTRIES]);
|
||||
assert!(xref_stream_subsection(&mut full, 0, MAX_XREF_ENTRIES as u32, 1, 0, 0, &mut map).is_some());
|
||||
assert_eq!(map.len(), MAX_XREF_ENTRIES);
|
||||
|
||||
// A new object past the limit is refused, from a stream, a table section or a merge.
|
||||
let mut over = Reader::new(&data[..1]);
|
||||
assert!(xref_stream_subsection(&mut over, MAX_XREF_ENTRIES as u32, 1, 1, 0, 0, &mut map).is_none());
|
||||
let mut other = XrefMap::default();
|
||||
other.insert(ObjectIdentifier::new(i32::MAX, 0), EntryType::Normal(0));
|
||||
assert!(merge_entries(&mut map, other).is_none());
|
||||
assert_eq!(map.len(), MAX_XREF_ENTRIES);
|
||||
|
||||
// Replacing an existing object (an incremental update) still works at the limit.
|
||||
let mut update = XrefMap::default();
|
||||
update.insert(ObjectIdentifier::new(0, 0), EntryType::Normal(42));
|
||||
assert!(merge_entries(&mut map, update).is_some());
|
||||
assert_eq!(map.get(&ObjectIdentifier::new(0, 0)), Some(&EntryType::Normal(42)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_stream_offsets_are_parsed_once() {
|
||||
let data = Data::new(PdfData::from(Vec::new()));
|
||||
|
||||
Reference in New Issue
Block a user