fix(debuginfo): Remove depth limits from function parsing - #1065
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 914a4af. Configure here.
There was a problem hiding this comment.
This raises a question—do we want the iterative Drop/Debug after all, now that we've taken away the parsing limits? The Debug impl at least is very low value compared to the complication it introduces, IMO.
There was a problem hiding this comment.
Without an iterative drop impl, symbolic can still stack-overflow on carefully-crafted dwarf files. The fmt::Debug impl is a bit more of a wash--it's only really used in tests, but it's pretty helpful to have something that will always work.
There was a problem hiding this comment.
The Drop makes sense, but I think the Debug is far more trouble than it's worth. I'd remove it.
We will now handle function parsing in both symcache convertor and dwarf DIE parsing iteratively, so there is no longer a need for bounding these by function depth. This also required introducing an iterative fmt::Debug implementation for Function, authored by Claude.
7670344 to
69cc4ca
Compare
| /// End address of the entire function body, including inlined functions. | ||
| /// | ||
| /// This address points at the first instruction after the function body. | ||
| pub fn end_address(&self) -> u64 { | ||
| self.address.saturating_add(self.size) | ||
| } | ||
| } | ||
|
|
||
| /// A dynamically dispatched iterator over items with the given lifetime. | ||
| pub type DynIterator<'a, T> = Box<dyn Iterator<Item = T> + 'a>; | ||
|
|
There was a problem hiding this comment.
Bug: The recursive fmt::Debug implementation for Function can cause a stack overflow when formatting deeply nested inlinees, which are now possible after depth limits were removed.
Severity: MEDIUM
Suggested Fix
Replace the recursive fmt::Debug implementation for Function with an iterative one. This can be done by manually iterating through the inlinees and formatting them without making a recursive call to f.debug_struct, similar to the approach taken for the Drop implementation.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: symbolic-debuginfo/src/base/mod.rs#L773-L787
Potential issue: The pull request removes the parsing depth limit for inline functions,
which allows for the creation of deeply nested `Function` structures. However, the
`fmt::Debug` implementation for `Function` remains recursive. When formatting a deeply
nested `Function` (for example, through logging or an assertion failure using `{:?}`),
the recursive calls to format the `inlinees` field will lead to a stack overflow and
crash the application. While a custom iterative `Drop` was added to prevent this issue
during object destruction, the debug formatting logic was not updated similarly, leaving
a latent vulnerability.
| next_line: Option<&'s [u8]>, | ||
| inline_origin_map: BreakpadInlineOriginMap<'s>, | ||
| lines: Lines<'s>, | ||
| max_function_parse_depth: u32, | ||
| } | ||
|
|
||
| impl<'s> BreakpadFunctionIterator<'s> { | ||
| fn new( | ||
| file_map: &'s BreakpadFileMap<'s>, | ||
| mut lines: Lines<'s>, | ||
| max_function_parse_depth: u32, | ||
| ) -> Self { | ||
| fn new(file_map: &'s BreakpadFileMap<'s>, mut lines: Lines<'s>) -> Self { | ||
| let next_line = lines.next(); | ||
| Self { | ||
| file_map, | ||
| next_line, | ||
| inline_origin_map: Default::default(), | ||
| lines, | ||
| max_function_parse_depth, | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Depth limit removed but Function Debug/Clone still recurse on inlinees
Removing max_function_parse_depth allows deep Breakpad inline trees, but Function's fmt::Debug and derived Clone still recurse on inlinees (unlike Drop), so formatting or cloning a crafted deep tree can stack-overflow abort the process.
Evidence
BreakpadFunctionIteratorno longer stores or enforcesmax_function_parse_depth;INLINEnest levels are passed straight intoFunctionBuilderInlinee.depth.FunctionBuilder::add_inlineeonly rejectsaddress < self.address; the comment still mentions a depth limit but no depth check remains.symbolic-symcache/tests/breakpad.rstest_breakpad_deep_inline_functionsbuilds and converts depth-1000 inline trees successfully after this change.Functionhas an iterativeDropto avoid inlinee recursion, butfmt::Debugstill does.field("inlinees", &self.inlinees)and#[derive(Clone)]remains recursive—contrary to the PR claim of an iterative Debug impl.
Identified by Warden · wrdn-dos-review · ZP3-TAA
| @@ -357,7 +355,6 @@ impl<'data> ElfObject<'data> { | |||
| data, | |||
| is_malformed: false, | |||
| max_decompressed_section_size: opts.max_decompressed_section_size, | |||
There was a problem hiding this comment.
ELF section decompression defaults to unbounded allocation
With default ParseObjectOptions, decompress_section treats the attacker-controlled decompressed size as unlimited (unwrap_or(usize::MAX)) and does Vec::with_capacity(size), so a tiny compressed ELF section can force a multi-GB allocation/OOM; set a finite max_decompressed_section_size by default or refuse decompression when unset.
Evidence
ParseObjectOptions::default()leavesmax_decompressed_section_sizeasNone(object.rs).- That option is stored on
ElfObjectat the hunk line and used indecompress_sectionasself.max_decompressed_section_size.unwrap_or(usize::MAX)beforeVec::with_capacity(size)/zstd::bulk::decompress(..., size). sizecomes from the GNUZLIBheader or ELFCompressionHeader.ch_sizein the untrusted section bytes.ElfObject::parse/Dwarf::sectiontake this path for compressed debug sections with no other cap when the option is unset.
Identified by Warden · wrdn-dos-review · YQ7-HNR

We will now handle function parsing in both symcache convertor and dwarf DIE parsing iteratively, so there is no longer a need for bounding these by function depth.
This also required introducing an iterative fmt::Debug implementation for Function, authored by Claude.