@@ -13,8 +13,55 @@ index 0f56728..6f1b4c2 100644
1313 let initialized_decoder = JxlDecoder::<states::Initialized>::new(decoder_options);
1414 let _ = initialized_decoder.process(&mut data);
1515 });
16+ diff --git a/jxl/fuzz/fuzz_targets/decode.rs b/jxl/fuzz/fuzz_targets/decode.rs
17+ index e2966b4..442c79d 100644
18+ --- a/jxl/fuzz/fuzz_targets/decode.rs
19+ +++ b/jxl/fuzz/fuzz_targets/decode.rs
20+ @@ -38,12 +38,24 @@ fn fuzz_decode(mut data: &[u8]) -> Result<(), ()> {
21+ let decoder_with_frame_info = as_complete(decoder_with_image_info.process(&mut data))?;
22+ let frame_header = decoder_with_frame_info.frame_header();
23+ let frame_size = frame_header.size;
24+ + let frame_pixels = frame_size.0.checked_mul(frame_size.1).ok_or(())?;
25+ + const MAX_FRAME_PIXELS: usize = 1 << 22;
26+ + if frame_pixels >= MAX_FRAME_PIXELS {
27+ + return Err(());
28+ + }
29+ +
30+ + let row_samples = frame_size.0.checked_mul(samples_per_pixel).ok_or(())?;
31+ + let output_samples = row_samples.checked_mul(frame_size.1).ok_or(())?;
32+ + const MAX_OUTPUT_SAMPLES: usize = 1 << 22;
33+ + if output_samples >= MAX_OUTPUT_SAMPLES {
34+ + return Err(());
35+ + }
36+
37+ let mut outputs =
38+ - vec![Image::<f32>::new((frame_size.0 * samples_per_pixel, frame_size.1)).unwrap()];
39+ + vec![Image::<f32>::new((row_samples, frame_size.1)).map_err(|_| ())?];
40+
41+ for _ in 0..extra_channels {
42+ - outputs.push(Image::<f32>::new(frame_size).unwrap());
43+ + outputs.push(Image::<f32>::new(frame_size).map_err(|_| ())?);
44+ }
45+
46+ let mut output_bufs: Vec<JxlOutputBuffer<'_>> = outputs
47+ diff --git a/jxl/src/api/inner/box_parser.rs b/jxl/src/api/inner/box_parser.rs
48+ index 6471b92..c1bab95 100644
49+ --- a/jxl/src/api/inner/box_parser.rs
50+ +++ b/jxl/src/api/inner/box_parser.rs
51+ @@ -360,7 +360,10 @@ impl BoxParser {
52+ if self.ooo_jxlp.buffered.contains_key(&idx) {
53+ return Err(Error::InvalidBox);
54+ }
55+ - if content_len == u64::MAX {
56+ + const MAX_BUFFERED_OOO_JXLP: u64 = 16 * 1024 * 1024;
57+ + if content_len == u64::MAX
58+ + || content_len > MAX_BUFFERED_OOO_JXLP
59+ + {
60+ return Err(Error::InvalidBox);
61+ }
62+ self.state = ParseState::BufferingOooJxlp {
1663diff --git a/jxl/src/api/inner/codestream_parser/non_section.rs b/jxl/src/api/inner/codestream_parser/non_section.rs
17- index cc35fae..732394a 100644
64+ index cc35fae..3e10767 100644
1865--- a/jxl/src/api/inner/codestream_parser/non_section.rs
1966+++ b/jxl/src/api/inner/codestream_parser/non_section.rs
2067@@ -231,12 +231,25 @@ impl CodestreamParser {
@@ -24,13 +71,13 @@ index cc35fae..732394a 100644
2471+ let (gw, gh) = frame_header.size_groups();
2572+ let ng = gw.checked_mul(gh).ok_or(Error::SizeOverflow)?;
2673+ const MAX_GRID_CELLS: usize = 1 << 22;
27- + if ng > MAX_GRID_CELLS {
74+ + if ng >= MAX_GRID_CELLS {
2875+ return Err(Error::ImageDimensionTooLarge(ng as u64));
2976+ }
3077+ if let Some(limit) = decode_options.sample_limit {
31- + let (pw, ph) = frame_header.size_padded ();
78+ + let (pw, ph) = frame_header.size_padded_upsampled ();
3279+ check_size_limit(Some(limit), (pw, ph), frame_header.num_extra_channels as usize)?;
33- + if ng > limit {
80+ + if ng >= limit {
3481+ return Err(Error::ImageDimensionTooLarge(ng as u64));
3582+ }
3683+ }
@@ -44,7 +91,58 @@ index cc35fae..732394a 100644
4491 .map(|_| (0..frame_header.passes.num_passes).map(|_| None).collect())
4592 .collect();
4693 self.candidate_hf_sections.clear();
47- @@ -325,32 +338,36 @@ impl CodestreamParser {
94+ @@ -251,8 +264,28 @@ impl CodestreamParser {
95+ let mut br = BitReader::new(&self.non_section_buf);
96+ br.skip_bits(self.non_section_bit_offset as usize)?;
97+ if self.toc_parser.is_none() {
98+ - let num_toc_entries = self.frame_header.as_ref().unwrap().num_toc_entries();
99+ - self.toc_parser = Some(IncrementalTocReader::new(num_toc_entries as u32, &mut br)?);
100+ + let fh = self.frame_header.as_ref().unwrap();
101+ + let (gw, gh) = fh.size_groups();
102+ + let ng = gw.checked_mul(gh).ok_or(Error::SizeOverflow)?;
103+ + let (lw, lh) = fh.size_lf_groups();
104+ + let nl = lw.checked_mul(lh).ok_or(Error::SizeOverflow)?;
105+ + let np = fh.passes.num_passes as usize;
106+ + let num_toc_entries = if ng == 1 && np == 1 {
107+ + 1usize
108+ + } else {
109+ + ng.checked_mul(np)
110+ + .and_then(|x| nl.checked_add(x))
111+ + .and_then(|x| 2usize.checked_add(x))
112+ + .ok_or(Error::SizeOverflow)?
113+ + };
114+ + const MAX_TOC_ENTRIES_USIZE: usize = 1 << 22;
115+ + if num_toc_entries > MAX_TOC_ENTRIES_USIZE {
116+ + return Err(Error::ImageDimensionTooLarge(num_toc_entries as u64));
117+ + }
118+ + self.toc_parser = Some(IncrementalTocReader::new(
119+ + num_toc_entries as u32,
120+ + &mut br,
121+ + )?);
122+ }
123+
124+ let toc_parser = self.toc_parser.as_mut().unwrap();
125+ @@ -292,6 +325,19 @@ impl CodestreamParser {
126+ self.toc_parser.take().unwrap().finalize()
127+ };
128+
129+ + const MAX_TOTAL_SECTION_BYTES: usize = 64 * 1024 * 1024;
130+ + let max_section_bytes = decode_options
131+ + .sample_limit
132+ + .map(|limit| limit.saturating_mul(8))
133+ + .unwrap_or(MAX_TOTAL_SECTION_BYTES)
134+ + .min(MAX_TOTAL_SECTION_BYTES);
135+ + let total_section_bytes = toc.entries.iter().try_fold(0usize, |acc, &entry| {
136+ + acc.checked_add(entry as usize).ok_or(Error::SizeOverflow)
137+ + })?;
138+ + if total_section_bytes > max_section_bytes {
139+ + return Err(Error::ImageDimensionTooLarge(total_section_bytes as u64));
140+ + }
141+ +
142+ let mut frame = Frame::from_header_and_toc(
143+ self.frame_header.take().unwrap(),
144+ toc,
145+ @@ -325,32 +358,36 @@ impl CodestreamParser {
48146 self.ready_section_data = 0;
49147
50148 // Move data from the pre-section buffer into the sections.
@@ -118,7 +216,7 @@ index 2870225..cb09e44 100644
118216
119217 pub(super) fn can_read_more(&self) -> bool {
120218diff --git a/jxl/src/frame/decode.rs b/jxl/src/frame/decode.rs
121- index 82d091e..51ea9d2 100644
219+ index 82d091e..93dbe79 100644
122220--- a/jxl/src/frame/decode.rs
123221+++ b/jxl/src/frame/decode.rs
124222@@ -156,6 +156,8 @@ impl Frame {
@@ -130,6 +228,24 @@ index 82d091e..51ea9d2 100644
130228 if frame_header.is_visible() {
131229 decoder_state.visible_frame_index += 1;
132230 decoder_state.nonvisible_frame_index = 0;
231+ @@ -211,7 +213,7 @@ impl Frame {
232+ let image_size = &decoder_state.file_header.size;
233+ let image_size = (image_size.xsize() as usize, image_size.ysize() as usize);
234+ let sz = if frame_header.save_before_ct {
235+ - frame_header.size_upsampled()
236+ + frame_header.size()
237+ } else {
238+ image_size
239+ };
240+ @@ -229,7 +231,7 @@ impl Frame {
241+ let lf_frame_data = if frame_header.lf_level != 0 {
242+ Some(
243+ (0..3)
244+ - .map(|_| Image::new(frame_header.size_upsampled()))
245+ + .map(|_| Image::new(frame_header.size()))
246+ .collect::<Result<Vec<_>, _>>()?
247+ .try_into()
248+ .unwrap(),
133249@@ -243,8 +245,8 @@ impl Frame {
134250 Ok(Self {
135251 #[cfg(test)]
@@ -141,11 +257,21 @@ index 82d091e..51ea9d2 100644
141257 header: frame_header,
142258 color_channels,
143259 toc,
260+ @@ -337,7 +339,8 @@ impl Frame {
261+
262+ if self.header.has_splines() {
263+ info!("decoding splines");
264+ - let s = Splines::read(br, self.header.width * self.header.height)?;
265+ + let spline_area = self.header.size().0.saturating_mul(self.header.size().1);
266+ + let s = Splines::read(br, u32::try_from(spline_area).unwrap_or(u32::MAX))?;
267+ *self.splines.borrow_mut() = s;
268+ }
269+
144270diff --git a/jxl/src/frame/group.rs b/jxl/src/frame/group.rs
145- index 26f0e8f..86eb153 100644
271+ index b7d8021..952eeea 100644
146272--- a/jxl/src/frame/group.rs
147273+++ b/jxl/src/frame/group.rs
148- @@ -223,7 +223,10 @@ fn dequant_and_transform_to_pixels<D: Simd >(
274+ @@ -223,7 +223,10 @@ fn dequant_and_transform_to_pixels<D: SimdDescriptor >(
149275 {
150276 let xs = covered_blocks_x(transform_type) as usize;
151277 let ys = covered_blocks_y(transform_type) as usize;
@@ -157,19 +283,72 @@ index 26f0e8f..86eb153 100644
157283 for (y, lf) in lf.chunks_exact_mut(xs).enumerate().take(ys) {
158284 lf.copy_from_slice(&rect.row(y)[0..xs]);
159285 }
160- diff --git a/jxl/src/api/inner/box_parser.rs b/jxl/src/api/inner/box_parser.rs
161- index fce0f7f..020443f 100644
162- --- a/jxl/src/api/inner/box_parser.rs
163- +++ b/jxl/src/api/inner/box_parser.rs
164- @@ -360,7 +360,10 @@ impl BoxParser {
165- if self.ooo_jxlp.buffered.contains_key(&idx) {
166- return Err(Error::InvalidBox);
167- }
168- - if content_len == u64::MAX {
169- + const MAX_BUFFERED_OOO_JXLP: u64 = 16 * 1024 * 1024;
170- + if content_len == u64::MAX
171- + || content_len > MAX_BUFFERED_OOO_JXLP
172- + {
173- return Err(Error::InvalidBox);
174- }
175- self.state = ParseState::BufferingOooJxlp {
286+ diff --git a/jxl/src/headers/permutation.rs b/jxl/src/headers/permutation.rs
287+ index c189c0b..c14e0cd 100644
288+ --- a/jxl/src/headers/permutation.rs
289+ +++ b/jxl/src/headers/permutation.rs
290+ @@ -39,12 +39,17 @@ impl Permutation {
291+ })
292+ }
293+
294+ + #[inline(never)]
295+ fn decode_inner(
296+ size: u32,
297+ skip: u32,
298+ end: u32,
299+ mut read: impl FnMut(usize) -> Result<u32>,
300+ ) -> Result<Self> {
301+ + const MAX_PERM: u32 = 1 << 22;
302+ + if size > MAX_PERM {
303+ + return Err(Error::ImageDimensionTooLarge(size as u64));
304+ + }
305+ if end > size - skip {
306+ return Err(Error::InvalidPermutationSize { size, skip, end });
307+ }
308+ diff --git a/jxl/src/headers/toc.rs b/jxl/src/headers/toc.rs
309+ index eecbe8e..6d30c5f 100644
310+ --- a/jxl/src/headers/toc.rs
311+ +++ b/jxl/src/headers/toc.rs
312+ @@ -43,6 +43,10 @@ pub struct IncrementalTocReader {
313+
314+ impl IncrementalTocReader {
315+ pub fn new(num_entries: u32, br: &mut BitReader) -> Result<Self> {
316+ + const MAX_TOC_ENTRIES: u32 = 1 << 22;
317+ + if num_entries > MAX_TOC_ENTRIES {
318+ + return Err(Error::ImageDimensionTooLarge(num_entries as u64));
319+ + }
320+ let permuted = bool::read_unconditional(&(), br, &Empty {})?;
321+ let mut entries = Vec::new();
322+ entries.try_reserve(num_entries as usize)?;
323+ diff --git a/jxl/src/frame/render.rs b/jxl/src/frame/render.rs
324+ index d20d693..edd57df 100644
325+ --- a/jxl/src/frame/render.rs
326+ +++ b/jxl/src/frame/render.rs
327+ @@ -23,7 +23,7 @@ use crate::headers::frame_header::FrameType;
328+ use crate::headers::{Orientation, color_encoding::ColorSpace, extra_channels::ExtraChannel};
329+ use crate::image::Image;
330+ use crate::image::Rect;
331+ - use crate::util::AtomicRefCell;
332+ + use crate::util::{AtomicRefCell, ShiftRightCeil};
333+ use std::sync::Arc;
334+
335+ #[cfg(test)]
336+ @@ -360,6 +360,18 @@ impl Frame {
337+ let num_channels = frame_header.num_extra_channels as usize + 3;
338+ let num_temp_channels = if frame_header.has_noise() { 3 } else { 0 };
339+ let metadata = &decoder_state.file_header.image_metadata;
340+ + // Match `RenderPipelineBuilder::new_with_chunk_size`: pipeline allocates
341+ + // O(group_chan_complete.len()) structures; bound before constructing the builder.
342+ + const MAX_RENDER_PIPELINE_GRID_CELLS: usize = 1 << 22;
343+ + let downsampling_shift = frame_header.upsampling.ilog2() as usize;
344+ + let log_group_size = frame_header.log_group_dim() + downsampling_shift;
345+ + let (uw, uh) = frame_header.size_upsampled();
346+ + let gw = uw.shrc(log_group_size);
347+ + let gh = uh.shrc(log_group_size);
348+ + let grid_cells = gw.checked_mul(gh).ok_or(Error::SizeOverflow)?;
349+ + if grid_cells >= MAX_RENDER_PIPELINE_GRID_CELLS {
350+ + return Err(Error::ImageDimensionTooLarge(grid_cells as u64));
351+ + }
352+ let mut pipeline = RenderPipelineBuilder::<T>::new(
353+ num_channels + num_temp_channels,
354+ frame_header.size_upsampled(),
0 commit comments