forked from 1b-agent/opencadstudio-upstream
fix(gdi): size EMF poly point vectors from the record
Bug: EMF polyline/polygon records carry a ring count as a raw u32. pts16/pts32 did Vec::with_capacity(count) on it directly - a crafted 4294967295 pre-allocates about 4 billion slots and the allocator aborts. The existing total > 400_000 guard only checked the sum of ring counts, never an individual ring's vector. Reproduce: 1. Craft an EMF whose POLYPOLYLINE/POLYGON count is 0xFFFFFFFF. 2. Attach/import it. 3. Before: "pre-allocated 4294967295 slots" then memory allocation failed (multi-GB pre-allocation). 4. After: the vector is sized from the bytes actually present in the record; the parse consumes what the record holds. Fix: pts16/pts32 size their capacity as count.min(available_bytes / point_size) - the record's own remaining length is the ceiling, so allocation is O(record). The iteration logic is untouched: it stops when the bytes run out, so output for valid records is byte-identical (the guard is a pure allocation bound). Tests: the u32::MAX ring no longer pre-allocates; valid single- and multi-ring records still parse.
This commit is contained in:
1 parent
eabc7e216b
commit
8b5e5a3408
1 file changed
+67
-2
@@ -127,7 +127,13 @@ impl Player {
|
||||
|
||||
/// Points of a 16-bit poly record at `off`, `n` entries.
|
||||
fn pts16(rec: &[u8], off: usize, n: usize) -> Vec<[f32; 2]> {
|
||||
let mut v = Vec::with_capacity(n);
|
||||
// `n` is a raw u32 out of the record — for POLYPOLYLINE it is the
|
||||
// per-ring count, which the `total > 400_000` check never bounds:
|
||||
// `u32::MAX` used to request a ~17 GB capacity up front and abort
|
||||
// on allocation failure. Only bytes present can be read (loop
|
||||
// below), so the capacity is sized from the record.
|
||||
let avail = rec.len().saturating_sub(off) / 4;
|
||||
let mut v = Vec::with_capacity(n.min(avail));
|
||||
for i in 0..n {
|
||||
let o = off + i * 4;
|
||||
if o + 4 > rec.len() {
|
||||
@@ -139,7 +145,8 @@ impl Player {
|
||||
}
|
||||
|
||||
fn pts32(rec: &[u8], off: usize, n: usize) -> Vec<[f32; 2]> {
|
||||
let mut v = Vec::with_capacity(n);
|
||||
let avail = rec.len().saturating_sub(off) / 8;
|
||||
let mut v = Vec::with_capacity(n.min(avail));
|
||||
for i in 0..n {
|
||||
let o = off + i * 8;
|
||||
if o + 8 > rec.len() {
|
||||
@@ -696,4 +703,62 @@ mod polypoly_size_tests {
|
||||
assert_eq!(rec.len(), 36);
|
||||
assert!(render(&emf_with(&[rec])).is_some());
|
||||
}
|
||||
|
||||
/// A per-ring point count is a raw u32 (`counts[0] @32`). `pts32` sized
|
||||
/// its vector straight from that field: `u32::MAX` requests a ~34 GB
|
||||
/// capacity and the allocator aborts the process. The `total > 400_000`
|
||||
/// check never bounds individual ring counts. Capacity must come from
|
||||
/// the bytes actually present.
|
||||
#[test]
|
||||
fn a_hostile_ring_point_count_is_sized_from_the_record_32bit() {
|
||||
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 (claimed, unused)
|
||||
body.extend_from_slice(&u32::MAX.to_le_bytes()); // counts[0] @32 ← defect
|
||||
body.extend_from_slice(&1i32.to_le_bytes()); // the one real point
|
||||
body.extend_from_slice(&2i32.to_le_bytes());
|
||||
let rec = record(7, &body);
|
||||
let pts = super::Player::pts32(&rec, 36, u32::MAX as usize);
|
||||
assert!(
|
||||
pts.capacity() <= rec.len() / 8 + 1,
|
||||
"pre-allocated {} slots",
|
||||
pts.capacity()
|
||||
);
|
||||
assert_eq!(pts, vec![[1.0, 2.0]]);
|
||||
}
|
||||
|
||||
/// Same defect through `pts16` (non-wide records, iType 90 → kind 7).
|
||||
#[test]
|
||||
fn a_hostile_ring_point_count_is_sized_from_the_record_16bit() {
|
||||
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(&u32::MAX.to_le_bytes()); // counts[0] @32 ← defect
|
||||
body.extend_from_slice(&3i16.to_le_bytes()); // the one real point
|
||||
body.extend_from_slice(&4i16.to_le_bytes());
|
||||
let rec = record(90, &body);
|
||||
let pts = super::Player::pts16(&rec, 36, u32::MAX as usize);
|
||||
assert!(
|
||||
pts.capacity() <= rec.len() / 4 + 1,
|
||||
"pre-allocated {} slots",
|
||||
pts.capacity()
|
||||
);
|
||||
assert_eq!(pts, vec![[3.0, 4.0]]);
|
||||
}
|
||||
|
||||
/// End-to-end: the hostile count reaches `render` via EMR_POLYPOLYLINE
|
||||
/// and must still render the points that exist, without dying.
|
||||
#[test]
|
||||
fn a_hostile_ring_count_still_renders_the_points_that_exist() {
|
||||
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 (≤ cap, claimed)
|
||||
body.extend_from_slice(&u32::MAX.to_le_bytes()); // counts[0] @32 ← defect
|
||||
body.extend_from_slice(&0i32.to_le_bytes()); // point (0,0)
|
||||
body.extend_from_slice(&0i32.to_le_bytes());
|
||||
body.extend_from_slice(&50i32.to_le_bytes()); // point (50,0)
|
||||
body.extend_from_slice(&0i32.to_le_bytes());
|
||||
let rec = record(7, &body);
|
||||
assert!(render(&emf_with(&[rec])).is_some());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user