From 537ac17c7c67e9729f9f331a76a4e20408e188e5 Mon Sep 17 00:00:00 2001 From: Higgs32584 Date: Tue, 20 Oct 2026 20:52:26 -0400 Subject: [PATCH] JPEG: cap the scans decoded from one stream at 500 Every scan walks its whole MCU grid, and an arithmetic-coded scan does so even with no entropy-coded data (the decoder is fed zeros), so a small file repeating empty scan headers multiplied the work by the frame size. Scans past the cap are dropped and the image is reported damaged, keeping what the earlier scans decoded. Co-Authored-By: Claude Opus 5.5 --- crates/jpeg/src/lib.rs | 13 ++++++++++++- crates/jpeg/src/tests.rs | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/jpeg/src/lib.rs b/crates/jpeg/src/lib.rs index 3885724c..7405347a 100644 --- a/crates/jpeg/src/lib.rs +++ b/crates/jpeg/src/lib.rs @@ -13,7 +13,8 @@ //! truncated). Restart intervals, 1, 3 and 4 components and any sampling factors are supported. //! //! The decoder never panics on input: every read from the stream is bounds-checked, the work -//! is bounded by the frame size, and allocations are checked against the [`Limits`] first. +//! is bounded by the frame size times the scan count (capped at `MAX_SCANS`), and +//! allocations are checked against the [`Limits`] first. //! Damaged entropy-coded data stops that scan; what was decoded is kept and the caller is told. use crate::Format; @@ -928,6 +929,12 @@ fn decode_scan(frame: &mut Frame, tables: &Tables, scan: &Scan, d: &[u8], done: } } +/// The most scans decoded from one stream; later ones are dropped and the image is reported +/// damaged. Every scan walks its whole MCU grid, and an arithmetic-coded scan does so even with +/// no data (the decoder is fed zeros, D.2.6), so without a cap a small file repeating empty +/// scan headers multiplies the work by the frame size. Real files have a few dozen at most. +const MAX_SCANS: usize = 500; + /// Parse the stream and decode every scan into coefficients. fn decode_coefficients(b: &[u8], limits: &Limits) -> Result<(Frame, Tables, bool), CodecError> { if b.get(..2) != Some(&[0xFF, 0xD8]) { @@ -983,6 +990,10 @@ fn decode_coefficients(b: &[u8], limits: &Limits) -> Result<(Frame, Tables, bool } 0xDA => { let f = frame.as_mut().ok_or_else(|| err("scan before the frame header"))?; + if scans >= MAX_SCANS { + damaged = true; + break; + } let scan = parse_scan(seg, f)?; let end = scan_end(b, i); let data = b.get(i..end).unwrap_or_default(); diff --git a/crates/jpeg/src/tests.rs b/crates/jpeg/src/tests.rs index 60b005c6..7cf1b4fd 100644 --- a/crates/jpeg/src/tests.rs +++ b/crates/jpeg/src/tests.rs @@ -537,6 +537,19 @@ fn malformed_streams_return_errors_not_panics() { assert!(crate::decode(&streams[0][..40]).is_err()); } +/// Scans past `MAX_SCANS` aren't decoded (each one walks the whole MCU grid, with or without +/// data): the image keeps what the earlier scans decoded and is reported damaged. +#[test] +fn scans_past_the_limit_are_not_decoded() { + let blocks = coefficients(1, 3, 500); + for (count, damaged) in [(MAX_SCANS, false), (MAX_SCANS + 1, true)] { + let stream = arith_stream(8, 1, 1, &blocks, &vec![(0, 0, 0, 0); count], 0); + let (frame, _, d) = decode_coefficients(&stream, &Limits::default()).unwrap(); + assert_eq!(d, damaged, "{count} scans"); + assert_eq!(frame.comps[0].coefs[0][0], blocks[0][0], "{count} scans"); + } +} + #[test] fn limits_are_checked_before_allocating() { let mut s = vec![0xFF, 0xD8];