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 <noreply@anthropic.com>
This commit is contained in:
Higgs32584
2026-10-24 20:37:15 +00:00
committed by aiongg
co-authored by Claude Opus 5.5
parent 425bd319c0
commit 537ac17c7c
2 changed files with 25 additions and 1 deletions
+12 -1
View File
@@ -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();
+13
View File
@@ -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];