From 6cbe62921bd7a6719aefae098acea7a2059ca9b6 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 16 Oct 2026 23:10:27 +0300 Subject: [PATCH] Bound DGN element reads that a crafted file could abuse A V8 B-spline header shorter than 110 bytes no longer panics: the order and closure word is read only when the element holds it. A V7 cell takes only the components its total length covers, and cells nested deeper than 16 levels are left out, so a long run of complex elements no longer recurses once per element until the stack overflows. A nested cell also stops swallowing the sibling elements after it. Only the chosen model's control stream is inflated for its reference attachments, within what is left of the budget the models were read under, instead of every model's stream without a total limit. Co-Authored-By: Claude Opus 5.5 --- src/scene/model/underlay_vector/dgn7.rs | 28 +++++++---- src/scene/model/underlay_vector/dgn8.rs | 65 +++++++++++++++---------- 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/src/scene/model/underlay_vector/dgn7.rs b/src/scene/model/underlay_vector/dgn7.rs index 1a626eec7..f9be987e6 100644 --- a/src/scene/model/underlay_vector/dgn7.rs +++ b/src/scene/model/underlay_vector/dgn7.rs @@ -172,11 +172,14 @@ struct Instance { origin: [f64; 2], } +/// How deep cells inside cells are read. +const MAX_CELL_DEPTH: usize = 16; + /// Reads a run of elements into raw geometry: plain elements, complex /// chains and shapes (joined), B-splines, text, cells and shared cell /// instances. In a 3D file (`d3`) points have a z and planar elements their /// plane; the drawn geometry is their plan. -fn read_run(run: &[&[u8]], colors: &[[u8; 3]], d3: bool) -> Raw { +fn read_run(run: &[&[u8]], colors: &[[u8; 3]], d3: bool, depth: usize) -> Raw { let mut raw = Raw::default(); // An open B-spline curve: order, pole count, colour, closed, and the // header as the element it imports as. @@ -280,14 +283,21 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]], d3: bool) -> Raw { let plan = |p: [f64; 3]| [p[0], p[1]]; let step = if d3 { 12 } else { 8 }; match kind { - // A cell: its components follow as complex elements; they are - // already placed, so the cell keeps them as they are. + // A cell: its components follow as complex elements, as many as + // its total length (36, the words after its first 38 bytes) + // covers; they are already placed, so the cell keeps them as + // they are. Nesting past `MAX_CELL_DEPTH` is left out. 2 => { + let mut left = (38 + uint16(e, 36).unwrap_or(0) as usize * 2).saturating_sub(e.len()); let start = index; - while index < run.len() && run[index][0] & 0x80 != 0 { + while left > 0 && index < run.len() && run[index][0] & 0x80 != 0 { + left = left.saturating_sub(run[index].len()); index += 1; } - let inner = read_run(&run[start..index], colors, d3); + if depth >= MAX_CELL_DEPTH { + continue; + } + let inner = read_run(&run[start..index], colors, d3, depth + 1); raw.paths.extend(inner.paths); raw.texts.extend(inner.texts); raw.instances.extend(inner.instances); @@ -836,14 +846,14 @@ pub fn model(bytes: &[u8], hidden: &[String]) -> Option { while j < all.len() && all[j][0] & 0x80 != 0 { j += 1; } - defs.insert(name, read_run(&all[i + 1..j], &colors, is_3d(bytes))); + defs.insert(name, read_run(&all[i + 1..j], &colors, is_3d(bytes), 0)); i = j; continue; } top.push(e); i += 1; } - let raw = read_run(&top, &colors, is_3d(bytes)); + let raw = read_run(&top, &colors, is_3d(bytes), 0); let per_unit = u.uor_per_sub * u.sub_per_master; let m: Affine = [ 1.0 / per_unit, @@ -910,7 +920,7 @@ pub fn import(bytes: &[u8]) -> Option { while j < all.len() && all[j][0] & 0x80 != 0 { j += 1; } - let def = read_run(&all[i + 1..j], &colors, is_3d(bytes)).elements; + let def = read_run(&all[i + 1..j], &colors, is_3d(bytes), 0).elements; cells.push((name, scaled_elements(&def, s, [0.0, 0.0]))); i = j; continue; @@ -918,7 +928,7 @@ pub fn import(bytes: &[u8]) -> Option { top.push(e); i += 1; } - let raw = read_run(&top, &colors, is_3d(bytes)); + let raw = read_run(&top, &colors, is_3d(bytes), 0); let levels = (0..64u32) .map(|l| { let name = names.get(&(l as u8)).cloned().unwrap_or_else(|| l.to_string()); diff --git a/src/scene/model/underlay_vector/dgn8.rs b/src/scene/model/underlay_vector/dgn8.rs index 29ecd4de8..ca2f5f63b 100644 --- a/src/scene/model/underlay_vector/dgn8.rs +++ b/src/scene/model/underlay_vector/dgn8.rs @@ -209,16 +209,22 @@ impl<'a> Cfb<'a> { } } +/// Far above any real element stream; stops a crafted one from inflating +/// until memory runs out. +const STREAM_LIMIT: u64 = 512 << 20; + /// A DGN stream: raw, or zlib data after a 16-byte header. fn inflate(data: &[u8]) -> Vec { - // Far above any real element stream; stops a crafted one from - // inflating until memory runs out. - const STREAM_LIMIT: u64 = 512 << 20; + inflate_at_most(data, STREAM_LIMIT) +} + +/// [`inflate`] stopping after `limit` bytes. +fn inflate_at_most(data: &[u8], limit: u64) -> Vec { for skip in [16usize, 0] { if let Some(z) = data.get(skip..) { let mut out = Vec::new(); if flate2::read::ZlibDecoder::new(z) - .take(STREAM_LIMIT) + .take(limit) .read_to_end(&mut out) .is_ok() && !out.is_empty() @@ -997,7 +1003,10 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw { 27 => { // B-spline curve header: order and closure in the word at // 108, pole count 112; its poles follow (type 21). - let flags = u16::from_le_bytes([e[108], e[109]]); + let Some(&[lo, hi]) = e.get(108..110) else { + continue; + }; + let flags = u16::from_le_bytes([lo, hi]); spline = Some(( ((flags & 0x0f) + 2) as usize, u32_at(e, 112).unwrap_or(0) as usize, @@ -1149,6 +1158,11 @@ fn units(header: &[u8]) -> (f64, f64) { (master, sub) } +/// How many bytes the streams of one file may inflate to in all. +fn inflate_budget(cfb: &Cfb) -> usize { + cfb.b.len().saturating_mul(64).max(256 << 20) +} + /// Models of the file: (name, graphic stream, header stream). fn models(cfb: &Cfb) -> Vec<(String, Vec, Vec)> { let streams = cfb.streams(); @@ -1163,7 +1177,7 @@ fn models(cfb: &Cfb) -> Vec<(String, Vec, Vec)> { // scales with the file (×64 covers any real compression ratio, so // legitimate files never trip it) with a 256 MiB floor for small ones; // models past the budget are dropped, the earlier ones still load. - let total_limit = cfb.b.len().saturating_mul(64).max(256 << 20); + let total_limit = inflate_budget(cfb); let mut total = 0usize; let mut out = Vec::new(); for dir in dirs { @@ -1400,24 +1414,28 @@ pub fn design_model_names(bytes: &[u8]) -> Option> { (!names.is_empty()).then_some(names) } -/// The control stream (Dgn^C) of each model, in the order `models` lists -/// them. -fn control_streams(cfb: &Cfb) -> Vec> { +/// The control stream (Dgn^C) of model `k` in the order `models` lists +/// them, inflated within what is left of the file's budget (`used` bytes +/// are taken by the models already read). +fn control_stream(cfb: &Cfb, k: usize, used: usize) -> Vec { let streams = cfb.streams(); - let mut dirs: Vec = streams + let mut dirs: Vec<&str> = streams .iter() - .filter_map(|(p, _)| p.strip_suffix("/Dgn^G/$1").map(|d| d.to_string())) + .filter_map(|(p, _)| p.strip_suffix("/Dgn^G/$1")) .collect(); dirs.sort(); - dirs.iter() - .map(|dir| { - streams - .iter() - .find(|(p, _)| *p == format!("{dir}/Dgn^C/$1")) - .map(|(_, e)| inflate(&cfb.stream(e))) - .unwrap_or_default() - }) - .collect() + let Some(dir) = dirs.get(k) else { + return Vec::new(); + }; + let left = inflate_budget(cfb).saturating_sub(used) as u64; + if left == 0 { + return Vec::new(); + } + streams + .iter() + .find(|(p, _)| *p == format!("{dir}/Dgn^C/$1")) + .map(|(_, e)| inflate_at_most(&cfb.stream(e), left.min(STREAM_LIMIT))) + .unwrap_or_default() } /// Every string of an element stored after the "ff fe 01 00" marker, with @@ -1476,7 +1494,6 @@ fn references(control: &[u8]) -> Vec { pub fn import(bytes: &[u8], name: &str) -> Option { let cfb = Cfb::open(bytes)?; let all = models(&cfb); - let controls = control_streams(&cfb); let designs: Vec = (0..all.len()).filter(|k| is_design(&all[*k].2)).collect(); let k = designs .iter() @@ -1536,10 +1553,8 @@ pub fn import(bytes: &[u8], name: &str) -> Option { (Some(n), Some(d)) if n > 0.0 && d > 0.0 => unit_name(d / n), _ => String::new(), }; - let references = controls - .get(k) - .map(|c| references(c)) - .unwrap_or_default() + let used = all.iter().map(|m| m.1.len() + m.2.len()).sum(); + let references = references(&control_stream(&cfb, k, used)) .into_iter() .map(|r| DgnReference { origin: [