Parse __eh_frame DWARF info on macOS for per-PC frame descriptions#430
Parse __eh_frame DWARF info on macOS for per-PC frame descriptions#430
Conversation
CI Test ResultsRun: #24088148738 | Commit:
Status Overview
Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled Summary: Total: 32 | Passed: 32 | Failed: 0 Updated: 2026-04-07 15:23:06 UTC |
There was a problem hiding this comment.
Pull request overview
Adds macOS support for parsing raw __eh_frame DWARF CFI to produce per-PC frame descriptions, improving unwinding for binaries that don’t provide __unwind_info (e.g., GCC-built libs).
Changes:
- Introduces a
DwarfParserconstructor +parseEhFrame()path to parse linear.eh_frame/__eh_framewithout an.eh_frame_hdrindex. - Extends macOS Mach-O parsing to locate
__eh_frameand populateCodeCache’s DWARF table; also addsSymbols::clearParsingCaches()for macOS. - Adds new gtest unit tests for
parseEhFrame().
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
ddprof-lib/src/main/cpp/dwarf.h |
Declares the new raw-eh-frame constructor and parseEhFrame() API. |
ddprof-lib/src/main/cpp/dwarf.cpp |
Implements linear __eh_frame parsing and augmentation handling. |
ddprof-lib/src/main/cpp/symbols_macos.cpp |
Finds __eh_frame in Mach-O __TEXT and wires parsed DWARF into CodeCache. |
ddprof-lib/src/test/cpp/dwarf_ut.cpp |
Adds unit tests exercising the new linear .eh_frame parsing path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Scan-Build Report
Bug Summary
Reports
|
||||||||||||||||||||||||||||||||||||
dfe7b83 to
4466cfb
Compare
Revert static thread_local jmp_buf in walkVM — thread_local access via __tls_get_addr is not async-signal-safe in a dlopen'd agent. Suppress core.StackAddressEscape in scan-build instead: the jmp_buf address is always restored to saved_exception before walkVM returns. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ddprof-lib/src/main/cpp/dwarf.cpp
Outdated
| const char *record_end = record_start + 4 + length; | ||
| if (record_end > section_end) { | ||
| break; | ||
| } |
There was a problem hiding this comment.
record_end is computed via pointer arithmetic (record_start + 4 + length) without validating that length fits within the remaining section size first. With malformed/truncated data, this can trigger pointer overflow/UB before the record_end > section_end guard. Consider checking length > (size_t)(section_end - record_start - 4) (or similar) before forming record_end and bailing out early.
| const char *record_end = record_start + 4 + length; | |
| if (record_end > section_end) { | |
| break; | |
| } | |
| size_t remaining = (size_t)(section_end - record_start); | |
| if ((size_t)length > remaining - 4) { | |
| break; | |
| } | |
| const char *record_end = record_start + 4 + length; |
There was a problem hiding this comment.
Fixed. Added if (length > (size_t)(section_end - record_start) - 4) { break; } before forming record_end.
| // binaries are not produced by the toolchains we target here. | ||
| _ptr++; // skip version |
There was a problem hiding this comment.
CIE parsing advances and dereferences _ptr (_ptr++ / *_ptr == 'z') without first ensuring there’s at least 1 byte available in the record. With a short/empty CIE body this can read past record_end. Add explicit if (_ptr >= record_end) { _ptr = record_end; continue; } (or similar) before skipping the version / reading the augmentation.
| // binaries are not produced by the toolchains we target here. | |
| _ptr++; // skip version | |
| // binaries are not produced by the toolchains we target here. | |
| if (_ptr >= record_end) { | |
| _ptr = record_end; | |
| continue; | |
| } | |
| _ptr++; // skip version | |
| if (_ptr >= record_end) { | |
| _ptr = record_end; | |
| continue; | |
| } |
There was a problem hiding this comment.
Fixed. Added if (_ptr >= record_end) { _ptr = record_end; continue; } guards before the version skip and before the augmentation string read.
ddprof-lib/src/main/cpp/dwarf.cpp
Outdated
| if (_ptr + 2 > record_end) { | ||
| _ptr = record_end; | ||
| continue; | ||
| } | ||
| _code_align = getLeb(); | ||
| _data_align = getSLeb(); |
There was a problem hiding this comment.
The new “bounds check” (if (_ptr + 2 > record_end)) is not sufficient to make getLeb() / getSLeb() safe: LEB128 values can span more than 2 bytes, and getLeb() will keep reading until it finds a byte without the 0x80 continuation bit. With truncated data this can overrun record_end and read arbitrary memory. Consider adding end-bounded LEB decode helpers (e.g., getLeb(end) / getSLeb(end) returning a status) and using them here (and for the FDE augmentation-length LEB).
There was a problem hiding this comment.
Fixed. Added bounded getLeb(end) and getSLeb(end) overloads that stop reading at end, and switched the CIE code_align/data_align reads to use them with record_end as the bound.
| if (_has_z_augmentation) { | ||
| _ptr += getLeb(); // skip augmentation data length + data | ||
| if (_ptr > record_end) { | ||
| break; | ||
| } |
There was a problem hiding this comment.
In the FDE path, _ptr += getLeb() assumes the augmentation-data-length LEB is fully readable. If the record ends mid-LEB, getLeb() can read past record_end (same issue as in CIE parsing). Use a bounded LEB decoder (or at least check _ptr < record_end per-byte) and fail the record if the LEB doesn’t complete within record_end.
There was a problem hiding this comment.
Fixed. The FDE augmentation-data-length read now uses getLeb(record_end) (the bounded overload).
- Add overflow guard before record_end in parseEhFrame - Guard _ptr accesses in short CIE body path - Add bounded getLeb/getSLeb overloads; use in parseEhFrame - Extract DwarfParser::init() to remove constructor duplication - Demote _eh_frame/_eh_frame_size to locals in MachOParser::parse() - Add bounds-guard tests: TruncatedRecord, ShortCieBody, FdeAugDataOverrun - Document table() ownership, CIE/FDE layout assumptions, EOF handling Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
What
Parse raw
__eh_frameDWARF CFI data on macOS to provide per-PC frame descriptions for GCC-built libraries, improving stack unwinding accuracy for functions with non-standard frame layouts (leaf functions,-fomit-frame-pointer).Changes
ddprof-lib/src/main/cpp/dwarf.h/dwarf.cppDwarfParserconstructor accepting a raw(eh_frame, size)pair (no binary-search index).parseEhFrame(): linearly iterates CIE/FDE records, extracts code/data alignment from each CIE, decodes per-PC frame descriptions from FDEs, thenqsorts the result._has_z_augmentationfield: set when the CIE augmentation string starts with'z'. The FDE augmentation-data-length LEB field is only present in that case; guarding the skip prevents corrupting the instruction stream for CIEs with empty or non-zaugmentation. The parser assumes a single CIE per module — the DWARF spec allows multiple CIEs with different augmentation strings, but macOS binaries compiled by clang always emit one CIE per module and this path is only exercised for macOS__eh_framesections.detectedDefaultFrame()tosetDwarfTable(), matching the Linux path and correctly selecting the clang frame layout on aarch64.getLeb()/getSLeb()calls to guard against malformed or truncated__eh_framedata.ddprof-lib/src/main/cpp/symbols_macos.cpp__eh_framesection in the__TEXTsegment during Mach-O parsing.DWARF_SUPPORTEDand the section is present, construct aDwarfParserand callcc->setDwarfTable()with the detected default frame.Symbols::clearParsingCaches()definition (fixes linker error on macOS).ddprof-lib/src/test/cpp/dwarf_ut.cpp(new)parseEhFrame(): empty section, terminator-only, CIE-only, and CIE+FDE.Why
On macOS, GCC-built shared libraries (
libgcc, Homebrew packages) emit__eh_framewith standard DWARF CFI, but no__unwind_info. Previously the profiler fell back to a single library-wide heuristic frame layout for these libraries. Parsing the DWARF FDE table yields exact CFA/FP/PC rules per instruction address, eliminating unwinding failures at function prologues and epilogues.JIRA: PROF-14088