Skip to content

elf: Only match sizeless .dynsym symbols on exact addresses#1610

Merged
d-e-s-o merged 1 commit into
libbpf:mainfrom
josefbacik:elf-dynsym-zero-size
Jul 13, 2026
Merged

elf: Only match sizeless .dynsym symbols on exact addresses#1610
d-e-s-o merged 1 commit into
libbpf:mainfrom
josefbacik:elf-dynsym-zero-size

Conversation

@josefbacik

@josefbacik josefbacik commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

find_sym() reports a symbol with st_size == 0 for any address at or past its st_value, bounded only by the next symbol in the table. For .symtab that is a reasonable best-effort heuristic: when present, it describes the code in the file reasonably completely, so the gap between a sizeless symbol and the next entry is typically just that symbol's body (hand-written assembly lacking a .size directive, and similar).

For .dynsym the assumption does not hold. It describes the dynamic linking interface, not the code layout, and in binaries whose regular symbol table has been stripped it is often extremely sparse relative to the contained code. Go binaries built with cgo are a prominent example (system components such as kubelet are shipped this way on some platforms): a handful of surviving exports over many megabytes of code. If any surviving entry is sizeless — assembly routines without .size, or marker symbols such as runtime.text — every address in the potentially huge gap following it is confidently attributed to that symbol. A profiler symbolizing such a binary reports wrong symbol names rather than honest misses, which is arguably worse than no data: the output looks healthy.

Reproduction

A stripped cgo Go binary whose .dynsym ends up with four defined FUNC entries, one of them a sizeless assembly stub (.type ... @function but no .size):

   48: 00000000004a01a9     0 FUNC    GLOBAL DEFAULT   15 early_asm_stub
   49: 0000000000488760    45 FUNC    GLOBAL DEFAULT   15 _cgo_panic
   50: 0000000000480f80    25 FUNC    GLOBAL DEFAULT   15 _cgo_topofstack
   51: 00000000004887e0   104 FUNC    GLOBAL DEFAULT   15 crosscall2

Before this change (blazecli symbolize elf; ground truth from the unstripped twin of the same binary):

0x4a01d0: early_asm_stub @ 0x4a01a9+0x27      <- actually x_cgo_clearenv
0x4a0ca0: early_asm_stub @ 0x4a01a9+0xaf7     <- actually _cgo_libc_setuid
0x600000: early_asm_stub @ 0x4a01a9+0x15fe57  <- 1.4 MiB past the stub, beyond .text entirely

After:

0x4a01a9: early_asm_stub @ 0x4a01a9+0x0       (exact match still reported)
0x4a01d0: <no-symbol>
0x4a0ca0: <no-symbol>
0x600000: <no-symbol>
0x488770: _cgo_panic @ 0x488760+0x10          (sized entries unaffected)
Reproduction script
cat > stub.s <<'ASM'
	.text
	.globl	early_asm_stub
	.type	early_asm_stub, @function
early_asm_stub:
	ret
/* deliberately no .size directive -> st_size stays 0 */
ASM
cat > main.go <<'GO'
package main

/*
extern void early_asm_stub(void);
*/
import "C"

func main() {
	C.early_asm_stub()
}
GO
cat > go.mod <<'MOD'
module repro

go 1.26
MOD
go build -ldflags='-linkmode=external -extldflags "-Wl,--no-export-dynamic -Wl,--export-dynamic-symbol=early_asm_stub"' -o repro .
cp repro repro-stripped && strip --strip-all repro-stripped
readelf -W --dyn-syms repro-stripped | awk '$7!="UND" && $4=="FUNC"'
# pick any address past the sizeless stub and compare:
#   blazecli symbolize elf --path repro-stripped <addr>
# against the unstripped twin:
#   blazecli symbolize elf --path repro <addr>

Why exact-match-only is the right line for .dynsym

Sizeless defined function symbols are essentially extinct in .dynsym in practice: across 39 common libraries of a current Linux distribution I count 42 of 330540 defined FUNC entries with st_size == 0 (0.013%), every one of them a compiler-emitted stub (_init, _fini, register_tm_clones, frame_dummy, ...). There is essentially nothing left for the stretch heuristic to help with in .dynsym, and plenty for it to corrupt. Restricting sizeless .dynsym matches to exact address hits therefore loses essentially nothing.

