From 9def84d7a577f0a8b6dcff115016a3bc5acbf3fe Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 23 Oct 2026 11:16:19 -0400 Subject: [PATCH] security: fallible bitmap allocation in vendored hayro-jbig2 (#242) * security: fallible bitmap allocation in vendored hayro-jbig2 (follow-up to #239) * security: reinitialize allocates before mutating the jbig2 bitmap (CodeRabbit) --- vendor/README.craft-reader.md | 3 ++- vendor/hayro-jbig2/SOURCE.json | 2 +- vendor/hayro-jbig2/src/bitmap.rs | 31 ++++++++++++++++++++++++++----- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/vendor/README.craft-reader.md b/vendor/README.craft-reader.md index 24f9c47d..d7444ba0 100644 --- a/vendor/README.craft-reader.md +++ b/vendor/README.craft-reader.md @@ -50,7 +50,8 @@ Hostile-input bounds (CodeRabbit findings): - Each object stream's `/N` header pairs are parsed once per document and cached beside its decoded data, instead of on every compressed-object lookup. - An embedded JBIG2 page over 2^28 pixels is refused before the output buffer is allocated, - and that buffer is reserved fallibly. A `JBIG2Globals` stream with an image filter is + and that buffer is reserved fallibly (`Bitmap::reinitialize` allocates before touching the + existing bitmap, so a failed reservation leaves it intact). A `JBIG2Globals` stream with an image filter is 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. diff --git a/vendor/hayro-jbig2/SOURCE.json b/vendor/hayro-jbig2/SOURCE.json index bb9d6a70..6ff0455d 100644 --- a/vendor/hayro-jbig2/SOURCE.json +++ b/vendor/hayro-jbig2/SOURCE.json @@ -62,7 +62,7 @@ "path": "src/bitmap.rs", "source_sha256": "59ed83d8bac6c39e40270a9856ca3faee628de7d9efae1b01f714e140e2b6b7e", "source_git_blob": "35ed97561597d30a70fe819cf8cfbd3780538f75", - "sha256": "59ed83d8bac6c39e40270a9856ca3faee628de7d9efae1b01f714e140e2b6b7e" + "sha256": "99dddbef5f7b36c55d0e914d07664326598919eb6d54cdfa93f300076a8ae69d" }, { "path": "src/decode/generic.rs", diff --git a/vendor/hayro-jbig2/src/bitmap.rs b/vendor/hayro-jbig2/src/bitmap.rs index 35ed9756..6d1e3c55 100644 --- a/vendor/hayro-jbig2/src/bitmap.rs +++ b/vendor/hayro-jbig2/src/bitmap.rs @@ -7,7 +7,6 @@ use crate::decode::CombinationOperator; use crate::error::{OverflowError, Result, bail}; -use alloc::vec; use alloc::vec::Vec; // To guard better against malicious files, we limit bitmap dimensions @@ -50,6 +49,14 @@ pub(crate) struct Bitmap { pub(crate) y_location: u32, } +/// PdfCraft patch: fallible replacement for `vec![word; len]` / `resize`. +fn fill_words(data: &mut Vec, len: usize, word: Word) -> Result<()> { + data.try_reserve_exact(len) + .map_err(|_| OverflowError::BitmapDimension)?; + data.resize(len, word); + Ok(()) +} + impl Bitmap { /// Create a new bitmap filled with white pixels (false). /// @@ -76,7 +83,9 @@ impl Bitmap { let stride = width.div_ceil(WORD_BITS); let default_word: Word = if default_pixel { !0 } else { 0 }; - let data = vec![default_word; stride as usize * height as usize]; + // PdfCraft patch: reserve fallibly so hostile dimensions error instead of aborting. + let mut data = Vec::new(); + fill_words(&mut data, stride as usize * height as usize, default_word)?; Ok(Self { width, height, @@ -103,14 +112,15 @@ impl Bitmap { let stride = width.div_ceil(WORD_BITS); let default_word: Word = if default_pixel { !0 } else { 0 }; + // Allocate first so a failed reservation leaves the bitmap untouched. + let mut data = Vec::new(); + fill_words(&mut data, stride as usize * height as usize, default_word)?; self.width = width; self.height = height; self.stride = stride; self.x_location = 0; self.y_location = 0; - self.data.clear(); - self.data - .resize(stride as usize * height as usize, default_word); + self.data = data; Ok(()) } @@ -225,3 +235,14 @@ impl Bitmap { self.data[(row * self.stride + word_idx) as usize] } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn failed_reservation_is_an_error() { + let mut data = Vec::new(); + assert!(fill_words(&mut data, usize::MAX / 2, 0).is_err()); + } +}