Skip to content

Commit aaa9af2

Browse files
mmat11ti-mo
andcommitted
link: implement kprobe_multi link type
As of Linux 5.18, or commit 5a5c11ee3e65 ("Merge branch 'bpf: Add kprobe multi link'"), attaching multiple k(ret)probes using a single system call is now possible. This commit adds support for this through the KprobeMulti() and KretprobeMulti() APIs in package link. Co-authored-by: Timo Beckers <[email protected]>
1 parent 91754df commit aaa9af2

File tree

11 files changed

+338
-11
lines changed

11 files changed

+338
-11
lines changed

attachtype_string.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

elf_reader.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1153,8 +1153,9 @@ func getProgType(sectionName string) (ProgramType, AttachType, uint32, string) {
11531153
{"cgroup/setsockopt", CGroupSockopt, AttachCGroupSetsockopt, 0},
11541154
{"struct_ops+", StructOps, AttachNone, 0},
11551155
{"sk_lookup/", SkLookup, AttachSkLookup, 0},
1156-
11571156
{"seccomp", SocketFilter, AttachNone, 0},
1157+
{"kprobe.multi", Kprobe, AttachTraceKprobeMulti, 0},
1158+
{"kretprobe.multi", Kprobe, AttachTraceKprobeMulti, 0},
11581159
}
11591160

