diff --git a/asm/func.go b/asm/func.go index b75a2934e..ba0a107c7 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: @@ -197,6 +201,8 @@ const ( FnGetFuncIp FnGetAttachCookie FnTaskPtRegs + + maxBuiltinFunc ) // Call emits a function call. diff --git a/features/prog.go b/features/prog.go index d09b95751..4132902df 100644 --- a/features/prog.go +++ b/features/prog.go @@ -16,6 +16,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 ( @@ -23,11 +31,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) (*sys.ProgLoadAttr, error) { +func createProgLoadAttr(pt ebpf.ProgramType, helper asm.BuiltinFunc) (*sys.ProgLoadAttr, error) { var expectedAttachType ebpf.AttachType var progFlags uint32 @@ -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 @@ -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) } @@ -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 @@ -145,6 +159,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() + if err, ok := pc.progHelpers[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.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 b0eb9fb65..18a8f82f9 100644 --- a/features/prog_test.go +++ b/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" ) @@ -92,3 +95,86 @@ func TestHaveProgTypeInvalid(t *testing.T) { t.Fatalf("Expected os.ErrInvalid but was: %v", err) } } + +func TestHaveProgHelper(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 := HaveProgHelper(tc.prog, tc.helper) + if !errors.Is(err, tc.expected) { + t.Fatalf("%s/%s: %v", tc.prog.String(), tc.helper.String(), err) + } + + }) + + } +} + +func TestHaveProgHelperUnsupported(t *testing.T) { + pt := ebpf.SocketFilter + minVersion := progTypeMinVersion[pt] + + feature := fmt.Sprintf("program type %s", 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 0a7c648a6..db4a1f5bf 100644 --- a/internal/unix/types_linux.go +++ b/internal/unix/types_linux.go @@ -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) diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go index abd8ea93d..133c267db 100644 --- a/internal/unix/types_other.go +++ b/internal/unix/types_other.go @@ -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)