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.
This commit is contained in:
Karim Jerbi committed 2026-09-30 19:15:49 +01:00
1 parent ad5f46309b
commit 8e4b405980
1 file changed
+73
+73
View File
@@ -570,6 +570,13 @@ pub fn render(data: &[u8]) -> Option<(Vec<u8>, 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<u8>, 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<u8>]) -> Vec<u8> {
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<u8> {
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());
}
}