Repository: cilium/ebpf
Pull request #541 (2022-01-08 12:46:49)

features: add misc probes
Signed-off-by: Florian Lehner <dev@der-flo.net>

Extend the features API with macros. So similar to`bpftool feature probe macros` one can now check if large instructions, v2 or v3 ISA or bounded loops are supported.

florianl: From the CI information that are shared publicly I can't tell, why `KERNEL_VERSION=5.4` is not passing. It seems to me the test is running into a timeout rather than failing.
lmb: Yeah the tests are due to semaphore CI bugs that I haven't been able to iron out.
lmb: @ti-mo  can you take a look please?
lmb: There is a real test failure:
```
--- FAIL: TestHaveMisc (0.00s) 00:30
    --- FAIL: TestHaveMisc/misc-2 (0.00s) 00:30
        misc_test.go:37: Feature misc-2 isn't supported even though kernel is at least 4.13: not supported 00:30
FAIL
```
florianl: > There is a real test failure:
> 
> ```
> --- FAIL: TestHaveMisc (0.00s) 00:30
>     --- FAIL: TestHaveMisc/misc-2 (0.00s) 00:30
>         misc_test.go:37: Feature misc-2 isn't supported even though kernel is at least 4.13: not supported 00:30
> FAIL
> ```


my fault :see_no_evil: the jump offset for this probe was not correct. I did test it yesterday only on a 5.16. kernel. fixed this.

But now `Run unit tests - KERNEL_VERSION=5.10` fails:
```
Testing on 5.1000:26
[..]
exit status 4200:39
FAIL	github.com/cilium/ebpf/features	0.339s
[..]
```
From the CI I can't tell exactly which test in `github.com/cilium/ebpf/features` failed. Locally I can reproduce the failure - see output https://gist.github.com/florianl/cb22bc3d9b00e65dfe2d43b84cb6c8e7
But the failing tests are `TestHaveMapType/InodeStorage` and `TestHaveProgType/LircMode2`.

lmb: `exit 42` is an issue that happens on CI somewhat regularly unfortunately. Not necessarily related to your PR.

Patch:
diff --git a/asm/instruction.go b/asm/instruction.go
index 22975e8..8c6d502 100644
--- a/asm/instruction.go
+++ b/asm/instruction.go
@@ -19,6 +19,9 @@ const InstructionSize = 8
 // RawInstructionOffset is an offset in units of raw BPF instructions.
 type RawInstructionOffset uint64
 
