mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-02 06:46:50 -06:00
M8: a progressive JPEG sampling a component finer than its first is refused, not decoded
zune-jpeg 0.5.15, the decoder behind `image`, asserts in its upsampler on a progressive frame whose later component is sampled more finely than its first (etemesi254/zune-image#236; an 8 × 8 Y 1×1, Cb 2×2 file is enough). The panic reached everywhere such an image is decoded: the CLI died writing a thumbnail, the desktop canvas's guard caught it on every render of the page, and in the browser it aborted the tab. The fix in zune-jpeg (36007420) is in 0.5.16 pre-releases only, which `image`'s requirement doesn't take. Such a JPEG is now refused from its frame header before the decoder sees it, as an image past the byte budget is. The decoder refuses the larger ones of this shape itself, so nothing that decoded is lost; baseline frames of the same shape decode and are left to it. The check can go once zune-jpeg 0.5.16 ships. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
2af5e232d5
commit
9aac391e04
@@ -100,6 +100,9 @@ fn decode_in(bytes: &[u8], target: ColorProfile) -> Option<(Pixmap, bool)> {
|
||||
/// tall.
|
||||
pub(crate) fn decode_within(bytes: &[u8], target: ColorProfile, max_side: u32, max_bytes: u64) -> Option<(Pixmap, bool)> {
|
||||
use image::ImageDecoder;
|
||||
if jpeg_finer_than_first(bytes) {
|
||||
return None;
|
||||
}
|
||||
let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?;
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_alloc = Some(max_bytes);
|
||||
@@ -364,6 +367,51 @@ pub fn apply_filters(p: &mut Pixmap, f: &ImageFilters) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `bytes` are a progressive JPEG whose frame samples some component more finely (across
|
||||
/// or down) than its first. zune-jpeg 0.5.15, the decoder behind `image`, asserts in its upsampler
|
||||
/// on such a frame (etemesi254/zune-image#236; fixed there by 36007420, unreleased as of
|
||||
/// 0.5.16-rc3), and in the browser a panic aborts the page. Until the fix ships, such a JPEG is
|
||||
/// refused from its header: the decoder refuses the larger ones itself, so nothing that decoded is
|
||||
/// lost. Baseline frames of the same shape decode and are left to it.
|
||||
pub(crate) fn jpeg_finer_than_first(bytes: &[u8]) -> bool {
|
||||
let Some((marker, frame)) = jpeg_frame(bytes) else { return false };
|
||||
if !matches!(marker, 0xC2 | 0xC6 | 0xCA | 0xCE) {
|
||||
return false;
|
||||
}
|
||||
// Length (2), precision (1), height (2), width (2), components (1), then id, sampling and
|
||||
// table for each component.
|
||||
let n = usize::from(frame.get(7).copied().unwrap_or(0));
|
||||
let sampling = |k: usize| frame.get(9 + 3 * k).map(|b| (b >> 4, b & 0x0F));
|
||||
let Some((h0, v0)) = sampling(0) else { return false };
|
||||
(1..n).any(|k| sampling(k).is_some_and(|(h, v)| h > h0 || v > v0))
|
||||
}
|
||||
|
||||
/// A JPEG's start-of-frame marker and the segment after it (from its length bytes), found as the
|
||||
/// decoders find it: fill bytes are skipped and markers without a length stepped over. `None` for
|
||||
/// anything but a JPEG, or one whose scan, end or bytes come before a frame.
|
||||
fn jpeg_frame(bytes: &[u8]) -> Option<(u8, &[u8])> {
|
||||
if !bytes.starts_with(&[0xFF, 0xD8]) {
|
||||
return None;
|
||||
}
|
||||
let mut at = 2;
|
||||
loop {
|
||||
if *bytes.get(at)? != 0xFF {
|
||||
return None;
|
||||
}
|
||||
let marker = *bytes.get(at + 1)?;
|
||||
match marker {
|
||||
0xFF => at += 1,
|
||||
0x01 | 0xD0..=0xD8 => at += 2,
|
||||
0xD9 | 0xDA => return None,
|
||||
0xC0..=0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => return Some((marker, bytes.get(at + 2..)?)),
|
||||
_ => {
|
||||
let len = usize::from(u16::from_be_bytes([*bytes.get(at + 2)?, *bytes.get(at + 3)?]));
|
||||
at = at.checked_add(2)?.checked_add(len)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read an image's pixel size without keeping the decode.
|
||||
pub fn dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
|
||||
let reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//! A progressive JPEG whose frame samples a component more finely than its first is refused from
|
||||
//! its header, not handed to the decoder: zune-jpeg 0.5.15, the decoder behind `image`, asserts
|
||||
//! in its upsampler on such a frame (etemesi254/zune-image#236), and in the browser a panic aborts
|
||||
//! the page. Baseline frames of the same shape are decoded (or refused) by the decoder itself.
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// An 8 × 8 progressive RGB JPEG with every component sampled 1 × 1, as jpeg-encoder 0.6 writes it
|
||||
/// at quality 50 (JFIF header, the standard tables, three scans). Its frame header (SOF2) starts at
|
||||
/// byte 20; the second component's sampling byte is [`SECOND_SAMPLING`], the third's follows 3 on.
|
||||
const TINY_PROGRESSIVE: &[u8] = &[
|
||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xC2, 0x00, 0x11,
|
||||
0x08, 0x00, 0x08, 0x00, 0x08, 0x03, 0x00, 0x11, 0x00, 0x01, 0x11, 0x01, 0x02, 0x11, 0x01, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E,
|
||||
0x0C, 0x0A, 0x10, 0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10, 0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, 0x25, 0x1D, 0x28, 0x3A, 0x33,
|
||||
0x3D, 0x3C, 0x39, 0x33, 0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40, 0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67, 0x68,
|
||||
0x67, 0x3E, 0x4D, 0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x11, 0x12, 0x12, 0x18, 0x15, 0x18, 0x2F,
|
||||
0x1A, 0x1A, 0x2F, 0x63, 0x42, 0x38, 0x42, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
|
||||
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
|
||||
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00,
|
||||
0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,
|
||||
0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24,
|
||||
0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A,
|
||||
0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A,
|
||||
0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,
|
||||
0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6,
|
||||
0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1,
|
||||
0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00,
|
||||
0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,
|
||||
0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0, 0x15,
|
||||
0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39,
|
||||
0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
|
||||
0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
|
||||
0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4,
|
||||
0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA,
|
||||
0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6F, 0xFF, 0xDA, 0x00, 0x08,
|
||||
0x01, 0x01, 0x11, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x11, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x00,
|
||||
0x00, 0x01, 0x14, 0x00, 0x4D, 0xB0, 0x97, 0x8E, 0x45, 0x01, 0x08, 0x03, 0x70, 0xAF, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x11, 0x01, 0x14, 0x00,
|
||||
0xAD, 0x17, 0xA5, 0x4A, 0x6C, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x11, 0x01, 0x14, 0x00, 0x8A, 0x75, 0xA1, 0xCB, 0x6D, 0x62, 0x7F, 0xFF, 0xDA,
|
||||
0x00, 0x08, 0x01, 0x00, 0x00, 0x15, 0x29, 0x00, 0xE0, 0x75, 0x1C, 0x8C, 0x74, 0xAF, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x11, 0x15, 0x29, 0x00,
|
||||
0x3F, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x11, 0x15, 0x29, 0x00, 0x3F, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x00, 0x00, 0x2A, 0x3F, 0x00, 0xC8, 0x3C,
|
||||
0x7E, 0x1F, 0x79, 0x07, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x11, 0x2A, 0x3F, 0x00, 0xFE, 0x96, 0x8E, 0x3F, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02,
|
||||
0x11, 0x2A, 0x3F, 0x00, 0xF9, 0x36, 0xFF, 0x00, 0x99, 0xFB, 0xDF, 0xFF, 0xD9,
|
||||
];
|
||||
const SECOND_SAMPLING: usize = 34;
|
||||
|
||||
fn with_second_sampled(hv: u8) -> Vec<u8> {
|
||||
let mut b = TINY_PROGRESSIVE.to_vec();
|
||||
b[SECOND_SAMPLING] = hv;
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_jpeg_as_written_is_not_finer_than_its_first_and_decodes() {
|
||||
assert_eq!(TINY_PROGRESSIVE[SECOND_SAMPLING], 0x11);
|
||||
assert!(!images::jpeg_finer_than_first(TINY_PROGRESSIVE));
|
||||
let p = images::decode(TINY_PROGRESSIVE).expect("decoded");
|
||||
assert_eq!((p.width(), p.height()), (8, 8));
|
||||
// Chroma sampled coarser than luma (the usual way round) isn't finer either.
|
||||
assert!(!images::jpeg_finer_than_first(&{
|
||||
let mut b = TINY_PROGRESSIVE.to_vec();
|
||||
b[SECOND_SAMPLING - 3] = 0x22;
|
||||
b
|
||||
}));
|
||||
}
|
||||
|
||||
/// Without the check this test panics in the decoder (an assertion in zune-jpeg's upsampler).
|
||||
#[test]
|
||||
fn a_progressive_jpeg_sampling_a_component_finer_than_its_first_is_refused_from_its_header() {
|
||||
for hv in [0x22, 0x21, 0x12] {
|
||||
let b = with_second_sampled(hv);
|
||||
assert!(images::jpeg_finer_than_first(&b), "second component {hv:#x}");
|
||||
assert!(images::decode(&b).is_none(), "second component {hv:#x}");
|
||||
}
|
||||
let mut third = TINY_PROGRESSIVE.to_vec();
|
||||
third[SECOND_SAMPLING + 3] = 0x22;
|
||||
assert!(images::jpeg_finer_than_first(&third));
|
||||
assert!(images::decode(&third).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn baseline_frames_and_other_files_are_left_to_the_decoder() {
|
||||
// A baseline JPEG (as `image` writes them) with its second component made finer: the decoder
|
||||
// takes baseline frames of this shape, so it isn't refused from its header.
|
||||
let mut out = Vec::new();
|
||||
image::RgbImage::from_fn(8, 8, |x, y| image::Rgb([x as u8 * 30, y as u8 * 30, 100]))
|
||||
.write_to(&mut Cursor::new(&mut out), image::ImageFormat::Jpeg)
|
||||
.unwrap();
|
||||
let sof = out.windows(2).position(|m| m == [0xFF, 0xC0]).expect("a baseline frame");
|
||||
out[sof + 14] = 0x22;
|
||||
assert!(!images::jpeg_finer_than_first(&out));
|
||||
let _ = images::decode(&out);
|
||||
// Not JPEGs, or JPEGs that end before their frame.
|
||||
let mut png = Vec::new();
|
||||
image::RgbaImage::from_pixel(4, 4, image::Rgba([1, 2, 3, 255])).write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
assert!(!images::jpeg_finer_than_first(&png));
|
||||
assert!(!images::jpeg_finer_than_first(&TINY_PROGRESSIVE[..19]));
|
||||
assert!(!images::jpeg_finer_than_first(&[0xFF, 0xD8]));
|
||||
assert!(!images::jpeg_finer_than_first(&[]));
|
||||
assert!(images::decode(&TINY_PROGRESSIVE[..19]).is_none());
|
||||
}
|
||||
@@ -492,6 +492,8 @@ mod image_budget_tests;
|
||||
#[cfg(test)]
|
||||
mod image_profile_tests;
|
||||
#[cfg(test)]
|
||||
mod jpeg_sampling_tests;
|
||||
#[cfg(test)]
|
||||
mod lod_tests;
|
||||
#[cfg(test)]
|
||||
mod magnified_image_tests;
|
||||
|
||||
Reference in New Issue
Block a user