Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -871,9 +878,10 @@
///
/// On return, the `entries` iterator is placed after the attributes of the last-read DIE.
fn parse_function(
&self,

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

View check run for this annotation

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

[QEF-D9J] Nested DW_TAG_subprogram recursion has no depth limit (additional location)

`max_inline_depth` only bounds `DW_TAG_inlined_subroutine` nesting, but nested `DW_TAG_subprogram` entries recurse through `parse_function` → `parse_function_children` without decrementing any limit, so a crafted DWARF binary with deeply nested subprograms can still exhaust the native stack.
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 @@ -1075,10 +1112,10 @@
// tooling created garbage reversed ranges which we filtered out.
// In the dead code case, a surrogate DIE remains with `DW_AT_low_pc(0)` and empty ranges.
// That DIE might still contain inlined functions with actual ranges - these must be skipped.
// However, non-inlined functions may be present in this subtree, so we must still descend

Check warning on line 1115 in symbolic-debuginfo/src/dwarf.rs

View check run for this annotation

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

parse_inlinee early return bypasses inline recursion limit

When an inline subroutine has empty ranges, `parse_inlinee` delegates to `parse_functions` without decrementing `remaining_inline_depth`. Since `parse_functions` can encounter nested subprograms whose children include further `DW_TAG_inlined_subroutine` entries, a crafted chain of empty-range inlinees (interleaved with subprograms) can recurse arbitrarily without ever reducing the depth budget, bypassing the intended stack-overflow protection.
// 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

Nested DW_TAG_subprogram recursion has no depth limit

`max_inline_depth` only bounds `DW_TAG_inlined_subroutine` nesting, but nested `DW_TAG_subprogram` entries recurse through `parse_function` → `parse_function_children` without decrementing any limit, so a crafted DWARF binary with 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 All @@ -1706,10 +1753,11 @@
/// Returns an iterator over all functions in this debug file.
pub fn functions(&self) -> DwarfFunctionIterator<'_> {
DwarfFunctionIterator {
units: self.cell.get().units(self.bcsymbolmap.as_deref()),

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

View check run for this annotation

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

[QEF-D9J] Nested DW_TAG_subprogram recursion has no depth limit (additional location)

`max_inline_depth` only bounds `DW_TAG_inlined_subroutine` nesting, but nested `DW_TAG_subprogram` entries recurse through `parse_function` → `parse_function_children` without decrementing any limit, so a crafted DWARF binary with deeply nested subprograms can still exhaust the native stack.
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) {
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 @@ -170,7 +171,8 @@
elf: obj,
data,
is_malformed: true,
max_decompressed_section_size: opts.max_decompressed_section_size,
max_inline_depth: opts.max_inline_depth,

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

View check run for this annotation

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

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.
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 @@ -354,7 +356,8 @@
elf: obj,
data,
is_malformed: false,
max_decompressed_section_size: opts.max_decompressed_section_size,

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

View check run for this annotation

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

[NRR-XKG] ELF decompression size guard defaults to unlimited, allowing OOM abort on tiny input (additional location)

`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.
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,
)
}

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