+var ErrUnsatisfiedMapReference = errors.New("unsatisfied map reference")
+var ErrUnsatisfiedReference = errors.New("unsatisfied reference")
+
 // Bytes returns the offset of an instruction in bytes.
 func (rio RawInstructionOffset) Bytes() uint64 {
 	return uint64(rio) * InstructionSize
@@ -177,6 +180,17 @@ func (ins *Instruction) RewriteMapOffset(offset uint32) error {
 	return nil
 }
 
+// RewriteJumpOffset sets the offset for a jump operation.
+//
+// Returns an error if the instruction is not a jump operation.
+func (ins *Instruction) RewriteJumpOffset(offset int16) error {
+	if ins.OpCode.JumpOp() == InvalidJumpOp {
+		return errors.New("not a jump operation")
+	}
+	ins.Offset = offset
+	return nil
+}
+
 func (ins *Instruction) mapOffset() uint32 {
 	return uint32(uint64(ins.Constant) >> 32)
 }
@@ -527,6 +541,65 @@ func (insns Instructions) Tag(bo binary.ByteOrder) (string, error) {
 	return hex.EncodeToString(h.Sum(nil)[:unix.BPF_TAG_SIZE]), nil
 }
 
+// FixupReferences updates all references to also take the symbol offset into account.
+//
+// Returns an error if a reference isn't used, see ErrUnsatisfiedMapReference and ErrUnsatisfiedReference.
+func (insns Instructions) FixupReferences() error {
+	symbolOffsets := make(map[string]RawInstructionOffset)
+	iter := insns.Iterate()
+	for iter.Next() {
+		ins := iter.Ins
+
+		if ins.Symbol == "" {
+			continue
+		}
+
+		if _, ok := symbolOffsets[ins.Symbol]; ok {
+			return fmt.Errorf("duplicate symbol %s", ins.Symbol)
+		}
+
+		symbolOffsets[ins.Symbol] = iter.Offset
+	}
+
+	iter = insns.Iterate()
+	for iter.Next() {
+		i := iter.Index
+		offset := iter.Offset
+		ins := iter.Ins
+
+		if ins.Reference == "" {
+			continue
+		}
+
+		symOffset, ok := symbolOffsets[ins.Reference]
+		switch {
+		case ins.IsFunctionReference() && ins.Constant == -1:
+			if !ok {
+				break
+			}
+
+			ins.Constant = int64(symOffset - offset - 1)
+			continue
+
+		case ins.OpCode.Class().IsJump() && ins.Offset == -1:
+			if !ok {
+				break
+			}
+
+			ins.Offset = int16(symOffset - offset - 1)
+			continue
+
+		case ins.IsLoadFromMap() && ins.MapPtr() == -1:
+			return fmt.Errorf("map %s: %w", ins.Reference, ErrUnsatisfiedMapReference)
+		default:
+			// no fixup needed
+			continue
+		}
+		return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference, ErrUnsatisfiedReference)
+	}
+	return nil
+}
+
 // Iterate allows iterating a BPF program while keeping track of
 // various offsets.
 //
