Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MapType probe API #321

Merged
merged 3 commits into from
Jul 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions features/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package features allows probing for BPF features available to the calling process.
package features
186 changes: 186 additions & 0 deletions features/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package features

import (
"errors"
"fmt"
"os"
"sync"
"unsafe"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/internal"
"github.com/cilium/ebpf/internal/unix"
)

func init() {
mc.mapTypes = make(map[ebpf.MapType]error)
}

var (
mc mapCache
)

type mapCache struct {
sync.Mutex
mapTypes map[ebpf.MapType]error
}

func createMapTypeAttr(mt ebpf.MapType) *internal.BPFMapCreateAttr {
var (
keySize uint32 = 4
valueSize uint32 = 4
maxEntries uint32 = 1
innerMapFd uint32
flags uint32
btfKeyTypeID uint32
btfValueTypeID uint32
btfFd uint32
)

// switch on map types to generate correct bpfMapCreateAttr
switch mt {
case ebpf.StackTrace:
// valueSize needs to be sizeof(uint64)
valueSize = 8
case ebpf.LPMTrie:
// keySize and valueSize need to be sizeof(struct{u32 + u8}) + 1 + padding = 8
// BPF_F_NO_PREALLOC needs to be set
// checked at allocation time for lpm_trie maps
keySize = 8
valueSize = 8
flags = unix.BPF_F_NO_PREALLOC
case ebpf.ArrayOfMaps, ebpf.HashOfMaps:
// assign invalid innerMapFd to pass validation check
// will return EBADF
innerMapFd = ^uint32(0)
case ebpf.CGroupStorage, ebpf.PerCPUCGroupStorage:
// keySize needs to be sizeof(struct{u32 + u64}) = 12 (+ padding = 16)
// by using unsafe.Sizeof(int) we are making sure that this works on 32bit and 64bit archs
// checked at allocation time
var align int
keySize = uint32(8 + unsafe.Sizeof(align))
maxEntries = 0
case ebpf.Queue, ebpf.Stack:
// keySize needs to be 0, see alloc_check for queue and stack maps
keySize = 0
case ebpf.RingBuf:
// keySize and valueSize need to be 0
// maxEntries needs to be power of 2 and PAGE_ALIGNED
// checked at allocation time
keySize = 0
valueSize = 0
maxEntries = uint32(os.Getpagesize())
case ebpf.SkStorage, ebpf.InodeStorage, ebpf.TaskStorage:
// maxEntries needs to be 0
// BPF_F_NO_PREALLOC needs to be set
// btf* fields need to be set
// see alloc_check for local_storage map types
maxEntries = 0
flags = unix.BPF_F_NO_PREALLOC
btfKeyTypeID = 1 // BTF_KIND_INT
btfValueTypeID = 3 // BTF_KIND_ARRAY
btfFd = ^uint32(0)
}

return &internal.BPFMapCreateAttr{
MapType: uint32(mt),
KeySize: keySize,
ValueSize: valueSize,
MaxEntries: maxEntries,
InnerMapFd: innerMapFd,
Flags: flags,
BTFKeyTypeID: btfKeyTypeID,
BTFValueTypeID: btfValueTypeID,
BTFFd: btfFd,
}

}

// HaveMapType probes the running kernel for the availability of the specified map type.
// 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 map creation may succeed
// despite an error being returned. Some map 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 HaveMapType(mt ebpf.MapType) error {
if err := validateMaptype(mt); err != nil {
return err
}

return haveMapType(mt)
}

func validateMaptype(mt ebpf.MapType) error {
if mt > mt.Max() {
return os.ErrInvalid
}

if mt == ebpf.StructOpts {
// A probe for a StructOpts map has vmlinux BTF requirements we currently cannot meet
// Once we figure out how to add a working probe in this package, we can remove this check
return errors.New("a probe for MapType StructOpts isn't implemented")
}

return nil
}

func haveMapType(mt ebpf.MapType) error {
mc.Lock()
defer mc.Unlock()
err, ok := mc.mapTypes[mt]
if ok {
return err
}

_, err = internal.BPFMapCreate(createMapTypeAttr(mt))

switch {
// For nested and storage map types we accept EBADF as indicator that these maps are supported
case errors.Is(err, unix.EBADF):
if isMapOfMaps(mt) || isStorageMap(mt) {
err = nil
}

rgo3 marked this conversation as resolved.
Show resolved Hide resolved
// EINVAL occurs when attempting to create a map with an unknown type.
// E2BIG occurs when BPFMapCreateAttr 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)
}

mc.mapTypes[mt] = err

return err
}

func isMapOfMaps(mt ebpf.MapType) bool {
switch mt {
case ebpf.ArrayOfMaps, ebpf.HashOfMaps:
return true
}
return false
}

func isStorageMap(mt ebpf.MapType) bool {
switch mt {
case ebpf.SkStorage, ebpf.InodeStorage, ebpf.TaskStorage:
return true
}

return false
}
87 changes: 87 additions & 0 deletions features/map_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package features