.symtab behavior is unchanged: the __libc_init_first-style case remains covered (lookup_symbol_with_unknown_size now pins both semantics), and kallsyms-based kernel symbolization is a separate code path that is untouched.

@josefbacik

josefbacik commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

A few notes for review:

Why table identity rather than an option: the distinction is a property of what the table is, not of any particular lookup — .symtab, when present, approximates a complete code map, so best-effort extension of sizeless symbols has a sound basis; .dynsym enumerates the dynamic linking interface, so a sizeless entry proves nothing beyond its own address. An opt-in flag would leave the default misattributing in exactly the binaries where users cannot easily tell (stripped, so there is nothing to cross-check against).

Cases considered:

  • Sized .dynsym entries: unchanged (bounded matching as before).
  • Sizeless .dynsym entry at the exact queried address: still reported, with size == None conveying the unknown extent, as before.
  • Aliased symbols at one address (sized + sizeless): unaffected — by_addr_idx orders size-descending within an address, so a sized alias already won before this change.
  • .symtab semantics: unchanged, including the open-ended tail case — lookup_symbol_with_unknown_size now pins both table semantics explicitly.
  • kallsyms-based kernel symbolization: separate code path (kernel/ksym.rs), untouched.
  • ensure_str2dynsym's duplicate filter searches .symtab and accordingly keeps Symtab semantics.

