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:
Higgs32584andClaude Opus 5.5 authored and aiongg committed 2026-10-24 20:37:15 +00:00
1 parent 425bd319c0
commit 537ac17c7c
2 files changed
+25 -1

No files matched your search

+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];