forked from open-telemetry/opentelemetry-ebpf-profiler
-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathutil.go
More file actions
269 lines (240 loc) · 7.75 KB
/
util.go
File metadata and controls
269 lines (240 loc) · 7.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package util // import "go.opentelemetry.io/ebpf-profiler/util"
import (
"bytes"
"errors"
"fmt"
"math/bits"
"os"
"strings"
"sync"
"sync/atomic"
"unicode"
"unicode/utf8"
"unsafe"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/asm"
"github.com/cilium/ebpf/link"
log "github.com/sirupsen/logrus"
"go.opentelemetry.io/ebpf-profiler/libpf/hash"
"golang.org/x/sys/unix"
)
// IsValidString checks if string is UTF-8-encoded and only contains expected characters.
func IsValidString(s string) bool {
if s == "" {
return false
}
if !utf8.ValidString(s) {
return false
}
for _, r := range s {
if !unicode.IsPrint(r) {
return false
}
}
return true
}
// NextPowerOfTwo returns input value if it's a power of two,
// otherwise it returns the next power of two.
func NextPowerOfTwo(v uint32) uint32 {
if v == 0 {
return 1
}
return 1 << bits.Len32(v-1)
}
// AtomicUpdateMaxUint32 updates the value in store using atomic memory primitives. newValue will
// only be placed in store if newValue is larger than the current value in store.
// To avoid inconsistency parallel updates to store should be avoided.
func AtomicUpdateMaxUint32(store *atomic.Uint32, newValue uint32) {
for {
// Load the current value
oldValue := store.Load()
if newValue <= oldValue {
// No update needed.
break
}
if store.CompareAndSwap(oldValue, newValue) {
// The value was atomically updated.
break
}
// The value changed between load and update attempt.
// Retry with the new value.
}
}
// VersionUint returns a single integer composed of major, minor, patch.
func VersionUint(major, minor, patch uint32) uint32 {
return (major << 16) + (minor << 8) + patch
}
// Range describes a range with Start and End values.
type Range struct {
Start uint64
End uint64
}
// OnDiskFileIdentifier can be used as unique identifier for a file.
// It is a structure to identify a particular file on disk by
// deviceID and inode number.
type OnDiskFileIdentifier struct {
DeviceID uint64 // dev_t as reported by stat.
InodeNum uint64 // ino_t should fit into 64 bits
}
func (odfi OnDiskFileIdentifier) Hash32() uint32 {
return uint32(hash.Uint64(odfi.InodeNum) + odfi.DeviceID)
}
// GetCurrentKernelVersion returns the major, minor and patch version of the kernel of the host
// from the utsname struct.
func GetCurrentKernelVersion() (major, minor, patch uint32, err error) {
var uname unix.Utsname
if err := unix.Uname(&uname); err != nil {
return 0, 0, 0, fmt.Errorf("could not get Kernel Version: %v", err)
}
_, _ = fmt.Fscanf(bytes.NewReader(uname.Release[:]), "%d.%d.%d", &major, &minor, &patch)
return major, minor, patch, nil
}
var (
// testOnlyMultiUprobeOverride allows tests to override HasMultiUprobeSupport
testOnlyMultiUprobeOverride *bool
// multiUprobeSupportCache caches the result of probing for multi-uprobe support
multiUprobeSupportOnce sync.Once
multiUprobeSupportCached bool
// bpfGetAttachCookieCache caches the result of probing for bpf_get_attach_cookie support
bpfGetAttachCookieOnce sync.Once
bpfGetAttachCookieCached bool
)
// SetTestOnlyMultiUprobeSupport overrides HasMultiUprobeSupport for testing.
// Pass nil to restore normal behavior.
func SetTestOnlyMultiUprobeSupport(override *bool) {
testOnlyMultiUprobeOverride = override
}
// probeBpfGetAttachCookie tests if the kernel supports bpf_get_attach_cookie by attempting
// to load a minimal BPF program that uses it. This is more reliable than checking kernel
// versions since support can be backported.
func probeBpfGetAttachCookie() bool {
// Create a minimal program that calls bpf_get_attach_cookie
// This is equivalent to libbpf's probe_kern_bpf_cookie function
insns := asm.Instructions{
// Call bpf_get_attach_cookie() - BPF_FUNC_get_attach_cookie = 80
asm.FnGetAttachCookie.Call(),
// Exit
asm.Return(),
}
spec := &ebpf.ProgramSpec{
Type: ebpf.TracePoint,
Instructions: insns,
License: "GPL",
}
prog, err := ebpf.NewProgramWithOptions(spec, ebpf.ProgramOptions{
LogDisabled: true,
})
if err != nil {
return false
}
if err := prog.Close(); err != nil {
log.Warnf("Failed to close test program: %v", err)
}
return true
}
// HasBpfGetAttachCookie checks if the kernel supports the bpf_get_attach_cookie helper.
// This function uses a cached, once-calculated value for performance.
//
// Note: This function requires CAP_BPF or CAP_SYS_ADMIN capabilities to load the probe
// program. The profiler should already have these privileges.
func HasBpfGetAttachCookie() bool {
bpfGetAttachCookieOnce.Do(func() {
bpfGetAttachCookieCached = probeBpfGetAttachCookie()
})
return bpfGetAttachCookieCached
}
// probeBpfUprobeMultiLink probes for uprobe_multi link support by attempting to create
// an invalid uprobe_multi link. This is modeled after libbpf's probe_uprobe_multi_link
// and cilium/ebpf's haveBPFLinkUprobeMulti which is not exposed publicly.
//
// Try to create a link to (invalid binary) which should fail with EBADF if supported
func probeBpfUprobeMultiLink() bool {
prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
Name: "probe_upm_link",
Type: ebpf.Kprobe,
Instructions: asm.Instructions{
asm.Mov.Imm(asm.R0, 0),
asm.Return(),
},
AttachType: ebpf.AttachTraceUprobeMulti,
License: "MIT",
})
if errors.Is(err, unix.E2BIG) {
// Kernel doesn't support AttachType field.
return false
}
if err != nil {
log.Warnf("Failed to create test program for uprobe_multi link probe: %v", err)
return false
}
defer prog.Close()
ex := link.Executable{}
_, err = ex.UprobeMulti([]string{""}, prog, &link.UprobeMultiOptions{
Addresses: []uint64{1},
})
if errors.Is(err, unix.EBADF) {
return true
}
if errors.Is(err, unix.EINVAL) {
return false
}
log.Warnf("Unexpected error when probing for uprobe_multi link support: %v", err)
return false
}
// HasMultiUprobeSupport checks if the kernel supports uprobe multi-attach.
// Multi-uprobes allow attaching one BPF program to multiple probe points with a single syscall,
// which is more efficient than individual uprobe attachments.
// This function probes for uprobe_multi link support, which was introduced in kernel 6.6.
//
// Note: This function requires CAP_BPF or CAP_SYS_ADMIN capabilities to load the probe
// program. The profiler should already have these privileges.
//
// The behavior can be overridden by:
// - Setting PARCA_DISABLE_MULTIPROBE=1 environment variable to force single-shot uprobe mode
// - Using SetTestOnlyMultiUprobeSupport() for testing purposes
func HasMultiUprobeSupport() bool {
// Check for test override first (takes precedence over everything)
if testOnlyMultiUprobeOverride != nil {
return *testOnlyMultiUprobeOverride
}
// Cache the probe result since it's expensive to check
multiUprobeSupportOnce.Do(func() {
// Check for environment variable override inside the Do() to ensure
// it's only evaluated once and consistently cached
if os.Getenv("PARCA_DISABLE_MULTIPROBE") == "1" {
multiUprobeSupportCached = false
} else {
multiUprobeSupportCached = probeBpfUprobeMultiLink()
}
})
return multiUprobeSupportCached
}
// ProgArrayReferences returns a list of instructions which load a specified tail
// call FD.
func ProgArrayReferences(perfTailCallMapFD int, insns asm.Instructions) []int {
insNos := []int{}
for i := range insns {
ins := &insns[i]
if asm.OpCode(ins.OpCode.Class()) != asm.OpCode(asm.LdClass) {
continue
}
m := ins.Map()
if m == nil {
continue
}
if perfTailCallMapFD == m.FD() {
insNos = append(insNos, i)
}
}
return insNos
}
// Convert a C-string to Go string.
func GoString(cstr []byte) string {
index := bytes.IndexByte(cstr, byte(0))
if index < 0 {
index = len(cstr)
}
return strings.Clone(unsafe.String(unsafe.SliceData(cstr), index))
}