Skip to content

symtab/elf: bound symbols by st_size to avoid misattributing stripped code#4

Open
gecube wants to merge 1 commit into
coroot:mainfrom
gecube:fix/stripped-binary-symbol-misattribution
Open

symtab/elf: bound symbols by st_size to avoid misattributing stripped code#4
gecube wants to merge 1 commit into
coroot:mainfrom
gecube:fix/stripped-binary-symbol-misattribution

Conversation

@gecube

@gecube gecube commented Jul 6, 2026

Copy link
Copy Markdown

Problem

Reported downstream in coroot/coroot-node-agent#262: when profiling a stripped binary (an Envoy/Istio sidecar) where most symbols are removed but a few dynamic symbols are kept in .dynsym (needed by dlopen'd Go/Lua modules), the profiler shows large amounts of time in exported functions that are never actually called — e.g. envoyGoFilterLogLevel. Compiling that function out just moves the time to the next surviving exported symbol (luaopen_jit).

Root cause

SymbolTable.Resolve uses PCIndex.FindIndex(addr), which returns the symbol with the greatest Value <= addr and no upper bound. The only guard is addr < firstSymbolValue. In a stripped binary whose symbol table holds only a handful of exported .dynsym entries, the nearest surviving symbol below a PC can be arbitrarily far away, so every sample that lands in a stripped-out function is attributed to whichever exported symbol precedes it.

Each ELF symbol's st_size was already read in getSymbols64/getSymbols32 but explicitly discarded (//Size: ... // not used), so there was no information to bound a symbol's range.

Fix

  • Keep st_size on SymbolIndex and carry it into FlatSymbolIndex.Sizes (parallel to Names/Values).
  • In Resolve, when the matched symbol's size is known, reject PCs at or beyond Value+Size and return "" (unknown) instead of the nearest symbol. This is what the downstream issue author asked for: show unknown rather than an unrelated symbol.
  • Symbols with st_size == 0 (size unknown, e.g. some hand-written asm) keep the previous nearest-symbol behavior — no regression for binaries that omit sizes.
  • Drive-by: the 32-bit symbol path never set symbols[i].Value; populate Value/Size there too.

GoTable.Resolve already applies an equivalent upper bound via Index.End; this brings the plain ELF SymbolTable in line.

Tests

  • New TestSymbolTableResolveBounds encodes the issue scenario (two exported symbols with real sizes plus a size-0 symbol) and asserts PCs in the gaps resolve to "", PCs inside a symbol resolve to its name, and size-0 symbols retain legacy behavior.
  • Existing TestElfSymbolComparison across all testdata/elfs fixtures still passes (Names/Values are unchanged; Sizes is additive).

Reproduction mechanics

A stripped ELF that mirrors the Envoy case (default-visibility functions exported via -rdynamic, everything else hidden, then strip --strip-all removing .symtab but keeping .dynsym), resolved through this package:

Before:

REAL FUNCTION    in .dynsym  RESOLVES TO
exported_low     yes         exported_low       OK
hidden_hot_a     no          exported_low       <- misattributed
hidden_hot_b     no          exported_low       <- misattributed
exported_high    yes         exported_high      OK
hidden_hot_c     no          exported_high      <- misattributed

After: the three hidden_* PCs resolve to <unknown> instead of an unrelated exported symbol.

… code

SymbolTable.Resolve mapped a PC to the symbol with the greatest value <= pc
with no upper bound. In a stripped binary that keeps only a handful of exported
.dynsym symbols (e.g. an Envoy sidecar that exports symbols for dlopen'd Go/Lua
modules while .symtab is stripped), the nearest surviving symbol can sit
arbitrarily far below the PC. As a result, samples that actually land in
stripped-out functions were attributed to an unrelated exported symbol, and
removing that symbol just shifted the samples to the next surviving one.

st_size was already parsed out of each Elf symbol but discarded. Keep it and
bound each symbol to [Value, Value+Size): when the size is known and the PC
falls past the symbol's end, resolve to "" (unknown) instead of the nearest
symbol. Symbols with size 0 (unknown) keep the previous nearest-symbol
behavior, so nothing regresses for binaries that omit sizes.

Also populate Value/Size in the 32-bit symbol path, which previously left the
symbol value unset.

Signed-off-by: Gaál György <gb12335@gmail.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.

1 participant