Repository: cilium/ebpf
Pull request #1118 (2023-08-16 08:38:42)

Add metadata to instructions returned by program info
The instructions returned by program info gotten from the kernel did
not contain any metadata such as line info or function info. This
makes it harder to read the instructions when comparing the xlated
instruction in the kernel with the spec.

This PR uses the line info and function info that is available via
the program info and adds it to the returned instructions.

dylandreimerink: > Some high level thoughts: seems like what you really want to do is calling Assign(). All of the other stuff with ExtInfoBuilder is just a side-show. What do you think about the following?
>
>* Export funcInfo and lineInfo
>* Export constructors for []FuncInfo, []LineInfo. Those should take a byte order, reader and a Spec probably. These are somewhere between parseFuncInfos and parseFuncInfoRecords: they take infos without the header and return a slice instead of a map.
> * Add a function AssignInstructionMetadata(Instructions, []FuncInfo, []LineInfo) and call that from ExtInfos.Assign. The core relocation part we can keep in ExtInfos.Assign for now.

Yea, that is fair, sounds like a good way to approach this. I will make it so

Patch:
diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml
index c85a833..315ce05 100644
--- a/.semaphore/semaphore.yml
+++ b/.semaphore/semaphore.yml
@@ -16,8 +16,8 @@ blocks:
       prologue:
         commands:
           - sudo sh -c 'swapoff -a && fallocate -l 2G /swapfile && chmod 0600 /swapfile && mkswap /swapfile && swapon /swapfile'
-          - sudo mkdir -p /usr/local/golang/1.21.0 && curl -fL "https://go.dev/dl/go1.21.0.linux-amd64.tar.gz" | sudo tar -xz -C /usr/local/golang/1.21.0
-          - sem-version go 1.21.0
+          - sudo mkdir -p /usr/local/golang/1.21.1 && curl -fL "https://go.dev/dl/go1.21.1.linux-amd64.tar.gz" | sudo tar -xz -C /usr/local/golang/1.21.1
+          - sem-version go 1.21.1
           - export PATH="$PATH:$(go env GOPATH)/bin"
           - checkout
           # Disabled, see https://github.com/cilium/ebpf/issues/898
@@ -38,7 +38,7 @@ blocks:
         - name: TMPDIR
           value: /tmp
         - name: CI_MAX_KERNEL_VERSION
-          value: "5.19"
+          value: "6.1"
         - name: CI_MIN_CLANG_VERSION
           value: "9"
       jobs:
@@ -61,7 +61,7 @@ blocks:
         execution_time_limit:
           minutes: 10
         commands:
-          - sem-version go 1.20.6
+          - sem-version go 1.20.8
           - go test -v ./cmd/bpf2go
           - gotestsum --raw-command --ignore-non-json-output-lines --junitfile junit.xml -- ./run-tests.sh $CI_MAX_KERNEL_VERSION -short -count 1 -json ./...
       - name: Run unit tests
@@ -69,6 +69,6 @@ blocks:
           minutes: 10
         matrix:
           - env_var: KERNEL_VERSION
-            values: ["5.19", "5.15", "5.10", "5.4", "4.19", "4.14", "4.9"]
+            values: ["6.1", "5.15", "5.10", "5.4", "4.19", "4.14", "4.9"]
         commands:
           - gotestsum --raw-command --ignore-non-json-output-lines --junitfile junit.xml -- ./run-tests.sh $KERNEL_VERSION -short -count 1 -json ./...
