|
| 1 | +//go:build integration && linux |
| 2 | + |
| 3 | +// Copyright The OpenTelemetry Authors |
| 4 | +// SPDX-License-Identifier: Apache-2.0 |
| 5 | + |
| 6 | +package kallsyms |
| 7 | + |
| 8 | +import ( |
| 9 | + "runtime" |
| 10 | + "strings" |
| 11 | + "testing" |
| 12 | + "time" |
| 13 | + |
| 14 | + "github.com/cilium/ebpf" |
| 15 | + "github.com/cilium/ebpf/asm" |
| 16 | + "github.com/stretchr/testify/assert" |
| 17 | + "github.com/stretchr/testify/require" |
| 18 | + |
| 19 | + "go.opentelemetry.io/ebpf-profiler/libpf" |
| 20 | + "go.opentelemetry.io/ebpf-profiler/rlimit" |
| 21 | +) |
| 22 | + |
| 23 | +const ( |
| 24 | + eventuallyWaitFor = 10 * time.Second |
| 25 | + eventuallyTick = 100 * time.Millisecond |
| 26 | + |
| 27 | + dynamicProgName = "otel_dyn_test" |
| 28 | + preexistingProgName = "otel_pre_test" |
| 29 | +) |
| 30 | + |
| 31 | +// linearCPUs returns []int{0, 1, ..., n-1} for n online CPUs. |
| 32 | +// This assumes contiguous CPU IDs, which is practical for integration tests. |
| 33 | +// The proper parsing of /sys/devices/system/cpu/online lives in tracer/helper.go, |
| 34 | +// but we don't want to export or duplicate it here. |
| 35 | +func linearCPUs() []int { |
| 36 | + cpus := make([]int, runtime.NumCPU()) |
| 37 | + for i := range cpus { |
| 38 | + cpus[i] = i |
| 39 | + } |
| 40 | + return cpus |
| 41 | +} |
| 42 | + |
| 43 | +// loadSocketFilter loads a minimal BPF socket filter program with the given name. |
| 44 | +// The program simply returns 0. The caller is responsible for closing it. |
| 45 | +func loadSocketFilter(t *testing.T, name string) *ebpf.Program { |
| 46 | + t.Helper() |
| 47 | + |
| 48 | + spec := &ebpf.ProgramSpec{ |
| 49 | + Name: name, |
| 50 | + Type: ebpf.SocketFilter, |
| 51 | + License: "GPL", |
| 52 | + Instructions: asm.Instructions{ |
| 53 | + asm.Mov.Imm(asm.R0, 0), |
| 54 | + asm.Return(), |
| 55 | + }, |
| 56 | + } |
| 57 | + |
| 58 | + prog, err := ebpf.NewProgram(spec) |
| 59 | + require.NoError(t, err) |
| 60 | + |
| 61 | + return prog |
| 62 | +} |
| 63 | + |
| 64 | +// findBPFSymbol searches the bpf module for a symbol whose kernel-assigned name |
| 65 | +// ends with "_<progName>". Returns the full symbol name and its address. |
| 66 | +func findBPFSymbol(s *bpfSymbolizer, progName string) (string, libpf.Address) { |
| 67 | + suffix := "_" + progName |
| 68 | + |
| 69 | + mod := s.Module() |
| 70 | + if mod == nil { |
| 71 | + return "", 0 |
| 72 | + } |
| 73 | + |
| 74 | + for _, sym := range mod.symbols { |
| 75 | + name := mod.stringAt(sym.index) |
| 76 | + if strings.HasSuffix(name, suffix) { |
| 77 | + return name, mod.start + libpf.Address(sym.offset) |
| 78 | + } |
| 79 | + } |
| 80 | + return "", 0 |
| 81 | +} |
| 82 | + |
| 83 | +// assertBPFSymbolFound polls the symbolizer until a BPF symbol matching progName |
| 84 | +// appears, then verifies the full symbolization path (address -> module -> symbol). |
| 85 | +func assertBPFSymbolFound(t *testing.T, s *Symbolizer, progName string) (string, libpf.Address) { |
| 86 | + t.Helper() |
| 87 | + |
| 88 | + var fullName string |
| 89 | + var progAddr libpf.Address |
| 90 | + require.Eventually(t, func() bool { |
| 91 | + fullName, progAddr = findBPFSymbol(s.bpf, progName) |
| 92 | + return fullName != "" |
| 93 | + }, eventuallyWaitFor, eventuallyTick, |
| 94 | + "BPF program with suffix %q not found by symbolizer", "_"+progName) |
| 95 | + |
| 96 | + t.Logf("Found BPF program %q at address 0x%x", fullName, progAddr) |
| 97 | + |
| 98 | + mod, err := s.GetModuleByAddress(progAddr) |
| 99 | + require.NoError(t, err) |
| 100 | + assert.Equal(t, "bpf", mod.Name()) |
| 101 | + |
| 102 | + funcName, offset, err := mod.LookupSymbolByAddress(progAddr) |
| 103 | + require.NoError(t, err) |
| 104 | + assert.Equal(t, fullName, funcName) |
| 105 | + assert.Equal(t, uint(0), offset) |
| 106 | + |
| 107 | + funcName, offset, err = mod.LookupSymbolByAddress(progAddr + 1) |
| 108 | + require.NoError(t, err) |
| 109 | + assert.Equal(t, fullName, funcName) |
| 110 | + assert.Equal(t, uint(1), offset) |
| 111 | + |
| 112 | + return fullName, progAddr |
| 113 | +} |
| 114 | + |
| 115 | +// assertBPFSymbolRemoved polls the symbolizer until the BPF symbol matching |
| 116 | +// progName disappears. |
| 117 | +func assertBPFSymbolRemoved(t *testing.T, s *Symbolizer, progName string) { |
| 118 | + t.Helper() |
| 119 | + |
| 120 | + require.Eventually(t, func() bool { |
| 121 | + name, _ := findBPFSymbol(s.bpf, progName) |
| 122 | + return name == "" |
| 123 | + }, eventuallyWaitFor, eventuallyTick, |
| 124 | + "BPF program with suffix %q not removed from symbolizer", "_"+progName) |
| 125 | + |
| 126 | + t.Logf("BPF program with suffix %q successfully removed from symbolizer", "_"+progName) |
| 127 | +} |
| 128 | + |
| 129 | +// TestBPFSymbolizerDynamic verifies that programs loaded after the monitor |
| 130 | +// starts are discovered via PERF_RECORD_KSYMBOL events and that unloading |
| 131 | +// them removes the symbols. |
| 132 | +func TestBPFSymbolizerDynamic(t *testing.T) { |
| 133 | + restoreRlimit, err := rlimit.MaximizeMemlock() |
| 134 | + require.NoError(t, err) |
| 135 | + defer restoreRlimit() |
| 136 | + |
| 137 | + s, err := NewSymbolizer() |
| 138 | + require.NoError(t, err) |
| 139 | + |
| 140 | + err = s.bpf.startMonitor(t.Context(), linearCPUs()) |
| 141 | + require.NoError(t, err) |
| 142 | + defer s.bpf.Close() |
| 143 | + |
| 144 | + // The program hasn't been loaded yet, so the symbolizer must not know about it. |
| 145 | + name, _ := findBPFSymbol(s.bpf, dynamicProgName) |
| 146 | + require.Empty(t, name, "BPF program %q found before loading", dynamicProgName) |
| 147 | + |
| 148 | + prog := loadSocketFilter(t, dynamicProgName) |
| 149 | + |
| 150 | + fullName, _ := assertBPFSymbolFound(t, s, dynamicProgName) |
| 151 | + |
| 152 | + prog.Close() |
| 153 | + assertBPFSymbolRemoved(t, s, dynamicProgName) |
| 154 | + |
| 155 | + t.Logf("Dynamic test passed: %q added and removed", fullName) |
| 156 | +} |
| 157 | + |
| 158 | +// TestBPFSymbolizerPreexisting verifies that programs loaded before the |
| 159 | +// monitor starts are discovered via the initial /proc/kallsyms parse. |
| 160 | +func TestBPFSymbolizerPreexisting(t *testing.T) { |
| 161 | + restoreRlimit, err := rlimit.MaximizeMemlock() |
| 162 | + require.NoError(t, err) |
| 163 | + defer restoreRlimit() |
| 164 | + |
| 165 | + // Load the program before starting the monitor. |
| 166 | + prog := loadSocketFilter(t, preexistingProgName) |
| 167 | + |
| 168 | + s, err := NewSymbolizer() |
| 169 | + require.NoError(t, err) |
| 170 | + |
| 171 | + err = s.bpf.startMonitor(t.Context(), linearCPUs()) |
| 172 | + require.NoError(t, err) |
| 173 | + defer s.bpf.Close() |
| 174 | + |
| 175 | + // The program was loaded before the monitor started, so it must be |
| 176 | + // discovered from /proc/kallsyms during the initial load. |
| 177 | + fullName, _ := assertBPFSymbolFound(t, s, preexistingProgName) |
| 178 | + t.Logf("Preexisting program %q found from initial kallsyms load", fullName) |
| 179 | + |
| 180 | + // Close the program and verify the symbol is removed via perf event. |
| 181 | + prog.Close() |
| 182 | + assertBPFSymbolRemoved(t, s, preexistingProgName) |
| 183 | + t.Logf("Preexisting program %q successfully removed", fullName) |
| 184 | +} |
0 commit comments