From d9f90c517d920f5882fb7f5d9a33d5804baf2e7c Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:57:36 -0500 Subject: [PATCH] fix: bound EMF+ region path reads (dc-emf-region-span) --- crates/metafile/src/emfplus.rs | 25 +++++++++++++++++++------ crates/metafile/src/tests.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/crates/metafile/src/emfplus.rs b/crates/metafile/src/emfplus.rs index 62e8f714..21c27759 100644 --- a/crates/metafile/src/emfplus.rs +++ b/crates/metafile/src/emfplus.rs @@ -50,6 +50,8 @@ const MAX_CHARS: usize = 32_768; const MAX_OBJECT_BYTES: usize = 64 * 1024 * 1024; /// Nesting depth of region nodes. const MAX_REGION_DEPTH: usize = 32; +/// Region nodes and path points parsed together, including nodes in sibling branches. +const MAX_REGION_WORK: usize = MAX_POINTS; const TA_RIGHT: u32 = 2; const TA_CENTER: u32 = 6; @@ -1106,12 +1108,14 @@ fn path(r: Bytes) -> Option> { /// A region object as a convex area in world units (non-convex parts become their bounding /// rectangle). fn region(r: Bytes) -> Option { - region_node(r, 8, 0).map(|(a, _)| Obj::Region(a)) + let mut work = 0; + region_node(r, 8, 0, &mut work).map(|(a, _)| Obj::Region(a)) } /// One region node at `off`: its area and its length in bytes. -fn region_node(r: Bytes, off: usize, depth: usize) -> Option<(Area, usize)> { - if depth > MAX_REGION_DEPTH { +fn region_node(r: Bytes, off: usize, depth: usize, work: &mut usize) -> Option<(Area, usize)> { + *work = work.checked_add(1)?; + if depth > MAX_REGION_DEPTH || *work > MAX_REGION_WORK { return None; } match r.u32(off)? { @@ -1121,14 +1125,23 @@ fn region_node(r: Bytes, off: usize, depth: usize) -> Option<(Area, usize)> { } 0x1000_0001 => { let size = r.u32(off + 4)? as usize; - let area = path(r.tail(off + 8)?).map(|ops| area_of_ops(&ops)).unwrap_or_default(); + if size < 12 { + return None; + } + let data = Bytes(r.slice(off + 8, size)?); + let points = (data.u32(4)? as usize).min(MAX_POINTS); + *work = work.checked_add(points)?; + if *work > MAX_REGION_WORK { + return None; + } + let area = area_of_ops(&path(data)?); Some((Some(area), 8usize.checked_add(size)?)) } 0x1000_0002 => Some((Some(Vec::new()), 4)), 0x1000_0003 => Some((None, 4)), mode @ 0..=5 => { - let (a, la) = region_node(r, off + 4, depth + 1)?; - let (b, lb) = region_node(r, off.checked_add(4 + la)?, depth + 1)?; + let (a, la) = region_node(r, off + 4, depth + 1, work)?; + let (b, lb) = region_node(r, off.checked_add(4 + la)?, depth + 1, work)?; let out = match mode { 0 => b, 1 => area_intersect(a, b), diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index 96497bf2..29fab81e 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -1875,6 +1875,34 @@ fn emfplus_region_clip_intersects_rectangles() { assert_eq!((p.width, p.height), (20.0, 20.0)); } +#[test] +fn emfplus_region_path_cannot_read_into_its_sibling() { + let rect = |r: [f32; 4]| cat(vec![h32(0x1000_0000), f32s(&r)]); + // The path declares only its 12-byte header, but claims three points. The following + // rectangle must not supply those points or be parsed twice as the next node. + let path = cat(vec![h32(0x1000_0001), h32(12), h32(0), h32(3), h32(0)]); + let region = cat(vec![h32(0xDBC0_1002), h32(2), h32(1), path, rect([10.0, 10.0, 20.0, 20.0])]); + let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[0.0, 0.0, 100.0, 100.0])])); + let p = plus_pic(vec![prec(0x4008, 0x0400 | 6, ®ion), prec(0x4034, 6, &[]), fill]); + assert_eq!(p.items.len(), 1); + assert_eq!((p.width, p.height), (100.0, 100.0)); +} + +#[test] +fn emfplus_region_path_keeps_its_shape() { + let mut path_data = cat(vec![h32(0), h32(4), h32(0x4000)]); + for (x, y) in [(10i16, 10i16), (30, 10), (30, 30), (10, 30)] { + path_data.extend(x.to_le_bytes()); + path_data.extend(y.to_le_bytes()); + } + path_data.extend([0, 1, 1, 0x81]); + let path_node = cat(vec![h32(0x1000_0001), h32(path_data.len() as u32), path_data]); + let region = cat(vec![h32(0xDBC0_1002), h32(1), path_node]); + let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[0.0, 0.0, 100.0, 100.0])])); + let p = plus_pic(vec![prec(0x4008, 0x0400 | 6, ®ion), prec(0x4034, 6, &[]), fill]); + assert_eq!((p.width, p.height), (20.0, 20.0)); +} + #[test] fn emfplus_pie_and_arc() { // A quarter pie from 0 to 90 degrees in a 0..100 circle: centre, then (100, 50) round to (50, 100).