fix(photo): sum dimensions in u64 so a huge header cannot wrap

plan_photo, target_dims and both pipeline branches added the two u32 dimensions before comparing against PHOTO_MAX_DIMENSION_SUM: u32::MAX + 2 wrapped to 1 and read 'within limits' — the plan said AsIs, and an absurd-sized image would go to Telegram untouched (in debug builds the addition panics instead). The four production sums now widen to u64 first; the test pins the wrap case as TooLarge. PNG caps each dimension at 2^31-1, so a spec-valid file cannot reach this today — the raw header is parsed before any crate validation, which is exactly where a hostile file lands (audit: photo.rs u32 wrap).
This commit is contained in:
2026-09-24 15:25:19 +08:00
parent ef8a1dae15
commit 715d6a59b0
+14 -4
View File
@@ -108,7 +108,7 @@ enum PhotoPlan {
/// [`PhotoPlan`] for a photo whose header said `w`×`h` in `channels` output
/// channels, `len` bytes long.
fn plan_photo(w: u32, h: u32, len: usize, channels: usize) -> PhotoPlan {
if w + h <= PHOTO_MAX_DIMENSION_SUM && len as u64 <= MAX_UPLOAD_BYTES {
if (w as u64) + (h as u64) <= PHOTO_MAX_DIMENSION_SUM as u64 && len as u64 <= MAX_UPLOAD_BYTES {
return PhotoPlan::AsIs;
}
let bytes = decode_bytes(w, h, channels);
@@ -310,7 +310,7 @@ fn write_temp(bytes: &[u8], ext: &str) -> Result<NamedTempFile, String> {
}
fn target_dims(w: u32, h: u32) -> (u32, u32) {
let scale = PHOTO_TARGET_DIMENSION_SUM as f64 / (w + h) as f64;
let scale = PHOTO_TARGET_DIMENSION_SUM as f64 / ((w as u64) + (h as u64)) as f64;
(
((w as f64 * scale).round() as u32).max(1),
((h as f64 * scale).round() as u32).max(1),
@@ -367,7 +367,7 @@ fn prepare_png(file: NamedTempFile, bytes: &[u8]) -> Result<PhotoPrep, String> {
};
let (mut w, mut h) = (out_w, out_h);
if w + h > PHOTO_MAX_DIMENSION_SUM {
if (w as u64) + (h as u64) > PHOTO_MAX_DIMENSION_SUM as u64 {
let (nw, nh) = target_dims(w, h);
pix = resize_pix(pix, w, h, nw, nh)?;
(w, h) = (nw, nh);
@@ -409,7 +409,7 @@ fn prepare_jpeg(file: NamedTempFile, bytes: &[u8]) -> Result<PhotoPrep, String>
let pixels = decoder.decode().map_err(|e| format!("jpeg decode: {e}"))?;
let mut pix = PixBuf::Rgb(pixels);
let (mut w, mut h) = (w, h);
if w + h > PHOTO_MAX_DIMENSION_SUM {
if (w as u64) + (h as u64) > PHOTO_MAX_DIMENSION_SUM as u64 {
let (nw, nh) = target_dims(w, h);
pix = resize_pix(pix, w, h, nw, nh)?;
(w, h) = (nw, nh);
@@ -525,6 +525,16 @@ mod tests {
);
}
#[test]
fn a_wrapping_dimension_sum_never_reads_as_within_limits() {
// u32::MAX + 2 wraps to 1: the pre-u64 sum advertised AsIs here and
// handed the absurd dimensions to Telegram untouched.
assert!(matches!(
plan_photo(u32::MAX, 2, 16, 3),
PhotoPlan::TooLarge
));
}
/// What the reservation is charged is decided by the header, and it has to
/// agree with what the pipeline does: a photo uploaded as-is costs nothing,
/// one that gets processed costs its decoded buffer.