elf: Only match sizeless .dynsym symbols on exact addresses#1610
Conversation
|
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 — Cases considered:
Testing: existing lookup tests extended to cover both table flavors; new end-to-end test ( One consequence worth calling out: on binaries with no |
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
10392bd to
7d8e3d1
Compare
|
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. |
Ah yeah, I fixed that up over in #1612 |
d-e-s-o
left a comment
There was a problem hiding this comment.
Overall this seems okay to me. Can you check whether the main test can be moved into the integration test suite, though?
| - 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) |
There was a problem hiding this comment.
| - 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 |
| /// The regular symbol table (`.symtab`). | ||
| /// | ||
| /// If present, it can be assumed to provide a reasonably complete | ||
| /// description of the code in the file. |
There was a problem hiding this comment.
| /// 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`). |
| /// 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. |
There was a problem hiding this comment.
| /// 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`). |
| /// 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() { |
There was a problem hiding this comment.
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?
| // size or an unknown size" cases -- but only for the regular | ||
| // symbol table. |
There was a problem hiding this comment.
| // 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. |
| // 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. |
There was a problem hiding this comment.
| // 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>
7d8e3d1 to
7da9ca1
Compare
|
Thanks for the review! All suggestions applied, and the synthetic-ELF unit test is gone — replaced with an integration test ( |
d-e-s-o
left a comment
There was a problem hiding this comment.
Looks good to me, thanks!
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>
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>
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>
find_sym()reports a symbol withst_size == 0for any address at or past itsst_value, bounded only by the next symbol in the table. For.symtabthat 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.sizedirective, and similar).For
.dynsymthe 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 asruntime.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
.dynsymends up with four definedFUNCentries, one of them a sizeless assembly stub (.type ... @functionbut no.size):Before this change (
blazecli symbolize elf; ground truth from the unstripped twin of the same binary):After:
Reproduction script
Why exact-match-only is the right line for
.dynsymSizeless defined function symbols are essentially extinct in
.dynsymin practice: across 39 common libraries of a current Linux distribution I count 42 of 330540 definedFUNCentries withst_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.dynsymmatches to exact address hits therefore loses essentially nothing..symtabbehavior is unchanged: the__libc_init_first-style case remains covered (lookup_symbol_with_unknown_sizenow pins both semantics), and kallsyms-based kernel symbolization is a separate code path that is untouched.