Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions symbolic-debuginfo/src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@ use crate::ParseObjectOptions;
use crate::sourcebundle::SourceFileDescriptor;
use crate::variable;

pub(crate) trait Parse<'data>: Sized {
/// A generic parser trait, used by the various debuginfos.
pub trait Parse<'data>: Sized {
/// The type of error to emit during parsing.
type Error;

fn parse_with_opts(data: &'data [u8], _opts: ParseObjectOptions) -> Result<Self, Self::Error>;
/// Parse the supplied bytes, with the specified parsing options.
fn parse_with_opts(data: &'data [u8], opts: ParseObjectOptions) -> Result<Self, Self::Error>;

/// Parse the supplied bytes with default parsing options.
fn parse(data: &'data [u8]) -> Result<Self, Self::Error> {
Self::parse_with_opts(data, Default::default())
}

/// Returns true if the supplied bytes can be successfully parsed.
fn test(data: &'data [u8]) -> bool {
Self::parse(data).is_ok()
}
Expand Down
16 changes: 6 additions & 10 deletions symbolic-debuginfo/src/breakpad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,7 @@ pub struct BreakpadObject<'data> {
arch: Arch,
module: BreakpadModuleRecord<'data>,
data: &'data [u8],
max_inline_depth: Option<u32>,
max_inline_depth: u32,
}

impl<'data> BreakpadObject<'data> {
Expand Down Expand Up @@ -1265,7 +1265,7 @@ impl<'data> Iterator for BreakpadSymbolIterator<'data> {
pub struct BreakpadDebugSession<'data> {
file_map: BreakpadFileMap<'data>,
lines: Lines<'data>,
max_inline_depth: Option<u32>,
max_inline_depth: u32,
}

impl BreakpadDebugSession<'_> {
Expand Down Expand Up @@ -1331,15 +1331,11 @@ pub struct BreakpadFunctionIterator<'s> {
next_line: Option<&'s [u8]>,
inline_origin_map: BreakpadInlineOriginMap<'s>,
lines: Lines<'s>,
max_inline_depth: Option<u32>,
max_inline_depth: u32,
}

impl<'s> BreakpadFunctionIterator<'s> {
fn new(
file_map: &'s BreakpadFileMap<'s>,
mut lines: Lines<'s>,
max_inline_depth: Option<u32>,
) -> Self {
fn new(file_map: &'s BreakpadFileMap<'s>, mut lines: Lines<'s>, max_inline_depth: u32) -> Self {
let next_line = lines.next();
Self {
file_map,
Expand Down Expand Up @@ -1390,8 +1386,8 @@ impl<'s> Iterator for BreakpadFunctionIterator<'s> {
b"",
fun_record.address,
fun_record.size,
)
.max_inline_depth(self.max_inline_depth);
self.max_inline_depth,
);

for line in self.lines.by_ref() {
// Stop parsing LINE records once other expected records are encountered.
Expand Down
93 changes: 71 additions & 22 deletions symbolic-debuginfo/src/dwarf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@
entry: &Die<'d>,
language: Language,
bcsymbolmap: Option<&'d BcSymbolMap<'d>>,
prior_offset: Option<UnitOffset>,
depth: u8,
) -> Result<Option<Name<'d>>, DwarfError> {
let mut fallback_name = None;
let mut reference_target = None;
Expand Down Expand Up @@ -553,21 +553,16 @@

if let Some(attr) = reference_target {
return self.resolve_reference(*attr, |ref_unit, ref_entry| {
// Self-references may have a layer of indirection. Avoid infinite recursion
// in this scenario.
if let Some(prior) = prior_offset {
if self.offset() == ref_unit.offset() && prior == ref_entry.offset() {
return Ok(None);
}
// Avoid infinite recursion.
if depth == 0 {
return Err(DwarfError::new(
DwarfErrorKind::CorruptedData,
"Exceeded maximum resolve_function_name depth",
));
}

if self.offset() != ref_unit.offset() || entry.offset() != ref_entry.offset() {
ref_unit.resolve_function_name(
ref_entry,
language,
bcsymbolmap,
Some(entry.offset()),
)
ref_unit.resolve_function_name(ref_entry, language, bcsymbolmap, depth - 1)
} else {
Ok(None)
}
Expand All @@ -589,6 +584,9 @@
}

impl<'d, 'a> DwarfUnit<'d, 'a> {
/// The maximum depth to recurse to in order to resolve a function name.
const MAX_RESOLVE_FUNCTION_DEPTH: u8 = 32;

/// Creates a DWARF unit from the gimli `Unit` type.
fn from_unit(
unit: &'a Unit<'d>,
Expand Down Expand Up @@ -841,6 +839,7 @@
fn parse_functions(
&self,
depth: isize,
remaining_inline_depth: u32,
entries: &mut EntriesRaw<'d, '_>,
output: &mut FunctionsOutput<'_, 'd>,
) -> Result<(), DwarfError> {
Expand All @@ -850,9 +849,17 @@
if next_depth <= depth {
return Ok(());
}

if let Some(abbrev) = entries.read_abbreviation()? {
if abbrev.tag() == constants::DW_TAG_subprogram {
self.parse_function(dw_die_offset, next_depth, entries, abbrev, output)?;
self.parse_function(
dw_die_offset,
next_depth,
remaining_inline_depth,
entries,
abbrev,
output,
)?;
} else {
entries.skip_attributes(abbrev.attributes())?;
}
Expand All @@ -874,6 +881,7 @@
&self,
dw_die_offset: gimli::UnitOffset<usize>,
depth: isize,
remaining_inline_depth: u32,
entries: &mut EntriesRaw<'d, '_>,
abbrev: &gimli::Abbreviation,
output: &mut FunctionsOutput<'_, 'd>,
Expand Down Expand Up @@ -909,57 +917,69 @@
// However, non-inlined functions may be present in this subtree, so we must still descend
// into it.
if ranges.is_empty() {
return self.parse_functions(depth, entries, output);
return self.parse_functions(depth, remaining_inline_depth, entries, output);
}

// Resolve functions in the symbol table first. Only if there is no entry, fall back
// to debug information only if there is no match. Sometimes, debug info contains a
// lesser quality of symbol names.
//
// XXX: Maybe we should actually parse the ranges in the resolve function and always
// look at the symbol table based on the start of the DIE range.

let entry = self.inner.unit.entry(dw_die_offset)?;
// With LTO the current CU may be an artificial unit with an incorrect language. Follow
// DW_AT_abstract_origin cross-unit to find the true source language. The resolved
// language is also propagated to all inlinees of this function.
let language = self.resolve_function_language(&entry, self.language);

let symbol_name = if self.prefer_dwarf_names {
None
} else {
let first_range_begin = ranges.iter().map(|range| range.begin).min().unwrap();
let function_address = offset(first_range_begin, self.inner.info.address_offset);
self.resolve_symbol_name(function_address, language)
};

let name = symbol_name
.or_else(|| {
self.inner
.resolve_function_name(&entry, language, self.bcsymbolmap, None)
.resolve_function_name(
&entry,
language,
self.bcsymbolmap,
DwarfUnit::MAX_RESOLVE_FUNCTION_DEPTH,
)
Comment thread
cursor[bot] marked this conversation as resolved.
.ok()
.flatten()
})
.unwrap_or_else(|| Name::new("", NameMangling::Unmangled, language));

// Create one function per range. In the common case there is only one range, so
// we usually only have one function builder here.
let mut builders: Vec<(Range, FunctionBuilder)> = ranges
.iter()
.map(|range| {
let address = offset(range.begin, self.inner.info.address_offset);
let size = range.end - range.begin;
(
*range,
FunctionBuilder::new(name.clone(), self.compilation_dir(), address, size),
FunctionBuilder::new(
name.clone(),
self.compilation_dir(),
address,
size,
remaining_inline_depth,
),
)
})
.collect();

let mut variables = Vec::new();
self.parse_function_children(
depth,
0,
remaining_inline_depth,

Check failure on line 982 in symbolic-debuginfo/src/dwarf.rs

View check run for this annotation

@sentry/warden / warden: wrdn-dos-review

Nested DW_TAG_subprogram parsing has no recursion depth limit

`parse_function` and `parse_functions` mutually recurse for each nested `DW_TAG_subprogram` in the input, but only inlinee nesting is bounded by `remaining_inline_depth`; deeply nested subprograms can still exhaust the native stack.
entries,
&mut builders,
output,
Expand Down Expand Up @@ -1000,6 +1020,7 @@
&self,
depth: isize,
inline_depth: u32,
remaining_inline_depth: u32,
entries: &mut EntriesRaw<'d, '_>,
builders: &mut [(Range, FunctionBuilder<'d>)],
output: &mut FunctionsOutput<'_, 'd>,
Expand All @@ -1019,13 +1040,21 @@
match abbrev.tag() {
constants::DW_TAG_subprogram => {
// Nested subprograms resolve their own language independently.
self.parse_function(dw_die_offset, next_depth, entries, abbrev, output)?;
self.parse_function(
dw_die_offset,
next_depth,
remaining_inline_depth,
entries,
abbrev,
output,
)?;
}
constants::DW_TAG_inlined_subroutine => {
self.parse_inlinee(
dw_die_offset,
next_depth,
inline_depth,
remaining_inline_depth,
entries,
abbrev,
builders,
Expand Down Expand Up @@ -1060,12 +1089,20 @@
dw_die_offset: gimli::UnitOffset<usize>,
depth: isize,
inline_depth: u32,
remaining_inline_depth: u32,
entries: &mut EntriesRaw<'d, '_>,
abbrev: &gimli::Abbreviation,
builders: &mut [(Range, FunctionBuilder<'d>)],
output: &mut FunctionsOutput<'_, 'd>,
language: Language,
) -> Result<(), DwarfError> {
if remaining_inline_depth == 0 {
return Err(DwarfError::new(
DwarfErrorKind::CorruptedData,
"Exceeded max parse inlinee depth",
));
}

let (ranges, call_location) = self.parse_ranges(entries, abbrev, &mut output.range_buf)?;

ranges.retain(|range| range.end > range.begin);
Expand All @@ -1078,7 +1115,7 @@
// However, non-inlined functions may be present in this subtree, so we must still descend
// into it.
if ranges.is_empty() {
return self.parse_functions(depth, entries, output);
return self.parse_functions(depth, remaining_inline_depth, entries, output);
Comment thread
cursor[bot] marked this conversation as resolved.
}
let ranges = ranges.clone();

Expand All @@ -1090,7 +1127,12 @@
// which carries the wrong language (e.g. a C++ LTO partial unit for C code).
let name = self
.inner
.resolve_function_name(&entry, language, self.bcsymbolmap, None)
.resolve_function_name(
&entry,
language,
self.bcsymbolmap,
DwarfUnit::MAX_RESOLVE_FUNCTION_DEPTH,
)
.ok()
.flatten()
.unwrap_or_else(|| Name::new("", NameMangling::Unmangled, language));
Expand All @@ -1105,6 +1147,7 @@
self.parse_function_children(
depth,
inline_depth + 1,
remaining_inline_depth - 1,
entries,
builders,
output,
Expand Down Expand Up @@ -1309,10 +1352,11 @@
fn functions(
&self,
seen_ranges: &mut BTreeSet<(u64, u64)>,
max_inline_depth: u32,
) -> Result<Vec<Function<'d>>, DwarfError> {
let mut entries = self.inner.unit.entries_raw(None)?;
let mut output = FunctionsOutput::with_seen_ranges(seen_ranges);
self.parse_functions(-1, &mut entries, &mut output)?;
self.parse_functions(-1, max_inline_depth, &mut entries, &mut output)?;

Check failure on line 1359 in symbolic-debuginfo/src/dwarf.rs

View check run for this annotation

@sentry/warden / warden: wrdn-dos-review

[UW4-FSB] Nested DW_TAG_subprogram parsing has no recursion depth limit (additional location)

`parse_function` and `parse_functions` mutually recurse for each nested `DW_TAG_subprogram` in the input, but only inlinee nesting is bounded by `remaining_inline_depth`; deeply nested subprograms can still exhaust the native stack.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_function / parse_function_children lack depth limit for nested DW_TAG_subprogram

While the PR caps inlined-subroutine recursion with max_inline_depth, nested DW_TAG_subprogram entries are still traversed via mutual recursion between parse_function and parse_function_children with no depth bound. A crafted DWARF file with a chain of deeply nested subprograms causes an unbounded stack growth and an uncatchable stack-overflow abort.

Evidence
  • DwarfUnit::functions() at line 1359 initiates parsing by calling self.parse_functions(-1, max_inline_depth, &mut entries, &mut output) — the added max_inline_depth is only checked later on inlinees, not on subprograms.
  • parse_functions (line 839) delegates DW_TAG_subprogram entries to parse_function (line 855), passing remaining_inline_depth unchanged.
  • parse_function (line 880) reads attributes then calls parse_function_children (line 979).
  • parse_function_children (line 1019) recursively calls parse_function for nested DW_TAG_subprogram children at greater next_depth (line 1043) with no limit on nesting depth.
  • A malicious DWARF with a chain of nested subprograms creates an unbounded call stack (parse_functionparse_function_childrenparse_function → …), while the sibling inlinee path was capped in parse_inlinee (line 1099).
Also found at 5 additional locations
  • symbolic-debuginfo/src/dwarf.rs:1760
  • symbolic-debuginfo/src/pe.rs:260
  • symbolic-debuginfo/src/dwarf.rs:586
  • symbolic-debuginfo/src/dwarf.rs:1893
  • symbolic-debuginfo/src/elf.rs:568

Identified by Warden · wrdn-dos-review · EVE-PMU

Ok(output.functions)
}
}
Expand Down Expand Up @@ -1661,6 +1705,7 @@
pub struct DwarfDebugSession<'data> {
cell: SelfCell<Box<DwarfSections<'data>>, DwarfInfo<'data>>,
bcsymbolmap: Option<Arc<BcSymbolMap<'data>>>,
max_inline_depth: u32,
}

impl<'data> DwarfDebugSession<'data> {
Expand All @@ -1670,6 +1715,7 @@
symbol_map: SymbolMap<'data>,
address_offset: i64,
kind: ObjectKind,
max_inline_depth: u32,
) -> Result<Self, DwarfError>
where
D: Dwarf<'data>,
Expand All @@ -1682,6 +1728,7 @@
Ok(DwarfDebugSession {
cell,
bcsymbolmap: None,
max_inline_depth,
})
}

Expand Down Expand Up @@ -1710,6 +1757,7 @@
functions: Vec::new().into_iter(),
seen_ranges: BTreeSet::new(),
finished: false,
max_inline_depth: self.max_inline_depth,
}
}

Expand Down Expand Up @@ -1820,6 +1868,7 @@
functions: std::vec::IntoIter<Function<'s>>,
seen_ranges: BTreeSet<(u64, u64)>,
finished: bool,
max_inline_depth: u32,
}

impl<'s> Iterator for DwarfFunctionIterator<'s> {
Expand All @@ -1841,7 +1890,7 @@
None => break,
};

self.functions = match unit.functions(&mut self.seen_ranges) {
self.functions = match unit.functions(&mut self.seen_ranges, self.max_inline_depth) {

Check failure on line 1893 in symbolic-debuginfo/src/dwarf.rs

View check run for this annotation

@sentry/warden / warden: wrdn-dos-review

[UW4-FSB] Nested DW_TAG_subprogram parsing has no recursion depth limit (additional location)

`parse_function` and `parse_functions` mutually recurse for each nested `DW_TAG_subprogram` in the input, but only inlinee nesting is bounded by `remaining_inline_depth`; deeply nested subprograms can still exhaust the native stack.

Check failure on line 1893 in symbolic-debuginfo/src/dwarf.rs

View check run for this annotation

@sentry/warden / warden: wrdn-dos-review

Empty-range DWARF inlinees bypass max_inline_depth limit

parse_inlinee returns early for empty-range DW_TAG_inlined_subroutine entries without decrementing remaining_inline_depth, allowing an attacker to chain empty inlinees and exhaust the stack before the depth limit is reached.
Ok(functions) => functions.into_iter(),
Err(error) => return Some(Err(error)),
};
Expand Down
11 changes: 10 additions & 1 deletion symbolic-debuginfo/src/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
data: &'data [u8],
is_malformed: bool,
max_decompressed_section_size: Option<usize>,
max_inline_depth: u32,
}

impl<'data> ElfObject<'data> {
Expand Down Expand Up @@ -171,6 +172,7 @@
data,
is_malformed: true,
max_decompressed_section_size: opts.max_decompressed_section_size,
max_inline_depth: opts.max_inline_depth,
Comment on lines 174 to +175

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ELF decompression size guard defaults to unlimited, allowing OOM abort on tiny input

max_decompressed_section_size defaults to None, which disables the size check and lets a tiny crafted ELF file declare a multi-GB compressed section, causing Vec::with_capacity(size) to abort the allocator.

Evidence
  • ParseObjectOptions::default() sets max_decompressed_section_size: Default::default() which is None (object.rs:152).
  • In decompress_section, size is read from the attacker-controlled ELF compression header (elf.rs:610–634).
  • The check size > self.max_decompressed_section_size.unwrap_or(usize::MAX) (elf.rs:637) becomes size > usize::MAX when the option is None, so it never triggers.
  • Vec::with_capacity(size) (elf.rs:643) is then called with the attacker-declared size, up to usize::MAX, causing an allocator abort on a tiny crafted input.
  • The same ElfObject struct initialized on line 174 carries this ineffective limit into decompress_section.
Also found at 2 additional locations
  • symbolic-debuginfo/src/elf.rs:359-359
  • symbolic-debuginfo/src/object.rs:148-160

Identified by Warden · wrdn-dos-review · NRR-XKG

});
}
};
Expand Down Expand Up @@ -355,6 +357,7 @@
data,
is_malformed: false,
max_decompressed_section_size: opts.max_decompressed_section_size,
max_inline_depth: opts.max_inline_depth,
Comment thread
sentry-warden[bot] marked this conversation as resolved.
Comment thread
sentry-warden[bot] marked this conversation as resolved.
})
}

Expand Down Expand Up @@ -562,7 +565,13 @@
/// [`has_debug_info`](struct.ElfObject.html#method.has_debug_info).
pub fn debug_session(&self) -> Result<DwarfDebugSession<'data>, DwarfError> {
let symbols = self.symbol_map();
DwarfDebugSession::parse(self, symbols, self.load_address() as i64, self.kind())
DwarfDebugSession::parse(
self,
symbols,
self.load_address() as i64,
self.kind(),
self.max_inline_depth,

Check failure on line 573 in symbolic-debuginfo/src/elf.rs

View check run for this annotation

@sentry/warden / warden: wrdn-dos-review

decompress_section pre-allocates from attacker-controlled size with default-unlimited cap

`decompress_section` reads a decompressed size from the ELF compression header and pre-allocates a buffer via `Vec::with_capacity(size)` before the data is decompressed. The size is superficially checked against `max_decompressed_section_size`, but that option defaults to `None` (treated as `usize::MAX`), so the check is inactive in default configurations. A tiny crafted ELF with a compressed section claiming a huge size causes an allocator abort.
)
}

/// Determines whether this object contains stack unwinding information.
Expand Down
Loading
Loading