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

features: add HaveProgramHelper API #375

Merged
merged 2 commits into from May 23, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -197,6 +201,8 @@ const (
FnGetFuncIp
FnGetAttachCookie
FnTaskPtRegs

maxBuiltinFunc
)

// Call emits a function call.
Expand Down
133 changes: 117 additions & 16 deletions features/prog.go
Expand Up @@ -15,19 +15,30 @@ import (
)

func init() {
pc.progTypes = make(map[ebpf.ProgramType]error)
pc.types = 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
progTypes map[ebpf.ProgramType]error
typeMu sync.Mutex
types 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 @@ -80,17 +95,22 @@ func createProgLoadAttr(pt ebpf.ProgramType) (*sys.ProgLoadAttr, error) {

// HaveProgType probes the running kernel for the availability of the specified program type.
//
// Deprecated: use HaveProgramType() instead.
var HaveProgType = HaveProgramType

// HaveProgramType probes the running kernel for the availability of the specified program type.
//
// See the package documentation for the meaning of the error return value.
func HaveProgType(pt ebpf.ProgramType) error {
if err := validateProgType(pt); err != nil {
func HaveProgramType(pt ebpf.ProgramType) error {
if err := validateProgramType(pt); err != nil {
return err
}

return haveProgType(pt)
return haveProgramType(pt)

}

func validateProgType(pt ebpf.ProgramType) error {
func validateProgramType(pt ebpf.ProgramType) error {
if pt > pt.Max() {
return os.ErrInvalid
}
Expand All @@ -105,15 +125,14 @@ func validateProgType(pt ebpf.ProgramType) error {
return nil
}

func haveProgType(pt ebpf.ProgramType) error {
pc.Lock()
defer pc.Unlock()
err, ok := pc.progTypes[pt]
if ok {
func haveProgramType(pt ebpf.ProgramType) error {
pc.typeMu.Lock()
defer pc.typeMu.Unlock()
if err, ok := pc.types[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 +143,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 @@ -140,7 +159,89 @@ func haveProgType(pt ebpf.ProgramType) error {
fd.Close()
}

pc.progTypes[pt] = err
pc.types[pt] = err

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 := validateProgramType(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
}
Expand Down
98 changes: 92 additions & 6 deletions features/prog_test.go
@@ -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 @@ -44,7 +47,7 @@ var progTypeMinVersion = map[ebpf.ProgramType]string{
ebpf.Syscall: "5.14",
}

func TestHaveProgType(t *testing.T) {
func TestHaveProgramType(t *testing.T) {
for progType := ebpf.UnspecifiedProgram + 1; progType <= progType.Max(); progType++ {
// Need inner loop copy to make use of t.Parallel()
pt := progType
Expand All @@ -66,7 +69,7 @@ func TestHaveProgType(t *testing.T) {
}
testutils.SkipOnOldKernel(t, minVersion, feature)

if err := HaveProgType(pt); err != nil {
if err := HaveProgramType(pt); err != nil {
if pt == 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
Expand All @@ -81,14 +84,97 @@ func TestHaveProgType(t *testing.T) {
}
}

func TestHaveProgTypeUnsupported(t *testing.T) {
if err := haveProgType(ebpf.ProgramType(math.MaxUint32)); err != ebpf.ErrNotSupported {
func TestHaveProgramTypeUnsupported(t *testing.T) {
if err := haveProgramType(ebpf.ProgramType(math.MaxUint32)); err != ebpf.ErrNotSupported {
t.Fatalf("Expected ebpf.ErrNotSupported but was: %v", err)
}
}

func TestHaveProgTypeInvalid(t *testing.T) {
if err := HaveProgType(ebpf.ProgramType(math.MaxUint32)); err != os.ErrInvalid {
func TestHaveProgramTypeInvalid(t *testing.T) {
if err := HaveProgramType(ebpf.ProgramType(math.MaxUint32)); err != os.ErrInvalid {
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
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
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