forked from 1b-agent/opencadstudio-upstream
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:
1 parent
2b807a7654
commit
6cbe62921b
2 files changed
+59
-34
No files matched your search
@@ -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());
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
Reference in new issue
Block a user