Repository: cilium/ebpf
Pull request #106 (2020-06-10 14:05:30)

bpf_link prerequisites
This is the refactoring done in the bpf_link PR #105 solit out into a separate PR, to make reviewing the changes less daunting. Thanks @tklauser for the suggestion.

@florianl I've incorporated your idea re the kernel BTF, thanks!


Patch:
diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml
index fc4d8f3..aa39469 100644
--- a/.semaphore/semaphore.yml
+++ b/.semaphore/semaphore.yml
@@ -30,7 +30,7 @@ blocks:
           - env_var: GO_VERSION
             values: [ "1.13", "1.14" ]
           - env_var: KERNEL_VERSION
-            values: ["5.4.5", "4.19.81", "4.9.198"]
+            values: ["5.4", "4.19", "4.9"]
         commands:
           - sem-version go $GO_VERSION
           - timeout -s KILL 90s ./run-tests.sh $KERNEL_VERSION
diff --git a/elf_reader.go b/elf_reader.go
index 1d35c8f..0469244 100644
--- a/elf_reader.go
+++ b/elf_reader.go
@@ -567,81 +567,61 @@ func (ec *elfCode) loadDataSections(maps map[string]*MapSpec, dataSections map[e
 	return nil
 }
 
-func getProgType(v string) (ProgramType, AttachType) {
-	types := map[string]ProgramType{
-		// From https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/lib/bpf/libbpf.c#n3568
-		"socket":          SocketFilter,
-		"seccomp":         SocketFilter,
-		"kprobe/":         Kprobe,
-		"uprobe/":         Kprobe,
-		"kretprobe/":      Kprobe,
-		"uretprobe/":      Kprobe,
-		"tracepoint/":     TracePoint,
-		"raw_tracepoint/": RawTracepoint,
-		"xdp":             XDP,
-		"perf_event":      PerfEvent,
-		"lwt_in":          LWTIn,
-		"lwt_out":         LWTOut,
-		"lwt_xmit":        LWTXmit,
-		"lwt_seg6local":   LWTSeg6Local,
-		"sockops":         SockOps,
-		"sk_skb":          SkSKB,
-		"sk_msg":          SkMsg,
-		"lirc_mode2":      LircMode2,
-		"flow_dissector":  FlowDissector,
-
-		"cgroup_skb/":       CGroupSKB,
-		"cgroup/dev":        CGroupDevice,
-		"cgroup/skb":        CGroupSKB,
-		"cgroup/sock":       CGroupSock,
-		"cgroup/post_bind":  CGroupSock,
-		"cgroup/bind":       CGroupSockAddr,
-		"cgroup/connect":    CGroupSockAddr,
-		"cgroup/sendmsg":    CGroupSockAddr,
-		"cgroup/recvmsg":    CGroupSockAddr,
-		"cgroup/sysctl":     CGroupSysctl,
-		"cgroup/getsockopt": CGroupSockopt,
-		"cgroup/setsockopt": CGroupSockopt,
-		"classifier":        SchedCLS,
-		"action":            SchedACT,
-	}
-	attachTypes := map[string]AttachType{
-		"cgroup_skb/ingress":    AttachCGroupInetIngress,
-		"cgroup_skb/egress":     AttachCGroupInetEgress,
-		"cgroup/sock":           AttachCGroupInetSockCreate,
-		"cgroup/post_bind4":     AttachCGroupInet4PostBind,
-		"cgroup/post_bind6":     AttachCGroupInet6PostBind,
-		"cgroup/dev":            AttachCGroupDevice,
-		"sockops":               AttachCGroupSockOps,
-		"sk_skb/stream_parser":  AttachSkSKBStreamParser,
-		"sk_skb/stream_verdict": AttachSkSKBStreamVerdict,
-		"sk_msg":                AttachSkSKBStreamVerdict,
-		"lirc_mode2":            AttachLircMode2,
-		"flow_dissector":        AttachFlowDissector,
-		"cgroup/bind4":          AttachCGroupInet4Bind,
-		"cgroup/bind6":          AttachCGroupInet6Bind,
-		"cgroup/connect4":       AttachCGroupInet4Connect,
-		"cgroup/connect6":       AttachCGroupInet6Connect,
-		"cgroup/sendmsg4":       AttachCGroupUDP4Sendmsg,
-		"cgroup/sendmsg6":       AttachCGroupUDP6Sendmsg,
-		"cgroup/recvmsg4":       AttachCGroupUDP4Recvmsg,
-		"cgroup/recvmsg6":       AttachCGroupUDP6Recvmsg,
-		"cgroup/sysctl":         AttachCGroupSysctl,
-		"cgroup/getsockopt":     AttachCGroupGetsockopt,
-		"cgroup/setsockopt":     AttachCGroupSetsockopt,
-	}
-	attachType := AttachNone
-	for k, t := range attachTypes {
-		if strings.HasPrefix(v, k) {
-			attachType = t
-		}
-	}
-
-	for k, t := range types {
-		if strings.HasPrefix(v, k) {
-			return t, attachType
+func getProgType(sectionName string) (ProgramType, AttachType) {
+	types := map[string]struct {
+		progType   ProgramType
+		attachType AttachType
+	}{
+		// From https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/lib/bpf/libbpf.c
+		"socket":                {SocketFilter, AttachNone},
+		"seccomp":               {SocketFilter, AttachNone},
+		"kprobe/":               {Kprobe, AttachNone},
+		"uprobe/":               {Kprobe, AttachNone},
+		"kretprobe/":            {Kprobe, AttachNone},
+		"uretprobe/":            {Kprobe, AttachNone},
+		"tracepoint/":           {TracePoint, AttachNone},
+		"raw_tracepoint/":       {RawTracepoint, AttachNone},
+		"xdp":                   {XDP, AttachNone},
+		"perf_event":            {PerfEvent, AttachNone},
+		"lwt_in":                {LWTIn, AttachNone},
+		"lwt_out":               {LWTOut, AttachNone},
+		"lwt_xmit":              {LWTXmit, AttachNone},
+		"lwt_seg6local":         {LWTSeg6Local, AttachNone},
+		"sockops":               {SockOps, AttachCGroupSockOps},
+		"sk_skb/stream_parser":  {SkSKB, AttachSkSKBStreamParser},
+		"sk_skb/stream_verdict": {SkSKB, AttachSkSKBStreamParser},
+		"sk_msg":                {SkMsg, AttachSkSKBStreamVerdict},
+		"lirc_mode2":            {LircMode2, AttachLircMode2},
+		"flow_dissector":        {FlowDissector, AttachFlowDissector},
+
+		"cgroup_skb/ingress": {CGroupSKB, AttachCGroupInetIngress},
+		"cgroup_skb/egress":  {CGroupSKB, AttachCGroupInetEgress},
+		"cgroup/dev":         {CGroupDevice, AttachCGroupDevice},
+		"cgroup/skb":         {CGroupSKB, AttachNone},
+		"cgroup/sock":        {CGroupSock, AttachCGroupInetSockCreate},
+		"cgroup/post_bind4":  {CGroupSock, AttachCGroupInet4PostBind},
+		"cgroup/post_bind6":  {CGroupSock, AttachCGroupInet6PostBind},
+		"cgroup/bind4":       {CGroupSockAddr, AttachCGroupInet4Bind},
+		"cgroup/bind6":       {CGroupSockAddr, AttachCGroupInet6Bind},
+		"cgroup/connect4":    {CGroupSockAddr, AttachCGroupInet4Connect},
+		"cgroup/connect6":    {CGroupSockAddr, AttachCGroupInet6Connect},
+		"cgroup/sendmsg4":    {CGroupSockAddr, AttachCGroupUDP4Sendmsg},
+		"cgroup/sendmsg6":    {CGroupSockAddr, AttachCGroupUDP6Sendmsg},
+		"cgroup/recvmsg4":    {CGroupSockAddr, AttachCGroupUDP4Recvmsg},
+		"cgroup/recvmsg6":    {CGroupSockAddr, AttachCGroupUDP6Recvmsg},
+		"cgroup/sysctl":      {CGroupSysctl, AttachCGroupSysctl},
+		"cgroup/getsockopt":  {CGroupSockopt, AttachCGroupGetsockopt},
+		"cgroup/setsockopt":  {CGroupSockopt, AttachCGroupSetsockopt},
+		"classifier":         {SchedCLS, AttachNone},
+		"action":             {SchedACT, AttachNone},
+	}
+
+	for prefix, t := range types {
+		if strings.HasPrefix(sectionName, prefix) {
+			return t.progType, t.attachType
 		}
 	}
+
 	return UnspecifiedProgram, AttachNone
 }
 
diff --git a/internal/btf/btf.go b/internal/btf/btf.go
index 85968cf..971078f 100644
--- a/internal/btf/btf.go
+++ b/internal/btf/btf.go
@@ -7,7 +7,9 @@ import (
 	"io"
 	"io/ioutil"
 	"math"
+	"os"
 	"reflect"
+	"sync"
 	"unsafe"
 
 	"github.com/cilium/ebpf/internal"
@@ -21,6 +23,7 @@ const btfMagic = 0xeB9F
 // Errors returned by BTF functions.
 var (
 	ErrNotSupported = internal.ErrNotSupported
+	ErrNotFound     = xerrors.New("not found")
 )
 
 // Spec represents decoded BTF.
@@ -108,42 +111,82 @@ func LoadSpecFromReader(rd io.ReaderAt) (*Spec, error) {
 		variableOffsets[variable{secName, symbol.Name}] = uint32(symbol.Value)
 	}
 
-	rawTypes, rawStrings, err := parseBTF(btfSection.Open(), file.ByteOrder)
+	spec, err := loadNakedSpec(btfSection.Open(), file.ByteOrder, sectionSizes, variableOffsets)
 	if err != nil {
 		return nil, err
 	}
 
-	err = fixupDatasec(rawTypes, rawStrings, sectionSizes, variableOffsets)
+	if btfExtSection == nil {
+		return spec, nil
+	}
+
+	spec.funcInfos, spec.lineInfos, err = parseExtInfos(btfExtSection.Open(), file.ByteOrder, spec.strings)
+	if err != nil {
+		return nil, xerrors.Errorf("can't read ext info: %w", err)
+	}
+
+	return spec, nil
+}
+
+func loadNakedSpec(btf io.ReadSeeker, bo binary.ByteOrder, sectionSizes map[string]uint32, variableOffsets map[variable]uint32) (*Spec, error) {
+	rawTypes, rawStrings, err := parseBTF(btf, bo)
 	if err != nil {
 		return nil, err
 	}
 
-	types, err := inflateRawTypes(rawTypes, rawStrings)
+	err = fixupDatasec(rawTypes, rawStrings, sectionSizes, variableOffsets)
 	if err != nil {
 		return nil, err
 	}
 
-	var (
-		funcInfos = make(map[string]extInfo)
-		lineInfos = make(map[string]extInfo)
-	)
-	if btfExtSection != nil {
-		funcInfos, lineInfos, err = parseExtInfos(btfExtSection.Open(), file.ByteOrder, rawStrings)
-		if err != nil {
-			return nil, xerrors.Errorf("can't read ext info: %w", err)
-		}
+	types, err := inflateRawTypes(rawTypes, rawStrings)
+	if err != nil {
+		return nil, err
 	}
 
 	return &Spec{
 		rawTypes:  rawTypes,
 		types:     types,
 		strings:   rawStrings,
-		funcInfos: funcInfos,
-		lineInfos: lineInfos,
-		byteOrder: file.ByteOrder,
+		byteOrder: bo,
 	}, nil
 }
 
+var kernelBTF struct {
+	sync.Mutex
+	*Spec
+}
+
+// LoadKernelSpec returns the current kernel's BTF information.
+//
+// Requires a >= 5.5 kernel with CONFIG_DEBUG_INFO_BTF enabled. Returns
+// ErrNotSupported if BTF is not enabled.
+func LoadKernelSpec() (*Spec, error) {
+	kernelBTF.Lock()
+	defer kernelBTF.Unlock()
+
+	if kernelBTF.Spec != nil {
+		return kernelBTF.Spec, nil
+	}
+
+	var err error
+	kernelBTF.Spec, err = loadKernelSpec()
+	return kernelBTF.Spec, err
+}
+
+func loadKernelSpec() (*Spec, error) {
+	fh, err := os.Open("/sys/kernel/btf/vmlinux")
+	if os.IsNotExist(err) {
+		return nil, xerrors.Errorf("can't open kernel BTF at /sys/kernel/btf/vmlinux: %w", ErrNotFound)
+	}
+	if err != nil {
+		return nil, xerrors.Errorf("can't read kernel BTF: %s", err)
+	}
+	defer fh.Close()
+
+	return loadNakedSpec(fh, internal.NativeEndian, nil, nil)
+}
+
 func parseBTF(btf io.ReadSeeker, bo binary.ByteOrder) ([]rawType, stringTable, error) {
 	rawBTF, err := ioutil.ReadAll(btf)
 	if err != nil {
@@ -366,13 +409,12 @@ func (s *Spec) Datasec(name string) (*Map, error) {
 	return &Map{s, &Void{}, &datasec}, nil
 }
 
-var errNotFound = xerrors.New("not found")
-
 // FindType searches for a type with a specific name.
 //
 // hint determines the type of the returned Type.
 //
-// Returns an error if there is no or multiple matches.
+// Returns an error wrapping ErrNotFound if no matching
+// type exists in spec.
 func (s *Spec) FindType(name string, typ Type) error {
 	var (
 		wanted    = reflect.TypeOf(typ)
@@ -392,7 +434,7 @@ func (s *Spec) FindType(name string, typ Type) error {
 	}
 
 	if candidate == nil {
-		return xerrors.Errorf("type %s: %w", name, errNotFound)
+		return xerrors.Errorf("type %s: %w", name, ErrNotFound)
 	}
 
 	value := reflect.Indirect(reflect.ValueOf(copyType(candidate)))
@@ -604,7 +646,7 @@ func minimalBTF(bo binary.ByteOrder) []byte {
 	return buf.Bytes()
 }
 
-var haveBTF = internal.FeatureTest("BTF", "5.1", func() bool {
+var haveBTF = internal.FeatureTest("BTF", "5.1", func() (bool, error) {
 	btf := minimalBTF(internal.NativeEndian)
 	fd, err := bpfLoadBTF(&bpfLoadBTFAttr{
 		btf:     internal.NewSlicePointer(btf),
@@ -615,5 +657,5 @@ var haveBTF = internal.FeatureTest("BTF", "5.1", func() bool {
 	}
 	// Check for EINVAL specifically, rather than err != nil since we
 	// otherwise misdetect due to insufficient permissions.
-	return !xerrors.Is(err, unix.EINVAL)
+	return !xerrors.Is(err, unix.EINVAL), nil
 })
diff --git a/internal/feature.go b/internal/feature.go
index 5ecf954..671a6a0 100644
--- a/internal/feature.go
+++ b/internal/feature.go
@@ -29,33 +29,63 @@ func (ufe *UnsupportedFeatureError) Is(target error) bool {
 	return target == ErrNotSupported
 }
 
+type featureTest struct {
+	sync.Mutex
+	successful bool
+	result     error
+}
+
+// FeatureTestFn is used to determine whether the kernel supports
+// a certain feature.
+//
+// The return values have the following semantics:
+//
+//   err != nil: the test couldn't be executed
+//   err == nil && available: the feature is available
+//   err == nil && !available: the feature isn't available
+type FeatureTestFn func() (available bool, err error)
+
 // FeatureTest wraps a function so that it is run at most once.
 //
 // name should identify the tested feature, while version must be in the
 // form Major.Minor[.Patch].
 //
-// Returns a descriptive UnsupportedFeatureError if the feature is not available.
-func FeatureTest(name, version string, fn func() bool) func() error {
+// Returns an error wrapping ErrNotSupported if the feature is not supported.
+func FeatureTest(name, version string, fn FeatureTestFn) func() error {
 	v, err := NewVersion(version)
 	if err != nil {
 		return func() error { return err }
 	}
 
-	var (
-		once   sync.Once
-		result error
-	)
-
+	ft := new(featureTest)
 	return func() error {
-		once.Do(func() {
-			if !fn() {
-				result = &UnsupportedFeatureError{
-					MinimumVersion: v,
-					Name:           name,
-				}
+		ft.Lock()
+		defer ft.Unlock()
+
+		if ft.successful {
+			return ft.result
+		}
+
+		available, err := fn()
+		if xerrors.Is(err, ErrNotSupported) {
+			// The feature test aborted because a dependent feature
+			// is missing, which we should cache.
+			available = false
+		} else if err != nil {
+			// We couldn't execute the feature test to a point
+			// where it could make a determination.
+			// Don't cache the result, just return it.
+			return xerrors.Errorf("can't detect support for %s: %w", name, err)
+		}
+
+		ft.successful = true
+		if !available {
+			ft.result = &UnsupportedFeatureError{
+				MinimumVersion: v,
+				Name:           name,
 			}
-		})
-		return result
+		}
+		return ft.result
 	}
 }
 
diff --git a/internal/syscall.go b/internal/syscall.go
index b32cf3b..586f2e4 100644
--- a/internal/syscall.go
+++ b/internal/syscall.go
@@ -7,10 +7,53 @@ import (
 	"github.com/cilium/ebpf/internal/unix"
 )
 
+//go:generate stringer -output syscall_string.go -type=BPFCmd
+
+// BPFCmd identifies a subcommand of the bpf syscall.
+type BPFCmd int
+
+// Well known BPF commands.
+const (
+	BPF_MAP_CREATE BPFCmd = iota
+	BPF_MAP_LOOKUP_ELEM
+	BPF_MAP_UPDATE_ELEM
+	BPF_MAP_DELETE_ELEM
+	BPF_MAP_GET_NEXT_KEY
+	BPF_PROG_LOAD
+	BPF_OBJ_PIN
+	BPF_OBJ_GET
+	BPF_PROG_ATTACH
+	BPF_PROG_DETACH
+	BPF_PROG_TEST_RUN
+	BPF_PROG_GET_NEXT_ID
+	BPF_MAP_GET_NEXT_ID
+	BPF_PROG_GET_FD_BY_ID
+	BPF_MAP_GET_FD_BY_ID
+	BPF_OBJ_GET_INFO_BY_FD
+	BPF_PROG_QUERY
+	BPF_RAW_TRACEPOINT_OPEN
+	BPF_BTF_LOAD
+	BPF_BTF_GET_FD_BY_ID
+	BPF_TASK_FD_QUERY
+	BPF_MAP_LOOKUP_AND_DELETE_ELEM
+	BPF_MAP_FREEZE
+	BPF_BTF_GET_NEXT_ID
+	BPF_MAP_LOOKUP_BATCH
+	BPF_MAP_LOOKUP_AND_DELETE_BATCH
+	BPF_MAP_UPDATE_BATCH
+	BPF_MAP_DELETE_BATCH
+	BPF_LINK_CREATE
+	BPF_LINK_UPDATE
+	BPF_LINK_GET_FD_BY_ID
+	BPF_LINK_GET_NEXT_ID
+	BPF_ENABLE_STATS
+	BPF_ITER_CREATE
+)
+
 // BPF wraps SYS_BPF.
 //
 // Any pointers contained in attr must use the Pointer type from this package.
-func BPF(cmd int, attr unsafe.Pointer, size uintptr) (uintptr, error) {
+func BPF(cmd BPFCmd, attr unsafe.Pointer, size uintptr) (uintptr, error) {
 	r1, _, errNo := unix.Syscall(unix.SYS_BPF, uintptr(cmd), uintptr(attr), size)
 	runtime.KeepAlive(attr)
 
diff --git a/internal/syscall_string.go b/internal/syscall_string.go
new file mode 100644
index 0000000..85df047
--- /dev/null
+++ b/internal/syscall_string.go
@@ -0,0 +1,56 @@
+// Code generated by "stringer -output syscall_string.go -type=BPFCmd"; DO NOT EDIT.
+
+package internal
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[BPF_MAP_CREATE-0]
+	_ = x[BPF_MAP_LOOKUP_ELEM-1]
+	_ = x[BPF_MAP_UPDATE_ELEM-2]
+	_ = x[BPF_MAP_DELETE_ELEM-3]
+	_ = x[BPF_MAP_GET_NEXT_KEY-4]
+	_ = x[BPF_PROG_LOAD-5]
+	_ = x[BPF_OBJ_PIN-6]
+	_ = x[BPF_OBJ_GET-7]
+	_ = x[BPF_PROG_ATTACH-8]
+	_ = x[BPF_PROG_DETACH-9]
+	_ = x[BPF_PROG_TEST_RUN-10]
+	_ = x[BPF_PROG_GET_NEXT_ID-11]
+	_ = x[BPF_MAP_GET_NEXT_ID-12]
+	_ = x[BPF_PROG_GET_FD_BY_ID-13]
+	_ = x[BPF_MAP_GET_FD_BY_ID-14]
+	_ = x[BPF_OBJ_GET_INFO_BY_FD-15]
+	_ = x[BPF_PROG_QUERY-16]
+	_ = x[BPF_RAW_TRACEPOINT_OPEN-17]
+	_ = x[BPF_BTF_LOAD-18]
+	_ = x[BPF_BTF_GET_FD_BY_ID-19]
+	_ = x[BPF_TASK_FD_QUERY-20]
+	_ = x[BPF_MAP_LOOKUP_AND_DELETE_ELEM-21]
+	_ = x[BPF_MAP_FREEZE-22]
+	_ = x[BPF_BTF_GET_NEXT_ID-23]
+	_ = x[BPF_MAP_LOOKUP_BATCH-24]
+	_ = x[BPF_MAP_LOOKUP_AND_DELETE_BATCH-25]
+	_ = x[BPF_MAP_UPDATE_BATCH-26]
+	_ = x[BPF_MAP_DELETE_BATCH-27]
+	_ = x[BPF_LINK_CREATE-28]
+	_ = x[BPF_LINK_UPDATE-29]
+	_ = x[BPF_LINK_GET_FD_BY_ID-30]
+	_ = x[BPF_LINK_GET_NEXT_ID-31]
+	_ = x[BPF_ENABLE_STATS-32]
+	_ = x[BPF_ITER_CREATE-33]
+}
+
+const _BPFCmd_name = "BPF_MAP_CREATEBPF_MAP_LOOKUP_ELEMBPF_MAP_UPDATE_ELEMBPF_MAP_DELETE_ELEMBPF_MAP_GET_NEXT_KEYBPF_PROG_LOADBPF_OBJ_PINBPF_OBJ_GETBPF_PROG_ATTACHBPF_PROG_DETACHBPF_PROG_TEST_RUNBPF_PROG_GET_NEXT_IDBPF_MAP_GET_NEXT_IDBPF_PROG_GET_FD_BY_IDBPF_MAP_GET_FD_BY_IDBPF_OBJ_GET_INFO_BY_FDBPF_PROG_QUERYBPF_RAW_TRACEPOINT_OPENBPF_BTF_LOADBPF_BTF_GET_FD_BY_IDBPF_TASK_FD_QUERYBPF_MAP_LOOKUP_AND_DELETE_ELEMBPF_MAP_FREEZEBPF_BTF_GET_NEXT_IDBPF_MAP_LOOKUP_BATCHBPF_MAP_LOOKUP_AND_DELETE_BATCHBPF_MAP_UPDATE_BATCHBPF_MAP_DELETE_BATCHBPF_LINK_CREATEBPF_LINK_UPDATEBPF_LINK_GET_FD_BY_IDBPF_LINK_GET_NEXT_IDBPF_ENABLE_STATSBPF_ITER_CREATE"
+
+var _BPFCmd_index = [...]uint16{0, 14, 33, 52, 71, 91, 104, 115, 126, 141, 156, 173, 193, 212, 233, 253, 275, 289, 312, 324, 344, 361, 391, 405, 424, 444, 475, 495, 515, 530, 545, 566, 586, 602, 617}
+
+func (i BPFCmd) String() string {
+	if i < 0 || i >= BPFCmd(len(_BPFCmd_index)-1) {
+		return "BPFCmd(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+	return _BPFCmd_name[_BPFCmd_index[i]:_BPFCmd_index[i+1]]
+}
diff --git a/map.go b/map.go
index a09e85e..96c630f 100644
--- a/map.go
+++ b/map.go
@@ -767,7 +767,7 @@ func (mi *MapIterator) Err() error {
 //
 // Returns ErrNotExist, if there is no next eBPF map.
 func MapGetNextID(startID MapID) (MapID, error) {
-	id, err := objGetNextID(_MapGetNextID, uint32(startID))
+	id, err := objGetNextID(internal.BPF_MAP_GET_NEXT_ID, uint32(startID))
 	return MapID(id), err
 }
 
@@ -775,7 +775,7 @@ func MapGetNextID(startID MapID) (MapID, error) {
 //
 // Returns ErrNotExist, if there is no eBPF map with the given id.
 func NewMapFromID(id MapID) (*Map, error) {
-	fd, err := bpfObjGetFDByID(_MapGetFDByID, uint32(id))
+	fd, err := bpfObjGetFDByID(internal.BPF_MAP_GET_FD_BY_ID, uint32(id))
 	if err != nil {
 		return nil, err
 	}
diff --git a/prog.go b/prog.go
index ed31ac5..68e239d 100644
--- a/prog.go
+++ b/prog.go
@@ -7,7 +7,6 @@ import (
 	"math"
 	"strings"
 	"time"
-	"unsafe"
 
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/internal"
@@ -337,7 +336,7 @@ func (p *Program) Benchmark(in []byte, repeat int, reset func()) (uint32, time.D
 	return ret, total, nil
 }
 
-var haveProgTestRun = internal.FeatureTest("BPF_PROG_TEST_RUN", "4.12", func() bool {
+var haveProgTestRun = internal.FeatureTest("BPF_PROG_TEST_RUN", "4.12", func() (bool, error) {
 	prog, err := NewProgram(&ProgramSpec{
 		Type: SocketFilter,
 		Instructions: asm.Instructions{
@@ -348,28 +347,23 @@ var haveProgTestRun = internal.FeatureTest("BPF_PROG_TEST_RUN", "4.12", func() b
 	})
 	if err != nil {
 		// This may be because we lack sufficient permissions, etc.
-		return false
+		return false, err
 	}
 	defer prog.Close()
 
-	fd, err := prog.fd.Value()
-	if err != nil {
-		return false
-	}
-
 	// Programs require at least 14 bytes input
 	in := make([]byte, 14)
 	attr := bpfProgTestRunAttr{
-		fd:         fd,
+		fd:         uint32(prog.FD()),
 		dataSizeIn: uint32(len(in)),
 		dataIn:     internal.NewSlicePointer(in),
 	}
 
-	_, err = internal.BPF(_ProgTestRun, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	err = bpfProgTestRun(&attr)
 
 	// Check for EINVAL specifically, rather than err != nil since we
 	// otherwise misdetect due to insufficient permissions.
-	return !xerrors.Is(err, unix.EINVAL)
+	return !xerrors.Is(err, unix.EINVAL), nil
 })
 
 func (p *Program) testRun(in []byte, repeat int, reset func()) (uint32, []byte, time.Duration, error) {
@@ -411,7 +405,7 @@ func (p *Program) testRun(in []byte, repeat int, reset func()) (uint32, []byte,
 	}
 
 	for {
-		_, err = internal.BPF(_ProgTestRun, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+		err = bpfProgTestRun(&attr)
 		if err == nil {
 			break
 		}
@@ -478,7 +472,7 @@ func (p *Program) Attach(fd int, typ AttachType, flags AttachFlags) error {
 		attachFlags: uint32(flags),
 	}
 
-	return bpfProgAlter(_ProgAttach, &attr)
+	return bpfProgAlter(internal.BPF_PROG_ATTACH, &attr)
 }
 
 // Detach a Program from a container object fd
@@ -499,7 +493,7 @@ func (p *Program) Detach(fd int, typ AttachType, flags AttachFlags) error {
 		attachFlags: uint32(flags),
 	}
 
-	return bpfProgAlter(_ProgDetach, &attr)
+	return bpfProgAlter(internal.BPF_PROG_DETACH, &attr)
 }
 
 // LoadPinnedProgram loads a Program from a BPF file.
@@ -540,7 +534,7 @@ func SanitizeName(name string, replacement rune) string {
 //
 // Returns ErrNotExist, if there is no next eBPF program.
 func ProgramGetNextID(startID ProgramID) (ProgramID, error) {
-	id, err := objGetNextID(_ProgGetNextID, uint32(startID))
+	id, err := objGetNextID(internal.BPF_PROG_GET_NEXT_ID, uint32(startID))
 	return ProgramID(id), err
 }
 
@@ -548,7 +542,7 @@ func ProgramGetNextID(startID ProgramID) (ProgramID, error) {
 //
 // Returns ErrNotExist, if there is no eBPF program with the given id.
 func NewProgramFromID(id ProgramID) (*Program, error) {
-	fd, err := bpfObjGetFDByID(_ProgGetFDByID, uint32(id))
+	fd, err := bpfObjGetFDByID(internal.BPF_PROG_GET_FD_BY_ID, uint32(id))
 	if err != nil {
 		return nil, err
 	}
diff --git a/syscalls.go b/syscalls.go
index 7d29344..628f689 100644
--- a/syscalls.go
+++ b/syscalls.go
@@ -164,7 +164,7 @@ type bpfObjGetNextIDAttr struct {
 
 func bpfProgLoad(attr *bpfProgLoadAttr) (*internal.FD, error) {
 	for {
-		fd, err := internal.BPF(_ProgLoad, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+		fd, err := internal.BPF(internal.BPF_PROG_LOAD, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
 		// As of ~4.20 the verifier can be interrupted by a signal,
 		// and returns EAGAIN in that case.
 		if err == unix.EAGAIN {
@@ -179,13 +179,18 @@ func bpfProgLoad(attr *bpfProgLoadAttr) (*internal.FD, error) {
 	}
 }
 
-func bpfProgAlter(cmd int, attr *bpfProgAlterAttr) error {
+func bpfProgTestRun(attr *bpfProgTestRunAttr) error {
+	_, err := internal.BPF(internal.BPF_PROG_TEST_RUN, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+	return err
+}
+
+func bpfProgAlter(cmd internal.BPFCmd, attr *bpfProgAlterAttr) error {
 	_, err := internal.BPF(cmd, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
 	return err
 }
 
 func bpfMapCreate(attr *bpfMapCreateAttr) (*internal.FD, error) {
-	fd, err := internal.BPF(_MapCreate, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+	fd, err := internal.BPF(internal.BPF_MAP_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
 	if xerrors.Is(err, os.ErrPermission) {
 		return nil, xerrors.New("permission denied or insufficient rlimit to lock memory for map")
 	}
@@ -197,7 +202,7 @@ func bpfMapCreate(attr *bpfMapCreateAttr) (*internal.FD, error) {
 	return internal.NewFD(uint32(fd)), nil
 }
 
-var haveNestedMaps = internal.FeatureTest("nested maps", "4.12", func() bool {
+var haveNestedMaps = internal.FeatureTest("nested maps", "4.12", func() (bool, error) {
 	inner, err := bpfMapCreate(&bpfMapCreateAttr{
 		mapType:    Array,
 		keySize:    4,
@@ -205,7 +210,7 @@ var haveNestedMaps = internal.FeatureTest("nested maps", "4.12", func() bool {
 		maxEntries: 1,
 	})
 	if err != nil {
-		return false
+		return false, err
 	}
 	defer inner.Close()
 
@@ -218,14 +223,14 @@ var haveNestedMaps = internal.FeatureTest("nested maps", "4.12", func() bool {
 		innerMapFd: innerFd,
 	})
 	if err != nil {
-		return false
+		return false, nil
 	}
 
 	_ = nested.Close()
-	return true
+	return true, nil
 })
 
-var haveMapMutabilityModifiers = internal.FeatureTest("read- and write-only maps", "5.2", func() bool {
+var haveMapMutabilityModifiers = internal.FeatureTest("read- and write-only maps", "5.2", func() (bool, error) {
 	// This checks BPF_F_RDONLY_PROG and BPF_F_WRONLY_PROG. Since
 	// BPF_MAP_FREEZE appeared in 5.2 as well we don't do a separate check.
 	m, err := bpfMapCreate(&bpfMapCreateAttr{
@@ -236,10 +241,10 @@ var haveMapMutabilityModifiers = internal.FeatureTest("read- and write-only maps
 		flags:      unix.BPF_F_RDONLY_PROG,
 	})
 	if err != nil {
-		return false
+		return false, nil
 	}
 	_ = m.Close()
-	return true
+	return true, nil
 })
 
 func bpfMapLookupElem(m *internal.FD, key, valueOut internal.Pointer) error {
@@ -253,7 +258,7 @@ func bpfMapLookupElem(m *internal.FD, key, valueOut internal.Pointer) error {
 		key:   key,
 		value: valueOut,
 	}
-	_, err = internal.BPF(_MapLookupElem, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	_, err = internal.BPF(internal.BPF_MAP_LOOKUP_ELEM, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
 	return wrapMapError(err)
 }
 
@@ -268,7 +273,7 @@ func bpfMapLookupAndDelete(m *internal.FD, key, valueOut internal.Pointer) error
 		key:   key,
 		value: valueOut,
 	}
-	_, err = internal.BPF(_MapLookupAndDeleteElem, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	_, err = internal.BPF(internal.BPF_MAP_LOOKUP_AND_DELETE_ELEM, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
 	return wrapMapError(err)
 }
 
@@ -284,7 +289,7 @@ func bpfMapUpdateElem(m *internal.FD, key, valueOut internal.Pointer, flags uint
 		value: valueOut,
 		flags: flags,
 	}
-	_, err = internal.BPF(_MapUpdateElem, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	_, err = internal.BPF(internal.BPF_MAP_UPDATE_ELEM, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
 	return wrapMapError(err)
 }
 
@@ -298,7 +303,7 @@ func bpfMapDeleteElem(m *internal.FD, key internal.Pointer) error {
 		mapFd: fd,
 		key:   key,
 	}
-	_, err = internal.BPF(_MapDeleteElem, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	_, err = internal.BPF(internal.BPF_MAP_DELETE_ELEM, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
 	return wrapMapError(err)
 }
 
@@ -313,11 +318,11 @@ func bpfMapGetNextKey(m *internal.FD, key, nextKeyOut internal.Pointer) error {
 		key:   key,
 		value: nextKeyOut,
 	}
-	_, err = internal.BPF(_MapGetNextKey, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	_, err = internal.BPF(internal.BPF_MAP_GET_NEXT_KEY, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
 	return wrapMapError(err)
 }
 
-func objGetNextID(cmd int, start uint32) (uint32, error) {
+func objGetNextID(cmd internal.BPFCmd, start uint32) (uint32, error) {
 	attr := bpfObjGetNextIDAttr{
 		startID: start,
 	}
@@ -361,7 +366,7 @@ func bpfMapFreeze(m *internal.FD) error {
 	attr := bpfMapFreezeAttr{
 		mapFd: fd,
 	}
-	_, err = internal.BPF(_MapFreeze, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	_, err = internal.BPF(internal.BPF_MAP_FREEZE, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
 	return err
 }
 
@@ -382,7 +387,7 @@ func bpfPinObject(fileName string, fd *internal.FD) error {
 		return err
 	}
 
-	_, err = internal.BPF(_ObjPin, unsafe.Pointer(&bpfPinObjAttr{
+	_, err = internal.BPF(internal.BPF_OBJ_PIN, unsafe.Pointer(&bpfPinObjAttr{
 		fileName: internal.NewStringPointer(fileName),
 		fd:       value,
 	}), 16)
@@ -393,7 +398,7 @@ func bpfPinObject(fileName string, fd *internal.FD) error {
 }
 
 func bpfGetObject(fileName string) (*internal.FD, error) {
-	ptr, err := internal.BPF(_ObjGet, unsafe.Pointer(&bpfPinObjAttr{
+	ptr, err := internal.BPF(internal.BPF_OBJ_GET, unsafe.Pointer(&bpfPinObjAttr{
 		fileName: internal.NewStringPointer(fileName),
 	}), 16)
 	if err != nil {
@@ -414,7 +419,7 @@ func bpfGetObjectInfoByFD(fd *internal.FD, info unsafe.Pointer, size uintptr) er
 		infoLen: uint32(size),
 		info:    internal.NewPointer(info),
 	}
-	_, err = internal.BPF(_ObjGetInfoByFD, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	_, err = internal.BPF(internal.BPF_OBJ_GET_INFO_BY_FD, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
 	if err != nil {
 		return xerrors.Errorf("fd %d: %w", fd, err)
 	}
@@ -438,7 +443,7 @@ func bpfGetMapInfoByFD(fd *internal.FD) (*bpfMapInfo, error) {
 	return &info, nil
 }
 
-var haveObjName = internal.FeatureTest("object names", "4.15", func() bool {
+var haveObjName = internal.FeatureTest("object names", "4.15", func() (bool, error) {
 	attr := bpfMapCreateAttr{
 		mapType:    Array,
 		keySize:    4,
@@ -449,16 +454,16 @@ var haveObjName = internal.FeatureTest("object names", "4.15", func() bool {
 
 	fd, err := bpfMapCreate(&attr)
 	if err != nil {
-		return false
+		return false, nil
 	}
 
 	_ = fd.Close()
-	return true
+	return true, nil
 })
 
-var objNameAllowsDot = internal.FeatureTest("dot in object names", "5.2", func() bool {
+var objNameAllowsDot = internal.FeatureTest("dot in object names", "5.2", func() (bool, error) {
 	if err := haveObjName(); err != nil {
-		return false
+		return false, err
 	}
 
 	attr := bpfMapCreateAttr{
@@ -471,14 +476,14 @@ var objNameAllowsDot = internal.FeatureTest("dot in object names", "5.2", func()
 
 	fd, err := bpfMapCreate(&attr)
 	if err != nil {
-		return false
+		return false, nil
 	}
 
 	_ = fd.Close()
-	return true
+	return true, nil
 })
 
-func bpfObjGetFDByID(cmd int, id uint32) (*internal.FD, error) {
+func bpfObjGetFDByID(cmd internal.BPFCmd, id uint32) (*internal.FD, error) {
 	attr := bpfGetFDByIDAttr{
 		id: id,
 	}
diff --git a/types.go b/types.go
index 94cdb30..2d84417 100644
--- a/types.go
+++ b/types.go
@@ -1,6 +1,6 @@
 package ebpf
 
-//go:generate stringer -output types_string.go -type=MapType,ProgramType
+//go:generate stringer -output types_string.go -type=MapType,ProgramType,AttachType
 
 // MapType indicates the type map structure
 // that will be initialized in the kernel.
@@ -91,33 +91,6 @@ func (mt MapType) hasPerCPUValue() bool {
 	return false
 }
 
-const (
-	_MapCreate = iota
-	_MapLookupElem
-	_MapUpdateElem
-	_MapDeleteElem
-	_MapGetNextKey
-	_ProgLoad
-	_ObjPin
-	_ObjGet
-	_ProgAttach
-	_ProgDetach
-	_ProgTestRun
-	_ProgGetNextID
-	_MapGetNextID
-	_ProgGetFDByID
-	_MapGetFDByID
-	_ObjGetInfoByFD
-	_ProgQuery
-	_RawTracepointOpen
-	_BTFLoad
-	_BTFGetFDByID
-	_TaskFDQuery
-	_MapLookupAndDeleteElem
-	_MapFreeze
-	_BTFGetNextID
-)
-
 // ProgramType of the eBPF program
 type ProgramType uint32
 
diff --git a/types_string.go b/types_string.go
index f41ba77..226cf4e 100644
--- a/types_string.go
+++ b/types_string.go
@@ -1,4 +1,4 @@
-// Code generated by "stringer -output types_string.go -type=MapType,ProgramType"; DO NOT EDIT.
+// Code generated by "stringer -output types_string.go -type=MapType,ProgramType,AttachType"; DO NOT EDIT.
 
 package ebpf
 
@@ -89,3 +89,46 @@ func (i ProgramType) String() string {
 	}
 	return _ProgramType_name[_ProgramType_index[i]:_ProgramType_index[i+1]]
 }
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[AttachNone-0]
+	_ = x[AttachCGroupInetIngress-0]
+	_ = x[AttachCGroupInetEgress-1]
+	_ = x[AttachCGroupInetSockCreate-2]
+	_ = x[AttachCGroupSockOps-3]
+	_ = x[AttachSkSKBStreamParser-4]
+	_ = x[AttachSkSKBStreamVerdict-5]
+	_ = x[AttachCGroupDevice-6]
+	_ = x[AttachSkMsgVerdict-7]
+	_ = x[AttachCGroupInet4Bind-8]
+	_ = x[AttachCGroupInet6Bind-9]
+	_ = x[AttachCGroupInet4Connect-10]
+	_ = x[AttachCGroupInet6Connect-11]
+	_ = x[AttachCGroupInet4PostBind-12]
+	_ = x[AttachCGroupInet6PostBind-13]
+	_ = x[AttachCGroupUDP4Sendmsg-14]
+	_ = x[AttachCGroupUDP6Sendmsg-15]
+	_ = x[AttachLircMode2-16]
+	_ = x[AttachFlowDissector-17]
+	_ = x[AttachCGroupSysctl-18]
+	_ = x[AttachCGroupUDP4Recvmsg-19]
+	_ = x[AttachCGroupUDP6Recvmsg-20]
+	_ = x[AttachCGroupGetsockopt-21]
+	_ = x[AttachCGroupSetsockopt-22]
+	_ = x[AttachTraceRawTp-23]
+	_ = x[AttachTraceFEntry-24]
+	_ = x[AttachTraceFExit-25]
+}
+
+const _AttachType_name = "AttachNoneAttachCGroupInetEgressAttachCGroupInetSockCreateAttachCGroupSockOpsAttachSkSKBStreamParserAttachSkSKBStreamVerdictAttachCGroupDeviceAttachSkMsgVerdictAttachCGroupInet4BindAttachCGroupInet6BindAttachCGroupInet4ConnectAttachCGroupInet6ConnectAttachCGroupInet4PostBindAttachCGroupInet6PostBindAttachCGroupUDP4SendmsgAttachCGroupUDP6SendmsgAttachLircMode2AttachFlowDissectorAttachCGroupSysctlAttachCGroupUDP4RecvmsgAttachCGroupUDP6RecvmsgAttachCGroupGetsockoptAttachCGroupSetsockoptAttachTraceRawTpAttachTraceFEntryAttachTraceFExit"
+
+var _AttachType_index = [...]uint16{0, 10, 32, 58, 77, 100, 124, 142, 160, 181, 202, 226, 250, 275, 300, 323, 346, 361, 380, 398, 421, 444, 466, 488, 504, 521, 537}
+
+func (i AttachType) String() string {
+	if i >= AttachType(len(_AttachType_index)-1) {
+		return "AttachType(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+	return _AttachType_name[_AttachType_index[i]:_AttachType_index[i+1]]
+}


Test patch:
diff --git a/elf_reader_test.go b/elf_reader_test.go
index 16dd4be..0136dc1 100644
--- a/elf_reader_test.go
+++ b/elf_reader_test.go
@@ -252,3 +252,27 @@ func TestLibBPFCompat(t *testing.T) {
 		coll.Close()
 	})
 }
+
+func TestGetProgType(t *testing.T) {
+	testcases := []struct {
+		section string
+		pt      ProgramType
+		at      AttachType
+	}{
+		{"socket/garbage", SocketFilter, AttachNone},
+		{"kprobe/func", Kprobe, AttachNone},
+		{"xdp/foo", XDP, AttachNone},
+		{"cgroup_skb/ingress", CGroupSKB, AttachCGroupInetIngress},
+	}
+
+	for _, tc := range testcases {
+		pt, at := getProgType(tc.section)
+		if pt != tc.pt {
+			t.Errorf("section %s: expected type %s, got %s", tc.section, tc.pt, pt)
+		}
+
+		if at != tc.at {
+			t.Errorf("section %s: expected attach type %s, got %s", tc.section, tc.at, at)
+		}
+	}
+}
diff --git a/internal/btf/btf_test.go b/internal/btf/btf_test.go
index 4b3e6a0..93c4ec5 100644
--- a/internal/btf/btf_test.go
+++ b/internal/btf/btf_test.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/testutils"
+	"golang.org/x/xerrors"
 )
 
 func TestParseVmlinux(t *testing.T) {
@@ -30,26 +31,23 @@ func TestParseVmlinux(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	_, _, err = parseBTF(bytes.NewReader(buf), binary.LittleEndian)
+	_, err = loadNakedSpec(bytes.NewReader(buf), binary.LittleEndian, nil, nil)
 	if err != nil {
 		t.Fatal("Can't load BTF:", err)
 	}
 }
 
 func TestParseCurrentKernelBTF(t *testing.T) {
-	if _, err := os.Stat("/sys/kernel/btf/vmlinux"); os.IsNotExist(err) {
-		t.Skip("/sys/kernel/btf/vmlinux is not available")
+	spec, err := loadKernelSpec()
+	if xerrors.Is(err, ErrNotFound) {
+		t.Skip("BTF is not available:", err)
 	}
-
-	fh, err := os.Open("/sys/kernel/btf/vmlinux")
 	if err != nil {
-		t.Fatal(err)
+		t.Fatal("Can't load BTF:", err)
 	}
-	defer fh.Close()
 
-	_, _, err = parseBTF(fh, binary.LittleEndian)
-	if err != nil {
-		t.Fatal("Can't load BTF:", err)
+	if len(spec.types) == 0 {
+		t.Fatal("Empty kernel BTF")
 	}
 }
 
@@ -84,7 +82,12 @@ func TestLoadSpecFromElf(t *testing.T) {
 
 		var bpfMapDef Struct
 		if err := spec.FindType("bpf_map_def", &bpfMapDef); err != nil {
-			t.Fatal("Can't find bpf_map_def:", err)
+			t.Error("Can't find bpf_map_def:", err)
+		}
+
+		var tmp Void
+		if err := spec.FindType("totally_bogus_type", &tmp); !xerrors.Is(err, ErrNotFound) {
+			t.Error("FindType doesn't return ErrNotFound:", err)
 		}
 
 		if spec.byteOrder != internal.NativeEndian {
diff --git a/internal/feature_test.go b/internal/feature_test.go
index ad36092..163e6d8 100644
--- a/internal/feature_test.go
+++ b/internal/feature_test.go
@@ -10,9 +10,9 @@ import (
 func TestFeatureTest(t *testing.T) {
 	var called bool
 
-	fn := FeatureTest("foo", "1.0", func() bool {
+	fn := FeatureTest("foo", "1.0", func() (bool, error) {
 		called = true
-		return true
+		return true, nil
 	})
 
 	if called {
@@ -28,8 +28,8 @@ func TestFeatureTest(t *testing.T) {
 		t.Error("Unexpected negative result:", err)
 	}
 
-	fn = FeatureTest("bar", "2.1.1", func() bool {
-		return false
+	fn = FeatureTest("bar", "2.1.1", func() (bool, error) {
+		return false, nil
 	})
 
 	err = fn()
@@ -49,6 +49,24 @@ func TestFeatureTest(t *testing.T) {
 	if !xerrors.Is(err, ErrNotSupported) {
 		t.Error("UnsupportedFeatureError is not ErrNotSupported")
 	}
+
+	fn = FeatureTest("bar", "2.1.1", func() (bool, error) {
+		return false, xerrors.New("foo")
+	})
+
+	err1, err2 := fn(), fn()
+	if err1 == err2 {
+		t.Error("Cached result of unsuccessful execution")
+	}
+
+	fn = FeatureTest("bar", "2.1.1", func() (bool, error) {
+		return false, xerrors.Errorf("bar: %w", ErrNotSupported)
+	})
+
+	err1, err2 = fn(), fn()
+	if err1 != err2 {
+		t.Error("Didn't cache an error wrapping ErrNotSupported")
+	}
 }
 
 func TestVersion(t *testing.T) {
diff --git a/internal/testutils/feature.go b/internal/testutils/feature.go
index 0d01261..49f49c2 100644
--- a/internal/testutils/feature.go
+++ b/internal/testutils/feature.go
@@ -43,8 +43,12 @@ func CheckFeatureTest(t *testing.T, fn func() error) {
 		return
 	}
 
-	ufe := err.(*internal.UnsupportedFeatureError)
-	checkKernelVersion(t, ufe)
+	var ufe *internal.UnsupportedFeatureError
+	if xerrors.As(err, &ufe) {
+		checkKernelVersion(t, ufe)
+	} else {
+		t.Error("Feature test failed:", err)
+	}
 }
 
 func SkipIfNotSupported(tb testing.TB, err error) {