11601161
for _, t := range types {

internal/sys/ptr.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,17 @@ func NewStringPointer(str string) Pointer {
3636

3737
return Pointer{ptr: unsafe.Pointer(p)}
3838
}
39+
40+
// NewStringSlicePointer allocates an array of Pointers to each string in the
41+
// given slice of strings and returns a 64-bit pointer to the start of the
42+
// resulting array.
43+
//
44+
// Use this function to pass arrays of strings as syscall arguments.
45+
func NewStringSlicePointer(strings []string) Pointer {
46+
sp := make([]Pointer, 0, len(strings))
47+
for _, s := range strings {
48+
sp = append(sp, NewStringPointer(s))
49+
}
50+
51+
return Pointer{ptr: unsafe.Pointer(&sp[0])}
52+
}

link/kprobe_multi.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package link
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"unsafe"
8+
9+
"github.com/cilium/ebpf"
10+
"github.com/cilium/ebpf/asm"
11+
"github.com/cilium/ebpf/internal"
12+
"github.com/cilium/ebpf/internal/sys"
13+
"github.com/cilium/ebpf/internal/unix"
14+
)
15+
16+
// KprobeMultiOptions defines additional parameters that will be used
17+
// when opening a KprobeMulti Link.
18+
type KprobeMultiOptions struct {
19+
// Symbols takes a list of kernel symbol names to attach an ebpf program to.
20+
//
21+
// Mutually exclusive with Addresses.
22+
Symbols []string
23+
24+
// Addresses takes a list of kernel symbol addresses in case they can not
25+
// be referred to by name.
26+
//
27+
// Note that only start addresses can be specified, since the fprobe API
28+
// limits the attach point to the function entry or return.
29+
//
30+
// Mutually exclusive with Symbols.
31+
Addresses []uint64
32+
33+
// Cookies specifies arbitrary values that can be fetched from an eBPF
34+
// program via `bpf_get_attach_cookie()`.
35+
//
36+
// If set, its length should be equal to the length of Symbols or Addresses.
37+
// Each Cookie is assigned to the Symbol or Address specified at the
38+
// corresponding slice index.
39+
Cookies []uint64
40+
}
41+
42+
// KprobeMulti attaches the given eBPF program to the entry point of a given set
43+
// of kernel symbols.
44+
//
45+
// The difference with Kprobe() is that multi-kprobe accomplishes this in a
46+
// single system call, making it significantly faster than attaching many
47+
// probes one at a time.
48+
//
49+
// Requires at least Linux 5.18.
50+
func KprobeMulti(prog *ebpf.Program, opts KprobeMultiOptions) (Link, error) {
51+
return kprobeMulti(prog, opts, 0)
52+
}
53+
54+
// KretprobeMulti attaches the given eBPF program to the return point of a given
55+
// set of kernel symbols.
56+
//
57+
// The difference with Kretprobe() is that multi-kprobe accomplishes this in a
58+
// single system call, making it significantly faster than attaching many
59+
// probes one at a time.
60+
//
61+
// Requires at least Linux 5.18.
62+
func KretprobeMulti(prog *ebpf.Program, opts KprobeMultiOptions) (Link, error) {
63+
return kprobeMulti(prog, opts, unix.BPF_F_KPROBE_MULTI_RETURN)
64+
}
65+
66+
func kprobeMulti(prog *ebpf.Program, opts KprobeMultiOptions, flags uint32) (Link, error) {
67+
if prog == nil {
68+
return nil, errors.New("cannot attach a nil program")
69+
}
70+
71+
syms := uint32(len(opts.Symbols))
72+
addrs := uint32(len(opts.Addresses))
73+
cookies := uint32(len(opts.Cookies))
74+
75+
if syms == 0 && addrs == 0 {
76+
return nil, fmt.Errorf("one of Symbols or Addresses is required: %w", errInvalidInput)
77+
}
78+
if syms != 0 && addrs != 0 {
79+
return nil, fmt.Errorf("Symbols and Addresses are mutually exclusive: %w", errInvalidInput)
80+
}
81+
if cookies > 0 && cookies != syms && cookies != addrs {
82+
return nil, fmt.Errorf("Cookies must be exactly Symbols or Addresses in length: %w", errInvalidInput)
83+
}
84+
85+
if err := haveBPFLinkKprobeMulti(); err != nil {
86+
return nil, err
87+
}
88+
89+
attr := &sys.LinkCreateKprobeMultiAttr{
90+
ProgFd: uint32(prog.FD()),
91+
AttachType: sys.BPF_TRACE_KPROBE_MULTI,
92+
KprobeMultiFlags: flags,
93+
}
94+
95+
switch {
96+
case syms != 0:
97+
attr.Count = syms
98+
attr.Syms = sys.NewStringSlicePointer(opts.Symbols)
99+
100+
case addrs != 0:
101+
attr.Count = addrs
102+
attr.Addrs = sys.NewPointer(unsafe.Pointer(&opts.Addresses[0]))
103+
}
104+
105+
if cookies != 0 {
106+
attr.Cookies = sys.NewPointer(unsafe.Pointer(&opts.Cookies[0]))
107+
}
108+
109+
fd, err := sys.LinkCreateKprobeMulti(attr)
110+
if errors.Is(err, unix.ESRCH) {
111+
return nil, fmt.Errorf("couldn't find one or more symbols: %w", os.ErrNotExist)
112+
}
113+
if errors.Is(err, unix.EINVAL) {
114+
return nil, fmt.Errorf("%w (missing kernel symbol or prog's AttachType not AttachTraceKprobeMulti?)", err)
115+
}
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
return &kprobeMultiLink{RawLink{fd, ""}}, nil
121+
}
122+
123+
type kprobeMultiLink struct {
124+
RawLink
125+
}
126+
127+
var _ Link = (*kprobeMultiLink)(nil)
128+
129+
func (kml *kprobeMultiLink) Update(prog *ebpf.Program) error {
130+
return fmt.Errorf("update kprobe_multi: %w", ErrNotSupported)
131+
}
132+
133+
func (kml *kprobeMultiLink) Pin(string) error {
134+
return fmt.Errorf("pin kprobe_multi: %w", ErrNotSupported)
135+
}
136+
137+
func (kml *kprobeMultiLink) Unpin() error {
138+
return fmt.Errorf("unpin kprobe_multi: %w", ErrNotSupported)
139+
}
140+
141+
var haveBPFLinkKprobeMulti = internal.FeatureTest("bpf_link_kprobe_multi", "5.18", func() error {
142+
prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
143+
Name: "probe_kpm_link",
144+
Type: ebpf.Kprobe,
145+
Instructions: asm.Instructions{
146+
asm.Mov.Imm(asm.R0, 0),
147+
asm.Return(),
148+
},
149+
AttachType: ebpf.AttachTraceKprobeMulti,
150+
License: "MIT",
151+
})
152+
if errors.Is(err, unix.E2BIG) {
153+
// Kernel doesn't support AttachType field.
154+
return internal.ErrNotSupported
155+
}
156+
if err != nil {
157+
return err
158+
}
159+
defer prog.Close()
160+
161+
fd, err := sys.LinkCreateKprobeMulti(&sys.LinkCreateKprobeMultiAttr{
162+
ProgFd: uint32(prog.FD()),
163+
AttachType: sys.BPF_TRACE_KPROBE_MULTI,
164+
Count: 1,
165+
Syms: sys.NewStringSlicePointer([]string{"vprintk"}),
166+
})
167+
if errors.Is(err, unix.EINVAL) {
168+
return internal.ErrNotSupported
169+
}
170+
if err != nil {
171+
return err
172+
}
173+
fd.Close()
174+
175+
return nil
176+
})

