Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions ebpf/symtab/gosym/pcindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,18 @@ func (it *PCIndex) First() uint64 {

func (it *PCIndex) FindIndex(addr uint64) int {
if it.i32 != nil {

if addr < uint64(it.i32[0]) {
// The upper bound is load-bearing, not defensive. Set() only stores
// into i32 when the value fits in 32 bits (it promotes to i64
// otherwise), so an address above MaxUint32 cannot be covered by this
// index. Meanwhile the BinarySearch below truncates addr to uint32.
//
// Without this check the two disagree, and a high address takes one of
// two bad paths depending on its low word:
// - low word below i32[0] -> BinarySearch returns (0,false), i--
// makes it -1, and it.i32[i] panics with index out of range [-1]
// - low word inside range -> a wrong entry matches, and the profiler
// silently attributes samples to the wrong symbol
if addr > math.MaxUint32 || addr < uint64(it.i32[0]) {
return -1
}
i, found := slices.BinarySearch(it.i32, uint32(addr))
Expand Down
89 changes: 89 additions & 0 deletions ebpf/symtab/gosym/pcindex_panic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package gosym

import "testing"

// TestFindIndexTruncationPanic reproduces the production panic
//
// panic: runtime error: index out of range [-1]
// gosym.(*PCIndex).FindIndex pcindex.go:81
//
// Root cause: the bounds guard compares the FULL 64-bit addr,
//
// if addr < uint64(it.i32[0]) { return -1 }
//
// but the search TRUNCATES it,
//
// slices.BinarySearch(it.i32, uint32(addr))
//
// For any addr >= 2^32 whose low 32 bits fall below i32[0], the guard passes
// while the search looks for a value smaller than every element. BinarySearch
// then returns (0, false), `i--` makes it -1, and `it.i32[i]` panics.
//
// FindIndex returning -1 is an expected outcome — elf.SymbolTable.Resolve
// explicitly checks `if i == -1 { return "" }` (symbol_table.go:126). So the
// correct behaviour here is to return -1, not to panic.
func TestFindIndexTruncationPanic(t *testing.T) {
idx := NewPCIndex(2)
idx.Set(0, 0x1000)
idx.Set(1, 0x2000)

// 0x1_0000_0500: above 2^32, low word 0x500 is below i32[0] (0x1000).
const addr = uint64(0x100000500)

if got := idx.FindIndex(addr); got != -1 {
t.Fatalf("FindIndex(%#x) = %d, want -1 (address is not covered by this index)", addr, got)
}
}

// TestFindIndexBelowFirstStillReturnsMinusOne pins the behaviour the guard
// already had, so a fix cannot regress it.
func TestFindIndexBelowFirstStillReturnsMinusOne(t *testing.T) {
idx := NewPCIndex(2)
idx.Set(0, 0x1000)
idx.Set(1, 0x2000)

if got := idx.FindIndex(0x500); got != -1 {
t.Fatalf("FindIndex(0x500) = %d, want -1", got)
}
}

// TestFindIndexNormalLookupsUnaffected pins the happy paths.
func TestFindIndexNormalLookupsUnaffected(t *testing.T) {
idx := NewPCIndex(3)
idx.Set(0, 0x1000)
idx.Set(1, 0x2000)
idx.Set(2, 0x3000)

for _, tc := range []struct {
addr uint64
want int
}{
{0x1000, 0}, // exact first
{0x1500, 0}, // between first and second
{0x2000, 1}, // exact middle
{0x3000, 2}, // exact last
{0x9999, 2}, // past the end still maps to the last entry
} {
if got := idx.FindIndex(tc.addr); got != tc.want {
t.Fatalf("FindIndex(%#x) = %d, want %d", tc.addr, got, tc.want)
}
}
}

// TestFindIndexTruncationWrongSymbol is the reason a bare `if i < 0` guard is
// the wrong fix. Here the truncated low word lands INSIDE the index range, so
// there is no negative index and no panic — FindIndex silently returns a match
// for an address the index does not cover, and the profiler attributes samples
// to the wrong symbol. Silent wrong data is worse than a crash.
func TestFindIndexTruncationWrongSymbol(t *testing.T) {
idx := NewPCIndex(2)
idx.Set(0, 0x1000)
idx.Set(1, 0x2000)

// low word 0x1500 sits between the two entries, so BinarySearch "succeeds"
const addr = uint64(0x100001500)

if got := idx.FindIndex(addr); got != -1 {
t.Fatalf("FindIndex(%#x) = %d, want -1 — a 32-bit index cannot cover an address above MaxUint32", addr, got)
}
}