Merge PR #1894: HEIF: max_pixels on uncropped SPS sizes and grid totals (#1518)

This commit is contained in:
Brandon Thomas committed 2026-10-18 09:20:39 -04:00
commit 8d41bf69f7
3 files changed
+69 -13

No files matched your search

+22
View File
@@ -277,6 +277,10 @@ fn decode_item(ctx: &Context<'_>, id: u32, options: &Options, with_alpha: bool)
})
}
/// How many times `max_pixels` the coded pictures of one item may hold together: a grid's tiles
/// cover its canvas rounded up to whole tiles, so legitimate grids sit near 1.
const GRID_BUDGET: u64 = 2;
/// The decoded pictures of an item: one, or a grid's tiles in `dimg` order.
struct Coded<'a> {
frames: Vec<Frame>,
@@ -297,6 +301,24 @@ impl<'a> Coded<'a> {
let first = *ids.first().ok_or_else(|| Error::Malformed("grid lists no tiles".into()))?;
let first_props = if first == id { p.clone() } else { ctx.props(first).map_err(err)? };
let chroma_format = first_props.hvcc.as_ref().map_or(1, |c| c.chroma_format);
// #1518: the decoder allocates each picture at its SPS's uncropped size, which a
// conformance window (or a slice picking another SPS) can make far larger than the
// declared one, and a grid holds all its tiles at once. Both are refused before decoding.
let mut total = 0u64;
for t in &ids {
let tp = ctx.props(*t).map_err(err)?;
let hvcc = tp.hvcc.as_ref().ok_or(heic_rs::Error::MissingBox("hvcC")).map_err(err)?;
let sizes = vui::coded_sizes(&hvcc.parameter_sets()).ok_or_else(|| Error::Malformed("unreadable sequence parameter set".into()))?;
let mut largest = 0u64;
for (w, h) in sizes {
heic_rs::image::check_pixels(w, h, limit).map_err(err)?;
largest = largest.max(u64::from(w) * u64::from(h));
}
total = total.saturating_add(largest);
}
if total > limit.saturating_mul(GRID_BUDGET) {
return Err(Error::Limit(format!("the coded pictures hold {total} pixels, over the {limit}-pixel limit")));
}
let decode_one = |t: &u32| -> Result<Frame, Error> {
let tp = ctx.props(*t).map_err(err)?;
let hvcc = tp.hvcc.as_ref().ok_or(heic_rs::Error::MissingBox("hvcC")).map_err(err)?;
+11
View File
@@ -146,6 +146,17 @@ fn limits_are_checked_before_decoding() {
assert!(matches!(r, Err(Error::Limit(_))), "{r:?}");
}
/// #1518: a grid's tiles together are held to the pixel budget before any is decoded. A
/// 33 × 33 canvas fits 1089 pixels, but its four 32 × 32 tiles hold 4096: over twice the limit.
#[test]
fn grid_tiles_are_held_to_the_pixel_budget_together() {
let bytes = build(&Spec::new(33, 33, &grey));
let r = decode(&bytes, &Options { max_pixels: 33 * 33, ..Default::default() });
assert!(matches!(r, Err(Error::Limit(_))), "{r:?}");
let d = decode(&bytes, &Options { max_pixels: 2 * 32 * 32, ..Default::default() }).unwrap();
assert_eq!((d.width, d.height), (33, 33));
}
#[test]
fn truncated_and_corrupted_files_are_errors_not_panics() {
let bytes = build(&Spec { props: vec![FULL_601, Prop::Irot(1)], thumbnail: Some(&grey), ..Spec::new(64, 64, &grey) });
+36 -13
View File
@@ -33,6 +33,41 @@ pub fn signal(parameter_sets: &[&[u8]]) -> Option<Signal> {
parse_sps(&unescape(sps.get(2..)?))
}
/// The uncropped picture size (`pic_width/height_in_luma_samples`) of every SPS among
/// `parameter_sets`: what the decoder allocates before it applies the conformance window, so the
/// pixel budget is checked against it (heic-rs reports only the first SPS, cropped). `None`: an
/// SPS this reader can't follow.
pub fn coded_sizes(parameter_sets: &[&[u8]]) -> Option<Vec<(u32, u32)>> {
parameter_sets
.iter()
.filter(|n| n.first().is_some_and(|b| (b >> 1) & 0x3f == SPS_NUT))
.map(|sps| {
let rbsp = unescape(sps.get(2..)?);
let r = &mut Bits { data: &rbsp, pos: 0 };
sps_size(r).map(|(w, h, _)| (w, h))
})
.collect()
}
/// Reads an SPS up to and including the picture size, leaving `r` after it: width, height and
/// `sps_max_sub_layers_minus1`.
fn sps_size(r: &mut Bits) -> Option<(u32, u32, u32)> {
r.skip(4)?; // sps_video_parameter_set_id
let max_sub_layers_minus1 = r.u(3)?;
if max_sub_layers_minus1 > 6 {
return None;
}
r.skip(1)?; // sps_temporal_id_nesting_flag
profile_tier_level(r, max_sub_layers_minus1)?;
r.ue()?; // sps_seq_parameter_set_id
if r.ue()? == 3 {
r.skip(1)?; // separate_colour_plane_flag
}
let width = r.ue()?; // pic_width_in_luma_samples
let height = r.ue()?; // pic_height_in_luma_samples
Some((width, height, max_sub_layers_minus1))
}
/// Removes emulation prevention bytes (`00 00 03` → `00 00`).
fn unescape(nal: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(nal.len());
@@ -165,19 +200,7 @@ fn st_ref_pic_set(r: &mut Bits, idx: usize, num_delta_pocs: &[u32]) -> Option<u3
fn parse_sps(rbsp: &[u8]) -> Option<Signal> {
let r = &mut Bits { data: rbsp, pos: 0 };
r.skip(4)?; // sps_video_parameter_set_id
let max_sub_layers_minus1 = r.u(3)?;
if max_sub_layers_minus1 > 6 {
return None;
}
r.skip(1)?; // sps_temporal_id_nesting_flag
profile_tier_level(r, max_sub_layers_minus1)?;
r.ue()?; // sps_seq_parameter_set_id
if r.ue()? == 3 {
r.skip(1)?; // separate_colour_plane_flag
}
r.ue()?; // pic_width_in_luma_samples
r.ue()?; // pic_height_in_luma_samples
let (_, _, max_sub_layers_minus1) = sps_size(r)?;
if r.flag()? {
for _ in 0..4 {
r.ue()?; // conf_win_*_offset