Skip to content

Commit

Permalink
features: add HaveProgHelper API
Browse files Browse the repository at this point in the history
`HaveProgHelper(pt ebpf.ProgramType, helper asm.BuiltinFunc) error`
allows to probe the available BPF helpers to a given BPF program
type. Probe results are cached and run at most once.

Signed-off-by: Robin Gögge <r.goegge@gmail.com>
  • Loading branch information
rgo3 committed Aug 13, 2021
1 parent 896b668 commit a4253d4
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 5 deletions.
6 changes: 6 additions & 0 deletions asm/func.go
Expand Up @@ -5,6 +5,10 @@ package asm
// BuiltinFunc is a built-in eBPF function.
type BuiltinFunc int32

func (_ BuiltinFunc) Max() BuiltinFunc {
return maxBuiltinFunc - 1
}

// eBPF built-in functions
//
// You can regenerate this list using the following gawk script:
Expand Down Expand Up @@ -184,6 +188,8 @@ const (
FnKtimeGetCoarseNs
FnImaInodeHash
FnSockFromFile

maxBuiltinFunc
)

// Call emits a function call.
Expand Down
107 changes: 102 additions & 5 deletions features/prog.go
Expand Up @@ -15,25 +15,40 @@ import (

func init() {
pc.progTypes = make(map[ebpf.ProgramType]error)
pc.progHelpers = make(map[ebpf.ProgramType]map[asm.BuiltinFunc]error)
allocHelperCache()
}

func allocHelperCache() {
for pt := ebpf.UnspecifiedProgram + 1; pt <= pt.Max(); pt++ {
pc.progHelpers[pt] = make(map[asm.BuiltinFunc]error)
}
}

var (
pc progCache
)

type progCache struct {
sync.Mutex
typeMu sync.Mutex
progTypes map[ebpf.ProgramType]error

helperMu sync.Mutex
progHelpers map[ebpf.ProgramType]map[asm.BuiltinFunc]error
}

func createProgLoadAttr(pt ebpf.ProgramType) (*internal.BPFProgLoadAttr, error) {
func createProgLoadAttr(pt ebpf.ProgramType, helper asm.BuiltinFunc) (*internal.BPFProgLoadAttr, error) {
var expectedAttachType ebpf.AttachType

insns := asm.Instructions{
asm.LoadImm(asm.R0, 0, asm.DWord),
asm.Return(),
}

if helper != asm.FnUnspec {
insns = append(asm.Instructions{helper.Call()}, insns...)
}

buf := bytes.NewBuffer(make([]byte, 0, len(insns)*asm.InstructionSize))
if err := insns.Marshal(buf, internal.NativeEndian); err != nil {
return nil, err
Expand Down Expand Up @@ -111,14 +126,14 @@ func validateProgType(pt ebpf.ProgramType) error {
}

func haveProgType(pt ebpf.ProgramType) error {
pc.Lock()
defer pc.Unlock()
pc.typeMu.Lock()
defer pc.typeMu.Unlock()
err, ok := pc.progTypes[pt]
if ok {
return err
}

attr, err := createProgLoadAttr(pt)
attr, err := createProgLoadAttr(pt, asm.FnUnspec)
if err != nil {
return fmt.Errorf("couldn't create the program load attribute: %w", err)
}
Expand Down Expand Up @@ -147,6 +162,88 @@ func haveProgType(pt ebpf.ProgramType) error {
return err
}

// HaveProgHelper probes the running kernel for the availability of the specified helper
// function to a specified program 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 program creation may
// succeed despite an error being returned.
// Only `nil` and `ebpf.ErrNotSupported` are conclusive.
//
// Probe results are cached and persist throughout any process capability changes.
func HaveProgHelper(pt ebpf.ProgramType, helper asm.BuiltinFunc) error {
if err := validateProgType(pt); err != nil {
return err
}

if err := validateProgHelper(helper); err != nil {
return err
}

return haveProgHelper(pt, helper)
}

func validateProgHelper(helper asm.BuiltinFunc) error {
if helper > helper.Max() {
return os.ErrInvalid
}

return nil
}

func haveProgHelper(pt ebpf.ProgramType, helper asm.BuiltinFunc) error {
pc.helperMu.Lock()
defer pc.helperMu.Unlock()
err, ok := pc.progHelpers[pt][helper]
if ok {
return err
}

attr, err := createProgLoadAttr(pt, helper)
if err != nil {
return fmt.Errorf("couldn't create the program load attribute: %w", err)
}

fd, err := internal.BPFProgLoad(attr)

switch {
// If there is no error we need to close the FD of the prog.
case err == nil:
fd.Close()
// EACCES occurs when attempting to create a program probe with a helper
// while the register args when calling this helper aren't set up properly.
// We interpret this as the helper being available, because the verifier
// returns EINVAL if the helper is not supported by the running kernel.
case errors.Is(err, unix.EACCES):
// TODO: possibly we need to check verifier output here to be sure
err = nil

// EINVAL occurs when attempting to create a program with an unknown helper.
// E2BIG occurs when BPFProgLoadAttr 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):
// TODO: possibly we need to check verifier output here to be sure
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)
}

pc.progHelpers[pt][helper] = err

return err
}

func progLoadProbeNotImplemented(pt ebpf.ProgramType) bool {
switch pt {
case ebpf.Tracing, ebpf.StructOps, ebpf.Extension, ebpf.LSM:
Expand Down
72 changes: 72 additions & 0 deletions features/prog_test.go
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

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

Expand Down Expand Up @@ -91,3 +92,74 @@ func TestHaveProgTypeInvalid(t *testing.T) {
t.Fatalf("Expected os.ErrInvalid but was: %v", err)
}
}

func TestHaveProgHelper(t *testing.T) {
// TODO: t.Parallel() ? if we parallelize everything the output is a bit chaotic
for progType := ebpf.UnspecifiedProgram + 1; progType <= progType.Max(); progType++ {
minVersion, ok := progTypeMinVersion[progType]
if !ok {
// In cases where a new prog type wasn't added to progTypeMinVersion
// we should make sure the test runs anyway and fails on old kernels
minVersion = "0.0"
}

t.Run(progType.String(), func(t *testing.T) {
for helper := asm.FnMapLookupElem; helper <= asm.FnMapDeleteElem; helper++ {
feature := fmt.Sprintf("helper %s for program type %s", helper.String(), progType.String())

t.Run(helper.String(), func(t *testing.T) {

if progLoadProbeNotImplemented(progType) {
t.Skipf("Test for prog type %s requires working probe", progType.String())
}
testutils.SkipOnOldKernel(t, minVersion, feature)

if err := HaveProgHelper(progType, helper); err != nil {
if progType == ebpf.LircMode2 {
// CI kernels are built with CONFIG_BPF_LIRC_MODE2, but some
// mainstream distro's don't ship with it. Make this prog type
// optional to retain compatibility with those kernels.
testutils.SkipIfNotSupported(t, err)
}

t.Fatalf("%s: %s", progType.String(), helper.String())
}
})
}

})

}
}

func TestHaveProgHelperUnsupported(t *testing.T) {
for progType := ebpf.UnspecifiedProgram + 1; progType <= progType.Max(); progType++ {
// Need inner loop copy to make use of t.Parallel()
pt := progType

minVersion, ok := progTypeMinVersion[pt]
if !ok {
// In cases where a new prog type wasn't added to progTypeMinVersion
// we should make sure the test runs anyway and fails on old kernels
minVersion = "0.0"
}

feature := fmt.Sprintf("program type %s", pt.String())

t.Run(pt.String(), func(t *testing.T) {
t.Parallel()

if progLoadProbeNotImplemented(pt) {
t.Skipf("Test for prog type %s requires working probe", pt.String())
}
testutils.SkipOnOldKernel(t, minVersion, feature)

if err := haveProgHelper(pt, asm.BuiltinFunc(math.MaxInt32)); err != ebpf.ErrNotSupported {
t.Fatalf("Expected ebpf.ErrNotSupported but was: %v", err)
}

})

}

}
1 change: 1 addition & 0 deletions internal/unix/types_linux.go
Expand Up @@ -22,6 +22,7 @@ const (
ENODEV = linux.ENODEV
EBADF = linux.EBADF
E2BIG = linux.E2BIG
EACCES = linux.EACCES
// ENOTSUPP is not the same as ENOTSUP or EOPNOTSUP
ENOTSUPP = syscall.Errno(0x20c)

Expand Down
1 change: 1 addition & 0 deletions internal/unix/types_other.go
Expand Up @@ -22,6 +22,7 @@ const (
ENODEV = syscall.ENODEV
EBADF = syscall.Errno(0)
E2BIG = syscall.Errno(0)
EACCES = syscall.Errno(0)
// ENOTSUPP is not the same as ENOTSUP or EOPNOTSUP
ENOTSUPP = syscall.Errno(0x20c)

Expand Down

0 comments on commit a4253d4

Please sign in to comment.