Repository: cilium/ebpf
Pull request #716 (2022-06-16 20:27:14)

link: implement kprobe.multi link
Leaving as draft until CI supports 5.18.
Also, support for pattern matching and addresses array (which libbpf already supports) is to be evaluated

Edit: I've added support for addresses array, I left "pattern matching" out for now since it's not part of the link_create API

lmb: Do you have a link to the corresponding upstream patch set?

Should we support transparent multi attach, with a fallback for old kernels instead of a separate multi attach API? How does libbpf expose this?
ti-mo: Thanks for picking this up! (cc @olsajiri)

> Should we support transparent multi attach, with a fallback for old kernels instead of a separate multi attach API?

I've been mulling this over for a while, was thinking the same. There's no straightforward way to make `K(ret)probe()` variadic since `symbol` is the first arg, though. This kind of addition that justifies breaking API in my book. `KprobeOptions` and `KprobeMultiOptions` are too similar to be separate IMO.

What about unifying them and something like `func Kprobe(prog *ebpf.Program, opts *KprobeOptions, symbols ...string) (Link, error)`? Also parsing strings for symbols vs. addresses.
lmb: The following make transparent fallback difficult:

https://patchwork.kernel.org/project/netdevbpf/patch/20220316122419.933957-4-jolsa@kernel.org/

> The fprobe API allows to attach probe on multiple functions at once
very fast, because it works on top of ftrace. On the other hand this
limits the probe point to the function entry or return.

This is incompatible with `KprobeOptions.Offset`. Possible to work around by returning an error I guess.

https://patchwork.kernel.org/project/netdevbpf/patch/20220316122419.933957-10-jolsa@kernel.org/

```patch
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 1ca520a29fdb..f3a31478e23b 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c 
@@ -8621,6 +8622,8 @@  static const struct bpf_sec_def section_defs[] = {
 	SEC_DEF("uprobe/",		KPROBE,	0, SEC_NONE),
 	SEC_DEF("kretprobe/",		KPROBE, 0, SEC_NONE, attach_kprobe),
 	SEC_DEF("uretprobe/",		KPROBE, 0, SEC_NONE),
+	SEC_DEF("kprobe.multi/",	KPROBE,	BPF_TRACE_KPROBE_MULTI, SEC_NONE, attach_kprobe_multi),
+	SEC_DEF("kretprobe.multi/",	KPROBE,	BPF_TRACE_KPROBE_MULTI, SEC_NONE, attach_kprobe_multi),
 	SEC_DEF("tc",			SCHED_CLS, 0, SEC_NONE),
 	SEC_DEF("classifier",		SCHED_CLS, 0, SEC_NONE | SEC_SLOPPY_PFX | SEC_DEPRECATED),
 	SEC_DEF("action",		SCHED_ACT, 0, SEC_NONE | SEC_SLOPPY_PFX),
```

Multi kprobes have a distinct `expected_attach_type`. This significantly reduces the likelihood / usefulness of a fallback, since you can't just take a regular kprobe and attach it to to multiple functions.
olsajiri: not sure fallback is possible..  attaching all symbols by separate kprobe, but that would be real slow, I'd just return error

it's looks good, I'm out this week, but next week I'll try to rebase my multi_kprobe tetragon code on top of this and test, thanks
olsajiri: I made first draft in here https://github.com/cilium/tetragon/pull/79/commits/ebccee5eb063c6e0e313acc12cdc41754fe58493 and I'm able to use the interface in tetragon, there are many loose ends I need to take care of, but the interface is fine for tetragon for now ;-) thanks
olsajiri: @mmat11 @ti-mo is there easy way to use the multi kprobe detection check from user code? 
mmat11: > @mmat11 @ti-mo is there easy way to use the multi kprobe detection check from user code?

I could expose `HaveKprobeMultiLink` in the features package
olsajiri: > > @mmat11 @ti-mo is there easy way to use the multi kprobe detection check from user code?
> 
> I could expose `HaveKprobeMultiLink` in the features package

that would work for me, thanks 
ti-mo: > > > @mmat11 @ti-mo is there easy way to use the multi kprobe detection check from user code?
> > 
> > I could expose `HaveKprobeMultiLink` in the features package
> 
> that would work for me, thanks 

Before we do that, could you give an example of how that would be used and why?
olsajiri: we are going to use kprobe_multi link instead of standard kprobe,
when there's support detected, like in https://github.com/cilium/tetragon/pull/79/commits/1065b26879f10ee25d79a5cb3d0c28dee228da9f
olsajiri: > Probably better to make the multi attach itself return ErrNotSupported instead of adding arbitrary feature probes. Ask for forgiveness, not permission etc.

there's lot of different configuration code for standard and multi kprobes,
and the link comes as last, so I'd like to know before all that 
mmat11: > As discussed offline, leaving this review to highlight the main points I'd liked to see differently while hacking on this code. Mostly cleaned up the middle part of kprobeMulti and moved all input validation to the top. Will push my changes shortly.

Thanks, looks better! 


Patch:
diff --git a/attachtype_string.go b/attachtype_string.go
index de355ed..add2a3b 100644
--- a/attachtype_string.go
+++ b/attachtype_string.go
@@ -51,11 +51,12 @@ func _() {
 	_ = x[AttachSkReuseportSelect-39]
 	_ = x[AttachSkReuseportSelectOrMigrate-40]
 	_ = x[AttachPerfEvent-41]
+	_ = x[AttachTraceKprobeMulti-42]
 }
 