import (
"fmt"
"math"
"os"
"testing"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/internal/testutils"
)

var mapTypeMinVersion = map[ebpf.MapType]string{
ebpf.Hash: "3.19",
ebpf.Array: "3.19",
ebpf.ProgramArray: "4.2",
ebpf.PerfEventArray: "4.3",
ebpf.PerCPUHash: "4.6",
ebpf.PerCPUArray: "4.6",
ebpf.StackTrace: "4.6",
ebpf.CGroupArray: "4.8",
ebpf.LRUHash: "4.10",
ebpf.LRUCPUHash: "4.10",
ebpf.LPMTrie: "4.11",
ebpf.ArrayOfMaps: "4.12",
ebpf.HashOfMaps: "4.12",
ebpf.DevMap: "4.14",
ebpf.SockMap: "4.14",
ebpf.CPUMap: "4.15",
ebpf.XSKMap: "4.18",
ebpf.SockHash: "4.18",
ebpf.CGroupStorage: "4.19",
ebpf.ReusePortSockArray: "4.19",
ebpf.PerCPUCGroupStorage: "4.20",
ebpf.Queue: "4.20",
ebpf.Stack: "4.20",
ebpf.SkStorage: "5.2",
ebpf.DevMapHash: "5.4",
ebpf.StructOpts: "5.6", // requires vmlinux BTF, skip for now
ebpf.RingBuf: "5.8",
ebpf.InodeStorage: "5.10",
ebpf.TaskStorage: "5.11",
}

func TestHaveMapType(t *testing.T) {
for mt := ebpf.UnspecifiedMap + 1; mt <= mt.Max(); mt++ {
minVersion, ok := mapTypeMinVersion[mt]
if !ok {
// In cases were a new map type wasn't added to testCases
// we should make sure the test runs anyway and fails on old kernels
minVersion = "0.0"
}

feature := fmt.Sprintf("map type %s", mt.String())

t.Run(mt.String(), func(t *testing.T) {
// skip StructOpts test as its not implemented
if mt == ebpf.StructOpts {
t.Skip("Test for map type StructOpts requires working probe")
}

testutils.SkipOnOldKernel(t, minVersion, feature)

if err := HaveMapType(mt); err != nil {
t.Fatalf("Map type %s isn't supported even though kernel is at least %s: %v", mt.String(), minVersion, err)
}
})

}

}

func TestHaveMapTypeUnsupported(t *testing.T) {
if err := haveMapType(ebpf.MapType(math.MaxUint32)); err != ebpf.ErrNotSupported {
t.Fatalf("Expected ebpf.ErrNotSupported but was: %v", err)
}
}

func TestHaveMapTypeInvalid(t *testing.T) {
if err := HaveMapType(ebpf.MapType(math.MaxUint32)); err != os.ErrInvalid {
t.Fatalf("Expected os.ErrInvalid but was: %v", err)
}

if err := HaveMapType(ebpf.MapType(ebpf.StructOpts)); err == nil {
t.Fatal("Expected but was nil")
}
}
3 changes: 2 additions & 1 deletion internal/unix/types_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ const (
EPERM = linux.EPERM
ESRCH = linux.ESRCH
ENODEV = linux.ENODEV
EBADF = linux.EBADF
E2BIG = linux.E2BIG
// ENOTSUPP is not the same as ENOTSUP or EOPNOTSUP
ENOTSUPP = syscall.Errno(0x20c)

EBADF = linux.EBADF
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
Expand Down
1 change: 1 addition & 0 deletions internal/unix/types_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
ESRCH = syscall.ESRCH
ENODEV = syscall.ENODEV
EBADF = syscall.Errno(0)
E2BIG = syscall.Errno(0)
// ENOTSUPP is not the same as ENOTSUP or EOPNOTSUP
ENOTSUPP = syscall.Errno(0x20c)

Expand Down
12 changes: 12 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import (
// that will be initialized in the kernel.
type MapType uint32

// Max returns the latest supported MapType.
func (_ MapType) Max() MapType {
return maxMapType - 1
}

// All the various map types that can be created
const (
UnspecifiedMap MapType = iota
Expand Down Expand Up @@ -85,10 +90,17 @@ const (
SkStorage
// DevMapHash - Hash-based indexing scheme for references to network devices.
DevMapHash
// StructOps - This map holds a kernel struct with its function pointer implemented in a BPF
// program.
StructOpts
// RingBuf - Similar to PerfEventArray, but shared across all CPUs.
RingBuf
// InodeStorage - Specialized local storage map for inodes.
InodeStorage
// TaskStorage - Specialized local storage map for task_struct.
TaskStorage
// maxMapType - Bound enum of MapTypes, has to be last in enum.
maxMapType
)

// hasPerCPUValue returns true if the Map stores a value per CPU.
Expand Down