diff --git a/features/misc.go b/features/misc.go
new file mode 100644
index 0000000..0934652
--- /dev/null
+++ b/features/misc.go
@@ -0,0 +1,232 @@
+package features
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"os"
+	"sync"
+
+	"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"
+)
+
+func init() {
+	miscs.miscTypes = make(map[miscType]error, maxMiscType)
+}
+
+var (
+	miscs miscCache
+)
+
+type miscCache struct {
+	sync.Mutex
+	miscTypes map[miscType]error
+}
+
+type miscType uint32
+
+// Max returns the latest supported MiscType.
+func (_ miscType) max() miscType {
+	return maxMiscType - 1
+}
+
+const (
+	// largeInsn support introduced in
+	// commit c04c0d2b968ac45d6ef020316808ef6c82325a82
+	largeInsn miscType = iota
+	// boundedLoops support introduced in
+	// commit 2589726d12a1b12eaaa93c7f1ea64287e383c7a5
+	boundedLoops
+	// v2ISA support introduced in
+	// commit 92b31a9af73b3a3fc801899335d6c47966351830
+	v2ISA
+	// v3ISA support introduced in
+	// commit 092ed0968bb648cd18e8a0430cd0a8a71727315c
+	v3ISA
+	// maxMiscType - Bound enum of FeatureTypes, has to be last in enum.
+	maxMiscType
+)
+
+const (
+	maxInsns = 4096
+)
+
+// HaveLargeInstructions probes the running kernel if more than 4096 instructions
+// per program are supported.
+// Return values have the following semantics:
+//
+//   err == nil: The feature is available.
+//   errors.Is(err, ebpf.ErrNotSupported): The feature is not available.
+//   err != nil: Any errors encountered during probe execution, wrapped.
+//
+// Note that the latter case may include false negatives, and that program creation may
+// succeed despite an error being returned. Some program types cannot reliably be probed and
+// will also return error. Only `nil` and `ebpf.ErrNotSupported` are conclusive.
+//
+// Probe results are cached and persist throughout any process capability changes.
+func HaveLargeInstructions() error {
+	return probeMisc(largeInsn)
+}
+
+// HaveBoundedLoops probes the running kernel if bounded loops are supported.
+// Return values have the following semantics:
+//
+//   err == nil: The feature is available.
+//   errors.Is(err, ebpf.ErrNotSupported): The feature is not available.
+//   err != nil: Any errors encountered during probe execution, wrapped.
+//
+// Note that the latter case may include false negatives, and that program creation may
+// succeed despite an error being returned. Some program types cannot reliably be probed and
+// will also return error. Only `nil` and `ebpf.ErrNotSupported` are conclusive.
+//
+// Probe results are cached and persist throughout any process capability changes.
+func HaveBoundedLoops() error {
+	return probeMisc(boundedLoops)
+}
+
+// HaveV2ISA probes the running kernel if instructions of the v2 ISA are supported.
+// Return values have the following semantics:
+//
+//   err == nil: The feature is available.
+//   errors.Is(err, ebpf.ErrNotSupported): The feature is not available.
+//   err != nil: Any errors encountered during probe execution, wrapped.
+//
+// Note that the latter case may include false negatives, and that program creation may
+// succeed despite an error being returned. Some program types cannot reliably be probed and
+// will also return error. Only `nil` and `ebpf.ErrNotSupported` are conclusive.
+//
+// Probe results are cached and persist throughout any process capability changes.
+func HaveV2ISA() error {
+	return probeMisc(v2ISA)
+}
+
+// HaveV3ISA probes the running kernel if instructions of the v3 ISA are supported.
+// Return values have the following semantics:
+//
+//   err == nil: The feature is available.
+//   errors.Is(err, ebpf.ErrNotSupported): The feature is not available.
+//   err != nil: Any errors encountered during probe execution, wrapped.
+//
+// Note that the latter case may include false negatives, and that program creation may
+// succeed despite an error being returned. Some program types cannot reliably be probed and
+// will also return error. Only `nil` and `ebpf.ErrNotSupported` are conclusive.
+//
+// Probe results are cached and persist throughout any process capability changes.
+func HaveV3ISA() error {
+	return probeMisc(v3ISA)
+}
+
+// probeMisc checks the kernel for a given supported misc by creating
+// a specialized program probe and loading it.
+// Results are cached and persist throughout any process capability changes.
+func probeMisc(mt miscType) error {
+	if mt > mt.max() {
+		return os.ErrInvalid
+	}
+	mc.Lock()
+	defer mc.Unlock()
+	err, ok := miscs.miscTypes[mt]
+	if ok {
+		return err
+	}
+
+	attr, err := createMiscProbeAttr(mt)
+	if err != nil {
+		return fmt.Errorf("couldn't create the attributes for the probe: %w", err)
+	}
+
+	fd, err := sys.ProgLoad(attr)
+
+	switch {
+	// EINVAL occurs when attempting to create a program with an unknown type.
+	// E2BIG occurs when ProgLoadAttr contains non-zero bytes past the end
+	// of the struct known by the running kernel, meaning the kernel is too old
+	// to support the given map type.
+	case errors.Is(err, unix.EINVAL), errors.Is(err, unix.E2BIG):
+		err = ebpf.ErrNotSupported
+
+	// EPERM is kept as-is and is not converted or wrapped.
+	case errors.Is(err, unix.EPERM):
+		break
+
+	// Wrap unexpected errors.
+	case err != nil:
+		err = fmt.Errorf("unexpected error during feature probe: %w", err)
+
+	default:
+		fd.Close()
+	}
+
+	miscs.miscTypes[mt] = err
+
+	return err
+}
+
+func createMiscProbeAttr(mt miscType) (*sys.ProgLoadAttr, error) {
+	var (
+		insns asm.Instructions
+		label string
+	)
+
+	switch mt {
+	case largeInsn:
+		for i := 0; i < maxInsns; i++ {
+			insns = append(insns, asm.Mov.Imm(asm.R0, 1))
+		}
+		insns = append(insns, asm.Return())
+	case boundedLoops:
+		label = "boundedLoop"
+		insns = asm.Instructions{
+			asm.Mov.Imm(asm.R0, 10),
+			asm.Sub.Imm(asm.R0, 1).Sym(label),
+			asm.JNE.Imm(asm.R0, 0, label),
+			asm.Return(),
+		}
+	case v2ISA:
+		label = "v2isa"
+		insns = asm.Instructions{
+			asm.Mov.Imm(asm.R0, 0).Sym(label),
+			asm.JLT.Imm(asm.R0, 0, label),
+			asm.Mov.Imm(asm.R0, 1),
+			asm.Return(),
+		}
+		// To test the v2 ISA we need a dedicated jump offset other
+		// than the one we would get from Instruction.FixupReferences().
+		if err := insns[1].RewriteJumpOffset(1); err != nil {
+			return nil, err
+		}
+	case v3ISA:
+		label = "v3isa"
+		insns = asm.Instructions{
+			asm.Mov.Imm(asm.R0, 0).Sym(label),
+			asm.JLT.Imm32(asm.R0, 0, label),
+			asm.Mov.Imm(asm.R0, 1),
+			asm.Return(),
+		}
+	default:
+		return nil, fmt.Errorf("feature %d not yet implemented", mt)
+	}
+
+	if err := insns.FixupReferences(); err != nil {
+		return nil, err
+	}
+
+	buf := bytes.NewBuffer(make([]byte, 0, insns.Size()))
+	if err := insns.Marshal(buf, internal.NativeEndian); err != nil {
+		return nil, err
+	}
+
+	bytecode := buf.Bytes()
+	instructions := sys.NewSlicePointer(bytecode)
+
+	return &sys.ProgLoadAttr{
+		ProgType: sys.BPF_PROG_TYPE_SOCKET_FILTER,
+		Insns:    instructions,
+		InsnCnt:  uint32(len(bytecode) / asm.InstructionSize),
+		License:  sys.NewStringPointer("MIT"),
+	}, nil
+}
diff --git a/internal/btf/btf.go b/internal/btf/btf.go
index 4b88559..df4f78e 100644
--- a/internal/btf/btf.go
+++ b/internal/btf/btf.go
@@ -90,6 +90,17 @@ func LoadSpecFromReader(rd io.ReaderAt) (*Spec, error) {
 	}
 	defer file.Close()
 
