LLVM Bugzilla is read-only and represents the historical archive of all LLVM issues filled before November 26, 2021. Use github to submit LLVM bugs

Bug 46829 - CFI_Parser::findFDE's end-of-section calculation looks broken
Summary: CFI_Parser::findFDE's end-of-section calculation looks broken
Status: RESOLVED FIXED
Alias: None
Product: Runtime Libraries
Classification: Unclassified
Component: libunwind (show other bugs)
Version: trunk
Hardware: PC Linux
: P enhancement
Assignee: Unassigned LLVM Bugs
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2020-07-23 23:32 PDT by Ryan Prichard
Modified: 2020-09-16 19:06 PDT (History)
2 users (show)

See Also:
Fixed By Commit(s):


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Ryan Prichard 2020-07-23 23:32:48 PDT
CFI_Parser<A>::findFDE scans an .eh_frame section looking for an FDE matching a PC. The start of .eh_frame is passed as an argument, ehSectionStart, and the function calculates the end of .eh_frame (ehSectionEnd):

    /// Scan an eh_frame section to find an FDE for a pc
    template <typename A>
    bool CFI_Parser<A>::findFDE(A &addressSpace, pint_t pc, pint_t ehSectionStart,
                                uint32_t sectionLength, pint_t fdeHint,
                                FDE_Info *fdeInfo, CIE_Info *cieInfo) {
      //fprintf(stderr, "findFDE(0x%llX)\n", (long long)pc);
      pint_t p = (fdeHint != 0) ? fdeHint : ehSectionStart;
      const pint_t ehSectionEnd = p + sectionLength;
      while (p < ehSectionEnd) {

There are two problems with the calculation:

 * AFAICT, when a hint is provided, ehSectionEnd should still use ehSectionStart instead of p.

 * sectionLength comes from UnwindInfoSections::dwarf_section_length, which on targets using dl_iterate_phdr, is the length of a PT_LOAD segment, not the length of .eh_frame.

To expand on the second bullet point, UnwindInfoSections has (among others) these five fields:

 - dso_base
 - dwarf_section
 - dwarf_section_length
 - dwarf_index_section
 - dwarf_index_section_length

dwarf_index_section and dwarf_index_section_length are the start and size of the .eh_frame_hdr section, which indexes into .eh_frame and allows binary searching.

I'm not sure what dso_base is, generally, but for targets that use dl_iterate_phdr, it seems to be the start of a PT_LOAD segment associated with the unwind info.

dwarf_section is always the start of .eh_frame.

For most configurations, dwarf_section_length is the size of .eh_frame, and UnwindCursor::getInfoFromDwarfSection assumes this is the case when it calls CFI_Parser::findFDE. For targets using dl_iterate_phdr, however, dwarf_section_length is instead the size of the PT_LOAD segment indicated by dso_base.

The recently-added FrameHeaderCache uses (dso_base, dwarf_section_length) to indicate each cache entry's range of PC values. That happens to match how we're initializing those fields. (I think initializing dwarf_section_length to the PT_LOAD size predated the new cache.)

I'm unsure of the practical consequences. I suppose if the unwinder tries to look up a function with no FDE, then it will enter this .eh_frame linear scan fallback, and it could read past the end of .eh_frame, unless there's an end marker (cfiLength == 0, "return false; // end marker").

---

Aside: On targets that use .eh_frame_hdr, it seems like a waste of code to scan .eh_frame (and a waste of memory in FrameHeaderCache to track .eh_frame bounds). I think these targets have to locate .eh_frame using .eh_frame_hdr, so it shouldn't ever find a match. Some Apple targets are different, because the compact unwind table can defer to using the .eh_frame scan (compactSaysUseDwarf).

Aside 2: If the .eh_frame scan does find a match, it would add the FDE to DwarfFDECache, whose entries are not invalidated on module unload (except with Apple OS's). On Android, the lack of automatic per-module invalidation should be OK as long as DwarfFDECache is only used for manually-registered FDEs (__register_frame / __unw_add_dynamic_fde).

Aside 3: The DwarfFDECache offset in getInfoFromDwarfSection is not quite a "hint". It's used as an offset into .eh_frame, so if it's wrong, we'll try to decode arbitrary data and may crash. If the hinted-scan ends without a match, then we try again with a complete .eh_frame scan. If *that* scan turns up an FDE, then we add it to the end of DwarfFDECache. Adding an entry doesn't remove the earlier entry, though, so AFAICT, each unwind lookup would find the bad hint and add another good hint to the end.
Comment 1 Ryan Prichard 2020-07-24 18:20:27 PDT
I think the size of the PT_LOAD segment is usually larger than the size of .eh_frame, because .text and .eh_frame are usually in the same segment. That's not the case for execute-only-memory (XOM) ("-mexecute-only -fuse-ld=lld"). Android has currently backed off of XOM, and I don't know whether/when it will reenable it.

It looks like the dl_iterate_phdr code was added in https://reviews.llvm.org/D6848. The .eh_frame_hdr doesn't provide the size of the .eh_frame[1], so I think we're really just trying to come up with a fake value that won't break CFI_Parser<A>::findFDE. I don't think we really want to call findFDE. Assuming .eh_frame ends with a CIE==0 marker, then findFDE doesn't actually need to know the size of .eh_frame, but it checks the size as a safety-check (I think).

(The safety-check will not work well if the end-of-section value is too big. If it is too small, then .eh_frame is truncated.)

This bug has also been reported against Rust ("ehSectionEnd is incorrectly set too big")[2]. A comment there claims that ld and/or gold don't always add a CIE==0 end marker.

[1] https://refspecs.linuxfoundation.org/LSB_1.3.0/gLSB/gLSB/ehframehdr.html
[2] https://github.com/rust-lang/rust/issues/47551#issuecomment-406709987
Comment 2 Ryan Prichard 2020-07-24 20:20:12 PDT
With ld.bfd and ld.gold, I think the end marker is expected to come from an ending crt*.o file. LLD, on the other hand, seems to discard the terminating .eh_frame section (it's unused?), and unconditionally output an end marker regardless of its input files.

The Android (Bionic) crtend*.o files only output the end marker for __i386__ and __x86_64__ architectures. I think it's an accident to omit arm64. The libc.so on older Android versions may have a end marker at the start of .eh_frame instead of the end (e.g. /system/lib[64]/libc.so in the Android 6.0 x86_64 emulator AVD). (Maybe it can appear in the middle, but I haven't seen that yet.)

One result is that most app shared objects on Android/arm64 currently won't have the end marker. (But that should start to change with the NDK switch to LLD.)
Comment 3 Ryan Prichard 2020-09-14 17:16:01 PDT
> That's not the case for execute-only-memory (XOM) ("-mexecute-only -fuse-ld=lld").

The problem is more general than this. When I build for x86_64 Linux, ld.bfd and LLD put .eh_frame into an R segment and .text into a separate R+E segment. (ld.gold seems to put both sections into an R+E segment.) XOM isn't needed.
Comment 4 Ryan Prichard 2020-09-16 19:06:12 PDT
Fixed by D87750.