Skip to content

Parse __eh_frame DWARF info on macOS for per-PC frame descriptions#430

Open
jbachorik wants to merge 4 commits intomainfrom
jb/prof-14088
Open

Parse __eh_frame DWARF info on macOS for per-PC frame descriptions#430
jbachorik wants to merge 4 commits intomainfrom
jb/prof-14088

Conversation

@jbachorik
Copy link
Copy Markdown
Collaborator

@jbachorik jbachorik commented Mar 22, 2026

What

Parse raw __eh_frame DWARF 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.cpp

  • Add new DwarfParser constructor accepting a raw (eh_frame, size) pair (no binary-search index).
  • Implement parseEhFrame(): linearly iterates CIE/FDE records, extracts code/data alignment from each CIE, decodes per-PC frame descriptions from FDEs, then qsorts the result.
  • Add _has_z_augmentation field: 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-z augmentation. 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_frame sections.
  • Pass detectedDefaultFrame() to setDwarfTable(), matching the Linux path and correctly selecting the clang frame layout on aarch64.
  • Add bounds check in CIE parsing before getLeb()/getSLeb() calls to guard against malformed or truncated __eh_frame data.

ddprof-lib/src/main/cpp/symbols_macos.cpp

  • Detect __eh_frame section in the __TEXT segment during Mach-O parsing.
  • When DWARF_SUPPORTED and the section is present, construct a DwarfParser and call cc->setDwarfTable() with the detected default frame.
  • Add missing Symbols::clearParsingCaches() definition (fixes linker error on macOS).

ddprof-lib/src/test/cpp/dwarf_ut.cpp (new)

  • Four gtest unit tests covering parseEhFrame(): empty section, terminator-only, CIE-only, and CIE+FDE.

Why

On macOS, GCC-built shared libraries (libgcc, Homebrew packages) emit __eh_frame with 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

@jbachorik jbachorik changed the title PROF-14088 Parse __eh_frame DWARF info on macOS for per-PC frame descriptions Mar 22, 2026
@dd-octo-sts
Copy link
Copy Markdown

dd-octo-sts bot commented Mar 22, 2026

CI Test Results

Run: #24088148738 | Commit: fc2c27a | Duration: 22m 32s (longest job)

All 32 test jobs passed

Status Overview

JDK glibc-aarch64/debug glibc-amd64/debug musl-aarch64/debug musl-amd64/debug
8 - - -
8-ibm - - -
8-j9 - -
8-librca - -
8-orcl - - -
11 - - -
11-j9 - -
11-librca - -
17 - -
17-graal - -
17-j9 - -
17-librca - -
21 - -
21-graal - -
21-librca - -
25 - -
25-graal - -
25-librca - -

Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled

Summary: Total: 32 | Passed: 32 | Failed: 0


Updated: 2026-04-07 15:23:06 UTC

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 DwarfParser constructor + parseEhFrame() path to parse linear .eh_frame / __eh_frame without an .eh_frame_hdr index.
  • Extends macOS Mach-O parsing to locate __eh_frame and populate CodeCache’s DWARF table; also adds Symbols::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.

@dd-octo-sts
Copy link
Copy Markdown

dd-octo-sts bot commented Apr 3, 2026

Scan-Build Report

User:runner@runnervmrg6be
Working Directory:/home/runner/work/java-profiler/java-profiler/ddprof-lib/src/test/make
Command Line:make -j4 all
Clang Version:Ubuntu clang version 18.1.3 (1ubuntu1)
Date:Fri Apr 3 14:40:28 2026

Bug Summary

Bug TypeQuantityDisplay?
All Bugs1
Logic error
Stack address stored into global variable1

Reports

Bug Group Bug Type ▾ File Function/Method Line Path Length
Logic errorStack address stored into global variablestackWalker.cppwalkVM84837

@jbachorik jbachorik force-pushed the jb/prof-14088 branch 3 times, most recently from dfe7b83 to 4466cfb Compare April 3, 2026 14:38
@jbachorik jbachorik marked this pull request as ready for review April 3, 2026 14:51
@jbachorik jbachorik requested a review from a team as a code owner April 3, 2026 14:51
@jbachorik jbachorik marked this pull request as draft April 3, 2026 15:43
jbachorik and others added 2 commits April 3, 2026 18:14
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>
@jbachorik jbachorik marked this pull request as ready for review April 3, 2026 16:49
@jbachorik jbachorik requested a review from Copilot April 7, 2026 10:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +186 to +189
const char *record_end = record_start + 4 + length;
if (record_end > section_end) {
break;
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. Added if (length > (size_t)(section_end - record_start) - 4) { break; } before forming record_end.

Comment on lines +202 to +203
// binaries are not produced by the toolchains we target here.
_ptr++; // skip version
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
// 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;
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. Added if (_ptr >= record_end) { _ptr = record_end; continue; } guards before the version skip and before the augmentation string read.

Comment on lines +207 to +212
if (_ptr + 2 > record_end) {
_ptr = record_end;
continue;
}
_code_align = getLeb();
_data_align = getSLeb();
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

Comment on lines +223 to +227
if (_has_z_augmentation) {
_ptr += getLeb(); // skip augmentation data length + data
if (_ptr > record_end) {
break;
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants