DwarfFrameParser's global cache holds non-owning Dwarf/Elf handles and is never evicted

Open
#2,435 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
48/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
c, cpp
Domain
backend, databases

Research direction

Start in dwarf/src/dwarfFrameParser.C and trace frameParsers, DwarfFrameParser::create() and setupCFIData; then inspect DwarfHandle destruction and the reader-lifetime paths described in the issue. Compare cache eviction with shared ownership, accounting for both dangling handles and pointer-address reuse. Done means closing a reader cannot leave a usable stale cache entry; add regression coverage for that lifetime case and the reported unwind tests.

Written by the indexing model from the issue text.

Description

DwarfFrameParser keeps a process-global cache that is keyed on raw pointers, holds them
without owning them, and is never evicted from
(dwarf/src/dwarfFrameParser.C):

struct frameParser_key {
  Dwarf *dbg;
  Elf *eh_frame;
  Architecture arch;
  ...
};
std::map<frameParser_key, DwarfFrameParser::Ptr> frameParsers;

create() only ever inserts; grep frameParsers finds no erase, clear or invalidate anywhere.
The Dwarf * and Elf * are owned by DwarfHandle, whose destructor calls dwarf_end().
Destroying the owner therefore leaves live cache entries pointing at freed handles.

1. Use-after-free

Any code that opens a symbol reader for a library nothing else holds, and then closes it,
destroys that library's Symtab, ends its handles, and leaves the cached parser using them.

Observed on ppc64le (RHEL 8, POWER9), stack from gdb:

#0  __gelf_getehdr_rdlock (elf=0x1188dc80)
#2  dwarf_getcfi_elf (elf=0x1188dc80)        dwarf_getcfi_elf.c:325
#6  DwarfFrameParser::setupCFIData            dwarfFrameParser.C:441
#7  DwarfFrameParser::getRegsForFunction
#12 DwarfWalker::getFrameBase                 dwarfWalker.C:1683
#13 DwarfWalker::parseSubprogram
#18 DwarfWalker::parse._omp_fn.0

Reproduction rates with a closeSymbolReader() on that path, per-test isolated, 10 runs each:

test with the close without it master
test1_27 10/10 3/10 3/10
test1_17 6/10 0/10 0/10
test1_3 6/10 0/10 0/10
snip_ref_shlib_var 2/10 0/10 0/10

(The residual 3/10 on test1_27 is the unrelated parallel-DWARF race in #2407.)

This is reachable from ordinary API use: SymtabReaderFactory reference counting is sound
(findOpenSymtab() does take a reference), so a caller that opens and closes symmetrically —
exactly what the refcount invites — can free handles the cache still holds.

2. The quieter problem: wrong parser on address reuse

Because the key is raw pointer values and entries are never removed, an address reused by a
later allocation matches a stale entry, and create() returns the parser for a different
file
. That is incorrect unwind data rather than a crash, with nothing to report it. I have not
constructed a case for this; it follows from the code.

Why it has not been hit more often

Every current caller avoids closing. common/src/addrtranslate-sysv.C has its
closeSymbolReader() commented out, and int_thread_db.C leaves its readers open — #2409 had
to keep doing so, with a comment, specifically because of this. So the tree currently works by
never releasing readers, which is a leak standing in for a lifetime fix.

Possible directions

  • Evict on destruction: have DwarfHandle's destructor (or Elf_X teardown) purge entries
    referencing the handles being ended. There is already a mutex for it.
  • Or give the parser shared ownership of what it points at, so the cache cannot outlive it.
  • Keying on something stable (file identity) rather than pointer values would remove the
    address-reuse hazard independently.

Found while working on #2409; not fixed there, deliberately.

Dominant language
C
Stars
836
Forks
177
Avg merge
3d 20h
Merged PRs (30d)
61

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dyninst/dyninst

All issues in dyninst/dyninst

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.