From 35d18945a9a483d247b158505535686a135ff6f8 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 23 Oct 2026 11:14:16 -0400 Subject: [PATCH] fix(hayro-syntax): read full /W[0] xref entry type width (#241) * fix(hayro-syntax): read full /W[0] width for xref stream entry type (follow-up to #239) * hayro-syntax: loader-path test for oversized /W[0]; document patch; drop committed build output (CodeRabbit) --- .gitignore | 1 + vendor/README.craft-reader.md | 2 ++ vendor/hayro-syntax/Cargo.lock | 39 ++++++++++++++++++++++++ vendor/hayro-syntax/SOURCE.json | 2 +- vendor/hayro-syntax/src/xref.rs | 53 +++++++++++++++++++++++++++------ 5 files changed, 87 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 9108c660..c725b3c4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ plans/ .DS_Store /.claude/ .idea/ +/vendor/*/target diff --git a/vendor/README.craft-reader.md b/vendor/README.craft-reader.md index a056e9e6..24f9c47d 100644 --- a/vendor/README.craft-reader.md +++ b/vendor/README.craft-reader.md @@ -54,6 +54,8 @@ Hostile-input bounds (CodeRabbit findings): ignored, so globals cannot recurse through further globals. - At most 4096 graphics states, holding at most 64 MiB together, are saved; a `q` past either limit is counted but not copied, and its `Q` still removes the clips added since. +- An xref-stream `/W` type field wider than one byte is read at its full width (big-endian) + instead of only its first byte, and `/W[0]` over 8 bytes is refused like `/W[1]`. A dash array over 256 entries strokes solid. This remains the patched 0.7 backend. It retains the existing Bradford Lab conversion, diff --git a/vendor/hayro-syntax/Cargo.lock b/vendor/hayro-syntax/Cargo.lock index ebc07dff..cced0bd8 100644 --- a/vendor/hayro-syntax/Cargo.lock +++ b/vendor/hayro-syntax/Cargo.lock @@ -168,6 +168,7 @@ dependencies = [ "rustc-hash", "sha2", "smallvec", + "stringprep", "zune-jpeg", ] @@ -244,12 +245,50 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "version_check" version = "0.9.5" diff --git a/vendor/hayro-syntax/SOURCE.json b/vendor/hayro-syntax/SOURCE.json index 462194b2..800895ae 100644 --- a/vendor/hayro-syntax/SOURCE.json +++ b/vendor/hayro-syntax/SOURCE.json @@ -338,7 +338,7 @@ "path": "src/xref.rs", "source_sha256": "51ffeeb7d4ac035bae2cd5ba9288c180088ae06cdcefe2b29dfeda560a793aa0", "source_git_blob": "20e33a410489d42e438a3f2b87d70c7bb8d66eae", - "sha256": "d5463a6d629e31a33c1d5b9a735a6e87353752dc0b4156dd84247dcf9b485804" + "sha256": "411ce7e3c0d385b1735f945b92dba164825456ef3b70ef464279ba0bac0879d4" }, { "path": "src/decode_limits_tests.rs", diff --git a/vendor/hayro-syntax/src/xref.rs b/vendor/hayro-syntax/src/xref.rs index ed504128..a86a1f4a 100644 --- a/vendor/hayro-syntax/src/xref.rs +++ b/vendor/hayro-syntax/src/xref.rs @@ -887,17 +887,13 @@ fn populate_from_xref_stream<'a>( let [f1_len, f2_len, f3_len] = stream.dict().get::<[u8; 3]>(W)?; - if f2_len > size_of::() as u8 { - error!("xref offset length is larger than the allowed limit"); + // PdfCraft patch: the type field may be wider than one byte (upstream behaviour); bound it. + if f1_len > size_of::() as u8 || f2_len > size_of::() as u8 { + error!("xref field length is larger than the allowed limit"); return None; } - // Do such files exist? - if f1_len != 1 { - warn!("first field in xref stream was longer than 1"); - } - let xref_data = stream.decoded().ok()?; let mut xref_reader = Reader::new(xref_data.as_ref()); @@ -980,11 +976,12 @@ fn xref_stream_subsection<'a>( } for i in 0..num_elements { + // A zero-width type field defaults to 1 (PDF 32000-1, 7.5.8.2). let f_type = if f1_len == 0 { 1 } else { - // We assume a length of 1. - xref_reader.read_bytes(1)?[0] + let bytes = xref_reader.read_bytes(f1_len as usize)?; + bytes.iter().fold(0u64, |acc, b| (acc << 8) | *b as u64) }; let obj_number = start + i; @@ -1276,6 +1273,44 @@ mod tests { out } + #[test] + fn xref_stream_type_field_uses_the_full_w0_width() { + // /W [2 1 1]: two-byte type 0x0001 (normal), offset 7, gen 0. + let mut map = XrefMap::default(); + let mut reader = Reader::new(&[0u8, 1, 7, 0]); + assert!(xref_stream_subsection(&mut reader, 5, 1, 2, 1, 1, &mut map).is_some()); + assert!(matches!( + map.get(&ObjectIdentifier::new(5, 0)), + Some(EntryType::Normal(7)) + )); + } + + #[test] + fn xref_stream_loader_rejects_oversized_w0() { + // Same one-entry stream; only /W[0] differs, so the 9 must be what is refused. + let load = |w: &str| { + let data = format!( + "1 0 obj\n<< /Type /XRef /Size 6 /Index [5 1] /W [{w}] /Length 4 >>\nstream\n" + ) + .into_bytes() + .into_iter() + .chain([0u8, 1, 7, 0]) + .chain(*b"\nendstream\nendobj\n") + .collect::>(); + let mut map = XrefMap::default(); + let got = populate_from_xref_stream( + &data, + &mut Reader::new(&data), + &mut map, + &mut BTreeSet::new(), + DecodeLimits::default(), + ); + got.is_some() + }; + assert!(load("2 1 1")); + assert!(!load("9 1 1")); + } + #[cfg(feature = "std")] #[test] fn concurrent_cold_object_stream_reads_all_succeed() {