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)
This commit is contained in:
Dylan M. Taylor
2026-10-23 11:14:16 -04:00
committed by GitHub
parent 6cefd467fc
commit 35d18945a9
5 changed files with 87 additions and 10 deletions
+1
View File
@@ -5,3 +5,4 @@ plans/
.DS_Store
/.claude/
.idea/
/vendor/*/target
+2
View File
@@ -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,
+39
View File
@@ -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"
+1 -1
View File
@@ -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",
+44 -9
View File
@@ -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::<u64>() 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::<u64>() as u8 || f2_len > size_of::<u64>() 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::<Vec<u8>>();
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() {