diff --git a/btf/ext_info.go b/btf/ext_info.go
index b764fb7..f0646a2 100644
--- a/btf/ext_info.go
+++ b/btf/ext_info.go
@@ -16,9 +16,9 @@ import (
 // ExtInfos contains ELF section metadata.
 type ExtInfos struct {
 	// The slices are sorted by offset in ascending order.
-	funcInfos       map[string][]funcInfo
-	lineInfos       map[string][]lineInfo
-	relocationInfos map[string][]coreRelocationInfo
+	funcInfos       map[string]FuncInfos
+	lineInfos       map[string]LineInfos
+	relocationInfos map[string]CORERelocationInfos
 }
 
 // loadExtInfosFromELF parses ext infos from the .BTF.ext section in an ELF.
@@ -58,7 +58,7 @@ func loadExtInfos(r io.ReaderAt, bo binary.ByteOrder, spec *Spec, strings *strin
 		return nil, fmt.Errorf("parsing BTF function info: %w", err)
 	}
 
-	funcInfos := make(map[string][]funcInfo, len(btfFuncInfos))
+	funcInfos := make(map[string]FuncInfos, len(btfFuncInfos))
 	for section, bfis := range btfFuncInfos {
 		funcInfos[section], err = newFuncInfos(bfis, spec)
 		if err != nil {
@@ -72,7 +72,7 @@ func loadExtInfos(r io.ReaderAt, bo binary.ByteOrder, spec *Spec, strings *strin
 		return nil, fmt.Errorf("parsing BTF line info: %w", err)
 	}
 
-	lineInfos := make(map[string][]lineInfo, len(btfLineInfos))
+	lineInfos := make(map[string]LineInfos, len(btfLineInfos))
 	for section, blis := range btfLineInfos {
 		lineInfos[section], err = newLineInfos(blis, strings)
 		if err != nil {
@@ -91,7 +91,7 @@ func loadExtInfos(r io.ReaderAt, bo binary.ByteOrder, spec *Spec, strings *strin
 		return nil, fmt.Errorf("parsing CO-RE relocation info: %w", err)
 	}
 
-	coreRelos := make(map[string][]coreRelocationInfo, len(btfCORERelos))
+	coreRelos := make(map[string]CORERelocationInfos, len(btfCORERelos))
 	for section, brs := range btfCORERelos {
 		coreRelos[section], err = newRelocationInfos(brs, spec, strings)
 		if err != nil {
@@ -111,21 +111,31 @@ func (ei *ExtInfos) Assign(insns asm.Instructions, section string) {
 	lineInfos := ei.lineInfos[section]
 	reloInfos := ei.relocationInfos[section]
 
+	AssignMetadataToInstructions(insns, funcInfos, lineInfos, reloInfos)
+}
+
+// Assign per-instruction metadata to the instructions in insns.
+func AssignMetadataToInstructions(
+	insns asm.Instructions,
+	funcInfos FuncInfos,
+	lineInfos LineInfos,
+	reloInfos CORERelocationInfos,
+) {
 	iter := insns.Iterate()
 	for iter.Next() {
-		if len(funcInfos) > 0 && funcInfos[0].offset == iter.Offset {
-			*iter.Ins = WithFuncMetadata(*iter.Ins, funcInfos[0].fn)
-			funcInfos = funcInfos[1:]
+		if len(funcInfos.infos) > 0 && funcInfos.infos[0].offset == iter.Offset {
+			*iter.Ins = WithFuncMetadata(*iter.Ins, funcInfos.infos[0].fn)
+			funcInfos.infos = funcInfos.infos[1:]
 		}
 
-		if len(lineInfos) > 0 && lineInfos[0].offset == iter.Offset {
-			*iter.Ins = iter.Ins.WithSource(lineInfos[0].line)
-			lineInfos = lineInfos[1:]
+		if len(lineInfos.infos) > 0 && lineInfos.infos[0].offset == iter.Offset {
+			*iter.Ins = iter.Ins.WithSource(lineInfos.infos[0].line)
+			lineInfos.infos = lineInfos.infos[1:]
 		}
 
-		if len(reloInfos) > 0 && reloInfos[0].offset == iter.Offset {
-			iter.Ins.Metadata.Set(coreRelocationMeta{}, reloInfos[0].relo)
-			reloInfos = reloInfos[1:]
+		if len(reloInfos.infos) > 0 && reloInfos.infos[0].offset == iter.Offset {
+			iter.Ins.Metadata.Set(coreRelocationMeta{}, reloInfos.infos[0].relo)
+			reloInfos.infos = reloInfos.infos[1:]
 		}
 	}
 }
@@ -323,6 +333,11 @@ func parseExtInfoRecordSize(r io.Reader, bo binary.ByteOrder) (uint32, error) {
 	return recordSize, nil
 }
 
+// FuncInfos contains a sorted list of func infos.
+type FuncInfos struct {
+	infos []funcInfo
+}
+
 // The size of a FuncInfo in BTF wire format.
 var FuncInfoSize = uint32(binary.Size(bpfFuncInfo{}))
 
@@ -359,21 +374,38 @@ func newFuncInfo(fi bpfFuncInfo, spec *Spec) (*funcInfo, error) {
 	}, nil
 }
 
-func newFuncInfos(bfis []bpfFuncInfo, spec *Spec) ([]funcInfo, error) {
-	fis := make([]funcInfo, 0, len(bfis))
+func newFuncInfos(bfis []bpfFuncInfo, spec *Spec) (FuncInfos, error) {
+	fis := FuncInfos{
+		infos: make([]funcInfo, 0, len(bfis)),
+	}
 	for _, bfi := range bfis {
 		fi, err := newFuncInfo(bfi, spec)
 		if err != nil {
-			return nil, fmt.Errorf("offset %d: %w", bfi.InsnOff, err)
+			return FuncInfos{}, fmt.Errorf("offset %d: %w", bfi.InsnOff, err)
 		}
-		fis = append(fis, *fi)
+		fis.infos = append(fis.infos, *fi)
 	}
-	sort.Slice(fis, func(i, j int) bool {
-		return fis[i].offset <= fis[j].offset
+	sort.Slice(fis.infos, func(i, j int) bool {
+		return fis.infos[i].offset <= fis.infos[j].offset
 	})
 	return fis, nil
 }
 
+// LoadFuncInfos parses btf func info in wire format.
+func LoadFuncInfos(reader io.Reader, bo binary.ByteOrder, recordNum uint32, spec *Spec) (FuncInfos, error) {
+	fis, err := parseFuncInfoRecords(
+		reader,
+		bo,
+		FuncInfoSize,
+		recordNum,
+	)
+	if err != nil {
+		return FuncInfos{}, fmt.Errorf("parsing BTF func info: %w", err)
+	}
+
+	return newFuncInfos(fis, spec)
+}
+
 // marshal into the BTF wire format.
 func (fi *funcInfo) marshal(w *bytes.Buffer, b *Builder) error {
 	id, err := b.Add(fi.fn)
@@ -480,6 +512,11 @@ func (li *Line) String() string {
 	return li.line
 }
 
+// LineInfos contains a sorted list of line infos.
+type LineInfos struct {
+	infos []lineInfo
+}
+
 type lineInfo struct {
 	line   *Line
 	offset asm.RawInstructionOffset
@@ -500,6 +537,21 @@ type bpfLineInfo struct {
 	LineCol     uint32
 }
 
+// LoadLineInfos parses btf line info in wire format.
+func LoadLineInfos(reader io.Reader, bo binary.ByteOrder, recordNum uint32, spec *Spec) (LineInfos, error) {
+	lis, err := parseLineInfoRecords(
+		reader,
+		bo,
+		LineInfoSize,
+		recordNum,
+	)
+	if err != nil {
+		return LineInfos{}, fmt.Errorf("parsing BTF line info: %w", err)
+	}
+
+	return newLineInfos(lis, spec.strings)
+}
+
 func newLineInfo(li bpfLineInfo, strings *stringTable) (*lineInfo, error) {
 	line, err := strings.Lookup(li.LineOff)
 	if err != nil {
@@ -525,17 +577,19 @@ func newLineInfo(li bpfLineInfo, strings *stringTable) (*lineInfo, error) {
 	}, nil
 }
 
-func newLineInfos(blis []bpfLineInfo, strings *stringTable) ([]lineInfo, error) {
-	lis := make([]lineInfo, 0, len(blis))
+func newLineInfos(blis []bpfLineInfo, strings *stringTable) (LineInfos, error) {
+	lis := LineInfos{
+		infos: make([]lineInfo, 0, len(blis)),
+	}
 	for _, bli := range blis {
 		li, err := newLineInfo(bli, strings)
 		if err != nil {
-			return nil, fmt.Errorf("offset %d: %w", bli.InsnOff, err)
+			return LineInfos{}, fmt.Errorf("offset %d: %w", bli.InsnOff, err)
 		}
-		lis = append(lis, *li)
+		lis.infos = append(lis.infos, *li)
 	}
-	sort.Slice(lis, func(i, j int) bool {
-		return lis[i].offset <= lis[j].offset
+	sort.Slice(lis.infos, func(i, j int) bool {
+		return lis.infos[i].offset <= lis.infos[j].offset
 	})
 	return lis, nil
 }
@@ -661,6 +715,11 @@ func CORERelocationMetadata(ins *asm.Instruction) *CORERelocation {
 	return relo
 }
 
+// CORERelocationInfos contains a sorted list of co:re relocation infos.
+type CORERelocationInfos struct {
+	infos []coreRelocationInfo
+}
+
 type coreRelocationInfo struct {
 	relo   *CORERelocation
 	offset asm.RawInstructionOffset
@@ -693,17 +752,19 @@ func newRelocationInfo(relo bpfCORERelo, spec *Spec, strings *stringTable) (*cor
 	}, nil
 }
 
-func newRelocationInfos(brs []bpfCORERelo, spec *Spec, strings *stringTable) ([]coreRelocationInfo, error) {
-	rs := make([]coreRelocationInfo, 0, len(brs))
+func newRelocationInfos(brs []bpfCORERelo, spec *Spec, strings *stringTable) (CORERelocationInfos, error) {
+	rs := CORERelocationInfos{
+		infos: make([]coreRelocationInfo, 0, len(brs)),
+	}
 	for _, br := range brs {
 		relo, err := newRelocationInfo(br, spec, strings)
 		if err != nil {
-			return nil, fmt.Errorf("offset %d: %w", br.InsnOff, err)
+			return CORERelocationInfos{}, fmt.Errorf("offset %d: %w", br.InsnOff, err)
 		}
-		rs = append(rs, *relo)
+		rs.infos = append(rs.infos, *relo)
 	}
-	sort.Slice(rs, func(i, j int) bool {
-		return rs[i].offset < rs[j].offset
+	sort.Slice(rs.infos, func(i, j int) bool {
+		return rs.infos[i].offset < rs.infos[j].offset
 	})
 	return rs, nil
 }
diff --git a/info.go b/info.go
index a02e8a4..79b11c9 100644
--- a/info.go
+++ b/info.go
@@ -101,6 +101,11 @@ type ProgramInfo struct {
 
 	maps  []MapID
 	insns []byte
+
+	lineInfos    []byte
+	numLineInfos uint32
+	funcInfos    []byte
+	numFuncInfos uint32
 }
 
 func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
@@ -128,10 +133,13 @@ func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
 	// Start with a clean struct for the second call, otherwise we may get EFAULT.
 	var info2 sys.ProgInfo
 
+	makeSecondCall := false
+
 	if info.NrMapIds > 0 {
 		pi.maps = make([]MapID, info.NrMapIds)
 		info2.NrMapIds = info.NrMapIds
 		info2.MapIds = sys.NewPointer(unsafe.Pointer(&pi.maps[0]))
+		makeSecondCall = true
 	} else if haveProgramInfoMapIDs() == nil {
 		// This program really has no associated maps.
 		pi.maps = make([]MapID, 0)
@@ -150,9 +158,28 @@ func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
 		pi.insns = make([]byte, info.XlatedProgLen)
 		info2.XlatedProgLen = info.XlatedProgLen
 		info2.XlatedProgInsns = sys.NewSlicePointer(pi.insns)
+		makeSecondCall = true
+	}
+
+	if info.NrLineInfo > 0 {
+		pi.lineInfos = make([]byte, btf.LineInfoSize*info.NrLineInfo)
+		info2.LineInfo = sys.NewSlicePointer(pi.lineInfos)
+		info2.LineInfoRecSize = btf.LineInfoSize
+		info2.NrLineInfo = info.NrLineInfo
+		pi.numLineInfos = info.NrLineInfo
+		makeSecondCall = true
 	}
 
-	if info.NrMapIds > 0 || info.XlatedProgLen > 0 {
+	if info.NrFuncInfo > 0 {
+		pi.funcInfos = make([]byte, btf.FuncInfoSize*info.NrFuncInfo)
+		info2.FuncInfo = sys.NewSlicePointer(pi.funcInfos)
+		info2.FuncInfoRecSize = btf.FuncInfoSize
+		info2.NrFuncInfo = info.NrFuncInfo
+		pi.numFuncInfos = info.NrFuncInfo
+		makeSecondCall = true
+	}
+
+	if makeSecondCall {
 		if err := sys.ObjInfo(fd, &info2); err != nil {
 			return nil, err
 		}
@@ -245,7 +272,13 @@ func (pi *ProgramInfo) Runtime() (time.Duration, bool) {
 //
 // The first instruction is marked as a symbol using the Program's name.
 //
-// Available from 4.13. Requires CAP_BPF or equivalent.
+// If available, the instructions will be annotated with metadata from the
+// BTF. This includes line information and function information. Reading
+// this metadata requires CAP_SYS_ADMIN or equivalent. If capability is
+// unavailable, the instructions will be returned without metadata.
+//
+// Available from 4.13. Requires CAP_BPF or equivalent for plain instructions.
+// Requires CAP_SYS_ADMIN for instructions with metadata.
 func (pi *ProgramInfo) Instructions() (asm.Instructions, error) {
 	// If the calling process is not BPF-capable or if the kernel doesn't
 	// support getting xlated instructions, the field will be zero.
@@ -259,8 +292,55 @@ func (pi *ProgramInfo) Instructions() (asm.Instructions, error) {
 		return nil, fmt.Errorf("unmarshaling instructions: %w", err)
 	}
 
-	// Tag the first instruction with the name of the program, if available.
-	insns[0] = insns[0].WithSymbol(pi.Name)
+	if pi.btf != 0 {
+		btfh, err := btf.NewHandleFromID(pi.btf)
+		if err != nil {
+			// Getting a BTF handle requires CAP_SYS_ADMIN, if not available we get an -EPERM.
+			// Ignore it and fall back to instructions without metadata.
+			if !errors.Is(err, unix.EPERM) {
+				return nil, fmt.Errorf("unable to get BTF handle: %w", err)
+			}
+		}
+
+		// If we have a BTF handle, we can use it to assign metadata to the instructions.
+		if btfh != nil {
+			defer btfh.Close()
+
+			spec, err := btfh.Spec(nil)
+			if err != nil {
+				return nil, fmt.Errorf("unable to get BTF spec: %w", err)
+			}
+
+			lineInfos, err := btf.LoadLineInfos(
+				bytes.NewReader(pi.lineInfos),
+				internal.NativeEndian,
+				pi.numLineInfos,
+				spec,
+			)
+			if err != nil {
+				return nil, fmt.Errorf("parse line info: %w", err)
+			}
+
+			funcInfos, err := btf.LoadFuncInfos(
+				bytes.NewReader(pi.funcInfos),
+				internal.NativeEndian,
+				pi.numFuncInfos,
+				spec,
+			)
+			if err != nil {
+				return nil, fmt.Errorf("parse func info: %w", err)
+			}
+
+			btf.AssignMetadataToInstructions(insns, funcInfos, lineInfos, btf.CORERelocationInfos{})
+		}
+	}
+
+	fn := btf.FuncMetadata(&insns[0])
+	name := pi.Name
+	if fn != nil {
+		name = fn.Name
+	}
+	insns[0] = insns[0].WithSymbol(name)
 
 	return insns, nil
 }
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index 09b2ea9..1a333f4 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -158,6 +158,8 @@ import (
 				replace(objName, "name"),
 				replace(pointer, "xlated_prog_insns"),
 				replace(pointer, "map_ids"),
+				replace(pointer, "line_info"),
+				replace(pointer, "func_info"),
 				replace(btfID, "btf_id", "attach_btf_obj_id"),
 				replace(typeID, "attach_btf_id"),
 			},
diff --git a/internal/sys/types.go b/internal/sys/types.go
index a1c9a75..b7e3244 100644
--- a/internal/sys/types.go
+++ b/internal/sys/types.go
@@ -530,10 +530,10 @@ type ProgInfo struct {
 	JitedFuncLens        uint64
 	BtfId                BTFID
 	FuncInfoRecSize      uint32
-	FuncInfo             uint64
+	FuncInfo             Pointer
 	NrFuncInfo           uint32
 	NrLineInfo           uint32
-	LineInfo             uint64
+	LineInfo             Pointer
 	JitedLineInfo        uint64
 	NrJitedLineInfo      uint32
 	LineInfoRecSize      uint32


Test patch:
diff --git a/btf/core_test.go b/btf/core_test.go
index 1cb75f8..561337e 100644
--- a/btf/core_test.go
+++ b/btf/core_test.go
@@ -586,7 +586,7 @@ func TestCORERelocation(t *testing.T) {
 			name := strings.TrimPrefix(section, "socket_filter/")
 			t.Run(name, func(t *testing.T) {
 				var relos []*CORERelocation
-				for _, reloInfo := range extInfos.relocationInfos[section] {
+				for _, reloInfo := range extInfos.relocationInfos[section].infos {
 					relos = append(relos, reloInfo.relo)
 				}
 
diff --git a/elf_reader_test.go b/elf_reader_test.go
index c9aa045..f732878 100644
--- a/elf_reader_test.go
+++ b/elf_reader_test.go
@@ -968,31 +968,29 @@ func TestLibBPFCompat(t *testing.T) {
 	)
 
 	testutils.Files(t, files, func(t *testing.T, path string) {
-		file := filepath.Base(path)
-		switch file {
-		case "test_map_in_map.o", "test_map_in_map.linked3.o",
-			"test_select_reuseport_kern.o", "test_select_reuseport_kern.linked3.o":
+		name := selftestName(path)
+		switch name {
+		case "test_map_in_map", "test_select_reuseport_kern":
 			t.Skip("Skipping due to missing InnerMap in map definition")
-		case "test_core_autosize.o":
+		case "test_core_autosize":
 			t.Skip("Skipping since the test generates dynamic BTF")
-		case "test_static_linked.linked3.o":
+		case "test_static_linked":
 			t.Skip("Skipping since .text contains 'subprog' twice")
-		case "linked_maps.linked3.o", "linked_funcs.linked3.o", "linked_vars.linked3.o",
-			"kprobe_multi.o", "kprobe_multi.linked3.o", "test_ksyms_weak.o",
-			"test_ksyms_weak.llinked3.o", "test_ksyms_weak.llinked2.o", "test_ksyms_weak.llinked1.o",
-			"test_ksyms_weak.linked3.o", "test_ksyms_module.o", "test_ksyms_module.llinked3.o",
-			"test_ksyms_module.llinked2.o", "test_ksyms_module.llinked1.o", "test_ksyms_module.linked3.o",
-			"test_ksyms.o", "test_ksyms.linked3.o":
+		case "linked_maps", "linked_funcs", "linked_vars", "kprobe_multi",
+			"test_ksyms_weak", "test_ksyms_module", "test_ksyms":
 			t.Skip("Skipping since weak relocations are not supported")
-		case "bloom_filter_map.o", "bloom_filter_map.linked3.o",
-			"bloom_filter_bench.o", "bloom_filter_bench.linked3.o":
+		case "bloom_filter_map", "bloom_filter_bench":
 			t.Skip("Skipping due to missing MapExtra field in MapSpec")
-		case "netif_receive_skb.linked3.o":
+		case "netif_receive_skb":
 			t.Skip("Skipping due to possible bug in upstream CO-RE generation")
-		case "test_usdt.o", "test_usdt.linked3.o", "test_urandom_usdt.o", "test_urandom_usdt.linked3.o", "test_usdt_multispec.o":
+		case "test_usdt", "test_urandom_usdt", "test_usdt_multispec":
 			t.Skip("Skipping due to missing support for usdt.bpf.h")
-		case "bpf_cubic.o", "bpf_cubic.linked3.o", "bpf_iter_tcp6.o", "bpf_iter_tcp6.linked3.o", "bpf_iter_tcp4.o", "bpf_iter_tcp4.linked3.o", "bpf_iter_ipv6_route.o", "bpf_iter_ipv6_route.linked3.o", "test_subskeleton_lib.o", "test_skeleton.o", "test_skeleton.linked3.o", "test_subskeleton_lib.linked3.o", "test_subskeleton.o", "test_subskeleton.linked3.o", "test_core_extern.linked3.o", "test_core_extern.o", "profiler1.linked3.o", "profiler3.o", "profiler3.linked3.o", "profiler2.o", "profiler2.linked3.o", "profiler1.o":
-			t.Skip("Skipping due to using CONFIG_* variable which are not supported at the moment")
+		case "lsm_cgroup", "bpf_iter_ipv6_route", "test_core_extern",
+			"profiler1", "profiler2", "profiler3":
+			t.Skip("Skipping due to using weak CONFIG_* variables")
+		case "linked_maps1", "linked_maps2", "linked_funcs1", "linked_funcs2",
+			"test_subskeleton", "test_subskeleton_lib":
+			t.Skip("Skipping due to relying on cross ELF linking")
 		}
 
 		t.Parallel()
@@ -1000,11 +998,11 @@ func TestLibBPFCompat(t *testing.T) {
 		spec, err := LoadCollectionSpec(path)
 		testutils.SkipIfNotSupported(t, err)
 		if err != nil {
-			t.Fatalf("Can't read %s: %s", file, err)
+			t.Fatal(err)
 		}
 
-		switch file {
-		case "test_sk_assign.o":
+		switch name {
+		case "test_sk_assign":
 			// Test contains a legacy iproute2 bpf_elf_map definition.
 			for _, m := range spec.Maps {
 				if m.Extra == nil || m.Extra.Len() == 0 {
@@ -1031,34 +1029,34 @@ func TestLibBPFCompat(t *testing.T) {
 		}
 
 		for _, coreFile := range coreFiles {
-			name := filepath.Base(coreFile)
+			name := selftestName(coreFile)
 			t.Run(name, func(t *testing.T) {
 				// Some files like btf__core_reloc_arrays___err_too_small.o
 				// trigger an error on purpose. Use the name to infer whether
 				// the test should succeed.
 				var valid bool
 				switch name {
-				case "btf__core_reloc_existence___err_wrong_arr_kind.o",
-					"btf__core_reloc_existence___err_wrong_arr_value_type.o",
-					"btf__core_reloc_existence___err_wrong_int_kind.o",
-					"btf__core_reloc_existence___err_wrong_int_sz.o",
-					"btf__core_reloc_existence___err_wrong_int_type.o",
-					"btf__core_reloc_existence___err_wrong_struct_type.o":
+				case "btf__core_reloc_existence___err_wrong_arr_kind",
+					"btf__core_reloc_existence___err_wrong_arr_value_type",
+					"btf__core_reloc_existence___err_wrong_int_kind",
+					"btf__core_reloc_existence___err_wrong_int_sz",
+					"btf__core_reloc_existence___err_wrong_int_type",
+					"btf__core_reloc_existence___err_wrong_struct_type":
 					// These tests are buggy upstream, see https://lore.kernel.org/bpf/20210420111639.155580-1-lmb@cloudflare.com/
 					valid = true
-				case "btf__core_reloc_ints___err_wrong_sz_16.o",
-					"btf__core_reloc_ints___err_wrong_sz_32.o",
-					"btf__core_reloc_ints___err_wrong_sz_64.o",
-					"btf__core_reloc_ints___err_wrong_sz_8.o",
-					"btf__core_reloc_arrays___err_wrong_val_type1.o",
-					"btf__core_reloc_arrays___err_wrong_val_type2.o":
+				case "btf__core_reloc_ints___err_wrong_sz_16",
+					"btf__core_reloc_ints___err_wrong_sz_32",
+					"btf__core_reloc_ints___err_wrong_sz_64",
+					"btf__core_reloc_ints___err_wrong_sz_8",
+					"btf__core_reloc_arrays___err_wrong_val_type1",
+					"btf__core_reloc_arrays___err_wrong_val_type2":
 					// These tests are valid according to current libbpf behaviour,
 					// see commit 42765ede5c54ca915de5bfeab83be97207e46f68.
 					valid = true
-				case "btf__core_reloc_type_id___missing_targets.o",
-					"btf__core_reloc_flavors__err_wrong_name.o":
+				case "btf__core_reloc_type_id___missing_targets",
+					"btf__core_reloc_flavors__err_wrong_name":
 					valid = false
-				case "btf__core_reloc_ints___err_bitfield.o":
+				case "btf__core_reloc_ints___err_bitfield":
 					// Bitfields are now valid.
 					valid = true
 				default:
@@ -1085,28 +1083,33 @@ func TestLibBPFCompat(t *testing.T) {
 }
 
 func loadTargetProgram(tb testing.TB, name string, opts CollectionOptions) (*Program, *Collection) {
-	file := "test_pkt_access.o"
+	file := "test_pkt_access.bpf.o"
 	program := "test_pkt_access"
 	switch name {
 	case "new_connect_v4_prog":
-		file = "connect4_prog.o"
+		file = "connect4_prog.bpf.o"
 		program = "connect_v4_prog"
 	case "new_do_bind":
-		file = "connect4_prog.o"
+		file = "connect4_prog.bpf.o"
 		program = "connect_v4_prog"
 	case "freplace_cls_redirect_test":
-		file = "test_cls_redirect.o"
+		file = "test_cls_redirect.bpf.o"
 		program = "cls_redirect"
 	case "new_handle_kprobe":
-		file = "test_attach_probe.o"
+		file = "test_attach_probe.bpf.o"
 		program = "handle_kprobe"
 	case "test_pkt_md_access_new":
-		file = "test_pkt_md_access.o"
+		file = "test_pkt_md_access.bpf.o"
 		program = "test_pkt_md_access"
 	default:
 	}
 
 	spec, err := LoadCollectionSpec(filepath.Join(*elfPath, file))
+	if errors.Is(err, os.ErrNotExist) && strings.HasSuffix(file, ".bpf.o") {
+		// Prior to v6.1 BPF ELF used a plain .o suffix.
+		file = strings.TrimSuffix(file, ".bpf.o") + ".o"
+		spec, err = LoadCollectionSpec(filepath.Join(*elfPath, file))
+	}
 	if err != nil {
 		tb.Fatalf("Can't read %s: %s", file, err)
 	}
@@ -1219,3 +1222,22 @@ func TestGetProgType(t *testing.T) {
 		}
 	}
 }
+
+// selftestName takes a path to a file and derives a canonical name from it.
+//
+// It strips various suffixes used by the selftest build system.
+func selftestName(path string) string {
+	file := filepath.Base(path)
+
+	name := strings.TrimSuffix(file, ".o")
+	// Strip various suffixes.
+	// Various linking suffixes.
+	name = strings.TrimSuffix(name, ".linked3")
+	name = strings.TrimSuffix(name, ".llinked1")
+	name = strings.TrimSuffix(name, ".llinked2")
+	name = strings.TrimSuffix(name, ".llinked3")
+	// v6.1 started adding .bpf to all BPF ELF.
+	name = strings.TrimSuffix(name, ".bpf")
+
+	return name
+}
diff --git a/info_test.go b/info_test.go
index c41ae6c..d0aca4e 100644
--- a/info_test.go
+++ b/info_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/cilium/ebpf/asm"
+	"github.com/cilium/ebpf/btf"
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/sys"
 	"github.com/cilium/ebpf/internal/testutils"
@@ -365,3 +366,42 @@ func testStats(prog *Program) error {
 func TestHaveProgramInfoMapIDs(t *testing.T) {
 	testutils.CheckFeatureTest(t, haveProgramInfoMapIDs)
 }
+
+func TestProgInfoExtBTF(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "5.0", "Program BTF (func/line_info)")
+
+	spec, err := LoadCollectionSpec("testdata/raw_tracepoint-el.elf")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	coll, err := NewCollection(spec)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer coll.Close()
+
+	info, err := coll.Programs["sched_process_exec"].Info()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	inst, err := info.Instructions()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	const expectedSource = "\treturn 0;"
+	if inst[0].Source().String() != expectedSource {
+		t.Fatalf("Source of first instruction incorrect. Got '%s', expected: '%s'", inst[0].Source().String(), expectedSource)
+	}
+
+	fn := btf.FuncMetadata(&inst[0])
+	if fn == nil {
+		t.Fatal("Func metadata missing")
+	}
+
+	if fn.Name != "sched_process_exec" {
+		t.Fatalf("Func metadata incorrect. Got '%s', expected: 'sched_process_exec'", fn.Name)
+	}
+}