源图解码加尺寸/分配上限,堵住解压炸弹风险

两处在缩图前直接 image::load_from_memory:背景色过滤的参考图(请求体 data URL)
与阿里云抠图源图。缺省 image::Limits 无尺寸上限、仅 512MiB alloc 兜底,12MB 请求体
即可构造巨幅压缩图在解码阶段撑爆内存,逐帧动画并发时叠加。
改用带 Limits 的 ImageReader(边长 ≤8192、alloc ≤128MiB),与结果解码同口径。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 08:38:07 +00:00
parent f5aa50a68b
commit fa64bfb475
2 changed files with 46 additions and 16 deletions
@@ -32,6 +32,11 @@ const DANGER_MASS_EXCLUSION_RATIO: f64 = 0.005;
const BIN_MASS_FLOOR_RATIO: f64 = 0.001;
/// 分析用缩略图最长边。
const ANALYSIS_MAX_EDGE: u32 = 192;
/// 参考图解码边长上限:源图直接来自请求体 data URL,缺省 `image::Limits` 无尺寸上限、
/// 仅 512MiB alloc 兜底,12MB 请求体即可构造巨幅压缩图(解压炸弹)在缩图前撑爆内存。
const ANALYSIS_SOURCE_MAX_DIMENSION: u32 = 8192;
/// 参考图解码分配上限。与 api-server BgFilter 结果解码口径一致(128MiB)。
const ANALYSIS_SOURCE_MAX_ALLOC_BYTES: u64 = 128 * 1024 * 1024;
/// 不透明图边框背景估计:主色覆盖率达到该值才认为边框是平坦背景。
const BORDER_BACKGROUND_MIN_COVERAGE: f64 = 0.5;
/// 不透明图中与边框背景色 ΔE 不超过该值的像素按背景剔除。
@@ -87,6 +92,23 @@ pub(crate) fn decode_image_data_url(value: &str) -> Option<Vec<u8>> {
.filter(|bytes| !bytes.is_empty())
}
/// 有界解码参考图:套上尺寸 / 分配上限,防止解压炸弹在缩图前撑爆内存。
fn decode_analysis_image_within_limits(bytes: &[u8]) -> Result<image::DynamicImage, String> {
use std::io::Cursor;
let mut reader = image::ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|error| format!("参考图无法识别格式:{error}"))?;
let mut limits = image::Limits::default();
limits.max_image_width = Some(ANALYSIS_SOURCE_MAX_DIMENSION);
limits.max_image_height = Some(ANALYSIS_SOURCE_MAX_DIMENSION);
limits.max_alloc = Some(ANALYSIS_SOURCE_MAX_ALLOC_BYTES);
reader.limits(limits);
reader
.decode()
.map_err(|error| format!("参考图无法解码:{error}"))
}
/// 按参考图前景配色过滤候选背景色。`apply_skin_veto` 为 true(角色/动作类)时,检测到皮肤 bin
/// 还会启用 Rule 2/3 皮肤硬否决。失败(图片不可解码、前景不足)返回原因字符串,
/// 调用方应降级为不过滤,而不是阻断生成。
@@ -94,8 +116,7 @@ pub(crate) fn filter_editor_screen_background_colors(
image_bytes: &[u8],
apply_skin_veto: bool,
) -> Result<ScreenBackgroundColorSafetyReport, String> {
let image = image::load_from_memory(image_bytes)
.map_err(|error| format!("参考图无法解码:{error}"))?;
let image = decode_analysis_image_within_limits(image_bytes)?;
let (width, height) = image.dimensions();
if width == 0 || height == 0 {
return Err("参考图尺寸为空".to_string());
+23 -14
View File
@@ -43,9 +43,9 @@ const MIN_INPUT_EDGE: u32 = 32;
/// 与 api-server BgFilter 降级路径的 `EDITOR_BACKGROUND_REMOVAL_MAX_RESPONSE_BYTES` 对齐。
const MAX_RESULT_RESPONSE_BYTES: usize = 32 * 1024 * 1024;
/// 抠图结果解码的最大边长像素。防止解码阶段按声明的巨幅尺寸分配像素缓冲。
/// 与 api-server BgFilter 路径的 `EDITOR_BACKGROUND_REMOVAL_MAX_IMAGE_DIMENSION` 对齐。
const MAX_RESULT_IMAGE_DIMENSION: u32 = 8192;
/// 图片解码的最大边长像素(源图与抠图结果共用)。防止解码阶段按声明的巨幅尺寸分配
/// 像素缓冲。与 api-server BgFilter 路径的 `EDITOR_BACKGROUND_REMOVAL_MAX_IMAGE_DIMENSION` 对齐。
const MAX_DECODE_IMAGE_DIMENSION: u32 = 8192;
#[derive(Clone, Debug)]
pub struct MattingConfig {
@@ -305,9 +305,7 @@ impl MattingClient {
bytes: &[u8],
file_name: &str,
) -> Result<Vec<u8>, MattingError> {
let source = image::load_from_memory(bytes).map_err(|error| {
MattingError::InvalidRequest(format!("解析待抠图图片失败:{error}"))
})?;
let source = decode_source_image(bytes)?;
let source_rgba = source.to_rgba8();
let (source_width, source_height) = source_rgba.dimensions();
@@ -653,23 +651,34 @@ fn result_response_too_large_error() -> MattingError {
))
}
/// 解码待抠图源图时套上尺寸 / 分配上限。源图直接来自请求体 / 生成产物,缺省
/// `image::Limits` 无尺寸上限、仅 512MiB alloc 兜底,12MB 请求体即可构造巨幅压缩图
/// (解压炸弹)在缩图前撑爆内存;逐帧动画并发调用时风险叠加。
fn decode_source_image(bytes: &[u8]) -> Result<image::DynamicImage, MattingError> {
decode_image_within_limits(bytes)
.map_err(|error| MattingError::InvalidRequest(format!("解析待抠图图片失败:{error}")))
}
/// 解码抠图结果时套上尺寸 / 分配上限,防止上游用巨幅尺寸声明在解码阶段撑爆内存。
fn decode_result_image(bytes: &[u8]) -> Result<image::DynamicImage, MattingError> {
decode_image_within_limits(bytes)
.map_err(|error| MattingError::Upstream(format!("解析抠图结果图片失败:{error}")))
}
/// 统一的有界解码:尺寸上限 `MAX_DECODE_IMAGE_DIMENSION`、分配上限
/// `MAX_RESULT_RESPONSE_BYTES * 4`,覆盖源图与抠图结果两条解码路径。
fn decode_image_within_limits(bytes: &[u8]) -> Result<image::DynamicImage, String> {
use std::io::Cursor;
let mut reader = image::ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|error| {
MattingError::Upstream(format!("识别抠图结果格式失败:{error}"))
})?;
.map_err(|error| format!("识别图片格式失败:{error}"))?;
let mut limits = image::Limits::default();
limits.max_image_width = Some(MAX_RESULT_IMAGE_DIMENSION);
limits.max_image_height = Some(MAX_RESULT_IMAGE_DIMENSION);
limits.max_image_width = Some(MAX_DECODE_IMAGE_DIMENSION);
limits.max_image_height = Some(MAX_DECODE_IMAGE_DIMENSION);
limits.max_alloc = Some(MAX_RESULT_RESPONSE_BYTES as u64 * 4);
reader.limits(limits);
reader.decode().map_err(|error| {
MattingError::Upstream(format!("解析抠图结果图片失败:{error}"))
})
reader.decode().map_err(|error| error.to_string())
}
struct ViapiStsToken {