Skip to content

Commit 711b29e

Browse files
committed
Add an integration test
1 parent fd2c644 commit 711b29e

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

kallsyms/bpf_integration_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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

Comments
 (0)