hayro-jbig2: cap long-form referred-to segment count (#176)

* hayro-jbig2: cap long-form referred-to-segment count

A crafted JBIG2 segment header can claim a 29-bit referred-to-segment count;
Vec::with_capacity reserved ~2 GiB from that field before a single
referred-to number was read, and the resulting allocation failure aborts
the process in a way catch_unwind cannot contain (fatal on wasm32, where
memory is capped well below 2 GiB). A ~100 KB Flate-compressed PDF was
enough to reach it inside the 256 MiB stream budget.

Real files refer to a handful of segments, so the count is now capped at
u16::MAX; larger claims fail as InvalidReferredCount before any
retention-flag bytes are consumed. Regression test included.

Updates SOURCE.json's local hash for src/segment.rs and records the patch
in vendor/README.craft-reader.md, following the vendored tree's provenance
practice.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* hayro-jbig2: record git-blob sha256 for patched segment.rs

The previous entry hashed the Windows working-tree checkout (CRLF) of
src/segment.rs instead of the LF content git stores, so every other
manifest entry's convention (git-blob content hash) was broken by this
one file. Re-hash against `git show HEAD:vendor/hayro-jbig2/src/segment.rs`
as the vendored manifests' other 30 entries do.

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
This commit is contained in:
James ChapmanandClaude Code authored and GitHub committed 2026-10-22 13:10:36 -04:00
1 parent ffe7f8fce8
commit f20f364d55
3 files changed
+44 -1

No files matched your search

+6
View File
@@ -36,6 +36,12 @@ Byte transfer-function samples use rounded 8-bit values; failed or non-finite ev
leave the sample unchanged. Historical source hashes remain in each `SOURCE.json`,
alongside local hashes and the compatibility reference.
The JBIG2 segment parser caps the long-form referred-to-segment count at `u16::MAX`
and rejects a larger claim as an invalid count before any retention-flag or
referred-to-number bytes are consumed, so a crafted header cannot turn
`Vec::with_capacity` into a multi-gigabyte reservation that aborts on allocation
failure.
This remains the patched 0.7 backend. It retains the existing Bradford Lab conversion,
floating component precision and codec versions, rather than claiming full 0.8
equivalence. The independent color oracles, backend tests and facade contracts cover
+1 -1
View File
@@ -176,7 +176,7 @@
"path": "src/segment.rs",
"source_sha256": "58d9fd05a15358b4ca945ffa3b8c1872f4ea57f8bbe50e8db3c6fce5bcac2914",
"source_git_blob": "35c17c38af62b1fece4e280cda923cdcb23a8661",
"sha256": "58d9fd05a15358b4ca945ffa3b8c1872f4ea57f8bbe50e8db3c6fce5bcac2914"
"sha256": "c64e2845dbdd26e33b5461acde399ba4820f9942e9090a32782f44ff0fd5e61f"
},
{
"path": "src/simd.rs",
+37
View File
@@ -8,6 +8,14 @@ use alloc::vec::Vec;
use crate::error::{ParseError, Result, SegmentError, bail, err};
use crate::reader::Reader;
/// PdfCraft patch: the most segments one segment may refer to. A crafted long-form
/// header can claim a 29-bit count, which `Vec::with_capacity` below would turn into
/// a multi-gigabyte reservation before a single referred-to number is read — an
/// allocation failure then aborts the process in a way `catch_unwind` cannot contain.
/// Real files refer to a handful of segments, so `u16::MAX` is far beyond any
/// legitimate need.
pub(crate) const MAX_REFERRED_TO_SEGMENTS: u32 = u16::MAX as u32;
/// "The segment type is a number between 0 and 63, inclusive. Not all values
/// are allowed." (7.3)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -183,6 +191,12 @@ pub(crate) fn parse_segment_header(reader: &mut Reader<'_>) -> Result<SegmentHea
u32::from_be_bytes([count_and_retention & 0x1F, rest[0], rest[1], rest[2]])
};
// Reject an absurd count before reserving memory for it, and before
// skipping the retention-flag bytes it claims to own.
if referred_to_count > MAX_REFERRED_TO_SEGMENTS {
bail!(SegmentError::InvalidReferredCount);
}
// Skip retention flag bytes in long form.
// "The first one-byte field following the initial four-byte field is formatted
// as follows: Bit 0: Retain bit for this segment. Bit 1-7: Retain bits for
@@ -416,4 +430,27 @@ mod tests {
assert_eq!(header.data_length, Some(32));
}
#[test]
fn test_referred_to_count_is_capped() {
use crate::error::DecodeError;
// A long-form header claiming the 29-bit maximum of 536,870,911
// referred-to segments would otherwise make `Vec::with_capacity`
// reserve ~2 GiB before a single referred-to number is read.
// The first byte is 0xFF: long form, count bits 0x1F; the count is
// 0x1FFFFFFF.
let data = [
0x00, 0x01, 0x00, 0x00, // Segment number = 65536
0x86, // Flags: type 6, page assoc 1 byte, deferred non-retain
0xFF, 0xFF, 0xFF, 0xFF, // Long form, count = 536,870,911
];
let mut reader = Reader::new(&data);
let error = parse_segment_header(&mut reader).unwrap_err();
assert!(matches!(
error,
DecodeError::Segment(SegmentError::InvalidReferredCount)
));
}
}