link/kprobe_multi_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package link
2+
3+
import (
4+
"errors"
5+
"math"
6+
"os"
7+
"testing"
8+
9+
"github.com/cilium/ebpf"
10+
"github.com/cilium/ebpf/internal/testutils"
11+
"github.com/cilium/ebpf/internal/unix"
12+
)
13+
14+
var kprobeMultiSyms = []string{"vprintk", "inet6_release"}
15+
16+
func TestKprobeMulti(t *testing.T) {
17+
testutils.SkipIfNotSupported(t, haveBPFLinkKprobeMulti())
18+
19+
prog := mustLoadProgram(t, ebpf.Kprobe, ebpf.AttachTraceKprobeMulti, "")
20+
21+
km, err := KprobeMulti(prog, KprobeMultiOptions{Symbols: kprobeMultiSyms})
22+
if err != nil {
23+
t.Fatal(err)
24+
}
25+
defer km.Close()
26+
27+
testLink(t, km, prog)
28+
}
29+
30+
func TestKprobeMultiInput(t *testing.T) {
31+
// Program type that loads on all kernels. Not expected to link successfully.
32+
prog := mustLoadProgram(t, ebpf.SocketFilter, 0, "")
33+
34+
// One of Symbols or Addresses must be given.
35+
_, err := KprobeMulti(prog, KprobeMultiOptions{})
36+
if !errors.Is(err, errInvalidInput) {
37+
t.Fatalf("expected errInvalidInput, got: %v", err)
38+
}
39+
40+
// Symbols and Addresses are mutually exclusive.
41+
_, err = KprobeMulti(prog, KprobeMultiOptions{
42+
Symbols: []string{"foo"},
43+
Addresses: []uint64{1},
44+
})
45+
if !errors.Is(err, errInvalidInput) {
46+
t.Fatalf("expected errInvalidInput, got: %v", err)
47+
}
48+
49+
// One Symbol, two cookies..
50+
_, err = KprobeMulti(prog, KprobeMultiOptions{
51+
Symbols: []string{"one"},
52+
Cookies: []uint64{2, 3},
53+
})
54+
if !errors.Is(err, errInvalidInput) {
55+
t.Fatalf("expected errInvalidInput, got: %v", err)
56+
}
57+
}
58+
59+
func TestKprobeMultiErrors(t *testing.T) {
60+
testutils.SkipIfNotSupported(t, haveBPFLinkKprobeMulti())
61+
62+
prog := mustLoadProgram(t, ebpf.Kprobe, ebpf.AttachTraceKprobeMulti, "")
63+
64+
// Nonexistent kernel symbol.
65+
_, err := KprobeMulti(prog, KprobeMultiOptions{Symbols: []string{"bogus"}})
66+
if !errors.Is(err, os.ErrNotExist) && !errors.Is(err, unix.EINVAL) {
67+
t.Fatalf("expected ErrNotExist or EINVAL, got: %s", err)
68+
}
69+
70+
// Only have a negative test for addresses as it would be hard to maintain a
71+
// proper one.
72+
if _, err := KprobeMulti(prog, KprobeMultiOptions{
73+
Addresses: []uint64{math.MaxUint64},
74+
}); !errors.Is(err, unix.EINVAL) {
75+
t.Fatalf("expected EINVAL, got: %s", err)
76+
}
77+
}
78+
79+
func TestKprobeMultiCookie(t *testing.T) {
80+
testutils.SkipIfNotSupported(t, haveBPFLinkKprobeMulti())
81+
82+
prog := mustLoadProgram(t, ebpf.Kprobe, ebpf.AttachTraceKprobeMulti, "")
83+
84+
if _, err := KprobeMulti(prog, KprobeMultiOptions{
85+
Symbols: kprobeMultiSyms,
86+
Cookies: []uint64{0, 1},
87+
}); err != nil {
88+
t.Fatal(err)
89+
}
90+
}
91+
92+
func TestKprobeMultiProgramCall(t *testing.T) {
93+
testutils.SkipIfNotSupported(t, haveBPFLinkKprobeMulti())
94+
95+
m, p := newUpdaterMapProg(t, ebpf.Kprobe, ebpf.AttachTraceKprobeMulti)
96+
97+
// For simplicity, just assert the increment happens with any symbol in the array.
98+
opts := KprobeMultiOptions{
99+
Symbols: []string{"__do_sys_getpid"},
100+
}
101+
km, err := KprobeMulti(p, opts)
102+
if err != nil {
103+
t.Fatal(err)
104+
}
105+
106+
// Trigger ebpf program call.
107+
unix.Getpid()
108+
109+
// Assert that the value at index 0 has been updated to 1.
110+
assertMapValue(t, m, 0, 1)
111+
112+
// Close the link.
113+
if err := km.Close(); err != nil {
114+
t.Fatal(err)
115+
}
116+
117+
// Reset map value to 0 at index 0.
118+
if err := m.Update(uint32(0), uint32(0), ebpf.UpdateExist); err != nil {
119+
t.Fatal(err)
120+
}
121+
122+
// Retrigger the ebpf program call.
123+
unix.Getpid()
124+
125+
// Assert that this time the value has not been updated.
126+
assertMapValue(t, m, 0, 0)
127+
}
128+
129+
func TestHaveBPFLinkKprobeMulti(t *testing.T) {
130+
testutils.CheckFeatureTest(t, haveBPFLinkKprobeMulti)
131+
}

