From f13161ad92c487dd6e3bd7cb85ec8d0cbad4e7e1 Mon Sep 17 00:00:00 2001 From: ladung Date: Mon, 10 Aug 2026 14:56:32 +0700 Subject: [PATCH] fix(gosym): prevent 32-bit truncation panic in PCIndex --- ebpf/symtab/gosym/pcindex.go | 14 +++- ebpf/symtab/gosym/pcindex_panic_test.go | 89 +++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 ebpf/symtab/gosym/pcindex_panic_test.go diff --git a/ebpf/symtab/gosym/pcindex.go b/ebpf/symtab/gosym/pcindex.go index fa8928a0b5..76a4209906 100644 --- a/ebpf/symtab/gosym/pcindex.go +++ b/ebpf/symtab/gosym/pcindex.go @@ -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)) diff --git a/ebpf/symtab/gosym/pcindex_panic_test.go b/ebpf/symtab/gosym/pcindex_panic_test.go new file mode 100644 index 0000000000..20d3e70fbf --- /dev/null +++ b/ebpf/symtab/gosym/pcindex_panic_test.go @@ -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) + } +}