Files
reverse-engineering-anything/docs/specs/native/elf-unwind-ranges/spec.md
T
43f06eb1e8 feat(native): recover ELF function ranges from unwind tables (#9585)
* feat(native): recover ELF function ranges from unwind tables

inspect_elf_unwind_ranges and inspect-elf-unwind-ranges read .eh_frame
and .eh_frame_hdr from one local ELF32 or ELF64 image and list the
function ranges its FDEs name, with CIE facts, LSDA and personality,
the containing section and an exact symbol where one starts at the
address. Nothing is executed and no disassembler is needed. Pointer
encodings follow the DW_EH_PE rules with raw zero as null, discarded
FDEs are counted and skipped, the .eh_frame_hdr search table is
compared with the decoded FDEs, and record, row and symbol budgets are
bounded. verify:elf:unwind-ranges compares the reported starts with
llvm-dwarfdump or readelf on a compiled C++ program.

* fix(elf): keep unresolved unwind facts out of reported ranges

Skip FDEs whose initial location is indirect and do not locate
.eh_frame through an indirect eh_frame_ptr, since pointer slots are
not functions. Stop an unbounded header-located scan at a CIE with an
unrecognized augmentation, report ignored duplicate PT_GNU_EH_FRAME
headers as incomplete coverage, and accept a null personality stored
under an indirect encoding.

Keep only symbols at decoded FDE starts, cache name observations per
string-table offset, and decode names only for the returned page under
a fixed 16 MiB text allowance, so repeated or long names no longer
grow memory with the symbol count.

* fix(scripts): run the ELF unwind verifier under owned process trees

Launch probes, compiler drivers, the stripper, the frame dump and the
inspection through createVerifierCommands, as verify-binary-hardening
does, so a timeout or interrupt stops the whole tree. The temporary
directory is removed only after the owned commands have closed.

* docs(elf): move ELF unwind ranges guide under docs/guides/native

The documentation guide assigns analyst workflows to docs/guides/<domain>/.
Move the guide there, place its navigation and directory entries in the
Native binaries group, and update links from the docs index, CLI guide,
spec and authored skill. The public /elf-unwind-ranges route is unchanged.

* fix(elf): give unwind text limits their own recovery and skip indirect tables

Throw a dedicated failure when a page's symbol names exceed the 16 MiB
text allowance, and map it to a resource error that tells the caller
to request a smaller limit rather than raise max_ranges.

Read a CIE augmentation with one bounded NUL search and keep it as a
byte view, decoded only for returned CIEs, so a long augmentation is
never built up character by character.

Leave an .eh_frame_hdr search table with an indirect encoding unread:
the header is incomplete, the comparison is unavailable, and the table
no longer bounds a header-located .eh_frame scan.

* fix(elf): resolve header count and table forms and name IFUNC resolvers

Leave a non-zero .eh_frame_hdr FDE count under an indirect encoding
unresolved, since it is the address of a slot that is not followed, and
mark the header incomplete without reading the table.

Treat a DW_EH_PE_omit table encoding as an omitted search table before
testing the indirect bit, so a header that declares a count without a
table stays present with complete coverage, and accept that form in
output validation.

Match STT_GNU_IFUNC symbols at exact FDE starts after function symbols,
with a limitation that the name is the resolver, not the implementation
it selects at run time.

* fix(elf): require allocated unwind pointer bases

* fix(elf): map unwind header relative base

* fix(elf): validate unwind load mappings

* test(elf): cover mapped nonallocated frame agreement

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-09 21:42:53 +08:00

5.1 KiB

ELF unwind function ranges

Problem and outcome

A stripped ELF often has no symbol table, but its .eh_frame still names the start and end of nearly every function so exceptions can unwind. REA can list symbols and disassemble with a provider, but it cannot recover function boundaries from unwind tables, so an agent without a disassembler has no function list for a stripped binary.

This feature adds inspect_elf_unwind_ranges and inspect-elf-unwind-ranges. They read .eh_frame and .eh_frame_hdr from one selected local ELF32 or ELF64 image and list the function ranges those tables name, without executing the image.

ELF unwind function ranges describes the current behavior and limitations. This directory is the feature specification, implementation plan and task list. The guide does not replace these files.

Requirements

  1. .eh_frame comes from section headers, or from PT_GNU_EH_FRAME when section headers are missing. The source and examined file range are reported. SHT_NOBITS, out-of-file and truncated tables are incomplete. Program headers come from the shared section directory, which rejects an unreadable program-header table, so a missing PT_GNU_EH_FRAME is observed absence. An .eh_frame located only through the header's eh_frame_ptr is read no further than the last FDE a decoded search table names. Without a usable table it stops at the first undecodable record, including a CIE with an unrecognized augmentation and its FDEs, or at the end of its PT_LOAD without a terminator, and coverage is incomplete.
  2. CIEs report version, augmentation, alignment factors, return-address register, pointer and LSDA encodings, personality, and signal-frame and B-key flags. A page returns only the CIEs its ranges name, so CIE output is bounded by limit, and coverage.total_cies counts every decoded CIE. CFA programs are counted, not evaluated. A CIE with an unrecognized augmentation letter is not decoded, its FDEs are skipped with a counted limitation, and coverage is incomplete.
  3. FDEs become ranges with start, end, size, containing SHF_ALLOC section, an exact symbol name when one starts at that address (a function, then an STT_GNU_IFUNC resolver, then an untyped symbol), CIE index, LSDA and personality, paged by offset and limit.
  4. Encoded pointers follow the DW_EH_PE rules. A raw zero is a null pointer. Resolved addresses and FDE ends wrap at the ELF32 or ELF64 address width. datarel in .eh_frame resolves against DT_PLTGOT, then .got.plt or .got, and an unresolved base leaves the record out with coverage incomplete. Indirect pointers are not dereferenced: an FDE with an indirect initial location is skipped with a counted limitation, and an indirect eh_frame_ptr does not locate .eh_frame, and a search table with an indirect encoding is neither compared nor used as a scan bound. Each leaves coverage incomplete.
  5. A 64-bit extended length keeps the .eh_frame CIE ID and pointer at 4 bytes.
  6. An FDE with a zero initial location is a discarded function. It is counted as examined, not reported, and a limitation gives the count.
  7. .eh_frame_hdr is decoded and its search table compared with the decoded FDEs as agree, disagree or unavailable. The table is read for at most max_ranges rows and no more rows than its bytes hold. A larger declared count makes the header incomplete with a limitation. A declared count with an omitted table is a present header with comparison unavailable. A non-zero indirect count is not followed, so the count is unknown and the header incomplete.
  8. .debug_frame presence is reported but not decoded.
  9. Budgets bound the file size, CIE and FDE counts, page size and the symbol lookup across .symtab and .dynsym. Only symbols at decoded FDE starts are kept, names are decoded only for the returned page, and that text has a fixed 16 MiB allowance whose recovery is a smaller limit. Only the first PT_GNU_EH_FRAME is read; a duplicate one that supplies the header makes coverage incomplete. Section and symbol lookups use indexes built once per image, so cost does not grow as sections times entries. Address translation uses SHF_ALLOC sections, then PT_LOAD; a non-ALLOC section's sh_addr is never used.
  10. Symbol names, section names and augmentation strings are marked target-derived and untrusted. MCP requires an absolute path.

Acceptance

  • Unit tests cover CIE and FDE decoding for both widths and endiannesses, pointer encodings, extended lengths, zero pointers, discarded FDEs, header agreement and caps, and the GOT base.
  • Service tests validate the report shape, including incomplete headers and null LSDAs.
  • CLI acceptance, filesystem boundary and MCP boundary tests exercise the public contract.
  • npm run verify:elf:unwind-ranges compares the reported FDE starts with readelf or llvm-dwarfdump on a compiled C++ program.

Non-goals

  • Decoding .debug_frame or evaluating CFA programs.
  • Applying relocations in relocatable objects.
  • Following indirect EH pointers.