mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-01 16:24:51 +00:00
security: bound vendored hayro xref/object-stream/JBIG2/graphics-state work and krilla PNG size (CodeRabbit) (#239)
* security: bound vendored hayro xref, object-stream, JBIG2 and graphics-state work; krilla PNG size hayro-syntax: - xref-stream subsections must be backed by decoded bytes: zero /W widths and counts longer than the data are refused before inserting (CodeRabbit CWE-400, xref.rs:962) - object-stream offset tables are parsed once per document and cached in Data (CodeRabbit CWE-407, xref.rs:1079) - embedded JBIG2 pages over 2^28 pixels are refused before allocating, with checked, fallible reservation (CodeRabbit CWE-789, filter/jbig2.rs:34) - a JBIG2Globals stream with an image filter is ignored, so globals cannot recurse (CodeRabbit CWE-674, filter/jbig2.rs:20) hayro-interpret: - at most 4096 saved graphics states / 64 MiB retained; dash arrays over 256 entries stroke solid (CodeRabbit CWE-400, context.rs:226) krilla: - PNGs implying more than 1 GiB of decoded output are refused at Image::from_png and in the deferred decoder; the buffer is reserved fallibly (CodeRabbit CWE-789, graphics/image.rs:565) SOURCE.json local hashes updated; patches recorded in vendor/README.craft-reader.md and vendor/krilla/README.craft.md. * vendor: fix SOURCE.json local hashes; reserve krilla's colour/alpha split buffers fallibly - SOURCE.json: only the local sha256 changes; the source/upstream hashes of the patched files are restored (CodeRabbit review) - krilla handle_u8_image/handle_u16_image reserve their split buffers with try_reserve_exact and return an error, so a valid image under the 1 GiB decode cap cannot abort the process on the second allocation (CodeRabbit review)
This commit is contained in:
1 parent
4919088427
commit
6cefd467fc
13 files changed
+407
-46
No files matched your search
Vendored
+14
@@ -42,6 +42,20 @@ 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.
|
||||
|
||||
Hostile-input bounds (CodeRabbit findings):
|
||||
|
||||
- An xref-stream subsection must be backed by its decoded bytes: all-zero `/W` widths
|
||||
are refused, and a declared count longer than the data is refused before any entry is
|
||||
inserted, so a tiny stream cannot declare billions of entries.
|
||||
- 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
|
||||
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.
|
||||
A dash array over 256 entries strokes solid.
|
||||
|
||||
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
|
||||
|
||||
Vendored
+4
-4
@@ -188,7 +188,7 @@
|
||||
"path": "src/context.rs",
|
||||
"source_sha256": "e6898005282196c77bf44357237c79f8c839e74f666e8157756230fd9e2f93ed",
|
||||
"source_git_blob": "3e83229c5f58cfbbefd15bf3b5e7b71380291c01",
|
||||
"sha256": "86e838ca19dfc47f5bb0d7a2ffff80709f2fd51bc6b62d65151841bcba212e08"
|
||||
"sha256": "c6f3ca9d12a53042a28dbb8088ee97ba8b2fc6b03ec35867b58fca99dd9b9807"
|
||||
},
|
||||
{
|
||||
"path": "src/convert.rs",
|
||||
@@ -362,7 +362,7 @@
|
||||
"path": "src/interpret/mod.rs",
|
||||
"source_sha256": "2b286df0c34b8194e1bdd1b63c1e08354d24f8389958fd782f42e2cabf364673",
|
||||
"source_git_blob": "931b3388cef188cee0a5a1d495b69a6ff04f5e51",
|
||||
"sha256": "6ae30906c8e6d0e869e0fac8ece4d97e0ac08d8abd7042c0242a069d4e609351"
|
||||
"sha256": "72650303e920e855cf4c78c82bee2c4b72a4f251c58e16d27bd5ae3860c9983b"
|
||||
},
|
||||
{
|
||||
"path": "src/interpret/path.rs",
|
||||
@@ -374,7 +374,7 @@
|
||||
"path": "src/interpret/state.rs",
|
||||
"source_sha256": "dce778291b13f6df83c8c13aee22f60edd033ec9f7d89309ef148d46d3ba9814",
|
||||
"source_git_blob": "95b773fb5cc434ab8db362d00e4e5180a8b740c9",
|
||||
"sha256": "dce778291b13f6df83c8c13aee22f60edd033ec9f7d89309ef148d46d3ba9814"
|
||||
"sha256": "a0d726f48645374f5cbf4ecb10906c13cabc747e3de878b9140c73b6d1303819"
|
||||
},
|
||||
{
|
||||
"path": "src/interpret/text.rs",
|
||||
@@ -422,7 +422,7 @@
|
||||
"path": "src/types.rs",
|
||||
"source_sha256": "0e66569b8a7dc243b0a384cf1d3615ca62bf660b5324236f2594609bdfddfd46",
|
||||
"source_git_blob": "64d2b849bdd3fdcd4ebfd76845edcef77b625fbd",
|
||||
"sha256": "0e66569b8a7dc243b0a384cf1d3615ca62bf660b5324236f2594609bdfddfd46"
|
||||
"sha256": "542fafbfcf8a472466377cacd580eda2fc2d40b834a8d5ecd1ad35d75445f532"
|
||||
},
|
||||
{
|
||||
"path": "src/util.rs",
|
||||
|
||||
+115
-2
@@ -166,6 +166,23 @@ pub struct Context<'a> {
|
||||
pub(crate) xref: &'a XRef,
|
||||
pub(crate) ocg_state: OcgState,
|
||||
nesting_depth: u32,
|
||||
/// PdfCraft patch: estimated bytes held by saved states, and for each `q` not saved because
|
||||
/// a limit was reached, the clip count to return to at its `Q`.
|
||||
saved_bytes: usize,
|
||||
skipped_saves: Vec<usize>,
|
||||
}
|
||||
|
||||
/// PdfCraft patch: most graphics states saved at once. Every `q` copies the current state, dash
|
||||
/// array and clip list included, so without a limit short operators multiply one large state.
|
||||
const MAX_SAVED_STATES: usize = 4096;
|
||||
/// PdfCraft patch: most bytes all saved states may hold together.
|
||||
const MAX_SAVED_STATE_BYTES: usize = 64 << 20;
|
||||
|
||||
/// Roughly what a saved copy of `state` holds.
|
||||
fn state_bytes(state: &State<'_>) -> usize {
|
||||
size_of::<State<'_>>()
|
||||
+ state.graphics_state.stroke_props.dash_array.len() * size_of::<f32>()
|
||||
+ state.clips.len() * size_of::<ClipType>()
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
@@ -220,15 +237,33 @@ impl<'a> Context<'a> {
|
||||
interpreter_cache: cache.clone(),
|
||||
ocg_state,
|
||||
nesting_depth,
|
||||
saved_bytes: 0,
|
||||
skipped_saves: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn save_state(&mut self) {
|
||||
let Some(cur) = self.states.last().cloned() else {
|
||||
let Some(cur) = self.states.last() else {
|
||||
warn!("attempted to save state without existing state");
|
||||
return;
|
||||
};
|
||||
|
||||
// PdfCraft patch: past either limit the `q` is recorded but nothing is copied. Its `Q`
|
||||
// still removes the clips added since; other changes made in between are kept.
|
||||
let bytes = state_bytes(cur);
|
||||
if !self.skipped_saves.is_empty()
|
||||
|| self.states.len() >= MAX_SAVED_STATES
|
||||
|| self.saved_bytes.saturating_add(bytes) > MAX_SAVED_STATE_BYTES
|
||||
{
|
||||
if self.skipped_saves.is_empty() {
|
||||
warn!("too many saved graphics states; further saves are not kept");
|
||||
}
|
||||
self.skipped_saves.push(cur.clips.len());
|
||||
return;
|
||||
}
|
||||
|
||||
let cur = cur.clone();
|
||||
self.saved_bytes += bytes;
|
||||
self.states.push(cur);
|
||||
}
|
||||
|
||||
@@ -305,6 +340,14 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn restore_state(&mut self, device: &mut impl Device<'a>) {
|
||||
// PdfCraft patch: a save that was not kept is the most recent one.
|
||||
if let Some(target_clips) = self.skipped_saves.pop() {
|
||||
while self.get().clips.len() > target_clips {
|
||||
self.pop_clip_path(device);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(target_clips) = self
|
||||
.states
|
||||
.get(self.states.len().saturating_sub(2))
|
||||
@@ -321,6 +364,10 @@ impl<'a> Context<'a> {
|
||||
// The first state should never be popped.
|
||||
if self.states.len() > 1 {
|
||||
self.states.pop();
|
||||
// PdfCraft patch: the state now on top is the copy that was charged when saved.
|
||||
if let Some(top) = self.states.last() {
|
||||
self.saved_bytes = self.saved_bytes.saturating_sub(state_bytes(top));
|
||||
}
|
||||
}
|
||||
|
||||
device.set_soft_mask(
|
||||
@@ -392,7 +439,8 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn num_states(&self) -> usize {
|
||||
self.states.len()
|
||||
// PdfCraft patch: saves not kept still count, so callers restore back past them.
|
||||
self.states.len() + self.skipped_saves.len()
|
||||
}
|
||||
|
||||
pub(crate) fn nesting_depth(&self) -> u32 {
|
||||
@@ -484,3 +532,68 @@ pub(crate) fn path_as_rect(path: &BezPath) -> Option<Rect> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod saved_state_tests {
|
||||
use super::*;
|
||||
use crate::device::DummyDevice;
|
||||
use hayro_syntax::Pdf;
|
||||
|
||||
fn pdf() -> Pdf {
|
||||
let objects = ["<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Count 0 /Kids [] >>"];
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, body) in objects.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n{body}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(b"xref\n0 3\n0000000000 65535 f \n");
|
||||
for offset in offsets {
|
||||
out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").as_bytes());
|
||||
Pdf::new(out).expect("synthetic pdf")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_saves_of_a_large_state_stay_bounded_and_balanced() {
|
||||
let pdf = pdf();
|
||||
let cache = InterpreterCache::new();
|
||||
let mut ctx = Context::new(Affine::IDENTITY, Rect::new(0.0, 0.0, 100.0, 100.0), &cache, pdf.xref(), InterpreterSettings::default());
|
||||
ctx.get_mut().graphics_state.stroke_props.dash_array = crate::types::dash_array((0..crate::types::MAX_DASH_ENTRIES).map(|i| i as f32 + 1.0));
|
||||
for _ in 0..20_000 {
|
||||
ctx.save_state();
|
||||
}
|
||||
assert!(ctx.states.len() <= MAX_SAVED_STATES);
|
||||
assert!(ctx.saved_bytes <= MAX_SAVED_STATE_BYTES);
|
||||
assert_eq!(ctx.num_states(), 20_001, "every q still counts");
|
||||
|
||||
// A clip added after a save that was not kept is removed by its Q.
|
||||
let mut device = DummyDevice;
|
||||
let mut triangle = BezPath::new();
|
||||
triangle.move_to((10.0, 10.0));
|
||||
triangle.line_to((90.0, 10.0));
|
||||
triangle.line_to((50.0, 90.0));
|
||||
triangle.close_path();
|
||||
let clips = ctx.get().clips.len();
|
||||
ctx.push_clip_path(triangle, FillRule::NonZero, &mut device);
|
||||
assert_eq!(ctx.get().clips.len(), clips + 1);
|
||||
ctx.restore_state(&mut device);
|
||||
assert_eq!(ctx.get().clips.len(), clips);
|
||||
|
||||
while ctx.num_states() > 1 {
|
||||
ctx.restore_state(&mut device);
|
||||
}
|
||||
assert_eq!(ctx.states.len(), 1);
|
||||
assert_eq!(ctx.saved_bytes, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_overlong_dash_array_strokes_solid() {
|
||||
let max = crate::types::MAX_DASH_ENTRIES;
|
||||
assert_eq!(crate::types::dash_array((0..max).map(|_| 2.0)).len(), max);
|
||||
assert!(crate::types::dash_array((0..max + 1).map(|_| 2.0)).is_empty());
|
||||
assert_eq!(crate::types::dash_array([0.0, 3.0].into_iter()).as_slice(), &[0.01, 3.0]);
|
||||
}
|
||||
}
|
||||
+2
-4
@@ -555,11 +555,9 @@ fn interpret_window<'a>(
|
||||
}
|
||||
TypedInstruction::DashPattern(p) => {
|
||||
context.get_mut().graphics_state.stroke_props.dash_offset = p.1.as_f32();
|
||||
// kurbo apparently cannot properly deal with offsets that are exactly 0.
|
||||
// PdfCraft patch: bounded, see `types::dash_array`.
|
||||
context.get_mut().graphics_state.stroke_props.dash_array =
|
||||
p.0.iter::<f32>()
|
||||
.map(|n| if n == 0.0 { 0.01 } else { n })
|
||||
.collect();
|
||||
crate::types::dash_array(p.0.iter::<f32>());
|
||||
}
|
||||
TypedInstruction::RenderingIntent(_) => {
|
||||
// Ignore for now.
|
||||
|
||||
+3
-5
@@ -402,11 +402,9 @@ pub(crate) fn handle_gs_single<'a>(
|
||||
let dash_phase = iter.next()?.into_number()?.as_f32();
|
||||
|
||||
context.get_mut().graphics_state.stroke_props.dash_offset = dash_phase;
|
||||
context.get_mut().graphics_state.stroke_props.dash_array = dash_array
|
||||
.iter::<f32>()
|
||||
// kurbo apparently cannot properly deal with offsets that are exactly 0.
|
||||
.map(|n| if n == 0.0 { 0.01 } else { n })
|
||||
.collect();
|
||||
// PdfCraft patch: bounded, see `types::dash_array`.
|
||||
context.get_mut().graphics_state.stroke_props.dash_array =
|
||||
crate::types::dash_array(dash_array.iter::<f32>());
|
||||
}
|
||||
"Type" => {}
|
||||
_ => {}
|
||||
|
||||
Vendored
+18
@@ -305,6 +305,24 @@ pub struct StrokeProps {
|
||||
pub dash_offset: f32,
|
||||
}
|
||||
|
||||
/// PdfCraft patch: longest dash array kept. A longer one strokes solid; it is copied by every
|
||||
/// saved graphics state, and real dash patterns have a handful of entries.
|
||||
pub(crate) const MAX_DASH_ENTRIES: usize = 256;
|
||||
|
||||
/// PdfCraft patch: the dash array a `d` operator or `/D` entry sets, from its numbers.
|
||||
pub(crate) fn dash_array(values: impl Iterator<Item = f32>) -> SmallVec<[f32; 4]> {
|
||||
// kurbo apparently cannot properly deal with offsets that are exactly 0.
|
||||
let dashes: SmallVec<[f32; 4]> = values
|
||||
.take(MAX_DASH_ENTRIES + 1)
|
||||
.map(|n| if n == 0.0 { 0.01 } else { n })
|
||||
.collect();
|
||||
if dashes.len() > MAX_DASH_ENTRIES {
|
||||
warn!("dash array is too long; stroking solid");
|
||||
return smallvec![];
|
||||
}
|
||||
dashes
|
||||
}
|
||||
|
||||
impl Default for StrokeProps {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
||||
Vendored
+3
-3
@@ -128,7 +128,7 @@
|
||||
"path": "src/data.rs",
|
||||
"source_sha256": "c8dc5a157358e024d73e405333ecac01e94ba6880488d5a10b952bd3fc47fd41",
|
||||
"source_git_blob": "9d5e2030f8d6dab7167cd2c5d8da36a7e5688c34",
|
||||
"sha256": "c8dc5a157358e024d73e405333ecac01e94ba6880488d5a10b952bd3fc47fd41"
|
||||
"sha256": "83c4417cfe9b301ef0fce8dc2f6a7f694d025e1fd82a68db6acded31155d3bee"
|
||||
},
|
||||
{
|
||||
"path": "src/filter/ascii_85.rs",
|
||||
@@ -158,7 +158,7 @@
|
||||
"path": "src/filter/jbig2.rs",
|
||||
"source_sha256": "0ac3c16e02cb13f2d5fe401edd35949320662906c3906cb39fe743acd5e32b09",
|
||||
"source_git_blob": "d87253f536e6b8ac18323f1f06164b516ad775df",
|
||||
"sha256": "0ac3c16e02cb13f2d5fe401edd35949320662906c3906cb39fe743acd5e32b09"
|
||||
"sha256": "4ce7d4c4f33528f5922395c99ddf2da67d15f166bfa593c43df5e5ebf75b3669"
|
||||
},
|
||||
{
|
||||
"path": "src/filter/jpx.rs",
|
||||
@@ -338,7 +338,7 @@
|
||||
"path": "src/xref.rs",
|
||||
"source_sha256": "51ffeeb7d4ac035bae2cd5ba9288c180088ae06cdcefe2b29dfeda560a793aa0",
|
||||
"source_git_blob": "20e33a410489d42e438a3f2b87d70c7bb8d66eae",
|
||||
"sha256": "2bd19a3df5f81c4c0babc80a473877714fe13d2542eafb5c8b5843f81af36d80"
|
||||
"sha256": "d5463a6d629e31a33c1d5b9a735a6e87353752dc0b4156dd84247dcf9b485804"
|
||||
},
|
||||
{
|
||||
"path": "src/decode_limits_tests.rs",
|
||||
|
||||
Vendored
+19
@@ -68,6 +68,10 @@ pub(crate) struct Data {
|
||||
decoded: SegmentList<Option<Vec<u8>>, 32>,
|
||||
map: Mutex<HashMap<ObjectIdentifier, usize>>,
|
||||
loading: LoadGate,
|
||||
// PdfCraft patch: each object stream's header pairs, parsed once. Looking up every object of
|
||||
// a stream otherwise reparsed all /N pairs per lookup, quadratic in /N. An entry is at most
|
||||
// one pair per four decoded bytes, and decoded streams are kept for the document anyway.
|
||||
object_offsets: Mutex<HashMap<ObjectIdentifier, Arc<Vec<(u32, usize)>>>>,
|
||||
}
|
||||
|
||||
impl Debug for Data {
|
||||
@@ -84,6 +88,7 @@ impl Data {
|
||||
decoded: SegmentList::new(),
|
||||
map: Mutex::new(HashMap::new()),
|
||||
loading: LoadGate::default(),
|
||||
object_offsets: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +97,20 @@ impl Data {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the header pairs of object stream `id`, from `parse` on first use.
|
||||
pub(crate) fn object_offsets(
|
||||
&self,
|
||||
id: ObjectIdentifier,
|
||||
parse: impl FnOnce() -> Option<Vec<(u32, usize)>>,
|
||||
) -> Option<Arc<Vec<(u32, usize)>>> {
|
||||
if let Some(offsets) = self.object_offsets.get().get(&id) {
|
||||
return Some(offsets.clone());
|
||||
}
|
||||
// Parsed without the lock held; a racing parse of the same stream gives the same table.
|
||||
let parsed = Arc::new(parse()?);
|
||||
Some(self.object_offsets.get().entry(id).or_insert(parsed).clone())
|
||||
}
|
||||
|
||||
/// Get access to the data of a decoded object stream.
|
||||
pub(crate) fn get_with(&self, id: ObjectIdentifier, ctx: &ReaderContext<'_>) -> Option<&[u8]> {
|
||||
// PdfCraft patch: (#307, see vendor/README.md hayro-syntax (10)) register exactly once:
|
||||
|
||||
+63
-3
@@ -1,13 +1,19 @@
|
||||
use crate::bit_reader::BitWriter;
|
||||
use crate::filter::Filter;
|
||||
use crate::object::Dict;
|
||||
use crate::object::Stream;
|
||||
use crate::object::dict::keys::JBIG2_GLOBALS;
|
||||
use crate::object::stream::{FilterResult, ImageColorSpace, ImageData, ImageDecodeParams};
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use core::iter;
|
||||
|
||||
/// PdfCraft patch: most pixels an embedded JBIG2 page may decode to, matching the renderer's
|
||||
/// `MAX_IMAGE_PIXELS` and `hayro_jbig2`'s region limit. The page header's own dimensions are
|
||||
/// checked here, before any output is allocated: the image dictionary's smaller ones say nothing
|
||||
/// about them.
|
||||
const MAX_PIXELS: u64 = 1 << 28;
|
||||
|
||||
/// Decode JBIG2 data from a PDF stream.
|
||||
///
|
||||
/// The `params` dictionary may contain a `JBIG2Globals` entry pointing to
|
||||
@@ -17,12 +23,28 @@ pub(crate) fn decode(
|
||||
params: &Dict<'_>,
|
||||
image_params: &ImageDecodeParams,
|
||||
) -> Option<FilterResult<'static>> {
|
||||
// PdfCraft patch: globals are plain segment data. A globals stream filtered as an image
|
||||
// could name further globals of its own, recursing once per stream in the chain.
|
||||
let globals = params
|
||||
.get::<Stream<'_>>(JBIG2_GLOBALS)
|
||||
.filter(|g| {
|
||||
!g.filters().iter().any(|f| {
|
||||
matches!(
|
||||
f,
|
||||
Filter::Jbig2Decode | Filter::DctDecode | Filter::JpxDecode | Filter::CcittFaxDecode
|
||||
)
|
||||
})
|
||||
})
|
||||
.and_then(|g| g.decoded().ok());
|
||||
|
||||
let image = hayro_jbig2::Image::new_embedded(data, globals.as_deref()).ok()?;
|
||||
|
||||
if u64::from(image.width()) * u64::from(image.height()) > MAX_PIXELS {
|
||||
warn!("embedded JBIG2 page is too large");
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
// Whenever possible (if we don't have an indexed color space), we convert
|
||||
// the data as 8-bit instead of 1-bit, so that it can be easier converted
|
||||
// into an RGBA8 image.
|
||||
@@ -31,7 +53,11 @@ pub(crate) fn decode(
|
||||
// white = 0, but PDF uses the opposite.
|
||||
let (decoded, bpc) = if image_params.is_indexed {
|
||||
let row_bytes = (image.width() as usize).div_ceil(8);
|
||||
let mut packed = vec![0_u8; row_bytes * image.height() as usize];
|
||||
// PdfCraft patch: checked and fallible, so a failed allocation is a decode error.
|
||||
let len = row_bytes.checked_mul(image.height() as usize)?;
|
||||
let mut packed = Vec::new();
|
||||
packed.try_reserve_exact(len).ok()?;
|
||||
packed.resize(len, 0_u8);
|
||||
|
||||
struct BitWriterDecoder<'a> {
|
||||
writer: BitWriter<'a>,
|
||||
@@ -77,7 +103,11 @@ pub(crate) fn decode(
|
||||
fn next_line(&mut self) {}
|
||||
}
|
||||
|
||||
let mut decoder = Luma8Decoder { output: Vec::new() };
|
||||
let mut output = Vec::new();
|
||||
output
|
||||
.try_reserve_exact((image.width() as usize).checked_mul(image.height() as usize)?)
|
||||
.ok()?;
|
||||
let mut decoder = Luma8Decoder { output };
|
||||
image.decode(&mut decoder).ok()?;
|
||||
|
||||
(decoder.output, 8)
|
||||
@@ -94,3 +124,33 @@ pub(crate) fn decode(
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A page information segment declaring `width` x `height`, and nothing else.
|
||||
fn page_information(width: u32, height: u32) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(&0u32.to_be_bytes()); // segment number
|
||||
out.push(48); // page information
|
||||
out.push(0); // no referred-to segments
|
||||
out.push(1); // page 1
|
||||
out.extend_from_slice(&19u32.to_be_bytes());
|
||||
out.extend_from_slice(&width.to_be_bytes());
|
||||
out.extend_from_slice(&height.to_be_bytes());
|
||||
out.extend_from_slice(&[0; 8]); // resolution
|
||||
out.push(0); // flags
|
||||
out.extend_from_slice(&[0; 2]); // striping
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_oversized_embedded_page_is_refused_before_allocating() {
|
||||
let data = page_information(65_535, 65_535);
|
||||
for is_indexed in [false, true] {
|
||||
let params = ImageDecodeParams { is_indexed, bpc: Some(1), num_components: Some(1), target_dimension: None, width: 1, height: 1 };
|
||||
assert!(decode(&data, &Dict::empty(), ¶ms).is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+63
-8
@@ -618,9 +618,13 @@ impl XRef {
|
||||
return None;
|
||||
}
|
||||
|
||||
let stream = self.get_with::<Stream<'_>>(obj_stream_id, &ctx)?;
|
||||
let data = repr.data.get_with(obj_stream_id, &ctx)?;
|
||||
let object_stream = ObjectStream::new(stream, data, &ctx)?;
|
||||
// PdfCraft patch: the offset table is parsed once per stream (see Data).
|
||||
let offsets = repr.data.object_offsets(obj_stream_id, || {
|
||||
let stream = self.get_with::<Stream<'_>>(obj_stream_id, &ctx)?;
|
||||
ObjectStream::offsets(stream, data)
|
||||
})?;
|
||||
let object_stream = ObjectStream::with_offsets(offsets, data, &ctx);
|
||||
object_stream.get(index)
|
||||
}
|
||||
}
|
||||
@@ -959,6 +963,22 @@ fn xref_stream_subsection<'a>(
|
||||
f3_len: u8,
|
||||
insert_map: &mut XrefMap,
|
||||
) -> Option<()> {
|
||||
// PdfCraft patch: every entry must be backed by stream bytes. With all widths zero an entry
|
||||
// reads nothing, so a tiny stream could declare billions of entries; and a count past the
|
||||
// decoded data is refused before any of it is inserted.
|
||||
let width = f1_len as usize + f2_len as usize + f3_len as usize;
|
||||
if width == 0 {
|
||||
warn!("xref stream entries have no width");
|
||||
|
||||
return None;
|
||||
}
|
||||
let available = xref_reader.len().saturating_sub(xref_reader.offset());
|
||||
if (num_elements as usize).checked_mul(width).is_none_or(|need| need > available) {
|
||||
warn!("xref stream subsection declares more entries than its data holds");
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
for i in 0..num_elements {
|
||||
let f_type = if f1_len == 0 {
|
||||
1
|
||||
@@ -1064,11 +1084,23 @@ fn get_decryptor(trailer_dict: &Dict<'_>, password: &[u8]) -> Result<Decryptor,
|
||||
struct ObjectStream<'a> {
|
||||
data: &'a [u8],
|
||||
ctx: ReaderContext<'a>,
|
||||
offsets: Vec<(u32, usize)>,
|
||||
offsets: Arc<Vec<(u32, usize)>>,
|
||||
}
|
||||
|
||||
impl<'a> ObjectStream<'a> {
|
||||
fn new(inner: Stream<'_>, data: &'a [u8], ctx: &ReaderContext<'a>) -> Option<Self> {
|
||||
Some(Self::with_offsets(Arc::new(Self::offsets(inner, data)?), data, ctx))
|
||||
}
|
||||
|
||||
fn with_offsets(offsets: Arc<Vec<(u32, usize)>>, data: &'a [u8], ctx: &ReaderContext<'a>) -> Self {
|
||||
let mut ctx = ctx.clone();
|
||||
ctx.set_in_object_stream(true);
|
||||
|
||||
Self { data, ctx, offsets }
|
||||
}
|
||||
|
||||
/// The `/N` pairs of object number and absolute offset at the start of `data`.
|
||||
fn offsets(inner: Stream<'_>, data: &[u8]) -> Option<Vec<(u32, usize)>> {
|
||||
let num_objects = inner.dict().get::<usize>(N)?;
|
||||
let first_offset = inner.dict().get::<usize>(FIRST)?;
|
||||
|
||||
@@ -1082,13 +1114,10 @@ impl<'a> ObjectStream<'a> {
|
||||
let obj_num = r.read_without_context::<u32>()?;
|
||||
r.skip_white_spaces_and_comments();
|
||||
let relative_offset = r.read_without_context::<usize>()?;
|
||||
offsets.push((obj_num, first_offset + relative_offset));
|
||||
offsets.push((obj_num, first_offset.checked_add(relative_offset)?));
|
||||
}
|
||||
|
||||
let mut ctx = ctx.clone();
|
||||
ctx.set_in_object_stream(true);
|
||||
|
||||
Some(Self { data, ctx, offsets })
|
||||
Some(offsets)
|
||||
}
|
||||
|
||||
fn get<T>(&self, index: u32) -> Option<T>
|
||||
@@ -1137,6 +1166,32 @@ fn parse_metadata(info_dict: &Dict<'_>) -> Metadata {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn xref_stream_entries_need_backing_bytes() {
|
||||
let mut map = XrefMap::default();
|
||||
// All widths zero: an entry reads nothing, so none may be declared.
|
||||
let mut empty = Reader::new(&[]);
|
||||
assert!(xref_stream_subsection(&mut empty, 0, u32::MAX, 0, 0, 0, &mut map).is_none());
|
||||
// Three entries of width 3 need nine bytes; eight are refused before any insert.
|
||||
let data = [1, 0, 9, 1, 0, 9, 1, 0];
|
||||
let mut short = Reader::new(&data);
|
||||
assert!(xref_stream_subsection(&mut short, 0, 3, 1, 1, 1, &mut map).is_none());
|
||||
assert!(map.is_empty());
|
||||
let mut whole = Reader::new(&data[..6]);
|
||||
assert!(xref_stream_subsection(&mut whole, 0, 2, 1, 1, 1, &mut map).is_some());
|
||||
assert_eq!(map.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_stream_offsets_are_parsed_once() {
|
||||
let data = Data::new(PdfData::from(Vec::new()));
|
||||
let id = ObjectIdentifier::new(7, 0);
|
||||
let first = data.object_offsets(id, || Some(vec![(1, 10), (2, 20)])).unwrap();
|
||||
let again = data.object_offsets(id, || panic!("reparsed a cached offset table")).unwrap();
|
||||
assert!(Arc::ptr_eq(&first, &again));
|
||||
assert!(data.object_offsets(ObjectIdentifier::new(8, 0), || None).is_none());
|
||||
}
|
||||
|
||||
// Contributor-original, MIT OR Apache-2.0. Matches the bounded, uncompressed
|
||||
// synthetic regression used to reproduce the cold-publication race. A second
|
||||
// stream optionally checks that racing registration never aliases two ids.
|
||||
|
||||
Vendored
+6
@@ -26,3 +26,9 @@ The shared writer's native subprocess regression covers the previously aborting
|
||||
cleanup and a successful fresh export afterward. Native and WASM export tests
|
||||
cover the unchanged successful path. WASM aborts, aborting panic configurations
|
||||
and process-wide allocation failures remain outside unwind recovery.
|
||||
|
||||
A PNG whose header implies more than 1 GiB of decoded output is refused by
|
||||
`Image::from_png` and by the deferred decoder, and the decoded buffer is reserved
|
||||
fallibly, so a small file (or a bitmap glyph in a font) cannot request an allocation that
|
||||
ends the process. The colour and alpha buffers that PNG, GIF and WebP decoding split an
|
||||
image into are reserved fallibly too, and a failure there is an image error.
|
||||
Vendored
+1
-1
@@ -172,7 +172,7 @@
|
||||
{
|
||||
"path": "src/graphics/image.rs",
|
||||
"upstream_sha256": "61324d6426ac79aa5322d0d33a95532934cd1e41797a9dad6302210a2b799d5d",
|
||||
"sha256": "61324d6426ac79aa5322d0d33a95532934cd1e41797a9dad6302210a2b799d5d"
|
||||
"sha256": "25fb30fc591e180f8ab4a6445cbda0d14f1cfc7860c4699b444c3fbaab97bd69"
|
||||
},
|
||||
{
|
||||
"path": "src/graphics/mask.rs",
|
||||
|
||||
Vendored
+96
-16
@@ -330,7 +330,7 @@ impl Image {
|
||||
Self(Arc::new(ImageRepr {
|
||||
inner: Deferred::new(move || {
|
||||
let (color_channel, alpha_channel, bits_per_component) =
|
||||
handle_u8_image(&data, ColorSpace::RGBA);
|
||||
handle_u8_image(&data, ColorSpace::RGBA)?;
|
||||
|
||||
Ok(Repr::Sampled(SampledRepr {
|
||||
color_channel,
|
||||
@@ -522,6 +522,13 @@ fn png_metadata(data: &[u8]) -> Result<ImageMetadata, String> {
|
||||
let reader = decoder
|
||||
.read_info()
|
||||
.map_err(|e| e.to_string().to_ascii_lowercase())?;
|
||||
// Craft patch: refused here too, so `Image::from_png` fails instead of the deferred decode.
|
||||
if reader
|
||||
.output_buffer_size()
|
||||
.is_none_or(|n| n > MAX_DECODED_PNG_BYTES)
|
||||
{
|
||||
return Err("image is too large".to_string());
|
||||
}
|
||||
let info = reader.info();
|
||||
|
||||
let size = (info.width, info.height);
|
||||
@@ -555,6 +562,11 @@ fn png_metadata(data: &[u8]) -> Result<ImageMetadata, String> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Craft patch: most bytes a PNG may decode to (16384 x 16384 RGBA8). The header alone sets the
|
||||
/// output size, so it is checked before any pixel data, and reserved fallibly, so that a bad
|
||||
/// image is an error instead of an allocation failure that ends the process.
|
||||
const MAX_DECODED_PNG_BYTES: usize = 1 << 30;
|
||||
|
||||
fn decode_png(data: &[u8]) -> Result<Repr, String> {
|
||||
let cursor = Cursor::new(data);
|
||||
let mut decoder = png::Decoder::new(cursor);
|
||||
@@ -562,7 +574,15 @@ fn decode_png(data: &[u8]) -> Result<Repr, String> {
|
||||
let mut reader = decoder
|
||||
.read_info()
|
||||
.map_err(|e| e.to_string().to_ascii_lowercase())?;
|
||||
let mut img_data = vec![0; reader.output_buffer_size().ok_or("image is too large")?];
|
||||
let size = reader
|
||||
.output_buffer_size()
|
||||
.filter(|&n| n <= MAX_DECODED_PNG_BYTES)
|
||||
.ok_or("image is too large")?;
|
||||
let mut img_data = Vec::new();
|
||||
img_data
|
||||
.try_reserve_exact(size)
|
||||
.map_err(|_| "image is too large".to_string())?;
|
||||
img_data.resize(size, 0);
|
||||
let _ = reader
|
||||
.next_frame(&mut img_data)
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -577,8 +597,8 @@ fn decode_png(data: &[u8]) -> Result<Repr, String> {
|
||||
};
|
||||
|
||||
let (color_channel, alpha_channel, bits_per_component) = match bit_depth {
|
||||
BitDepth::Eight => handle_u8_image(&img_data, color_space),
|
||||
BitDepth::Sixteen => handle_u16_image(&img_data, color_space),
|
||||
BitDepth::Eight => handle_u8_image(&img_data, color_space)?,
|
||||
BitDepth::Sixteen => handle_u16_image(&img_data, color_space)?,
|
||||
_ => return Err("image has an unsupported bit-depth".to_string()),
|
||||
};
|
||||
|
||||
@@ -663,7 +683,7 @@ fn decode_gif(data: Data) -> Result<Repr, String> {
|
||||
.ok_or("GIF image seems to be empty".to_string())?;
|
||||
|
||||
let (color_channel, alpha_channel, bits_per_component) =
|
||||
handle_u8_image(first_frame.buffer.as_ref(), ColorSpace::RGBA);
|
||||
handle_u8_image(first_frame.buffer.as_ref(), ColorSpace::RGBA)?;
|
||||
|
||||
Ok(Repr::Sampled(SampledRepr {
|
||||
color_channel,
|
||||
@@ -719,7 +739,7 @@ fn decode_webp(data: Data) -> Result<Repr, String> {
|
||||
};
|
||||
|
||||
let (color_channel, alpha_channel, bits_per_component) =
|
||||
handle_u8_image(&first_frame, color_space);
|
||||
handle_u8_image(&first_frame, color_space)?;
|
||||
|
||||
Ok(Repr::Sampled(SampledRepr {
|
||||
color_channel,
|
||||
@@ -728,9 +748,21 @@ fn decode_webp(data: Data) -> Result<Repr, String> {
|
||||
}))
|
||||
}
|
||||
|
||||
fn handle_u8_image(data: &[u8], cs: ColorSpace) -> (Vec<u8>, Option<Vec<u8>>, BitsPerComponent) {
|
||||
/// Craft patch: an empty vector with room for `capacity` bytes, or an error instead of the
|
||||
/// allocation failure that would end the process. Splitting a decoded image into colour and alpha
|
||||
/// briefly needs about twice its size beside the decoded pixels.
|
||||
fn try_buffer(capacity: usize) -> Result<Vec<u8>, String> {
|
||||
let mut v = Vec::new();
|
||||
v.try_reserve_exact(capacity)
|
||||
.map_err(|_| "image is too large".to_string())?;
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
type SplitImage = (Vec<u8>, Option<Vec<u8>>, BitsPerComponent);
|
||||
|
||||
fn handle_u8_image(data: &[u8], cs: ColorSpace) -> Result<SplitImage, String> {
|
||||
let mut alphas = if cs.has_alpha() {
|
||||
Vec::with_capacity(data.len() / cs.num_components())
|
||||
try_buffer(data.len() / cs.num_components())?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -738,7 +770,7 @@ fn handle_u8_image(data: &[u8], cs: ColorSpace) -> (Vec<u8>, Option<Vec<u8>>, Bi
|
||||
let color_channel = match cs {
|
||||
ColorSpace::RGB => deflate_encode(data),
|
||||
ColorSpace::RGBA => {
|
||||
let mut buf = Vec::with_capacity(data.len() * 3 / 4);
|
||||
let mut buf = try_buffer(data.len() * 3 / 4)?;
|
||||
data.chunks_exact(4).for_each(|data| {
|
||||
buf.extend_from_slice(&data[0..3]);
|
||||
alphas.push(data[3]);
|
||||
@@ -747,7 +779,7 @@ fn handle_u8_image(data: &[u8], cs: ColorSpace) -> (Vec<u8>, Option<Vec<u8>>, Bi
|
||||
}
|
||||
ColorSpace::Luma => deflate_encode(data),
|
||||
ColorSpace::LumaA => {
|
||||
let mut buf = Vec::with_capacity(data.len() / 2);
|
||||
let mut buf = try_buffer(data.len() / 2)?;
|
||||
data.chunks_exact(2).for_each(|data| {
|
||||
buf.push(data[0]);
|
||||
alphas.push(data[1]);
|
||||
@@ -764,12 +796,12 @@ fn handle_u8_image(data: &[u8], cs: ColorSpace) -> (Vec<u8>, Option<Vec<u8>>, Bi
|
||||
None
|
||||
};
|
||||
|
||||
(color_channel, alpha_channel, BitsPerComponent::Eight)
|
||||
Ok((color_channel, alpha_channel, BitsPerComponent::Eight))
|
||||
}
|
||||
|
||||
fn handle_u16_image(data: &[u8], cs: ColorSpace) -> (Vec<u8>, Option<Vec<u8>>, BitsPerComponent) {
|
||||
fn handle_u16_image(data: &[u8], cs: ColorSpace) -> Result<SplitImage, String> {
|
||||
let mut alphas = if cs.has_alpha() {
|
||||
Vec::with_capacity(data.len() / cs.num_components())
|
||||
try_buffer(data.len() / cs.num_components())?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -777,7 +809,7 @@ fn handle_u16_image(data: &[u8], cs: ColorSpace) -> (Vec<u8>, Option<Vec<u8>>, B
|
||||
let encoded_image = match cs {
|
||||
ColorSpace::RGB => deflate_encode(data),
|
||||
ColorSpace::RGBA => {
|
||||
let mut buf = Vec::with_capacity(data.len() * 3 / 4);
|
||||
let mut buf = try_buffer(data.len() * 3 / 4)?;
|
||||
data.chunks_exact(8).for_each(|data| {
|
||||
buf.extend_from_slice(&data[0..6]);
|
||||
alphas.extend_from_slice(&data[6..]);
|
||||
@@ -786,7 +818,7 @@ fn handle_u16_image(data: &[u8], cs: ColorSpace) -> (Vec<u8>, Option<Vec<u8>>, B
|
||||
}
|
||||
ColorSpace::Luma => deflate_encode(data),
|
||||
ColorSpace::LumaA => {
|
||||
let mut buf = Vec::with_capacity(data.len() / 2);
|
||||
let mut buf = try_buffer(data.len() / 2)?;
|
||||
data.chunks_exact(4).for_each(|data| {
|
||||
buf.extend_from_slice(&data[0..2]);
|
||||
alphas.extend_from_slice(&data[2..]);
|
||||
@@ -803,7 +835,7 @@ fn handle_u16_image(data: &[u8], cs: ColorSpace) -> (Vec<u8>, Option<Vec<u8>>, B
|
||||
None
|
||||
};
|
||||
|
||||
(encoded_image, encoded_mask, BitsPerComponent::Sixteen)
|
||||
Ok((encoded_image, encoded_mask, BitsPerComponent::Sixteen))
|
||||
}
|
||||
|
||||
fn get_icc_profile_type(data: &[u8], color_space: ImageColorspace) -> Option<GenericICCProfile> {
|
||||
@@ -827,6 +859,54 @@ mod tests {
|
||||
assert!(e.is_err());
|
||||
}
|
||||
|
||||
/// A PNG whose header alone claims `width` x `height` RGBA16, with an empty IDAT.
|
||||
fn png_header(width: u32, height: u32) -> Vec<u8> {
|
||||
fn crc(bytes: &[u8]) -> u32 {
|
||||
let mut c = !0u32;
|
||||
for &b in bytes {
|
||||
c ^= u32::from(b);
|
||||
for _ in 0..8 {
|
||||
c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 };
|
||||
}
|
||||
}
|
||||
!c
|
||||
}
|
||||
fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
|
||||
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
|
||||
let start = out.len();
|
||||
out.extend_from_slice(kind);
|
||||
out.extend_from_slice(data);
|
||||
let sum = crc(&out[start..]);
|
||||
out.extend_from_slice(&sum.to_be_bytes());
|
||||
}
|
||||
let mut out = b"\x89PNG\r\n\x1a\n".to_vec();
|
||||
let mut ihdr = Vec::new();
|
||||
ihdr.extend_from_slice(&width.to_be_bytes());
|
||||
ihdr.extend_from_slice(&height.to_be_bytes());
|
||||
ihdr.extend_from_slice(&[16, 6, 0, 0, 0]);
|
||||
chunk(&mut out, b"IHDR", &ihdr);
|
||||
chunk(&mut out, b"IDAT", &[0x78, 0x9C, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01]);
|
||||
chunk(&mut out, b"IEND", &[]);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_png_header_claiming_a_huge_image_is_refused_before_allocating() {
|
||||
// 65535 x 65535 RGBA16 would be 34 GB of output.
|
||||
let huge = png_header(65_535, 65_535);
|
||||
assert!(Image::from_png(Arc::new(huge.clone()).into(), false).is_err());
|
||||
assert!(super::decode_png(&huge).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgba_split_buffers_are_reserved_fallibly() {
|
||||
assert!(super::try_buffer(usize::MAX).is_err());
|
||||
let (color, alpha, _) = super::handle_u8_image(&[1, 2, 3, 128, 4, 5, 6, 255], super::ColorSpace::RGBA).unwrap();
|
||||
assert!(!color.is_empty() && alpha.is_some());
|
||||
let (_, alpha, _) = super::handle_u16_image(&[0; 16], super::ColorSpace::RGBA).unwrap();
|
||||
assert!(alpha.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_jpeg_image() {
|
||||
let e = Image::from_jpeg(Arc::new(b"dfngiudfg".to_vec()).into(), false);
|
||||
|
||||
Reference in new issue
Block a user