+	return loadSpecFromELF(file)
+}
+
+// variableOffsets extracts all symbols offsets from an ELF and indexes them by
+// section and variable name.
+//
+// References to variables in BTF data sections carry unsigned 32-bit offsets.
+// Some ELF symbols (e.g. in vmlinux) may point to virtual memory that is well
+// beyond this range. Since these symbols cannot be described by BTF info,
+// ignore them here.
+func variableOffsets(file *internal.SafeELFFile) (map[variable]uint32, error) {
 	symbols, err := file.Symbols()
 	if err != nil {
 		return nil, fmt.Errorf("can't read symbols: %v", err)
@@ -102,22 +113,23 @@ func LoadSpecFromReader(rd io.ReaderAt) (*Spec, error) {
 			continue
 		}
 
+		if symbol.Value > math.MaxUint32 {
+			// VarSecinfo offset is u32, cannot reference symbols in higher regions.
+			continue
+		}
+
 		if int(symbol.Section) >= len(file.Sections) {
 			return nil, fmt.Errorf("symbol %s: invalid section %d", symbol.Name, symbol.Section)
 		}
 
 		secName := file.Sections[symbol.Section].Name
-		if symbol.Value > math.MaxUint32 {
-			return nil, fmt.Errorf("section %s: symbol %s: size exceeds maximum", secName, symbol.Name)
-		}
-
 		variableOffsets[variable{secName, symbol.Name}] = uint32(symbol.Value)
 	}
 
-	return loadSpecFromELF(file, variableOffsets)
+	return variableOffsets, nil
 }
 
