mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-24 10:10:05 -06:00
security(export): bound image overlay decoding (#2944)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
//! apply the Export effects (image, name and timecode overlays, video limiter) and convert to the
|
||||
//! encoder's input (straight sRGB RGBA8, or encoded R'G'B' floats for HDR).
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
|
||||
use filmcraft_geom::Vec2;
|
||||
@@ -14,6 +15,29 @@ use rayon::prelude::*;
|
||||
use crate::settings::{ExportEffects, Placement, Scaling, TextOverlay};
|
||||
use crate::{ExportError, ExportSettings, FrameRenderer, GpuRendering, Result, build_frame_renderer, note_gpu_fallback, note_gpu_frame};
|
||||
|
||||
/// Image overlays originate outside the project and are expanded to f32 working images. This
|
||||
/// admits ordinary 4K artwork while keeping both source decode and resampling bounded.
|
||||
const MAX_OVERLAY_PIXELS: usize = 16 << 20;
|
||||
|
||||
fn bounded_overlay_dimensions(w: usize, h: usize) -> Result<()> {
|
||||
if w == 0 || h == 0 || w.checked_mul(h).is_none_or(|pixels| pixels > MAX_OVERLAY_PIXELS) {
|
||||
return Err(ExportError::Unsupported("image overlay exceeds the pixel budget".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod overlay_budget_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn overlay_pixel_budget_is_checked_without_overflow() {
|
||||
assert!(bounded_overlay_dimensions(4096, 4096).is_ok());
|
||||
assert!(bounded_overlay_dimensions(4097, 4096).is_err());
|
||||
assert!(bounded_overlay_dimensions(usize::MAX, 2).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the rendered picture lands in the output frame.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct Geometry {
|
||||
@@ -392,8 +416,16 @@ fn composite(dst: &mut Image, src: &Image, x: i64, y: i64, opacity: f32) {
|
||||
/// Load a PNG / JPEG overlay as premultiplied linear light, `width` pixels wide.
|
||||
fn load_overlay(path: &str, width: f32) -> Result<Image> {
|
||||
let bytes = std::fs::read(path).map_err(|e| ExportError::Io(format!("image overlay {path}: {e}")))?;
|
||||
let rgba = image::load_from_memory(&bytes).map_err(|e| ExportError::Unsupported(format!("image overlay {path}: {e}")))?.to_rgba8();
|
||||
let mut reader = image::ImageReader::new(Cursor::new(bytes));
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_image_width = Some(16_384);
|
||||
limits.max_image_height = Some(16_384);
|
||||
limits.max_alloc = Some((MAX_OVERLAY_PIXELS * 4) as u64);
|
||||
reader.limits(limits);
|
||||
let reader = reader.with_guessed_format().map_err(|e| ExportError::Unsupported(format!("image overlay {path}: {e}")))?;
|
||||
let rgba = reader.decode().map_err(|e| ExportError::Unsupported(format!("image overlay {path}: {e}")))?.to_rgba8();
|
||||
let (iw, ih) = (rgba.width() as usize, rgba.height() as usize);
|
||||
bounded_overlay_dimensions(iw, ih)?;
|
||||
let mut img = Image::new(iw, ih);
|
||||
for (o, p) in img.px.as_chunks_mut::<4>().0.iter_mut().zip(rgba.as_raw().as_chunks::<4>().0) {
|
||||
let a = p[3] as f32 / 255.0;
|
||||
@@ -402,8 +434,12 @@ fn load_overlay(path: &str, width: f32) -> Result<Image> {
|
||||
}
|
||||
o[3] = a;
|
||||
}
|
||||
if !width.is_finite() || width <= 0.0 {
|
||||
return Err(ExportError::Unsupported("image overlay width is invalid".into()));
|
||||
}
|
||||
let nw = width.round().max(1.0) as usize;
|
||||
let nh = ((ih as f32 * nw as f32 / iw.max(1) as f32).round() as usize).max(1);
|
||||
let nh = ((ih as f64 * nw as f64 / iw.max(1) as f64).round() as usize).max(1);
|
||||
bounded_overlay_dimensions(nw, nh)?;
|
||||
Ok(if (nw, nh) == (iw, ih) { img } else { resample(&img, nw, nh) })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user