Testing: existing lookup tests extended to cover both table flavors; new end-to-end test (lookup_dynsym_sizeless_no_extension) synthesizes a dynsym-only ELF in-memory (exercised via both the mmap and io backends) asserting exact-match reported / gap not attributed / sized entries unaffected. Suite, clippy, and rustfmt clean locally apart from environment-limited cases on this machine (tests needing bpf//proc/kcore access and 32-bit fixtures); expecting CI to cover those.

One consequence worth calling out: on binaries with no .symtab and a sparse .dynsym, addresses that previously (mis)resolved now surface Reason::MissingSyms (from the empty-.symtab arm of the reason selection). If distinguishing UnknownAddr-when-.dynsym-is-nonempty would be preferable, happy to adjust — I kept it out of this change to keep the diff minimal.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.36%. Comparing base (a081757) to head (7da9ca1).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1610      +/-   ##
==========================================
+ Coverage   96.35%   96.36%   +0.01%     
==========================================
  Files          56       56              
  Lines       11126    11165      +39     
==========================================
+ Hits        10720    10759      +39     
  Misses        406      406              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@josefbacik
josefbacik force-pushed the elf-dynsym-zero-size branch from 10392bd to 7d8e3d1 Compare July 10, 2026 02:00
@josefbacik

Copy link
Copy Markdown
Contributor Author

Hey guys, clippy was failing on upstream stuff since there's a new clippy. I had my bot add it as a separate pre-req patch so that the CI would pass. Happy to let you do that and drop mine. If you fix it yourself and merge it then this will conflict and my bot will automatically fix this up.

@d-e-s-o

d-e-s-o commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Hey guys, clippy was failing on upstream stuff since there's a new clippy.

Ah yeah, I fixed that up over in #1612

@d-e-s-o d-e-s-o left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall this seems okay to me. Can you check whether the main test can be moved into the integration test suite, though?

Comment thread CHANGELOG.md Outdated
Comment on lines +3 to +6
- Adjusted ELF symbolization to report sizeless `.dynsym` entries only on
exact address matches, preventing misattribution of unrelated code in
binaries whose dynamic symbol table is sparse relative to the contained
code (e.g., stripped Go binaries using cgo)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
- Adjusted ELF symbolization to report sizeless `.dynsym` entries only on
exact address matches, preventing misattribution of unrelated code in
binaries whose dynamic symbol table is sparse relative to the contained
code (e.g., stripped Go binaries using cgo)
- Adjusted ELF symbolization to report sizeless `.dynsym` entries only on
exact address matches

Comment thread src/elf/parser.rs Outdated
Comment on lines +151 to +154
/// The regular symbol table (`.symtab`).
///
/// If present, it can be assumed to provide a reasonably complete
/// description of the code in the file.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// The regular symbol table (`.symtab`).
///
/// If present, it can be assumed to provide a reasonably complete
/// description of the code in the file.
/// The regular symbol table (`.symtab`).

Comment thread src/elf/parser.rs Outdated
Comment on lines +156 to +160
/// The dynamic symbol table (`.dynsym`).
///
/// It only describes the dynamic linking interface and so the
/// symbols contained may be sparse relative to the code in the
/// file.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// The dynamic symbol table (`.dynsym`).
///
/// It only describes the dynamic linking interface and so the
/// symbols contained may be sparse relative to the code in the
/// file.
/// The dynamic symbol table (`.dynsym`).

Comment thread src/elf/parser.rs Outdated
/// using cgo being a prominent example). Extending a sizeless symbol up
/// to the next entry would attribute unrelated code to it.
#[test]
fn lookup_dynsym_sizeless_no_extension() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

IMO this is way too much setup for such a trivially to construct case. Can we instead just have a proper integration test against the public API that just constructs such a case with libtest-so.so?

Comment thread src/elf/parser.rs Outdated
Comment on lines +2132 to +2133
// size or an unknown size" cases -- but only for the regular
// symbol table.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// size or an unknown size" cases -- but only for the regular
// symbol table.
// For a regular symbol table, because the symbol has a size
// of 0 and is the only conceivable match, we report it on
// the basis that ELF reserves these for "no size or an
// unknown size" cases.

Comment thread src/elf/parser.rs Outdated
Comment on lines +202 to +211
// The dynamic symbol table only describes the dynamic
// linking interface. In binaries with a stripped
// regular symbol table it is often sparse relative to
// the contained code (e.g., Go binaries using cgo
// export a few hundred symbols while containing many
// megabytes of code). Extending a sizeless symbol up
// to the next entry can attribute large swaths of
// unrelated code to it, resulting in confidently
// reported but wrong symbol names. Hence, only report
// sizeless symbols on exact address matches here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// The dynamic symbol table only describes the dynamic
// linking interface. In binaries with a stripped
// regular symbol table it is often sparse relative to
// the contained code (e.g., Go binaries using cgo
// export a few hundred symbols while containing many
// megabytes of code). Extending a sizeless symbol up
// to the next entry can attribute large swaths of
// unrelated code to it, resulting in confidently
// reported but wrong symbol names. Hence, only report
// sizeless symbols on exact address matches here.
// The dynamic symbol table is often sparse relative to
// the contained code. Extending a sizeless symbol up
// to the next entry can attribute large swaths of
// unrelated code to it, resulting in confidently
// reported but wrong symbol names. Hence, only report
// sizeless symbols on exact address matches here.

find_sym() reports a symbol with st_size == 0 for any address at or
past its st_value, bounded only by the next symbol in the table. For
the regular symbol table that is a reasonable best-effort heuristic:
.symtab, when present, describes the code in the file reasonably
completely, so the gap between a sizeless symbol and the next entry is
typically just that symbol's body (e.g., hand-written assembly lacking
a .size directive).

For the dynamic symbol table the assumption does not hold. .dynsym
describes the dynamic linking interface, not the code layout, and in
binaries whose regular symbol table has been stripped it is often
extremely sparse relative to the contained code. Go binaries built
with cgo are a prominent example: they retain a handful of exports
(crosscall2, _cgo_panic, ...) while containing many megabytes of code.
If any surviving entry is sizeless -- assembly routines without .size
or marker symbols such as runtime.text -- every address in the
potentially huge gap following it is confidently attributed to that
symbol. Symbolizing such binaries then produces wrong symbol names
instead of honest misses; in one observed case every address within
1.4 MiB past an unrelated assembly stub was attributed to it.

Sizeless defined function symbols are essentially extinct in .dynsym
in practice: across 39 common libraries of a current Linux
distribution, 42 of 330540 defined FUNC entries have st_size == 0, all
of them compiler-emitted stubs (_init, _fini, register_tm_clones,
frame_dummy, ...). Restricting sizeless .dynsym matches to exact
address hits therefore loses essentially nothing, while making
symbolization of sparse-.dynsym binaries honest.

