From a4253d47259ff1ceb6942af7b86ce3322ec9a016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20G=C3=B6gge?= Date: Fri, 6 Aug 2021 19:29:54 +0200 Subject: [PATCH] features: add HaveProgHelper API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- asm/func.go | 6 ++ features/prog.go | 107 +++++++++++++++++++++++++++++++++-- features/prog_test.go | 72 +++++++++++++++++++++++ internal/unix/types_linux.go | 1 + internal/unix/types_other.go | 1 + 5 files changed, 182 insertions(+), 5 deletions(-) diff --git a/asm/func.go b/asm/func.go index aee2c7ac8..63ffd127e 100644 --- a/asm/func.go +++ b/asm/func.go @@ -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: @@ -184,6 +188,8 @@ const ( FnKtimeGetCoarseNs FnImaInodeHash FnSockFromFile + + maxBuiltinFunc ) // Call emits a function call. diff --git a/features/prog.go b/features/prog.go index f5e2b9a40..36419e497 100644 --- a/features/prog.go +++ b/features/prog.go @@ -15,6 +15,14 @@ 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 ( @@ -22,11 +30,14 @@ var ( ) 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{ @@ -34,6 +45,10 @@ func createProgLoadAttr(pt ebpf.ProgramType) (*internal.BPFProgLoadAttr, error) 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 @@ -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) } @@ -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: diff --git a/features/prog_test.go b/features/prog_test.go index 91d4fb4d0..4f3b8276a 100644 --- a/features/prog_test.go +++ b/features/prog_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" "github.com/cilium/ebpf/internal/testutils" ) @@ -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) + } + + }) + + } + +} diff --git a/internal/unix/types_linux.go b/internal/unix/types_linux.go index e502b039d..28d221b4c 100644 --- a/internal/unix/types_linux.go +++ b/internal/unix/types_linux.go @@ -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) diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go index d99b225e9..418edd403 100644 --- a/internal/unix/types_other.go +++ b/internal/unix/types_other.go @@ -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)