-func loadSpecFromELF(file *internal.SafeELFFile, variableOffsets map[variable]uint32) (*Spec, error) {
+func loadSpecFromELF(file *internal.SafeELFFile) (*Spec, error) {
 	var (
 		btfSection    *elf.Section
 		btfExtSection *elf.Section
@@ -147,7 +159,12 @@ func loadSpecFromELF(file *internal.SafeELFFile, variableOffsets map[variable]ui
 		return nil, fmt.Errorf("btf: %w", ErrNotFound)
 	}
 
-	spec, err := loadRawSpec(btfSection.Open(), file.ByteOrder, sectionSizes, variableOffsets)
+	vars, err := variableOffsets(file)
+	if err != nil {
+		return nil, err
+	}
+
+	spec, err := loadRawSpec(btfSection.Open(), file.ByteOrder, sectionSizes, vars)
 	if err != nil {
 		return nil, err
 	}
@@ -295,6 +312,9 @@ func LoadKernelSpec() (*Spec, error) {
 	return kernelBTF.Spec, err
 }
 
+// loadKernelSpec attempts to load the raw vmlinux BTF blob at
+// /sys/kernel/btf/vmlinux and falls back to scanning the file system
+// for vmlinux ELFs.
 func loadKernelSpec() (*Spec, error) {
 	fh, err := os.Open("/sys/kernel/btf/vmlinux")
 	if err == nil {
@@ -303,13 +323,21 @@ func loadKernelSpec() (*Spec, error) {
 		return loadRawSpec(fh, internal.NativeEndian, nil, nil)
 	}
 
-	var uname unix.Utsname
-	if err := unix.Uname(&uname); err != nil {
-		return nil, fmt.Errorf("uname failed: %w", err)
+	file, err := findVMLinux()
+	if err != nil {
+		return nil, err
 	}
+	defer file.Close()
+
+	return loadSpecFromELF(file)
+}
 
-	end := bytes.IndexByte(uname.Release[:], 0)
-	release := string(uname.Release[:end])
+// findVMLinux scans multiple well-known paths for vmlinux kernel images.
+func findVMLinux() (*internal.SafeELFFile, error) {
+	release, err := internal.KernelRelease()
+	if err != nil {
+		return nil, err
+	}
 
 	// use same list of locations as libbpf
 	// https://github.com/libbpf/libbpf/blob/9a3a42608dbe3731256a5682a125ac1e23bced8f/src/btf.c#L3114-L3122
@@ -324,24 +352,14 @@ func loadKernelSpec() (*Spec, error) {
 	}
 
 	for _, loc := range locations {
-		path := fmt.Sprintf(loc, release)
-
-		fh, err := os.Open(path)
+		fh, err := os.Open(fmt.Sprintf(loc, release))
 		if err != nil {
 			continue
 		}
-		defer fh.Close()
-
-		file, err := internal.NewSafeELFFile(fh)
-		if err != nil {
-			return nil, err
-		}
-		defer file.Close()
-
-		return loadSpecFromELF(file, nil)
+		return internal.NewSafeELFFile(fh)
 	}
 
-	return nil, fmt.Errorf("no BTF for kernel version %s: %w", release, internal.ErrNotSupported)
+	return nil, fmt.Errorf("no BTF found for kernel version %s: %w", release, internal.ErrNotSupported)
 }
 
 // parseBTFHeader parses the header of the .BTF section.
diff --git a/internal/version.go b/internal/version.go
index 690c552..370e01e 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -3,6 +3,8 @@ package internal
 import (
 	"fmt"
 	"sync"
+
+	"github.com/cilium/ebpf/internal/unix"
 )
 
 const (
@@ -105,3 +107,16 @@ func detectKernelVersion() (Version, error) {
 	}
 	return NewVersionFromCode(vc), nil
 }
+
+// KernelRelease returns the release string of the running kernel.
+// Its format depends on the Linux distribution and corresponds to directory
+// names in /lib/modules by convention. Some examples are 5.15.17-1-lts and
+// 4.19.0-16-amd64.
+func KernelRelease() (string, error) {
+	var uname unix.Utsname
+	if err := unix.Uname(&uname); err != nil {
+		return "", fmt.Errorf("uname failed: %w", err)
+	}
+
+	return unix.ByteSliceToString(uname.Release[:]), nil
+}
diff --git a/linker.go b/linker.go
index b056f99..21f7db8 100644
--- a/linker.go
+++ b/linker.go
@@ -115,63 +115,13 @@ func marshalLineInfos(layout []reference) ([]byte, error) {
 }
 
 func fixupJumpsAndCalls(insns asm.Instructions) error {
-	symbolOffsets := make(map[string]asm.RawInstructionOffset)
-	iter := insns.Iterate()
-	for iter.Next() {
-		ins := iter.Ins
-
-		if ins.Symbol == "" {
-			continue
-		}
-
-		if _, ok := symbolOffsets[ins.Symbol]; ok {
-			return fmt.Errorf("duplicate symbol %s", ins.Symbol)
-		}
-
-		symbolOffsets[ins.Symbol] = iter.Offset
-	}
-
-	iter = insns.Iterate()
-	for iter.Next() {
-		i := iter.Index
-		offset := iter.Offset
-		ins := iter.Ins
-
-		if ins.Reference == "" {
-			continue
-		}
-
-		symOffset, ok := symbolOffsets[ins.Reference]
-		switch {
-		case ins.IsFunctionReference() && ins.Constant == -1:
-			if !ok {
-				break
-			}
-
-			ins.Constant = int64(symOffset - offset - 1)
-			continue
-
-		case ins.OpCode.Class().IsJump() && ins.Offset == -1:
-			if !ok {
-				break
-			}
-
-			ins.Offset = int16(symOffset - offset - 1)
-			continue
-
-		case ins.IsLoadFromMap() && ins.MapPtr() == -1:
-			return fmt.Errorf("map %s: %w", ins.Reference, errUnsatisfiedMap)
-		default:
-			// no fixup needed
-			continue
-		}
-
-		return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference, errUnsatisfiedProgram)
+	if err := insns.FixupReferences(); err != nil {
+		return err
 	}
 
 	// fixupBPFCalls replaces bpf_probe_read_{kernel,user}[_str] with bpf_probe_read[_str] on older kernels
 	// https://github.com/libbpf/libbpf/blob/master/src/libbpf.c#L6009
-	iter = insns.Iterate()
+	iter := insns.Iterate()
 	for iter.Next() {
 		ins := iter.Ins
 		if !ins.IsBuiltinCall() {
diff --git a/prog.go b/prog.go
index 523e6a5..df2c710 100644
--- a/prog.go
+++ b/prog.go
@@ -21,9 +21,6 @@ import (
 // ErrNotSupported is returned whenever the kernel doesn't support a feature.
 var ErrNotSupported = internal.ErrNotSupported
 
-var errUnsatisfiedMap = errors.New("unsatisfied map reference")
-var errUnsatisfiedProgram = errors.New("unsatisfied program reference")
-
 // ProgramID represents the unique ID of an eBPF program.
 type ProgramID uint32
 
@@ -235,7 +232,7 @@ func NewProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er
 	defer handles.close()
 
 	prog, err := newProgramWithOptions(spec, opts, handles)
-	if errors.Is(err, errUnsatisfiedMap) {
+	if errors.Is(err, asm.ErrUnsatisfiedMapReference) {
 		return nil, fmt.Errorf("cannot load program without loading its whole collection: %w", err)
 	}
 	return prog, err


Test patch:
diff --git a/features/misc_test.go b/features/misc_test.go
new file mode 100644
index 0000000..207dd64
--- /dev/null
+++ b/features/misc_test.go
@@ -0,0 +1,42 @@
+package features
+
+import (
+	"errors"
+	"fmt"
+	"math"
+	"os"
+	"testing"
+
+	"github.com/cilium/ebpf/internal/testutils"
+)
+
+func TestInvalidMisc(t *testing.T) {
+	if err := probeMisc(miscType(math.MaxUint32)); !errors.Is(err, os.ErrInvalid) {
+		t.Fatalf("Expected os.ErrInvalid but was: %v", err)
+	}
+}
+
+func TestHaveMisc(t *testing.T) {
+	tests := map[miscType]struct {
+		probe     func() error
+		minKernel string
+	}{
+		largeInsn:    {probe: HaveLargeInstructions, minKernel: "5.1"},
+		boundedLoops: {probe: HaveBoundedLoops, minKernel: "5.2"},
+		v2ISA:        {probe: HaveV2ISA, minKernel: "4.13"},
+		v3ISA:        {probe: HaveV3ISA, minKernel: "5.0"},
+	}
+
+	for misc, test := range tests {
+		test := test
+		probe := fmt.Sprintf("misc-%d", misc)
+		t.Run(probe, func(t *testing.T) {
+			testutils.SkipOnOldKernel(t, test.minKernel, probe)
+
+			if err := test.probe(); err != nil {
+				t.Fatalf("Feature %s isn't supported even though kernel is at least %s: %v",
+					probe, test.minKernel, err)
+			}
+		})
+	}
+}
diff --git a/internal/btf/btf_test.go b/internal/btf/btf_test.go
index f4e881d..65e8440 100644
--- a/internal/btf/btf_test.go
+++ b/internal/btf/btf_test.go
@@ -186,6 +186,23 @@ func TestParseCurrentKernelBTF(t *testing.T) {
 	}
 }
 
+func TestFindVMLinux(t *testing.T) {
+	file, err := findVMLinux()
+	testutils.SkipIfNotSupported(t, err)
+	if err != nil {
+		t.Fatal("Can't find vmlinux:", err)
+	}
+
+	spec, err := loadSpecFromELF(file)
+	if err != nil {
+		t.Fatal("Can't load BTF:", err)
+	}
+
+	if len(spec.namedTypes) == 0 {
+		t.Fatal("Empty kernel BTF")
+	}
+}
+
 func TestLoadSpecFromElf(t *testing.T) {
 	testutils.Files(t, testutils.Glob(t, "../../testdata/loader-e*.elf"), func(t *testing.T, file string) {
 		spec := parseELFBTF(t, file)
diff --git a/internal/version_test.go b/internal/version_test.go
index 95ead52..9febb60 100644
--- a/internal/version_test.go
+++ b/internal/version_test.go
@@ -84,3 +84,14 @@ func TestVersionFromCode(t *testing.T) {
 		})
 	}
 }
+
+func TestKernelRelease(t *testing.T) {
+	r, err := KernelRelease()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if r == "" {
+		t.Fatal("unexpected empty kernel release")
+	}
+}
diff --git a/linker_test.go b/linker_test.go
index cb75c74..bcb7635 100644
--- a/linker_test.go
+++ b/linker_test.go
@@ -74,7 +74,7 @@ func TestForwardFunctionDeclaration(t *testing.T) {
 
 		// This program calls an unimplemented forward function declaration.
 		_, err = NewProgram(spec)
-		if !errors.Is(err, errUnsatisfiedProgram) {
+		if !errors.Is(err, asm.ErrUnsatisfiedReference) {
 			t.Fatal("Expected an error wrapping errUnsatisfiedProgram, got:", err)
 		}
 
diff --git a/prog_test.go b/prog_test.go
index 079fa1e..448fa0a 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -374,8 +374,8 @@ func TestProgramWithUnsatisfiedMap(t *testing.T) {
 	progSpec.ByteOrder = nil
 
 	_, err = NewProgram(progSpec)
-	if !errors.Is(err, errUnsatisfiedMap) {
-		t.Fatal("Expected an error wrapping errUnsatisfiedMap, got", err)
+	if !errors.Is(err, asm.ErrUnsatisfiedMapReference) {
+		t.Fatal("Expected an error wrapping errUnsatisfiedMapReference, got", err)
 	}
 	t.Log(err)
 }