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 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-16 23:10:27 +03:00
1 parent 2b807a7654
commit 6cbe62921b
2 files changed
+59 -34

No files matched your search

+19 -9
View File
@@ -172,11 +172,14 @@ struct Instance {
origin: [f64; 2], 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 /// Reads a run of elements into raw geometry: plain elements, complex
/// chains and shapes (joined), B-splines, text, cells and shared cell /// 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 /// instances. In a 3D file (`d3`) points have a z and planar elements their
/// plane; the drawn geometry is their plan. /// 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(); let mut raw = Raw::default();
// An open B-spline curve: order, pole count, colour, closed, and the // An open B-spline curve: order, pole count, colour, closed, and the
// header as the element it imports as. // 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 plan = |p: [f64; 3]| [p[0], p[1]];
let step = if d3 { 12 } else { 8 }; let step = if d3 { 12 } else { 8 };
match kind { match kind {
// A cell: its components follow as complex elements; they are // A cell: its components follow as complex elements, as many as
// already placed, so the cell keeps them as they are. // 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 => { 2 => {
let mut left = (38 + uint16(e, 36).unwrap_or(0) as usize * 2).saturating_sub(e.len());
let start = index; 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; 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.paths.extend(inner.paths);
raw.texts.extend(inner.texts); raw.texts.extend(inner.texts);
raw.instances.extend(inner.instances); raw.instances.extend(inner.instances);
@@ -836,14 +846,14 @@ pub fn model(bytes: &[u8], hidden: &[String]) -> Option<Sheet> {
while j < all.len() && all[j][0] & 0x80 != 0 { while j < all.len() && all[j][0] & 0x80 != 0 {
j += 1; 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; i = j;
continue; continue;
} }
top.push(e); top.push(e);
i += 1; 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 per_unit = u.uor_per_sub * u.sub_per_master;
let m: Affine = [ let m: Affine = [
1.0 / per_unit, 1.0 / per_unit,
@@ -910,7 +920,7 @@ pub fn import(bytes: &[u8]) -> Option<DgnImport> {
while j < all.len() && all[j][0] & 0x80 != 0 { while j < all.len() && all[j][0] & 0x80 != 0 {
j += 1; 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]))); cells.push((name, scaled_elements(&def, s, [0.0, 0.0])));
i = j; i = j;
continue; continue;
@@ -918,7 +928,7 @@ pub fn import(bytes: &[u8]) -> Option<DgnImport> {
top.push(e); top.push(e);
i += 1; 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) let levels = (0..64u32)
.map(|l| { .map(|l| {
let name = names.get(&(l as u8)).cloned().unwrap_or_else(|| l.to_string()); let name = names.get(&(l as u8)).cloned().unwrap_or_else(|| l.to_string());
+40 -25
View File
@@ -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. /// A DGN stream: raw, or zlib data after a 16-byte header.
fn inflate(data: &[u8]) -> Vec<u8> { fn inflate(data: &[u8]) -> Vec<u8> {
// Far above any real element stream; stops a crafted one from inflate_at_most(data, STREAM_LIMIT)
// inflating until memory runs out. }
const STREAM_LIMIT: u64 = 512 << 20;
/// [`inflate`] stopping after `limit` bytes.
fn inflate_at_most(data: &[u8], limit: u64) -> Vec<u8> {
for skip in [16usize, 0] { for skip in [16usize, 0] {
if let Some(z) = data.get(skip..) { if let Some(z) = data.get(skip..) {
let mut out = Vec::new(); let mut out = Vec::new();
if flate2::read::ZlibDecoder::new(z) if flate2::read::ZlibDecoder::new(z)
.take(STREAM_LIMIT) .take(limit)
.read_to_end(&mut out) .read_to_end(&mut out)
.is_ok() .is_ok()
&& !out.is_empty() && !out.is_empty()
@@ -997,7 +1003,10 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
27 => { 27 => {
// B-spline curve header: order and closure in the word at // B-spline curve header: order and closure in the word at
// 108, pole count 112; its poles follow (type 21). // 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(( spline = Some((
((flags & 0x0f) + 2) as usize, ((flags & 0x0f) + 2) as usize,
u32_at(e, 112).unwrap_or(0) as usize, u32_at(e, 112).unwrap_or(0) as usize,
@@ -1149,6 +1158,11 @@ fn units(header: &[u8]) -> (f64, f64) {
(master, sub) (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). /// Models of the file: (name, graphic stream, header stream).
fn models(cfb: &Cfb) -> Vec<(String, Vec<u8>, Vec<u8>)> { fn models(cfb: &Cfb) -> Vec<(String, Vec<u8>, Vec<u8>)> {
let streams = cfb.streams(); let streams = cfb.streams();
@@ -1163,7 +1177,7 @@ fn models(cfb: &Cfb) -> Vec<(String, Vec<u8>, Vec<u8>)> {
// scales with the file (×64 covers any real compression ratio, so // scales with the file (×64 covers any real compression ratio, so
// legitimate files never trip it) with a 256 MiB floor for small ones; // legitimate files never trip it) with a 256 MiB floor for small ones;
// models past the budget are dropped, the earlier ones still load. // 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 total = 0usize;
let mut out = Vec::new(); let mut out = Vec::new();
for dir in dirs { for dir in dirs {
@@ -1400,24 +1414,28 @@ pub fn design_model_names(bytes: &[u8]) -> Option<Vec<String>> {
(!names.is_empty()).then_some(names) (!names.is_empty()).then_some(names)
} }
/// The control stream (Dgn^C) of each model, in the order `models` lists /// The control stream (Dgn^C) of model `k` in the order `models` lists
/// them. /// them, inflated within what is left of the file's budget (`used` bytes
fn control_streams(cfb: &Cfb) -> Vec<Vec<u8>> { /// are taken by the models already read).
fn control_stream(cfb: &Cfb, k: usize, used: usize) -> Vec<u8> {
let streams = cfb.streams(); let streams = cfb.streams();
let mut dirs: Vec<String> = streams let mut dirs: Vec<&str> = streams
.iter() .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(); .collect();
dirs.sort(); dirs.sort();
dirs.iter() let Some(dir) = dirs.get(k) else {
.map(|dir| { return Vec::new();
streams };
.iter() let left = inflate_budget(cfb).saturating_sub(used) as u64;
.find(|(p, _)| *p == format!("{dir}/Dgn^C/$1")) if left == 0 {
.map(|(_, e)| inflate(&cfb.stream(e))) return Vec::new();
.unwrap_or_default() }
}) streams
.collect() .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 /// Every string of an element stored after the "ff fe 01 00" marker, with
@@ -1476,7 +1494,6 @@ fn references(control: &[u8]) -> Vec<DgnReference> {
pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> { pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
let cfb = Cfb::open(bytes)?; let cfb = Cfb::open(bytes)?;
let all = models(&cfb); let all = models(&cfb);
let controls = control_streams(&cfb);
let designs: Vec<usize> = (0..all.len()).filter(|k| is_design(&all[*k].2)).collect(); let designs: Vec<usize> = (0..all.len()).filter(|k| is_design(&all[*k].2)).collect();
let k = designs let k = designs
.iter() .iter()
@@ -1536,10 +1553,8 @@ pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
(Some(n), Some(d)) if n > 0.0 && d > 0.0 => unit_name(d / n), (Some(n), Some(d)) if n > 0.0 && d > 0.0 => unit_name(d / n),
_ => String::new(), _ => String::new(),
}; };
let references = controls let used = all.iter().map(|m| m.1.len() + m.2.len()).sum();
.get(k) let references = references(&control_stream(&cfb, k, used))
.map(|c| references(c))
.unwrap_or_default()
.into_iter() .into_iter()
.map(|r| DgnReference { .map(|r| DgnReference {
origin: [ origin: [