Skip to content

Commit

Permalink
features: add HaveProgramHelper API
Browse files Browse the repository at this point in the history
`HaveProgramHelper(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 authored and ti-mo committed May 23, 2022
1 parent c4f6259 commit d1edf5a
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 8 deletions.
6 changes: 6 additions & 0 deletions asm/func.go
Original file line number Diff line number Diff line change
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 @@ -197,6 +201,8 @@ const (
FnGetFuncIp
FnGetAttachCookie
FnTaskPtRegs

maxBuiltinFunc
)

// Call emits a function call.
Expand Down
112 changes: 104 additions & 8 deletions features/prog.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,29 @@ import (

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

func allocHelperCache() {
for pt := ebpf.UnspecifiedProgram + 1; pt <= pt.Max(); pt++ {
pc.helpers[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
helpers map[ebpf.ProgramType]map[asm.BuiltinFunc]error
}

func createProgLoadAttr(pt ebpf.ProgramType) (*sys.ProgLoadAttr, error) {
func createProgLoadAttr(pt ebpf.ProgramType, helper asm.BuiltinFunc) (*sys.ProgLoadAttr, error) {
var expectedAttachType ebpf.AttachType
var progFlags uint32

Expand All @@ -36,6 +47,10 @@ func createProgLoadAttr(pt ebpf.ProgramType) (*sys.ProgLoadAttr, error) {
asm.Return(),
}

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

buf := bytes.NewBuffer(make([]byte, 0, insns.Size()))
if err := insns.Marshal(buf, internal.NativeEndian); err != nil {
return nil, err
Expand Down Expand Up @@ -106,14 +121,13 @@ func validateProgType(pt ebpf.ProgramType) error {
}

func haveProgType(pt ebpf.ProgramType) error {
pc.Lock()
defer pc.Unlock()
err, ok := pc.progTypes[pt]
if ok {
pc.typeMu.Lock()
defer pc.typeMu.Unlock()
if err, ok := pc.progTypes[pt]; 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 All @@ -124,7 +138,7 @@ func haveProgType(pt ebpf.ProgramType) error {
// 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.
// to support the given prog type.
case errors.Is(err, unix.EINVAL), errors.Is(err, unix.E2BIG):
err = ebpf.ErrNotSupported

Expand All @@ -145,6 +159,88 @@ func haveProgType(pt ebpf.ProgramType) error {
return err
}

// HaveProgramHelper 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 HaveProgramHelper(pt ebpf.ProgramType, helper asm.BuiltinFunc) error {
if err := validateProgType(pt); err != nil {
return err
}

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

return haveProgramHelper(pt, helper)
}

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

return nil
}

func haveProgramHelper(pt ebpf.ProgramType, helper asm.BuiltinFunc) error {
pc.helperMu.Lock()
defer pc.helperMu.Unlock()
if err, ok := pc.helpers[pt][helper]; 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 := sys.ProgLoad(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 prog 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.helpers[pt][helper] = err

return err
}

func progLoadProbeNotImplemented(pt ebpf.ProgramType) bool {
switch pt {
case ebpf.Tracing, ebpf.StructOps, ebpf.Extension, ebpf.LSM:
Expand Down
86 changes: 86 additions & 0 deletions features/prog_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package features

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

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

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

func TestHaveProgramHelper(t *testing.T) {
type testCase struct {
prog ebpf.ProgramType
helper asm.BuiltinFunc
expected error
version string
}

// Referencing linux kernel commits to track the kernel version required to pass these test cases.
// They cases are derived from libbpf's selftests and helper/prog combinations that are
// probed for in cilium/cilium.
// Still missing since those helpers are not available in the lib yet, are:
// - Kprobe, GetBranchSnapshot
// - SchedCLS, SkbSetTstamp
// These two test cases depend on CI kernels supporting those:
// {ebpf.Kprobe, asm.FnKtimeGetCoarseNs, ebpf.ErrNotSupported, "5.16"}, // 5e0bc3082e2e
// {ebpf.CGroupSockAddr, asm.FnGetCgroupClassid, nil, "5.10"}, // b426ce83baa7
testCases := []testCase{
{ebpf.Kprobe, asm.FnMapLookupElem, nil, "3.19"}, // d0003ec01c66
{ebpf.SocketFilter, asm.FnKtimeGetCoarseNs, nil, "5.11"}, // d05512618056
{ebpf.SchedCLS, asm.FnSkbVlanPush, nil, "4.3"}, // 4e10df9a60d9
{ebpf.Kprobe, asm.FnSkbVlanPush, ebpf.ErrNotSupported, "4.3"}, // 4e10df9a60d9
{ebpf.Kprobe, asm.FnSysBpf, ebpf.ErrNotSupported, "5.14"}, // 79a7f8bdb159
{ebpf.Syscall, asm.FnSysBpf, nil, "5.14"}, // 79a7f8bdb159
{ebpf.XDP, asm.FnJiffies64, nil, "5.5"}, // 5576b991e9c1
{ebpf.XDP, asm.FnKtimeGetBootNs, nil, "5.7"}, // 71d19214776e
{ebpf.SchedCLS, asm.FnSkbChangeHead, nil, "5.8"}, // 6f3f65d80dac
{ebpf.SchedCLS, asm.FnRedirectNeigh, nil, "5.10"}, // b4ab31414970
{ebpf.SchedCLS, asm.FnSkbEcnSetCe, nil, "5.1"}, // f7c917ba11a6
{ebpf.SchedACT, asm.FnSkAssign, nil, "5.6"}, // cf7fbe660f2d
{ebpf.SchedACT, asm.FnFibLookup, nil, "4.18"}, // 87f5fc7e48dd
{ebpf.Kprobe, asm.FnFibLookup, ebpf.ErrNotSupported, "4.18"}, // 87f5fc7e48dd
{ebpf.CGroupSockAddr, asm.FnGetsockopt, nil, "5.8"}, // beecf11bc218
{ebpf.CGroupSockAddr, asm.FnSkLookupTcp, nil, "4.20"}, // 6acc9b432e67
{ebpf.CGroupSockAddr, asm.FnGetNetnsCookie, nil, "5.7"}, // f318903c0bf4
{ebpf.CGroupSock, asm.FnGetNetnsCookie, nil, "5.7"}, // f318903c0bf4
}

for _, tc := range testCases {
minVersion := progTypeMinVersion[tc.prog]

progVersion, err := internal.NewVersion(minVersion)
if err != nil {
t.Fatalf("Could not read kernel version required for program: %v", err)
}

helperVersion, err := internal.NewVersion(tc.version)
if err != nil {
t.Fatalf("Could not read kernel version required for helper: %v", err)
}

if progVersion.Less(helperVersion) {
minVersion = tc.version
}

t.Run(fmt.Sprintf("%s/%s", tc.prog.String(), tc.helper.String()), func(t *testing.T) {
feature := fmt.Sprintf("helper %s for program type %s", tc.helper.String(), tc.prog.String())

testutils.SkipOnOldKernel(t, minVersion, feature)

err := HaveProgramHelper(tc.prog, tc.helper)
if !errors.Is(err, tc.expected) {
t.Fatalf("%s/%s: %v", tc.prog.String(), tc.helper.String(), err)
}

})

}
}

func TestHaveProgramHelperUnsupported(t *testing.T) {
pt := ebpf.SocketFilter
minVersion := progTypeMinVersion[pt]

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

testutils.SkipOnOldKernel(t, minVersion, feature)

if err := haveProgramHelper(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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
EBADF = linux.EBADF
E2BIG = linux.E2BIG
EFAULT = linux.EFAULT
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
EBADF = syscall.Errno(0)
E2BIG = syscall.Errno(0)
EFAULT = syscall.EFAULT
EACCES = syscall.Errno(0)
// ENOTSUPP is not the same as ENOTSUP or EOPNOTSUP
ENOTSUPP = syscall.Errno(0x20c)

Expand Down

0 comments on commit d1edf5a

Please sign in to comment.