diff --git a/btf/btf.go b/btf/btf.go index 41e1f8a6f..e0c2ee136 100644 --- a/btf/btf.go +++ b/btf/btf.go @@ -21,6 +21,7 @@ const btfMagic = 0xeB9F // Errors returned by BTF functions. var ( ErrNotSupported = internal.ErrNotSupported + ErrNotPermitted = internal.ErrNotPermitted ErrNotFound = errors.New("not found") ErrNoExtendedInfo = errors.New("no extended info") ErrMultipleMatches = errors.New("multiple matching types") diff --git a/btf/feature.go b/btf/feature.go index 5b427f5d3..c30fa77df 100644 --- a/btf/feature.go +++ b/btf/feature.go @@ -2,6 +2,7 @@ package btf import ( "errors" + "fmt" "math" "github.com/cilium/ebpf/internal" @@ -14,10 +15,14 @@ import ( var haveBTF = internal.NewFeatureTest("BTF", func() error { // 0-length anonymous integer err := probeBTF(&Int{}) - if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.EPERM) { + switch { + case errors.Is(err, unix.EINVAL): return internal.ErrNotSupported + case errors.Is(err, unix.EPERM): + return fmt.Errorf("%w: %w", internal.ErrNotPermitted, err) + default: + return err } - return err }, "4.18") // haveMapBTF attempts to load a minimal BTF blob containing a Var. It is @@ -34,11 +39,12 @@ var haveMapBTF = internal.NewFeatureTest("Map BTF (Var/Datasec)", func() error { } err := probeBTF(v) - if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.EPERM) { - // Treat both EINVAL and EPERM as not supported: creating the map may still - // succeed without Btf* attrs. + if errors.Is(err, unix.EINVAL) { return internal.ErrNotSupported } + if errors.Is(err, unix.EPERM) { + return fmt.Errorf("%w: %w", internal.ErrNotPermitted, err) + } return err }, "5.2") @@ -56,9 +62,12 @@ var haveProgBTF = internal.NewFeatureTest("Program BTF (func/line_info)", func() } err := probeBTF(fn) - if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.EPERM) { + if errors.Is(err, unix.EINVAL) { return internal.ErrNotSupported } + if errors.Is(err, unix.EPERM) { + return fmt.Errorf("%w: %w", internal.ErrNotPermitted, err) + } return err }, "5.0") diff --git a/docs/examples/features_test.go b/docs/examples/features_test.go index 8cd6a8317..7d509c6b4 100644 --- a/docs/examples/features_test.go +++ b/docs/examples/features_test.go @@ -16,13 +16,17 @@ func DocDetectXDP() { fmt.Println("XDP program type is not supported") return } + if errors.Is(err, ebpf.ErrNotPermitted) { + fmt.Println("XDP program type is supported but permission denied") + return + } if err != nil { // Feature detection was inconclusive. // // Note: always log and investigate these errors! These can be caused - // by a lack of permissions, verifier errors, etc. Unless stated - // otherwise, probes are expected to be conclusive. Please file - // an issue if this is not the case in your environment. + // by verifier errors, etc. Unless stated otherwise, probes are + // expected to be conclusive. Please file an issue if this is not the + // case in your environment. panic(err) } diff --git a/features/link.go b/features/link.go index 4f440e7bc..44ea0a654 100644 --- a/features/link.go +++ b/features/link.go @@ -2,6 +2,7 @@ package features import ( "errors" + "fmt" "github.com/cilium/ebpf" "github.com/cilium/ebpf/asm" @@ -51,6 +52,8 @@ var haveBPFLinkUprobeMulti = internal.NewFeatureTest("bpf_link_uprobe_multi", fu return nil case errors.Is(err, unix.EINVAL): return ebpf.ErrNotSupported + case errors.Is(err, unix.EPERM): + return fmt.Errorf("%w: %w", ebpf.ErrNotPermitted, err) case err != nil: return err } @@ -99,6 +102,8 @@ var haveBPFLinkKprobeMulti = internal.NewFeatureTest("bpf_link_kprobe_multi", fu // If CONFIG_FPROBE isn't set. case errors.Is(err, unix.EOPNOTSUPP): return ebpf.ErrNotSupported + case errors.Is(err, unix.EPERM): + return fmt.Errorf("%w: %w", ebpf.ErrNotPermitted, err) case err != nil: return err } @@ -147,6 +152,8 @@ var haveBPFLinkKprobeSession = internal.NewFeatureTest("bpf_link_kprobe_session" // If CONFIG_FPROBE isn't set. case errors.Is(err, unix.EOPNOTSUPP): return ebpf.ErrNotSupported + case errors.Is(err, unix.EPERM): + return fmt.Errorf("%w: %w", ebpf.ErrNotPermitted, err) case err != nil: return err } diff --git a/features/map.go b/features/map.go index 75212552c..1112a9836 100644 --- a/features/map.go +++ b/features/map.go @@ -90,6 +90,10 @@ func createMap(attr *sys.MapCreateAttr) error { // to support the given map type. case errors.Is(err, unix.EINVAL), errors.Is(err, unix.E2BIG): return ebpf.ErrNotSupported + // EPERM occurs when the caller lacks permission. The feature exists but + // cannot be used due to insufficient privileges. + case errors.Is(err, unix.EPERM): + return fmt.Errorf("%w: %w", ebpf.ErrNotPermitted, err) } return err diff --git a/features/prog.go b/features/prog.go index 6441d5931..47f870572 100644 --- a/features/prog.go +++ b/features/prog.go @@ -42,6 +42,10 @@ func probeProgram(spec *ebpf.ProgramSpec) error { // to support the given prog type. case errors.Is(err, unix.EINVAL), errors.Is(err, unix.E2BIG): err = ebpf.ErrNotSupported + // EPERM occurs when the caller lacks permission. The feature exists but + // cannot be used due to insufficient privileges. + case errors.Is(err, unix.EPERM): + err = fmt.Errorf("%w: %w", ebpf.ErrNotPermitted, err) } return err diff --git a/internal/feature.go b/internal/feature.go index e27064c23..008132150 100644 --- a/internal/feature.go +++ b/internal/feature.go @@ -12,6 +12,11 @@ import ( // ErrNotSupported indicates that a feature is not supported. var ErrNotSupported = errors.New("not supported") +// ErrNotPermitted indicates that an operation was denied by the kernel due to +// insufficient permissions. This is distinct from ErrNotSupported: the feature +// exists in the kernel but the caller lacks permission to use it. +var ErrNotPermitted = errors.New("operation not permitted") + // ErrNotSupportedOnOS indicates that a feature is not supported on the current // operating system. var ErrNotSupportedOnOS = fmt.Errorf("%w on %s", ErrNotSupported, runtime.GOOS) diff --git a/internal/feature_test.go b/internal/feature_test.go index b6e7e0017..333ccc455 100644 --- a/internal/feature_test.go +++ b/internal/feature_test.go @@ -2,6 +2,7 @@ package internal import ( "errors" + "fmt" "runtime" "strings" "testing" @@ -88,3 +89,14 @@ func TestFeatureTestNotSupportedOnOS(t *testing.T) { qt.Assert(t, qt.ErrorIs(NewFeatureTest("foo", fn, "1.0")(), sentinel)) } } + +func TestErrNotPermitted(t *testing.T) { + // ErrNotPermitted should be distinct from ErrNotSupported + qt.Assert(t, qt.Not(qt.ErrorIs(ErrNotPermitted, ErrNotSupported))) + qt.Assert(t, qt.Not(qt.ErrorIs(ErrNotSupported, ErrNotPermitted))) + + // Wrapped errors should be matchable + wrapped := fmt.Errorf("%w: some details", ErrNotPermitted) + qt.Assert(t, qt.ErrorIs(wrapped, ErrNotPermitted)) + qt.Assert(t, qt.Not(qt.ErrorIs(wrapped, ErrNotSupported))) +} diff --git a/link/link.go b/link/link.go index 32be58982..adada5b22 100644 --- a/link/link.go +++ b/link/link.go @@ -16,6 +16,7 @@ import ( type Type = sys.LinkType var ErrNotSupported = internal.ErrNotSupported +var ErrNotPermitted = internal.ErrNotPermitted // Link represents a Program attached to a BPF hook. type Link interface { diff --git a/link/syscalls.go b/link/syscalls.go index 9948dead4..454b71e20 100644 --- a/link/syscalls.go +++ b/link/syscalls.go @@ -4,6 +4,7 @@ package link import ( "errors" + "fmt" "github.com/cilium/ebpf" "github.com/cilium/ebpf/asm" @@ -105,6 +106,11 @@ var haveProgQuery = internal.NewFeatureTest("BPF_PROG_QUERY", func() error { if errors.Is(err, unix.EBADF) { return nil } + // EPERM means the kernel recognized the syscall but denied permission. + // The feature exists but is restricted (e.g., in a user namespace). + if errors.Is(err, unix.EPERM) { + return fmt.Errorf("%w: %w", ErrNotPermitted, err) + } if err != nil { return ErrNotSupported } diff --git a/prog.go b/prog.go index 46c1f2e19..f581a564a 100644 --- a/prog.go +++ b/prog.go @@ -25,6 +25,10 @@ import ( // ErrNotSupported is returned whenever the kernel doesn't support a feature. var ErrNotSupported = internal.ErrNotSupported +// ErrNotPermitted is returned when the kernel denied an operation due to +// insufficient permissions. The feature exists but the caller lacks permission. +var ErrNotPermitted = internal.ErrNotPermitted + // ErrProgIncompatible is returned when a loaded Program is incompatible with a // given spec. var ErrProgIncompatible = errors.New("program is incompatible")