From 8e4b405980ceb179d932a8f94779066ae3175491 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Tue, 29 Sep 2026 10:01:49 +0100 Subject: [PATCH] fix(gdi): skip undersized EMF polypoly records Bug: The EMF record reader accepts nSize = 28 (header-only) so legal empty polylines load, but the sub-arm handling those iTypes also reads a u32 count at offset 28 for records that are not empty - rec.get(28..32) on a 28-byte record is out of bounds, panicking while importing the file. Reproduce: 1. Craft an EMF with an EMR_POLYLINE (or related iType) record whose nSize is exactly 28. 2. Attach/import it. 3. Before: out-of-bounds panic reading at offset 28. 4. After: the undersized record is skipped; a full 36-byte record still renders. Fix: The sub-arm that reads at offset 28 now requires rec.len() >= 32 (its first read), while the shared arm's guard stays at 28 so legal header-only empty polylines keep loading. Undersized records are skipped the way other malformed records already are. Tests: iType 7 and 91 records of exactly 28 bytes are skipped, a 36-byte record still renders. --- src/scene/model/gdi/emf.rs | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/scene/model/gdi/emf.rs b/src/scene/model/gdi/emf.rs index f73362f39..c859b8f87 100644 --- a/src/scene/model/gdi/emf.rs +++ b/src/scene/model/gdi/emf.rs @@ -570,6 +570,13 @@ pub fn render(data: &[u8]) -> Option<(Vec, u32, u32)> { } // POLYPOLYLINE(7)/POLYPOLYGON(8) 7 | 8 => { + // The shared arm guard only promises 28 bytes, but + // this sub-arm reads `total` at 28 and starts the + // counts array at 32: `nSize = 28` records used to + // panic `range end index 32 out of len 28`. + if rec.len() < 32 { + continue; + } let n_polys = u32_at(rec, 24) as usize; let total = u32_at(rec, 28) as usize; if n_polys == 0 || n_polys > 100_000 || total > 400_000 { @@ -624,3 +631,69 @@ pub fn render(data: &[u8]) -> Option<(Vec, u32, u32)> { let (px, w, h) = canvas.downsample(super::SS); Some((px, w, h)) } + +#[cfg(test)] +mod polypoly_size_tests { + use super::render; + + /// 88-byte EMR_HEADER (`iType=1`, `nSize=88`, bounds 0..100, `" EMF"` + /// signature at 40) + raw records + EMR_EOF. + fn emf_with(records: &[Vec]) -> Vec { + let mut emf = Vec::new(); + emf.extend_from_slice(&1u32.to_le_bytes()); + emf.extend_from_slice(&88u32.to_le_bytes()); + emf.extend_from_slice(&0i32.to_le_bytes()); // rclBounds + emf.extend_from_slice(&0i32.to_le_bytes()); + emf.extend_from_slice(&100i32.to_le_bytes()); + emf.extend_from_slice(&100i32.to_le_bytes()); + emf.extend_from_slice(&[0u8; 16]); // rclFrame + emf.extend_from_slice(b" EMF"); // dSignature @40 + emf.extend_from_slice(&[0u8; 44]); + assert_eq!(emf.len(), 88); + for rec in records { + emf.extend_from_slice(rec); + } + emf.extend_from_slice(&14u32.to_le_bytes()); // EMR_EOF + emf.extend_from_slice(&8u32.to_le_bytes()); + emf + } + + /// Record with `iType`/`nSize` header + body (body length %4). + fn record(ty: u32, body: &[u8]) -> Vec { + let mut rec = Vec::with_capacity(8 + body.len()); + rec.extend_from_slice(&ty.to_le_bytes()); + rec.extend_from_slice(&((8 + body.len()) as u32).to_le_bytes()); + rec.extend_from_slice(body); + rec + } + + /// `nSize = 28` (bounds + nPolys only): the arm guard accepts 28 but + /// `7 | 8` reads `u32_at(rec, 28)` — `range end index 32 out of len 28`. + #[test] + fn a_28_byte_polypolyline_record_is_skipped_not_oob() { + let rec = record(7, &[0u8; 20]); + assert_eq!(rec.len(), 28); + assert!(render(&emf_with(&[rec])).is_some()); + } + + /// `iType = 91` maps to kind 8 the same way (`ty - 83`). + #[test] + fn a_28_byte_polypolygon_record_is_skipped_not_oob() { + let rec = record(91, &[0u8; 20]); + assert_eq!(rec.len(), 28); + assert!(render(&emf_with(&[rec])).is_some()); + } + + /// A well-formed record (nPolys + total + counts fit in 36 bytes) still + /// goes through the reads at 24/28 — the fix must not skip it. + #[test] + fn a_full_size_polypolyline_record_still_renders() { + let mut body = vec![0u8; 16]; // bounds + body.extend_from_slice(&1u32.to_le_bytes()); // nPolys @24 + body.extend_from_slice(&1u32.to_le_bytes()); // total @28 + body.extend_from_slice(&0u32.to_le_bytes()); // counts[0] @32 (empty ring) + let rec = record(7, &body); + assert_eq!(rec.len(), 36); + assert!(render(&emf_with(&[rec])).is_some()); + } +}