Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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 @@ -588,7 +583,10 @@
prefer_dwarf_names: bool,
}

impl<'d, 'a> DwarfUnit<'d, 'a> {

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

View check run for this annotation

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

[EVE-PMU] parse_function / parse_function_children lack depth limit for nested DW_TAG_subprogram (additional location)

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.
/// 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,7 +917,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);
}

// Resolve functions in the symbol table first. Only if there is no entry, fall back
Expand All @@ -936,7 +944,12 @@
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()
})
Expand All @@ -951,7 +964,13 @@
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();
Expand All @@ -960,6 +979,7 @@
self.parse_function_children(
depth,
0,
remaining_inline_depth,
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

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.

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,

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

View check run for this annotation

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

[EVE-PMU] parse_function / parse_function_children lack depth limit for nested DW_TAG_subprogram (additional location)

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.
}
}

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

[EVE-PMU] parse_function / parse_function_children lack depth limit for nested DW_TAG_subprogram (additional location)

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.
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,

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

View check run for this annotation

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

ELF decompress_section defaults to unlimited size, allowing unbounded allocation

`max_decompressed_section_size` defaults to `None`, which `decompress_section` unwraps to `usize::MAX`. An attacker-controlled compressed section header can declare any decompressed size, causing `Vec::with_capacity(size)` or `zstd::bulk::decompress` to attempt a multi-gigabyte allocation before any bytes are validated against real input.

Check warning on line 360 in symbolic-debuginfo/src/elf.rs

View check run for this annotation

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

Unbounded symbol table count passed to goblin without caller-side limit

`parse_with_opts` derives `count = shdr.sh_size / shdr.sh_entsize` from attacker-controlled ELF section headers and passes it unbounded to `elf::Symtab::parse`. If `sh_entsize` is 1 and `sh_size` is huge, goblin may pre-allocate a Vec with up to `usize::MAX` entries before validating against the actual data length.
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(

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

View check run for this annotation

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

[EVE-PMU] parse_function / parse_function_children lack depth limit for nested DW_TAG_subprogram (additional location)

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.
self,
symbols,
self.load_address() as i64,
self.kind(),
self.max_inline_depth,
)
}

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