.symtab lookup behavior is unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
@josefbacik
josefbacik force-pushed the elf-dynsym-zero-size branch from 7d8e3d1 to 7da9ca1 Compare July 13, 2026 20:13
@josefbacik

josefbacik commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! All suggestions applied, and the synthetic-ELF unit test is gone — replaced with an integration test (symbolize_elf_dynsym_sizeless) against the public API using libtest-so-stripped.so, per your suggestion: test-so.c gains a small assembly function without a .size directive (so its .dynsym entry carries st_size == 0), and the test looks up its address via the inspect API and asserts that the exact address symbolizes while addr + 1 does not. Also rebased onto main to drop the interim clippy commit now that #1612 landed.

@d-e-s-o d-e-s-o left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good to me, thanks!

@d-e-s-o
d-e-s-o merged commit 862c2cf into libbpf:main Jul 13, 2026
46 checks passed
josefbacik added a commit to josefbacik/systing that referenced this pull request Jul 14, 2026
Move the blazesym dependency from crates.io 0.2.3 to a git pin on
upstream main at 862c2cf5d424, picking up the fix that stops sizeless
.dynsym symbols from absorbing every address up to the next symbol
(libbpf/blazesym#1610). Stripped cgo Go binaries no longer get whole
text ranges misattributed to stray dynamic symbols; those frames now
fall through honestly to the gopclntab resolver (or report as
unresolved) instead of resolving to confidently wrong names.

The pin also carries upstream 0.2.4/0.2.5: relocatable ELF/DWARF
support, kernel module DWARF symbolization, and performance
improvements. Upstream's switch of flate2's zlib backend from
miniz_oxide to zlib-rs propagates via feature unification and grows
GzEncoder enough to trip clippy's large_enum_variant on ExportSink;
box the encoder variant. Upstream MSRV moves to 1.88.

Version goes to 1.11.7; 1.11.6 is already claimed by another open PR.

Co-authored-by: Claude <noreply@anthropic.com>
josefbacik added a commit to josefbacik/systing that referenced this pull request Jul 14, 2026
Move the blazesym dependency from crates.io 0.2.3 to a git pin on
upstream main at 862c2cf5d424, picking up the fix that stops sizeless
.dynsym symbols from absorbing every address up to the next symbol
(libbpf/blazesym#1610). Stripped cgo Go binaries no longer get whole
text ranges misattributed to stray dynamic symbols; those frames now
fall through honestly to the gopclntab resolver (or report as
unresolved) instead of resolving to confidently wrong names.

The pin also carries upstream 0.2.4/0.2.5: relocatable ELF/DWARF
support, kernel module DWARF symbolization, and performance
improvements. Upstream's switch of flate2's zlib backend from
miniz_oxide to zlib-rs propagates via feature unification and grows
GzEncoder enough to trip clippy's large_enum_variant on ExportSink;
box the encoder variant. Upstream MSRV moves to 1.88.

Version goes to 1.11.11; the previously claimed 1.11.7 was superseded
when 1.11.10 landed first.

Co-authored-by: Claude <noreply@anthropic.com>
josefbacik added a commit to josefbacik/systing that referenced this pull request Jul 14, 2026
Move the blazesym dependency from crates.io 0.2.3 to a git pin on
upstream main at 862c2cf5d424, picking up the fix that stops sizeless
.dynsym symbols from absorbing every address up to the next symbol
(libbpf/blazesym#1610). Stripped cgo Go binaries no longer get whole
text ranges misattributed to stray dynamic symbols; those frames now
fall through honestly to the gopclntab resolver (or report as
unresolved) instead of resolving to confidently wrong names.

The pin also carries upstream 0.2.4/0.2.5: relocatable ELF/DWARF
support, kernel module DWARF symbolization, and performance
improvements. Upstream's switch of flate2's zlib backend from
miniz_oxide to zlib-rs propagates via feature unification and grows
GzEncoder enough to trip clippy's large_enum_variant on ExportSink;
box the encoder variant. Upstream MSRV moves to 1.88.

Co-authored-by: Claude <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