link/kprobe_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ func TestKprobeTraceFSGroup(t *testing.T) {
308308
}
309309

310310
func TestKprobeProgramCall(t *testing.T) {
311-
m, p := newUpdaterMapProg(t, ebpf.Kprobe)
311+
m, p := newUpdaterMapProg(t, ebpf.Kprobe, 0)
312312

313313
// Open Kprobe on `sys_getpid` and attach it
314314
// to the ebpf program created above.
@@ -340,7 +340,7 @@ func TestKprobeProgramCall(t *testing.T) {
340340
assertMapValue(t, m, 0, 0)
341341
}
342342

343-
func newUpdaterMapProg(t *testing.T, typ ebpf.ProgramType) (*ebpf.Map, *ebpf.Program) {
343+
func newUpdaterMapProg(t *testing.T, typ ebpf.ProgramType, attach ebpf.AttachType) (*ebpf.Map, *ebpf.Program) {
344344
// Create ebpf map. Will contain only one key with initial value 0.
345345
m, err := ebpf.NewMap(&ebpf.MapSpec{
346346
Type: ebpf.Array,
@@ -378,7 +378,8 @@ func newUpdaterMapProg(t *testing.T, typ ebpf.ProgramType) (*ebpf.Map, *ebpf.Pro
378378
asm.Mov.Imm(asm.R0, 0),
379379
asm.Return(),
380380
},
381-
License: "Dual MIT/GPL",
381+
AttachType: attach,
382+
License: "Dual MIT/GPL",
382383
})
383384
if err != nil {
384385
t.Fatal(err)

link/link.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,8 @@ func (l *RawLink) Info() (*Info, error) {
286286
extra = &TracingInfo{}
287287
case XDPType:
288288
extra = &XDPInfo{}
289-
case RawTracepointType, IterType, PerfEventType:
289+
case RawTracepointType, IterType,
290+
PerfEventType, KprobeMultiType:
290291
// Extra metadata not supported.
291292
default:
292293
return nil, fmt.Errorf("unknown link info type: %d", info.Type)
@@ -296,7 +297,7 @@ func (l *RawLink) Info() (*Info, error) {
296297
buf := bytes.NewReader(info.Extra[:])
297298
err := binary.Read(buf, internal.NativeEndian, extra)
298299
if err != nil {
299-
return nil, fmt.Errorf("can not read extra link info: %w", err)
300+
return nil, fmt.Errorf("cannot read extra link info: %w", err)
300301
}
301302
}
302303

0 commit comments

Comments
 (0)