From f20f364d554eb23e315f7697afee7a2675eaea71 Mon Sep 17 00:00:00 2001 From: James Chapman Date: Thu, 22 Oct 2026 18:10:36 +0100 Subject: [PATCH] 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 * 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 --------- Co-authored-by: Claude Code --- vendor/README.craft-reader.md | 6 +++++ vendor/hayro-jbig2/SOURCE.json | 2 +- vendor/hayro-jbig2/src/segment.rs | 37 +++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/vendor/README.craft-reader.md b/vendor/README.craft-reader.md index 704ce25d..e7ab08cf 100644 --- a/vendor/README.craft-reader.md +++ b/vendor/README.craft-reader.md @@ -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 diff --git a/vendor/hayro-jbig2/SOURCE.json b/vendor/hayro-jbig2/SOURCE.json index 50ec8937..bb9d6a70 100644 --- a/vendor/hayro-jbig2/SOURCE.json +++ b/vendor/hayro-jbig2/SOURCE.json @@ -176,7 +176,7 @@ "path": "src/segment.rs", "source_sha256": "58d9fd05a15358b4ca945ffa3b8c1872f4ea57f8bbe50e8db3c6fce5bcac2914", "source_git_blob": "35c17c38af62b1fece4e280cda923cdcb23a8661", - "sha256": "58d9fd05a15358b4ca945ffa3b8c1872f4ea57f8bbe50e8db3c6fce5bcac2914" + "sha256": "c64e2845dbdd26e33b5461acde399ba4820f9942e9090a32782f44ff0fd5e61f" }, { "path": "src/simd.rs", diff --git a/vendor/hayro-jbig2/src/segment.rs b/vendor/hayro-jbig2/src/segment.rs index 35c17c38..1c6fcf31 100644 --- a/vendor/hayro-jbig2/src/segment.rs +++ b/vendor/hayro-jbig2/src/segment.rs @@ -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 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) + )); + } }