-const _AttachType_name = "NoneCGroupInetEgressCGroupInetSockCreateCGroupSockOpsSkSKBStreamParserSkSKBStreamVerdictCGroupDeviceSkMsgVerdictCGroupInet4BindCGroupInet6BindCGroupInet4ConnectCGroupInet6ConnectCGroupInet4PostBindCGroupInet6PostBindCGroupUDP4SendmsgCGroupUDP6SendmsgLircMode2FlowDissectorCGroupSysctlCGroupUDP4RecvmsgCGroupUDP6RecvmsgCGroupGetsockoptCGroupSetsockoptTraceRawTpTraceFEntryTraceFExitModifyReturnLSMMacTraceIterCgroupInet4GetPeernameCgroupInet6GetPeernameCgroupInet4GetSocknameCgroupInet6GetSocknameXDPDevMapCgroupInetSockReleaseXDPCPUMapSkLookupXDPSkSKBVerdictSkReuseportSelectSkReuseportSelectOrMigratePerfEvent"
+const _AttachType_name = "NoneCGroupInetEgressCGroupInetSockCreateCGroupSockOpsSkSKBStreamParserSkSKBStreamVerdictCGroupDeviceSkMsgVerdictCGroupInet4BindCGroupInet6BindCGroupInet4ConnectCGroupInet6ConnectCGroupInet4PostBindCGroupInet6PostBindCGroupUDP4SendmsgCGroupUDP6SendmsgLircMode2FlowDissectorCGroupSysctlCGroupUDP4RecvmsgCGroupUDP6RecvmsgCGroupGetsockoptCGroupSetsockoptTraceRawTpTraceFEntryTraceFExitModifyReturnLSMMacTraceIterCgroupInet4GetPeernameCgroupInet6GetPeernameCgroupInet4GetSocknameCgroupInet6GetSocknameXDPDevMapCgroupInetSockReleaseXDPCPUMapSkLookupXDPSkSKBVerdictSkReuseportSelectSkReuseportSelectOrMigratePerfEventTraceKprobeMulti"
 
