From e96bf9552b75d28c1b397c83e301e8cc182a3cf1 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 22:02:49 +0000 Subject: [PATCH] jpeg: arithmetic scans take zeros at a marker without limit libjpeg's encoder drops trailing zero bytes, so a flat tail of a few thousand blocks decodes from zeros supplied at EOI. Damage is data that runs to the end of the file or a lost restart marker. whitetail-ari.jpg (cjpeg -arithmetic) and its Huffman transcode cover it. --- crates/jpeg/README.md | 7 +++- .../jpeg/fuzz/corpus/decode/whitetail-ari.jpg | Bin 0 -> 3492 bytes crates/jpeg/src/lib.rs | 35 +++++++++--------- crates/jpeg/src/tests.rs | 30 +++++++++++---- crates/jpeg/tests/conformance.rs | 19 +++++++++- crates/jpeg/tests/data/SHA256SUMS | 2 + crates/jpeg/tests/data/SOURCE.md | 22 ++++++++++- crates/jpeg/tests/data/whitetail-ari.jpg | Bin 0 -> 3492 bytes crates/jpeg/tests/data/whitetail-huffman.jpg | Bin 0 -> 7011 bytes 9 files changed, 86 insertions(+), 29 deletions(-) create mode 100644 crates/jpeg/fuzz/corpus/decode/whitetail-ari.jpg create mode 100644 crates/jpeg/tests/data/whitetail-ari.jpg create mode 100644 crates/jpeg/tests/data/whitetail-huffman.jpg diff --git a/crates/jpeg/README.md b/crates/jpeg/README.md index c45f290e..606ea080 100644 --- a/crates/jpeg/README.md +++ b/crates/jpeg/README.md @@ -57,7 +57,10 @@ if img.damaged { /* warn: part of the image is missing */ } - `damaged` is set when entropy-coded data was cut short or corrupt after at least one MCU had decoded, or its scans would decode more than 64 passes over the frame's blocks. The rest of that scan, and every scan past the budget, is left out; those blocks keep what earlier scans - decoded. A stream with no decodable image data is an error. + decoded. A stream with no decodable image data is an error. Arithmetic-coded data that stops + early at a marker reads as the zero bytes encoders drop at the end of a scan (libjpeg-turbo + treats it the same way), so for arithmetic scans only data that runs to the end of the file + and a missing restart marker count as damage. ## Limits @@ -105,6 +108,8 @@ SOF11 (lossless) falls in that range and returns `Unsupported`. - `tests/conformance.rs`, on libjpeg-turbo's test images in `tests/data` (provenance and how the derived files were made: `tests/data/SOURCE.md`): - `testimgari.jpg` (SOF9) decodes to exactly the samples of its jpegtran Huffman transcode. + - `whitetail-ari.jpg` (SOF9 from cjpeg, a white lower part that decodes from the zeros + supplied at EOI) decodes undamaged and equal to its Huffman transcode. - `testorig.jpg` matches `zune-jpeg`, and `testorig12.jpg` matches libjpeg-turbo's 12-bit decode, within the float-against-integer IDCT difference (mean under 0.75, max 4). - `tests/mutation.rs`: a seeded mutation run (truncations, bit flips, byte and frame-header diff --git a/crates/jpeg/fuzz/corpus/decode/whitetail-ari.jpg b/crates/jpeg/fuzz/corpus/decode/whitetail-ari.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e7011fb6e1cea60f901bf5362a46fbff6ba1c37b GIT binary patch literal 3492 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 S{{a60{{XV^@U+Bo>i^l{%!YIT literal 0 HcmV?d00001 diff --git a/crates/jpeg/src/lib.rs b/crates/jpeg/src/lib.rs index 262e03b3..a89614b4 100644 --- a/crates/jpeg/src/lib.rs +++ b/crates/jpeg/src/lib.rs @@ -595,45 +595,45 @@ const QE: [(u32, u8, u8, bool); 113] = [ /// Qe index in bits 0–6 and the MPS in bit 7. struct Arith<'a> { d: &'a [u8], + /// `d` ends at a marker rather than at the end of the file. + at_marker: bool, pos: usize, c: u32, a: u32, ct: u32, - /// Zero bytes fed in since the start or the last restart because the data had ended. - pad: u32, + /// The decoder needed bytes past the end of the file. + eof: bool, /// A restart was due but its marker was missing. lost: bool, } impl<'a> Arith<'a> { - /// The most zero bytes a scan or restart interval may consume past the end of its data. - /// The decoder reads two bytes ahead (INITDEC), and encoders drop trailing zero bytes - /// (libjpeg's `finish_pass` does, as D.1.8 allows), so a valid stream consumes a few. - /// Past that the data was cut short, and the decoder would only decode the zeros. - const MAX_PAD: u32 = 8; - - fn new(d: &'a [u8]) -> Self { - let mut ar = Arith { d, pos: 0, c: 0, a: 0, ct: 0, pad: 0, lost: false }; + fn new(d: &'a [u8], at_marker: bool) -> Self { + let mut ar = Arith { d, at_marker, pos: 0, c: 0, a: 0, ct: 0, eof: false, lost: false }; ar.init(); ar } + /// At a marker the decoder is fed zeros for as long as it asks (D.2.6): encoders drop + /// trailing zero bytes (libjpeg's `finish_pass` does, as D.1.8 allows), and a flat tail of + /// a few thousand blocks can decode entirely from those zeros. Running out at the end of + /// the file is damage. fn byte_in(&mut self) { let v = next_byte(self.d, &mut self.pos).unwrap_or_else(|| { - self.pad = self.pad.saturating_add(1); + let at_restart = matches!(self.d.get(self.pos + 1), Some(0xD0..=0xD7)); + self.eof |= !at_restart && !self.at_marker; 0 }); self.c = self.c.wrapping_add(u32::from(v) << 8); } - /// The scan's data ran out, or a restart marker is missing. + /// The data ended without a marker, or a restart marker is missing. fn exhausted(&self) -> bool { - self.lost || self.pad > Self::MAX_PAD + self.lost || self.eof } /// INITDEC (D.2.7). fn init(&mut self) { - self.pad = 0; self.a = 0x10000; self.c = 0; self.byte_in(); @@ -1037,7 +1037,8 @@ fn parse_scan(seg: &[u8], frame: &Frame) -> Result { } /// Decode one scan's entropy-coded data `d`, counting the decoded MCUs in `done`. -fn decode_scan(frame: &mut Frame, tables: &Tables, scan: &Scan, d: &[u8], done: &mut usize) -> Result<(), Error> { +/// `at_marker` says whether a marker follows `d` (otherwise it runs to the end of the file). +fn decode_scan(frame: &mut Frame, tables: &Tables, scan: &Scan, d: &[u8], at_marker: bool, done: &mut usize) -> Result<(), Error> { for &(ci, _, _) in &scan.comps { let comp = frame.comps.get_mut(ci).ok_or_else(|| err("bad scan component"))?; if comp.quant.is_none() { @@ -1054,7 +1055,7 @@ fn decode_scan(frame: &mut Frame, tables: &Tables, scan: &Scan, d: &[u8], done: (true, _, _) => ArithMode::AcRefine, }; let mut dec = ArithScan { - ar: Arith::new(d), + ar: Arith::new(d, at_marker), mode, ss: scan.ss, se: scan.se, @@ -1154,7 +1155,7 @@ fn decode_coefficients(b: &[u8], limits: &Limits) -> Result<(Frame, Tables, bool let end = scan_end(b, i); let data = b.get(i..end).unwrap_or_default(); scans += 1; - if let Err(e) = decode_scan(f, &tables, &scan, data, &mut mcus) { + if let Err(e) = decode_scan(f, &tables, &scan, data, end < b.len(), &mut mcus) { if mcus == 0 { return Err(e); } diff --git a/crates/jpeg/src/tests.rs b/crates/jpeg/src/tests.rs index 20cbbba6..3398b0e7 100644 --- a/crates/jpeg/src/tests.rs +++ b/crates/jpeg/src/tests.rs @@ -688,8 +688,9 @@ fn huffman_scan_out_of_data_is_not_clean() { assert!(decode(&cut, &Limits::default(), None).unwrap().damaged); } -/// Arithmetic data cut short before a valid EOI is reported damaged, and the blocks before the -/// cut are kept: past the end the decoder is fed zeros, and a restart marker goes missing. +/// Arithmetic data that runs to the end of the file, or loses a restart marker, is reported +/// damaged, and the blocks before the cut are kept. Data cut short before EOI with no restart +/// interval reads as dropped trailing zeros and can't be detected. #[test] fn arithmetic_scan_out_of_data_is_damaged() { let blocks = coefficients(64, 11, 500); @@ -698,16 +699,29 @@ fn arithmetic_scan_out_of_data_is_damaged() { assert_eq!(decoded_blocks(&full), blocks, "ri={ri}"); let data = last_scan_data(&full); let end = full.len() - 2; - for cut in [data + (end - data) / 2, data] { + for cut in [data + (end - data) / 2, data + 1] { let mut s = full[..cut].to_vec(); if s.last() == Some(&0xFF) { s.pop(); } - s.extend([0xFF, 0xD9]); - let (frame, _, damaged) = decode_coefficients(&s, &Limits::default()).unwrap(); - assert!(damaged, "ri={ri} cut at {}", cut - data); - if cut > data { - assert_eq!(frame.comps[0].coefs[0], blocks[0], "ri={ri}"); + for eoi in [false, true] { + if eoi && ri == 0 { + continue; + } + let mut s = s.clone(); + if eoi { + s.extend([0xFF, 0xD9]); + } + // With one byte of data the first MCU fails, which is an error, not damage. + match decode_coefficients(&s, &Limits::default()) { + Ok((frame, _, damaged)) => { + assert!(damaged, "ri={ri} eoi={eoi} cut at {}", cut - data); + if cut > data + 1 { + assert_eq!(frame.comps[0].coefs[0], blocks[0], "ri={ri} eoi={eoi}"); + } + } + Err(_) => assert_eq!(cut, data + 1, "ri={ri} eoi={eoi}"), + } } } } diff --git a/crates/jpeg/tests/conformance.rs b/crates/jpeg/tests/conformance.rs index 3e449263..e9fab449 100644 --- a/crates/jpeg/tests/conformance.rs +++ b/crates/jpeg/tests/conformance.rs @@ -7,13 +7,18 @@ fn fixture(name: &str) -> Vec { std::fs::read(&path).unwrap_or_else(|e| panic!("{path}: {e}")) } -fn decode_fixture(name: &str) -> Decoded { +fn decode_clean(name: &str) -> Decoded { let img = decode(&fixture(name), &Limits::default(), None).unwrap_or_else(|e| panic!("{name}: {e}")); - assert_eq!((img.width, img.height, img.components), (227, 149, 3), "{name}"); assert!(!img.damaged, "{name}"); img } +fn decode_fixture(name: &str) -> Decoded { + let img = decode_clean(name); + assert_eq!((img.width, img.height, img.components), (227, 149, 3), "{name}"); + img +} + /// Mean and maximum absolute difference. fn diff(a: impl Iterator, b: impl Iterator) -> (f64, f64) { let (mut sum, mut max, mut n) = (0.0, 0.0f64, 0usize); @@ -36,6 +41,16 @@ fn arithmetic_coded_image_matches_its_huffman_transcode() { assert!(huffman.samples == arithmetic.samples, "testimgari.jpg decodes differently from testimgari-huffman.jpg"); } +/// libjpeg's arithmetic encoder drops the trailing zero bytes of a scan, so the white lower +/// part of whitetail-ari.jpg decodes from zeros supplied at the EOI marker. It must decode +/// whole and match its Huffman transcode. +#[test] +fn arithmetic_white_tail_decodes_from_implied_zeros() { + let arithmetic = decode_clean("whitetail-ari.jpg"); + assert_eq!((arithmetic.width, arithmetic.height, arithmetic.components), (512, 512, 1)); + assert!(decode_clean("whitetail-huffman.jpg").samples == arithmetic.samples, "whitetail-ari.jpg decodes differently from whitetail-huffman.jpg"); +} + /// The 8-bit path against zune-jpeg. Both use centered linear chroma upsampling; the residual is /// float against integer IDCT and color conversion. #[test] diff --git a/crates/jpeg/tests/data/SHA256SUMS b/crates/jpeg/tests/data/SHA256SUMS index 551a5dba..2309d1b5 100644 --- a/crates/jpeg/tests/data/SHA256SUMS +++ b/crates/jpeg/tests/data/SHA256SUMS @@ -3,3 +3,5 @@ acc6ec555d41d15b368320edaa3b20958ee6fa97cb6e4a18d1213d5ae8bec73b testorig.jpg 34790770f76db8e60a7765b52ca4edf5f16bc21bcb8c6045ca2efef39a8a013e testorig12.jpg 62d182ed042e243544d3d753c440ada1a6992464717311fe6efd546632f44dc4 testorig12.ppm +0031b82a06300b5b5d19ef5553cc39d3fafafa4fbc474199e6cfbc7818312a87 whitetail-ari.jpg +0f6e17a44be7a005d6610e66f91fa119388dc6b8438e95cb8816b6db58f8c365 whitetail-huffman.jpg diff --git a/crates/jpeg/tests/data/SOURCE.md b/crates/jpeg/tests/data/SOURCE.md index 3876fe87..8e916998 100644 --- a/crates/jpeg/tests/data/SOURCE.md +++ b/crates/jpeg/tests/data/SOURCE.md @@ -2,7 +2,8 @@ The JPEG files come from libjpeg-turbo 3.0.0, `testimages/` (https://github.com/libjpeg-turbo/libjpeg-turbo/tree/3.0.0/testimages), under the IJG License -(README.ijg in this directory). The other two files are derived from them as described below. +(README.ijg in this directory). The other files are derived from them, or made with +libjpeg-turbo's tools, as described below. `SHA256SUMS` lists every file. | File | What it is | @@ -12,6 +13,8 @@ The JPEG files come from libjpeg-turbo 3.0.0, `testimages/` | `testimgari-huffman.jpg` | `testimgari.jpg` losslessly transcoded to baseline Huffman (SOF0) | | `testorig12.jpg` | 227×149 YCbCr, 12-bit extended sequential Huffman (SOF1) | | `testorig12.ppm` | libjpeg-turbo's decode of `testorig12.jpg`: 12-bit RGB, binary PPM, maxval 4095 | +| `whitetail-ari.jpg` | 512×512 gray, arithmetic coded (SOF9), noise in the top 16 rows, white below | +| `whitetail-huffman.jpg` | `whitetail-ari.jpg` losslessly transcoded to baseline Huffman (SOF0) | `testimgari.jpg` and `testorig.jpg` share their quantization tables but not their coefficients: libjpeg-turbo decodes them to different pixels. `testimgari-huffman.jpg` holds exactly the @@ -43,4 +46,21 @@ with open(dst, "wb") as f: `djpeg -dct int testorig12.jpg` from libjpeg-turbo 3.1.0 writes a byte-identical file. +`whitetail-ari.jpg` and `whitetail-huffman.jpg`, with cjpeg and jpegtran from libjpeg-turbo 3.1.3 +(Alpine 3.24, `libjpeg-turbo-utils`). libjpeg's arithmetic encoder drops the trailing zero bytes +of a scan, so the white rows decode almost entirely from zeros supplied at the EOI marker. +`python3 whitetail.py whitetail.pgm` writes the source image: + +```python +import sys +w, h, top = 512, 512, 16 +px = bytes(((x * 3 + y * 5) ^ (x * y)) & 0xFF if y < top else 255 for y in range(h) for x in range(w)) +open(sys.argv[1], "wb").write(b"P5\n%d %d\n255\n" % (w, h) + px) +``` + +```sh +cjpeg -arithmetic -quality 75 whitetail.pgm > whitetail-ari.jpg +jpegtran -copy none whitetail-ari.jpg > whitetail-huffman.jpg +``` + "This software is based in part on the work of the Independent JPEG Group." diff --git a/crates/jpeg/tests/data/whitetail-ari.jpg b/crates/jpeg/tests/data/whitetail-ari.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e7011fb6e1cea60f901bf5362a46fbff6ba1c37b GIT binary patch literal 3492 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 S{{a60{{XV^@U+Bo>i^l{%!YIT literal 0 HcmV?d00001 diff --git a/crates/jpeg/tests/data/whitetail-huffman.jpg b/crates/jpeg/tests/data/whitetail-huffman.jpg new file mode 100644 index 0000000000000000000000000000000000000000..888cc1b67ca90813179cd70727a797652eb2648d GIT binary patch literal 7011 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 literal 0 HcmV?d00001