-var _AttachType_index = [...]uint16{0, 4, 20, 40, 53, 70, 88, 100, 112, 127, 142, 160, 178, 197, 216, 233, 250, 259, 272, 284, 301, 318, 334, 350, 360, 371, 381, 393, 399, 408, 430, 452, 474, 496, 505, 526, 535, 543, 546, 558, 575, 601, 610}
+var _AttachType_index = [...]uint16{0, 4, 20, 40, 53, 70, 88, 100, 112, 127, 142, 160, 178, 197, 216, 233, 250, 259, 272, 284, 301, 318, 334, 350, 360, 371, 381, 393, 399, 408, 430, 452, 474, 496, 505, 526, 535, 543, 546, 558, 575, 601, 610, 626}
 
 func (i AttachType) String() string {
 	if i >= AttachType(len(_AttachType_index)-1) {
diff --git a/elf_reader.go b/elf_reader.go
index e0e8fd6..a5b6353 100644
--- a/elf_reader.go
+++ b/elf_reader.go
@@ -1153,8 +1153,9 @@ func getProgType(sectionName string) (ProgramType, AttachType, uint32, string) {
 		{"cgroup/setsockopt", CGroupSockopt, AttachCGroupSetsockopt, 0},
 		{"struct_ops+", StructOps, AttachNone, 0},
 		{"sk_lookup/", SkLookup, AttachSkLookup, 0},
-
 		{"seccomp", SocketFilter, AttachNone, 0},
+		{"kprobe.multi", Kprobe, AttachTraceKprobeMulti, 0},
+		{"kretprobe.multi", Kprobe, AttachTraceKprobeMulti, 0},
 	}
 
 	for _, t := range types {
diff --git a/go.mod b/go.mod
index bcf59c4..cf7c7cb 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.18
 require (
 	github.com/frankban/quicktest v1.14.0
 	github.com/google/go-cmp v0.5.6
-	golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34
+	golang.org/x/sys v0.0.0-20220829200755-d48e67d00261
 )
 
 require (
diff --git a/go.sum b/go.sum
index f503d16..087ab91 100644
--- a/go.sum
+++ b/go.sum
@@ -12,8 +12,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 h1:GkvMjFtXUmahfDtashnc1mnrCtuBVcwse5QV2lUk/tI=
-golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
+golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index cc9d88c..f6ba841 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -392,6 +392,21 @@ import (
 				flattenAnon,
 			},
 		},
+		{
+			"LinkCreateKprobeMulti", retFd, "link_create", "BPF_LINK_CREATE",
+			[]patch{
+				chooseNth(4, 3),
+				replace(enumTypes["AttachType"], "attach_type"),
+				modify(func(m *btf.Member) error {
+					return rename("flags", "kprobe_multi_flags")(m.Type.(*btf.Struct))
+				}, "kprobe_multi"),
+				flattenAnon,
+				replace(pointer, "cookies"),
+				replace(pointer, "addrs"),
+				replace(pointer, "syms"),
+				rename("cnt", "count"),
+			},
+		},
 		{
 			"LinkUpdate", retError, "link_update", "BPF_LINK_UPDATE",
 			nil,
diff --git a/internal/sys/ptr.go b/internal/sys/ptr.go
index a221006..bc7ebb4 100644
--- a/internal/sys/ptr.go
+++ b/internal/sys/ptr.go
@@ -36,3 +36,17 @@ func NewStringPointer(str string) Pointer {
 
 	return Pointer{ptr: unsafe.Pointer(p)}
 }
+
+// NewStringSlicePointer allocates an array of Pointers to each string in the
+// given slice of strings and returns a 64-bit pointer to the start of the
+// resulting array.
+//
+// Use this function to pass arrays of strings as syscall arguments.
+func NewStringSlicePointer(strings []string) Pointer {
+	sp := make([]Pointer, 0, len(strings))
+	for _, s := range strings {
+		sp = append(sp, NewStringPointer(s))
+	}
+
+	return Pointer{ptr: unsafe.Pointer(&sp[0])}
+}
diff --git a/internal/sys/types.go b/internal/sys/types.go
index 9fb6e08..c823248 100644
--- a/internal/sys/types.go
+++ b/internal/sys/types.go
@@ -646,6 +646,26 @@ func LinkCreateIter(attr *LinkCreateIterAttr) (*FD, error) {
 	return NewFD(int(fd))
 }
 
+type LinkCreateKprobeMultiAttr struct {
+	ProgFd           uint32
+	TargetFd         uint32
+	AttachType       AttachType
+	Flags            uint32
+	KprobeMultiFlags uint32
+	Count            uint32
+	Syms             Pointer
+	Addrs            Pointer
+	Cookies          Pointer
+}
+
+func LinkCreateKprobeMulti(attr *LinkCreateKprobeMultiAttr) (*FD, error) {
+	fd, err := BPF(BPF_LINK_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+	if err != nil {
+		return nil, err
+	}
+	return NewFD(int(fd))
+}
+
 type LinkCreatePerfEventAttr struct {
 	ProgFd     uint32
 	TargetFd   uint32
diff --git a/internal/unix/types_linux.go b/internal/unix/types_linux.go
index 8a030da..9cb2537 100644
--- a/internal/unix/types_linux.go
+++ b/internal/unix/types_linux.go
@@ -26,49 +26,50 @@ const (
 	EACCES  = linux.EACCES
 	EILSEQ  = linux.EILSEQ
 
-	BPF_F_NO_PREALLOC        = linux.BPF_F_NO_PREALLOC
-	BPF_F_NUMA_NODE          = linux.BPF_F_NUMA_NODE
-	BPF_F_RDONLY             = linux.BPF_F_RDONLY
-	BPF_F_WRONLY             = linux.BPF_F_WRONLY
-	BPF_F_RDONLY_PROG        = linux.BPF_F_RDONLY_PROG
-	BPF_F_WRONLY_PROG        = linux.BPF_F_WRONLY_PROG
-	BPF_F_SLEEPABLE          = linux.BPF_F_SLEEPABLE
-	BPF_F_MMAPABLE           = linux.BPF_F_MMAPABLE
-	BPF_F_INNER_MAP          = linux.BPF_F_INNER_MAP
-	BPF_OBJ_NAME_LEN         = linux.BPF_OBJ_NAME_LEN
-	BPF_TAG_SIZE             = linux.BPF_TAG_SIZE
-	BPF_RINGBUF_BUSY_BIT     = linux.BPF_RINGBUF_BUSY_BIT
-	BPF_RINGBUF_DISCARD_BIT  = linux.BPF_RINGBUF_DISCARD_BIT
-	BPF_RINGBUF_HDR_SZ       = linux.BPF_RINGBUF_HDR_SZ
-	SYS_BPF                  = linux.SYS_BPF
-	F_DUPFD_CLOEXEC          = linux.F_DUPFD_CLOEXEC
-	EPOLL_CTL_ADD            = linux.EPOLL_CTL_ADD
-	EPOLL_CLOEXEC            = linux.EPOLL_CLOEXEC
-	O_CLOEXEC                = linux.O_CLOEXEC
-	O_NONBLOCK               = linux.O_NONBLOCK
-	PROT_READ                = linux.PROT_READ
-	PROT_WRITE               = linux.PROT_WRITE
-	MAP_SHARED               = linux.MAP_SHARED
-	PERF_ATTR_SIZE_VER1      = linux.PERF_ATTR_SIZE_VER1
-	PERF_TYPE_SOFTWARE       = linux.PERF_TYPE_SOFTWARE
-	PERF_TYPE_TRACEPOINT     = linux.PERF_TYPE_TRACEPOINT
-	PERF_COUNT_SW_BPF_OUTPUT = linux.PERF_COUNT_SW_BPF_OUTPUT
-	PERF_EVENT_IOC_DISABLE   = linux.PERF_EVENT_IOC_DISABLE
-	PERF_EVENT_IOC_ENABLE    = linux.PERF_EVENT_IOC_ENABLE
-	PERF_EVENT_IOC_SET_BPF   = linux.PERF_EVENT_IOC_SET_BPF
-	PerfBitWatermark         = linux.PerfBitWatermark
-	PERF_SAMPLE_RAW          = linux.PERF_SAMPLE_RAW
-	PERF_FLAG_FD_CLOEXEC     = linux.PERF_FLAG_FD_CLOEXEC
-	RLIM_INFINITY            = linux.RLIM_INFINITY
-	RLIMIT_MEMLOCK           = linux.RLIMIT_MEMLOCK
-	BPF_STATS_RUN_TIME       = linux.BPF_STATS_RUN_TIME
-	PERF_RECORD_LOST         = linux.PERF_RECORD_LOST
-	PERF_RECORD_SAMPLE       = linux.PERF_RECORD_SAMPLE
-	AT_FDCWD                 = linux.AT_FDCWD
-	RENAME_NOREPLACE         = linux.RENAME_NOREPLACE
-	SO_ATTACH_BPF            = linux.SO_ATTACH_BPF
-	SO_DETACH_BPF            = linux.SO_DETACH_BPF
-	SOL_SOCKET               = linux.SOL_SOCKET
+	BPF_F_NO_PREALLOC         = linux.BPF_F_NO_PREALLOC
+	BPF_F_NUMA_NODE           = linux.BPF_F_NUMA_NODE
+	BPF_F_RDONLY              = linux.BPF_F_RDONLY
+	BPF_F_WRONLY              = linux.BPF_F_WRONLY
+	BPF_F_RDONLY_PROG         = linux.BPF_F_RDONLY_PROG
+	BPF_F_WRONLY_PROG         = linux.BPF_F_WRONLY_PROG
+	BPF_F_SLEEPABLE           = linux.BPF_F_SLEEPABLE
+	BPF_F_MMAPABLE            = linux.BPF_F_MMAPABLE
+	BPF_F_INNER_MAP           = linux.BPF_F_INNER_MAP
+	BPF_F_KPROBE_MULTI_RETURN = linux.BPF_F_KPROBE_MULTI_RETURN
+	BPF_OBJ_NAME_LEN          = linux.BPF_OBJ_NAME_LEN
+	BPF_TAG_SIZE              = linux.BPF_TAG_SIZE
+	BPF_RINGBUF_BUSY_BIT      = linux.BPF_RINGBUF_BUSY_BIT
+	BPF_RINGBUF_DISCARD_BIT   = linux.BPF_RINGBUF_DISCARD_BIT
+	BPF_RINGBUF_HDR_SZ        = linux.BPF_RINGBUF_HDR_SZ
+	SYS_BPF                   = linux.SYS_BPF
+	F_DUPFD_CLOEXEC           = linux.F_DUPFD_CLOEXEC
+	EPOLL_CTL_ADD             = linux.EPOLL_CTL_ADD
+	EPOLL_CLOEXEC             = linux.EPOLL_CLOEXEC
+	O_CLOEXEC                 = linux.O_CLOEXEC
+	O_NONBLOCK                = linux.O_NONBLOCK
+	PROT_READ                 = linux.PROT_READ
+	PROT_WRITE                = linux.PROT_WRITE
+	MAP_SHARED                = linux.MAP_SHARED
+	PERF_ATTR_SIZE_VER1       = linux.PERF_ATTR_SIZE_VER1
+	PERF_TYPE_SOFTWARE        = linux.PERF_TYPE_SOFTWARE
+	PERF_TYPE_TRACEPOINT      = linux.PERF_TYPE_TRACEPOINT
+	PERF_COUNT_SW_BPF_OUTPUT  = linux.PERF_COUNT_SW_BPF_OUTPUT
+	PERF_EVENT_IOC_DISABLE    = linux.PERF_EVENT_IOC_DISABLE
+	PERF_EVENT_IOC_ENABLE     = linux.PERF_EVENT_IOC_ENABLE
+	PERF_EVENT_IOC_SET_BPF    = linux.PERF_EVENT_IOC_SET_BPF
+	PerfBitWatermark          = linux.PerfBitWatermark
+	PERF_SAMPLE_RAW           = linux.PERF_SAMPLE_RAW
+	PERF_FLAG_FD_CLOEXEC      = linux.PERF_FLAG_FD_CLOEXEC
+	RLIM_INFINITY             = linux.RLIM_INFINITY
+	RLIMIT_MEMLOCK            = linux.RLIMIT_MEMLOCK
+	BPF_STATS_RUN_TIME        = linux.BPF_STATS_RUN_TIME
+	PERF_RECORD_LOST          = linux.PERF_RECORD_LOST
+	PERF_RECORD_SAMPLE        = linux.PERF_RECORD_SAMPLE
+	AT_FDCWD                  = linux.AT_FDCWD
+	RENAME_NOREPLACE          = linux.RENAME_NOREPLACE
+	SO_ATTACH_BPF             = linux.SO_ATTACH_BPF
+	SO_DETACH_BPF             = linux.SO_DETACH_BPF
+	SOL_SOCKET                = linux.SOL_SOCKET
 )
 
 // Statfs_t is a wrapper
diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go
index 1bc6c87..be3fdeb 100644
--- a/internal/unix/types_other.go
+++ b/internal/unix/types_other.go
@@ -27,50 +27,51 @@ const (
 	EACCES = syscall.Errno(0)
 	EILSEQ = syscall.Errno(0)
 
-	BPF_F_NO_PREALLOC        = 1 << 0
-	BPF_F_NUMA_NODE          = 0
-	BPF_F_RDONLY             = 0
-	BPF_F_WRONLY             = 0
-	BPF_F_RDONLY_PROG        = 1 << 7
-	BPF_F_WRONLY_PROG        = 1 << 8
-	BPF_F_SLEEPABLE          = 0
-	BPF_F_MMAPABLE           = 1 << 10
-	BPF_F_INNER_MAP          = 0
-	BPF_OBJ_NAME_LEN         = 0x10
-	BPF_TAG_SIZE             = 0x8
-	BPF_RINGBUF_BUSY_BIT     = 0
-	BPF_RINGBUF_DISCARD_BIT  = 0
-	BPF_RINGBUF_HDR_SZ       = 0
-	SYS_BPF                  = 321
-	F_DUPFD_CLOEXEC          = 0x406
-	EPOLLIN                  = 0x1
-	EPOLL_CTL_ADD            = 0x1
-	EPOLL_CLOEXEC            = 0x80000
-	O_CLOEXEC                = 0x80000
-	O_NONBLOCK               = 0x800
-	PROT_READ                = 0x1
-	PROT_WRITE               = 0x2
-	MAP_SHARED               = 0x1
-	PERF_ATTR_SIZE_VER1      = 0
-	PERF_TYPE_SOFTWARE       = 0x1
-	PERF_TYPE_TRACEPOINT     = 0
-	PERF_COUNT_SW_BPF_OUTPUT = 0xa
-	PERF_EVENT_IOC_DISABLE   = 0
-	PERF_EVENT_IOC_ENABLE    = 0
-	PERF_EVENT_IOC_SET_BPF   = 0
-	PerfBitWatermark         = 0x4000
-	PERF_SAMPLE_RAW          = 0x400
-	PERF_FLAG_FD_CLOEXEC     = 0x8
-	RLIM_INFINITY            = 0x7fffffffffffffff
-	RLIMIT_MEMLOCK           = 8
-	BPF_STATS_RUN_TIME       = 0
-	PERF_RECORD_LOST         = 2
-	PERF_RECORD_SAMPLE       = 9
-	AT_FDCWD                 = -0x2
-	RENAME_NOREPLACE         = 0x1
-	SO_ATTACH_BPF            = 0x32
-	SO_DETACH_BPF            = 0x1b
-	SOL_SOCKET               = 0x1
+	BPF_F_NO_PREALLOC         = 1 << 0
+	BPF_F_NUMA_NODE           = 0
+	BPF_F_RDONLY              = 0
+	BPF_F_WRONLY              = 0
+	BPF_F_RDONLY_PROG         = 1 << 7
+	BPF_F_WRONLY_PROG         = 1 << 8
+	BPF_F_SLEEPABLE           = 0
+	BPF_F_MMAPABLE            = 1 << 10
+	BPF_F_INNER_MAP           = 0
+	BPF_F_KPROBE_MULTI_RETURN = 0
+	BPF_OBJ_NAME_LEN          = 0x10
+	BPF_TAG_SIZE              = 0x8
+	BPF_RINGBUF_BUSY_BIT      = 0
+	BPF_RINGBUF_DISCARD_BIT   = 0
+	BPF_RINGBUF_HDR_SZ        = 0
+	SYS_BPF                   = 321
+	F_DUPFD_CLOEXEC           = 0x406
+	EPOLLIN                   = 0x1
+	EPOLL_CTL_ADD             = 0x1
+	EPOLL_CLOEXEC             = 0x80000
+	O_CLOEXEC                 = 0x80000
+	O_NONBLOCK                = 0x800
+	PROT_READ                 = 0x1
+	PROT_WRITE                = 0x2
+	MAP_SHARED                = 0x1
+	PERF_ATTR_SIZE_VER1       = 0
+	PERF_TYPE_SOFTWARE        = 0x1
+	PERF_TYPE_TRACEPOINT      = 0
+	PERF_COUNT_SW_BPF_OUTPUT  = 0xa
+	PERF_EVENT_IOC_DISABLE    = 0
+	PERF_EVENT_IOC_ENABLE     = 0
+	PERF_EVENT_IOC_SET_BPF    = 0
+	PerfBitWatermark          = 0x4000
+	PERF_SAMPLE_RAW           = 0x400
+	PERF_FLAG_FD_CLOEXEC      = 0x8
+	RLIM_INFINITY             = 0x7fffffffffffffff
+	RLIMIT_MEMLOCK            = 8
+	BPF_STATS_RUN_TIME        = 0
+	PERF_RECORD_LOST          = 2
+	PERF_RECORD_SAMPLE        = 9
+	AT_FDCWD                  = -0x2
+	RENAME_NOREPLACE          = 0x1
+	SO_ATTACH_BPF             = 0x32
+	SO_DETACH_BPF             = 0x1b
+	SOL_SOCKET                = 0x1
 )
 
 // Statfs_t is a wrapper
diff --git a/link/kprobe_multi.go b/link/kprobe_multi.go
new file mode 100644
index 0000000..06db518
--- /dev/null
+++ b/link/kprobe_multi.go
@@ -0,0 +1,176 @@
+package link
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"unsafe"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/asm"
+	"github.com/cilium/ebpf/internal"
+	"github.com/cilium/ebpf/internal/sys"
+	"github.com/cilium/ebpf/internal/unix"
+)
+
+// KprobeMultiOptions defines additional parameters that will be used
+// when opening a KprobeMulti Link.
+type KprobeMultiOptions struct {
+	// Symbols takes a list of kernel symbol names to attach an ebpf program to.
+	//
+	// Mutually exclusive with Addresses.
+	Symbols []string
+
+	// Addresses takes a list of kernel symbol addresses in case they can not
+	// be referred to by name.
+	//
+	// Note that only start addresses can be specified, since the fprobe API
+	// limits the attach point to the function entry or return.
+	//
+	// Mutually exclusive with Symbols.
+	Addresses []uint64
+
+	// Cookies specifies arbitrary values that can be fetched from an eBPF
+	// program via `bpf_get_attach_cookie()`.
+	//
+	// If set, its length should be equal to the length of Symbols or Addresses.
+	// Each Cookie is assigned to the Symbol or Address specified at the
+	// corresponding slice index.
+	Cookies []uint64
+}
+
+// KprobeMulti attaches the given eBPF program to the entry point of a given set
+// of kernel symbols.
+//
+// The difference with Kprobe() is that multi-kprobe accomplishes this in a
+// single system call, making it significantly faster than attaching many
+// probes one at a time.
+//
+// Requires at least Linux 5.18.
+func KprobeMulti(prog *ebpf.Program, opts KprobeMultiOptions) (Link, error) {
+	return kprobeMulti(prog, opts, 0)
+}
+
+// KretprobeMulti attaches the given eBPF program to the return point of a given
+// set of kernel symbols.
+//
+// The difference with Kretprobe() is that multi-kprobe accomplishes this in a
+// single system call, making it significantly faster than attaching many
+// probes one at a time.
+//
+// Requires at least Linux 5.18.
+func KretprobeMulti(prog *ebpf.Program, opts KprobeMultiOptions) (Link, error) {
+	return kprobeMulti(prog, opts, unix.BPF_F_KPROBE_MULTI_RETURN)
+}
+
+func kprobeMulti(prog *ebpf.Program, opts KprobeMultiOptions, flags uint32) (Link, error) {
+	if prog == nil {
+		return nil, errors.New("cannot attach a nil program")
+	}
+
+	syms := uint32(len(opts.Symbols))
+	addrs := uint32(len(opts.Addresses))
+	cookies := uint32(len(opts.Cookies))
+
+	if syms == 0 && addrs == 0 {
+		return nil, fmt.Errorf("one of Symbols or Addresses is required: %w", errInvalidInput)
+	}
+	if syms != 0 && addrs != 0 {
+		return nil, fmt.Errorf("Symbols and Addresses are mutually exclusive: %w", errInvalidInput)
+	}
+	if cookies > 0 && cookies != syms && cookies != addrs {
+		return nil, fmt.Errorf("Cookies must be exactly Symbols or Addresses in length: %w", errInvalidInput)
+	}
+
+	if err := haveBPFLinkKprobeMulti(); err != nil {
+		return nil, err
+	}
+
+	attr := &sys.LinkCreateKprobeMultiAttr{
+		ProgFd:           uint32(prog.FD()),
+		AttachType:       sys.BPF_TRACE_KPROBE_MULTI,
+		KprobeMultiFlags: flags,
+	}
+
+	switch {
+	case syms != 0:
+		attr.Count = syms
+		attr.Syms = sys.NewStringSlicePointer(opts.Symbols)
+
+	case addrs != 0:
+		attr.Count = addrs
+		attr.Addrs = sys.NewPointer(unsafe.Pointer(&opts.Addresses[0]))
+	}
+
+	if cookies != 0 {
+		attr.Cookies = sys.NewPointer(unsafe.Pointer(&opts.Cookies[0]))
+	}
+
+	fd, err := sys.LinkCreateKprobeMulti(attr)
+	if errors.Is(err, unix.ESRCH) {
+		return nil, fmt.Errorf("couldn't find one or more symbols: %w", os.ErrNotExist)
+	}
+	if errors.Is(err, unix.EINVAL) {
+		return nil, fmt.Errorf("%w (missing kernel symbol or prog's AttachType not AttachTraceKprobeMulti?)", err)
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	return &kprobeMultiLink{RawLink{fd, ""}}, nil
+}
+
+type kprobeMultiLink struct {
+	RawLink
+}
+
+var _ Link = (*kprobeMultiLink)(nil)
+
+func (kml *kprobeMultiLink) Update(prog *ebpf.Program) error {
+	return fmt.Errorf("update kprobe_multi: %w", ErrNotSupported)
+}
+
+func (kml *kprobeMultiLink) Pin(string) error {
+	return fmt.Errorf("pin kprobe_multi: %w", ErrNotSupported)
+}
+
+func (kml *kprobeMultiLink) Unpin() error {
+	return fmt.Errorf("unpin kprobe_multi: %w", ErrNotSupported)
+}
+
+var haveBPFLinkKprobeMulti = internal.FeatureTest("bpf_link_kprobe_multi", "5.18", func() error {
+	prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
+		Name: "probe_kpm_link",
+		Type: ebpf.Kprobe,
+		Instructions: asm.Instructions{
+			asm.Mov.Imm(asm.R0, 0),
+			asm.Return(),
+		},
+		AttachType: ebpf.AttachTraceKprobeMulti,
+		License:    "MIT",
+	})
+	if errors.Is(err, unix.E2BIG) {
+		// Kernel doesn't support AttachType field.
+		return internal.ErrNotSupported
+	}
+	if err != nil {
+		return err
+	}
+	defer prog.Close()
+
+	fd, err := sys.LinkCreateKprobeMulti(&sys.LinkCreateKprobeMultiAttr{
+		ProgFd:     uint32(prog.FD()),
+		AttachType: sys.BPF_TRACE_KPROBE_MULTI,
+		Count:      1,
+		Syms:       sys.NewStringSlicePointer([]string{"vprintk"}),
+	})
+	if errors.Is(err, unix.EINVAL) {
+		return internal.ErrNotSupported
+	}
+	if err != nil {
+		return err
+	}
+	fd.Close()
+
+	return nil
+})
diff --git a/link/link.go b/link/link.go
index 8ae1ba5..1127850 100644
--- a/link/link.go
+++ b/link/link.go
@@ -280,27 +280,24 @@ func (l *RawLink) Info() (*Info, error) {
 	switch info.Type {
 	case CgroupType:
 		extra = &CgroupInfo{}
-	case IterType:
-		// not supported
 	case NetNsType:
 		extra = &NetNsInfo{}
-	case RawTracepointType:
-		// not supported
 	case TracingType:
 		extra = &TracingInfo{}
 	case XDPType:
 		extra = &XDPInfo{}
-	case PerfEventType:
-		// no extra
+	case RawTracepointType, IterType,
+		PerfEventType, KprobeMultiType:
+		// Extra metadata not supported.
 	default:
 		return nil, fmt.Errorf("unknown link info type: %d", info.Type)
 	}
 
-	if info.Type != RawTracepointType && info.Type != IterType && info.Type != PerfEventType {
+	if extra != nil {
 		buf := bytes.NewReader(info.Extra[:])
 		err := binary.Read(buf, internal.NativeEndian, extra)
 		if err != nil {
-			return nil, fmt.Errorf("can not read extra link info: %w", err)
+			return nil, fmt.Errorf("cannot read extra link info: %w", err)
 		}
 	}
 
diff --git a/link/syscalls.go b/link/syscalls.go
index a661395..035d875 100644
--- a/link/syscalls.go
+++ b/link/syscalls.go
@@ -23,6 +23,7 @@ const (
 	NetNsType         = sys.BPF_LINK_TYPE_NETNS
 	XDPType           = sys.BPF_LINK_TYPE_XDP
 	PerfEventType     = sys.BPF_LINK_TYPE_PERF_EVENT
+	KprobeMultiType   = sys.BPF_LINK_TYPE_KPROBE_MULTI
 )
 
 var haveProgAttach = internal.FeatureTest("BPF_PROG_ATTACH", "4.10", func() error {
diff --git a/types.go b/types.go
index 4659f1b..03b7f11 100644
--- a/types.go
+++ b/types.go
@@ -230,6 +230,7 @@ const (
 	AttachSkReuseportSelect
 	AttachSkReuseportSelectOrMigrate
 	AttachPerfEvent
+	AttachTraceKprobeMulti
 )
 
 // AttachFlags of the eBPF program used in BPF_PROG_ATTACH command


Test patch:
diff --git a/link/kprobe_multi_test.go b/link/kprobe_multi_test.go
new file mode 100644
index 0000000..57c91a2
--- /dev/null
+++ b/link/kprobe_multi_test.go
@@ -0,0 +1,131 @@
+package link
+
+import (
+	"errors"
+	"math"
+	"os"
+	"testing"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/internal/testutils"
+	"github.com/cilium/ebpf/internal/unix"
+)
+
+var kprobeMultiSyms = []string{"vprintk", "inet6_release"}
+
+func TestKprobeMulti(t *testing.T) {
+	testutils.SkipIfNotSupported(t, haveBPFLinkKprobeMulti())
+
+	prog := mustLoadProgram(t, ebpf.Kprobe, ebpf.AttachTraceKprobeMulti, "")
+
+	km, err := KprobeMulti(prog, KprobeMultiOptions{Symbols: kprobeMultiSyms})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer km.Close()
+
+	testLink(t, km, prog)
+}
+
+func TestKprobeMultiInput(t *testing.T) {
+	// Program type that loads on all kernels. Not expected to link successfully.
+	prog := mustLoadProgram(t, ebpf.SocketFilter, 0, "")
+
+	// One of Symbols or Addresses must be given.
+	_, err := KprobeMulti(prog, KprobeMultiOptions{})
+	if !errors.Is(err, errInvalidInput) {
+		t.Fatalf("expected errInvalidInput, got: %v", err)
+	}
+
+	// Symbols and Addresses are mutually exclusive.
+	_, err = KprobeMulti(prog, KprobeMultiOptions{
+		Symbols:   []string{"foo"},
+		Addresses: []uint64{1},
+	})
+	if !errors.Is(err, errInvalidInput) {
+		t.Fatalf("expected errInvalidInput, got: %v", err)
+	}
+
+	// One Symbol, two cookies..
+	_, err = KprobeMulti(prog, KprobeMultiOptions{
+		Symbols: []string{"one"},
+		Cookies: []uint64{2, 3},
+	})
+	if !errors.Is(err, errInvalidInput) {
+		t.Fatalf("expected errInvalidInput, got: %v", err)
+	}
+}
+
+func TestKprobeMultiErrors(t *testing.T) {
+	testutils.SkipIfNotSupported(t, haveBPFLinkKprobeMulti())
+
+	prog := mustLoadProgram(t, ebpf.Kprobe, ebpf.AttachTraceKprobeMulti, "")
+
+	// Nonexistent kernel symbol.
+	_, err := KprobeMulti(prog, KprobeMultiOptions{Symbols: []string{"bogus"}})
+	if !errors.Is(err, os.ErrNotExist) && !errors.Is(err, unix.EINVAL) {
+		t.Fatalf("expected ErrNotExist or EINVAL, got: %s", err)
+	}
+
+	// Only have a negative test for addresses as it would be hard to maintain a
+	// proper one.
+	if _, err := KprobeMulti(prog, KprobeMultiOptions{
+		Addresses: []uint64{math.MaxUint64},
+	}); !errors.Is(err, unix.EINVAL) {
+		t.Fatalf("expected EINVAL, got: %s", err)
+	}
+}
+
+func TestKprobeMultiCookie(t *testing.T) {
+	testutils.SkipIfNotSupported(t, haveBPFLinkKprobeMulti())
+
+	prog := mustLoadProgram(t, ebpf.Kprobe, ebpf.AttachTraceKprobeMulti, "")
+
+	if _, err := KprobeMulti(prog, KprobeMultiOptions{
+		Symbols: kprobeMultiSyms,
+		Cookies: []uint64{0, 1},
+	}); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestKprobeMultiProgramCall(t *testing.T) {
+	testutils.SkipIfNotSupported(t, haveBPFLinkKprobeMulti())
+
+	m, p := newUpdaterMapProg(t, ebpf.Kprobe, ebpf.AttachTraceKprobeMulti)
+
+	// For simplicity, just assert the increment happens with any symbol in the array.
+	opts := KprobeMultiOptions{
+		Symbols: []string{"__do_sys_getpid"},
+	}
+	km, err := KprobeMulti(p, opts)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Trigger ebpf program call.
+	unix.Getpid()
+
+	// Assert that the value at index 0 has been updated to 1.
+	assertMapValue(t, m, 0, 1)
+
+	// Close the link.
+	if err := km.Close(); err != nil {
+		t.Fatal(err)
+	}
+
+	// Reset map value to 0 at index 0.
+	if err := m.Update(uint32(0), uint32(0), ebpf.UpdateExist); err != nil {
+		t.Fatal(err)
+	}
+
+	// Retrigger the ebpf program call.
+	unix.Getpid()
+
+	// Assert that this time the value has not been updated.
+	assertMapValue(t, m, 0, 0)
+}
+
+func TestHaveBPFLinkKprobeMulti(t *testing.T) {
+	testutils.CheckFeatureTest(t, haveBPFLinkKprobeMulti)
+}
diff --git a/link/kprobe_test.go b/link/kprobe_test.go
index a12378e..e9ba12a 100644
--- a/link/kprobe_test.go
+++ b/link/kprobe_test.go
@@ -308,7 +308,7 @@ func TestKprobeTraceFSGroup(t *testing.T) {
 }
 
 func TestKprobeProgramCall(t *testing.T) {
-	m, p := newUpdaterMapProg(t, ebpf.Kprobe)
+	m, p := newUpdaterMapProg(t, ebpf.Kprobe, 0)
 
 	// Open Kprobe on `sys_getpid` and attach it
 	// to the ebpf program created above.
@@ -340,7 +340,7 @@ func TestKprobeProgramCall(t *testing.T) {
 	assertMapValue(t, m, 0, 0)
 }
 
-func newUpdaterMapProg(t *testing.T, typ ebpf.ProgramType) (*ebpf.Map, *ebpf.Program) {
+func newUpdaterMapProg(t *testing.T, typ ebpf.ProgramType, attach ebpf.AttachType) (*ebpf.Map, *ebpf.Program) {
 	// Create ebpf map. Will contain only one key with initial value 0.
 	m, err := ebpf.NewMap(&ebpf.MapSpec{
 		Type:       ebpf.Array,
@@ -378,7 +378,8 @@ func newUpdaterMapProg(t *testing.T, typ ebpf.ProgramType) (*ebpf.Map, *ebpf.Pro
 			asm.Mov.Imm(asm.R0, 0),
 			asm.Return(),
 		},
-		License: "Dual MIT/GPL",
+		AttachType: attach,
+		License:    "Dual MIT/GPL",
 	})
 	if err != nil {
 		t.Fatal(err)
diff --git a/link/tracepoint_test.go b/link/tracepoint_test.go
index f7c054a..18d3612 100644
--- a/link/tracepoint_test.go
+++ b/link/tracepoint_test.go
@@ -75,7 +75,7 @@ func TestTracepointProgramCall(t *testing.T) {
 	// Kernels before 4.14 don't support attaching to syscall tracepoints.
 	testutils.SkipOnOldKernel(t, "4.14", "syscalls tracepoint support")
 
-	m, p := newUpdaterMapProg(t, ebpf.TracePoint)
+	m, p := newUpdaterMapProg(t, ebpf.TracePoint, 0)
 
 	// Open Tracepoint at /sys/kernel/debug/tracing/events/syscalls/sys_enter_getpid
 	// and attach it to the ebpf program created above.
diff --git a/link/uprobe_test.go b/link/uprobe_test.go
index 33f57a8..f0ef040 100644
--- a/link/uprobe_test.go
+++ b/link/uprobe_test.go
@@ -379,7 +379,7 @@ func TestUprobeProgramCall(t *testing.T) {
 				testutils.SkipOnOldKernel(t, "4.14", "uprobes on Go binaries silently fail on kernel < 4.14")
 			}
 
-			m, p := newUpdaterMapProg(t, ebpf.Kprobe)
+			m, p := newUpdaterMapProg(t, ebpf.Kprobe, 0)
 
 			// Load the executable.
 			ex, err := OpenExecutable(tt.elf)
@@ -431,7 +431,7 @@ func TestUprobeProgramCall(t *testing.T) {
 }
 
 func TestUprobeProgramWrongPID(t *testing.T) {
-	m, p := newUpdaterMapProg(t, ebpf.Kprobe)
+	m, p := newUpdaterMapProg(t, ebpf.Kprobe, 0)
 
 	// Load the '/bin/bash' executable.
 	ex, err := OpenExecutable("/bin/bash")