package ebpf import ( "bytes" "encoding/binary" "errors" "flag" "fmt" "os" "path/filepath" "strings" "syscall" "testing" "github.com/cilium/ebpf/btf" "github.com/cilium/ebpf/internal" "github.com/cilium/ebpf/internal/kallsyms" "github.com/cilium/ebpf/internal/sys" "github.com/cilium/ebpf/internal/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/go-quicktest/qt" ) var csCmpOpts = cmp.Options{ // Dummy Comparer that works with empty readers to support test cases. cmp.Comparer(func(a, b bytes.Reader) bool { if a.Len() == 0 && b.Len() == 0 { return true } return false }), cmp.Comparer(func(a, b *VariableSpec) bool { if a.name != b.name || a.offset != b.offset || a.size != b.size { return false } return true }), cmpopts.IgnoreTypes(btf.Spec{}), cmpopts.IgnoreFields(CollectionSpec{}, "ByteOrder", "Types"), cmpopts.IgnoreFields(ProgramSpec{}, "Instructions", "ByteOrder"), cmpopts.IgnoreFields(MapSpec{}, "Key", "Value", "Contents"), cmpopts.IgnoreUnexported(ProgramSpec{}), } func TestLoadCollectionSpec(t *testing.T) { coll := &CollectionSpec{ Maps: map[string]*MapSpec{ "hash_map": { Name: "hash_map", Type: Hash, KeySize: 4, ValueSize: 8, MaxEntries: 1, Flags: sys.BPF_F_NO_PREALLOC, }, "hash_map2": { Name: "hash_map2", Type: Hash, KeySize: 4, ValueSize: 8, MaxEntries: 2, }, "array_of_hash_map": { Name: "array_of_hash_map", Type: ArrayOfMaps, KeySize: 4, MaxEntries: 2, }, "perf_event_array": { Name: "perf_event_array", Type: PerfEventArray, MaxEntries: 4096, }, "btf_pin": { Name: "btf_pin", Type: Hash, KeySize: 4, ValueSize: 8, MaxEntries: 1, Pinning: PinByName, }, "bpf_decl_map": { Name: "bpf_decl_map", Type: Array, KeySize: 4, ValueSize: 8, MaxEntries: 1, Tags: []string{"a", "b"}, }, "btf_decl_map": { Name: "btf_decl_map", Type: Array, KeySize: 4, ValueSize: 8, MaxEntries: 1, Tags: []string{"a", "b"}, }, "btf_outer_map": { Name: "btf_outer_map", Type: ArrayOfMaps, KeySize: 4, ValueSize: 4, MaxEntries: 1, InnerMap: &MapSpec{ Name: "btf_outer_map_inner", Type: Hash, KeySize: 4, ValueSize: 4, MaxEntries: 1, }, }, "btf_outer_map_anon": { Name: "btf_outer_map_anon", Type: ArrayOfMaps, KeySize: 4, ValueSize: 4, MaxEntries: 1, InnerMap: &MapSpec{ Name: "btf_outer_map_anon_inner", Type: Hash, KeySize: 4, ValueSize: 4, MaxEntries: 1, }, }, "btf_typedef_map": { Name: "btf_typedef_map", Type: Array, KeySize: 4, ValueSize: 8, MaxEntries: 1, }, ".bss": { Name: ".bss", Type: Array, KeySize: 4, ValueSize: 4, MaxEntries: 1, }, ".data": { Name: ".data", Type: Array, KeySize: 4, ValueSize: 4, MaxEntries: 1, }, ".data.test": { Name: ".data.test", Type: Array, KeySize: 4, ValueSize: 4, MaxEntries: 1, }, ".rodata": { Name: ".rodata", Type: Array, KeySize: 4, ValueSize: 24, MaxEntries: 1, Flags: sys.BPF_F_RDONLY_PROG, }, ".rodata.test": { Name: ".rodata.test", Type: Array, KeySize: 4, ValueSize: 4, MaxEntries: 1, Flags: sys.BPF_F_RDONLY_PROG, }, ".rodata.cst32": { Name: ".rodata.cst32", Type: Array, KeySize: 4, ValueSize: 32, MaxEntries: 1, Flags: sys.BPF_F_RDONLY_PROG, }, }, Programs: map[string]*ProgramSpec{ "xdp_prog": { Name: "xdp_prog", Type: XDP, SectionName: "xdp", License: "MIT", }, "no_relocation": { Name: "no_relocation", Type: SocketFilter, SectionName: "socket", License: "MIT", }, "asm_relocation": { Name: "asm_relocation", Type: SocketFilter, SectionName: "socket/2", License: "MIT", }, "data_sections": { Name: "data_sections", Type: SocketFilter, SectionName: "socket/3", License: "MIT", }, "global_fn3": { Name: "global_fn3", Type: UnspecifiedProgram, SectionName: "other", License: "MIT", }, "static_fn": { Name: "static_fn", Type: UnspecifiedProgram, SectionName: "static", License: "MIT", }, "anon_const": { Name: "anon_const", Type: SocketFilter, SectionName: "socket/4", License: "MIT", }, }, Variables: map[string]*VariableSpec{ "arg": {name: "arg", offset: 4, size: 4}, "arg2": {name: "arg2", offset: 0, size: 4}, "arg3": {name: "arg3", offset: 0, size: 4}, "key1": {name: "key1", offset: 0, size: 4}, "key2": {name: "key2", offset: 0, size: 4}, "key3": {name: "key3", offset: 0, size: 4}, "neg": {name: "neg", offset: 12, size: 4}, "uneg": {name: "uneg", offset: 8, size: 4}, }, } testutils.Files(t, testutils.Glob(t, "testdata/loader-*.elf"), func(t *testing.T, file string) { have, err := LoadCollectionSpec(file) if err != nil { t.Fatal("Can't parse ELF:", err) } err = have.RewriteConstants(map[string]interface{}{ "arg": uint32(1), "arg2": uint32(2), }) if err != nil { t.Fatal("Can't rewrite constant:", err) } err = have.RewriteConstants(map[string]interface{}{ "totallyBogus": uint32(1), "totallyBogus2": uint32(2), }) if err == nil { t.Error("Rewriting a bogus constant doesn't fail") } var mErr *MissingConstantsError if !errors.As(err, &mErr) { t.Fatal("Error doesn't wrap MissingConstantsError:", err) } qt.Assert(t, qt.ContentEquals(mErr.Constants, []string{"totallyBogus", "totallyBogus2"})) qt.Assert(t, qt.Equals(have.Maps["perf_event_array"].ValueSize, 0)) qt.Assert(t, qt.IsNotNil(have.Maps["perf_event_array"].Value)) qt.Assert(t, qt.CmpEquals(have, coll, csCmpOpts)) if have.ByteOrder != internal.NativeEndian { return } have.Maps["array_of_hash_map"].InnerMap = have.Maps["hash_map"] coll, err := newCollection(t, have, &CollectionOptions{ Maps: MapOptions{ PinPath: testutils.TempBPFFS(t), }, Programs: ProgramOptions{ LogLevel: LogLevelBranch, }, }) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal(err) } ret, _, err := coll.Programs["xdp_prog"].Test(internal.EmptyBPFContext) if err != nil { t.Fatal("Can't run program:", err) } if ret != 7 { t.Error("Unexpected return value:", ret) } }) } func BenchmarkELFLoader(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _, _ = LoadCollectionSpec("testdata/loader-el.elf") } } func TestDataSections(t *testing.T) { file := testutils.NativeFile(t, "testdata/loader-%s.elf") coll, err := LoadCollectionSpec(file) if err != nil { t.Fatal(err) } t.Log(coll.Programs["data_sections"].Instructions) var obj struct { Program *Program `ebpf:"data_sections"` } mustLoadAndAssign(t, coll, &obj, nil) defer obj.Program.Close() ret, _, err := obj.Program.Test(internal.EmptyBPFContext) if err != nil { t.Fatal(err) } if ret != 0 { t.Error("BPF assertion failed on line", ret) } } func TestInlineASMConstant(t *testing.T) { file := testutils.NativeFile(t, "testdata/loader-%s.elf") coll, err := LoadCollectionSpec(file) if err != nil { t.Fatal(err) } spec := coll.Programs["asm_relocation"] if spec.Instructions[0].Reference() != "MY_CONST" { t.Fatal("First instruction is not a reference to MY_CONST") } // -1 is used by the loader to find unrewritten maps. spec.Instructions[0].Constant = -1 t.Log(spec.Instructions) var obj struct { Program *Program `ebpf:"asm_relocation"` } mustLoadAndAssign(t, coll, &obj, nil) obj.Program.Close() } func TestFreezeRodata(t *testing.T) { testutils.SkipOnOldKernel(t, "5.9", "sk_lookup program type") file := testutils.NativeFile(t, "testdata/constants-%s.elf") spec, err := LoadCollectionSpec(file) if err != nil { t.Fatal(err) } var obj struct { Program *Program `ebpf:"freeze_rodata"` } if err := spec.RewriteConstants(map[string]interface{}{ "ret": uint32(1), }); err != nil { t.Fatal(err) } mustLoadAndAssign(t, spec, &obj, nil) obj.Program.Close() } func TestCollectionSpecDetach(t *testing.T) { coll := Collection{ Maps: map[string]*Map{ "foo": new(Map), }, Programs: map[string]*Program{ "bar": new(Program), }, } foo := coll.DetachMap("foo") if foo == nil { t.Error("Program not returned from DetachMap") } if _, ok := coll.Programs["foo"]; ok { t.Error("DetachMap doesn't remove map from Maps") } bar := coll.DetachProgram("bar") if bar == nil { t.Fatal("Program not returned from DetachProgram") } if _, ok := coll.Programs["bar"]; ok { t.Error("DetachProgram doesn't remove program from Programs") } } func TestLoadInvalidMap(t *testing.T) { file := testutils.NativeFile(t, "testdata/invalid_map-%s.elf") cs, err := LoadCollectionSpec(file) if err != nil { t.Fatal("Can't load CollectionSpec", err) } ms, ok := cs.Maps["invalid_map"] if !ok { t.Fatal("invalid_map not found in CollectionSpec") } m, err := NewMap(ms) t.Log(err) if err == nil { m.Close() t.Fatal("Creating a Map from a MapSpec with non-zero Extra is expected to fail.") } } func TestLoadInvalidMapMissingSymbol(t *testing.T) { file := testutils.NativeFile(t, "testdata/invalid_map_static-%s.elf") _, err := LoadCollectionSpec(file) t.Log(err) if err == nil { t.Fatal("Loading a map with static qualifier should fail") } } func TestLoadInitializedBTFMap(t *testing.T) { testutils.Files(t, testutils.Glob(t, "testdata/btf_map_init-*.elf"), func(t *testing.T, file string) { coll, err := LoadCollectionSpec(file) if err != nil { t.Fatal(err) } t.Run("NewCollection", func(t *testing.T) { if coll.ByteOrder != internal.NativeEndian { t.Skipf("Skipping %s collection", coll.ByteOrder) } _, err := newCollection(t, coll, nil) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal("NewCollection failed:", err) } }) t.Run("prog_array", func(t *testing.T) { m, ok := coll.Maps["prog_array_init"] if !ok { t.Fatal("map prog_array_init not found in program") } if len(m.Contents) != 1 { t.Error("expecting exactly 1 item in MapSpec contents") } p := m.Contents[0] if cmp.Equal(p.Key, 1) { t.Errorf("expecting MapSpec entry Key to equal 1, got %v", p.Key) } if _, ok := p.Value.(string); !ok { t.Errorf("expecting MapSpec entry Value to be a string, got %T", p.Value) } if p.Value != "tail_1" { t.Errorf("expected MapSpec entry Value 'tail_1', got: %s", p.Value) } }) t.Run("array_of_maps", func(t *testing.T) { m, ok := coll.Maps["outer_map_init"] if !ok { t.Fatal("map outer_map_init not found in program") } if len(m.Contents) != 1 { t.Error("expecting exactly 1 item in MapSpec contents") } if m.Key == nil { t.Error("Expected non-nil key") } if m.Value == nil { t.Error("Expected non-nil value") } if m.InnerMap.Key == nil { t.Error("Expected non-nil InnerMap key") } if m.InnerMap.Value == nil { t.Error("Expected non-nil InnerMap value") } p := m.Contents[0] if cmp.Equal(p.Key, 1) { t.Errorf("expecting MapSpec entry Key to equal 1, got %v", p.Key) } if _, ok := p.Value.(string); !ok { t.Errorf("expecting MapSpec entry Value to be a string, got %T", p.Value) } if p.Value != "inner_map" { t.Errorf("expected MapSpec entry Value 'inner_map', got: %s", p.Value) } }) }) } func TestLoadInvalidInitializedBTFMap(t *testing.T) { file := testutils.NativeFile(t, "testdata/invalid_btf_map_init-%s.elf") _, err := LoadCollectionSpec(file) t.Log(err) if !errors.Is(err, internal.ErrNotSupported) { t.Fatal("Loading an initialized BTF map should be unsupported") } } func TestStringSection(t *testing.T) { file := testutils.NativeFile(t, "testdata/strings-%s.elf") spec, err := LoadCollectionSpec(file) if err != nil { t.Fatalf("load collection spec: %s", err) } for name := range spec.Maps { t.Log(name) } strMap := spec.Maps[".rodata.str1.1"] if strMap == nil { t.Fatal("Unable to find map '.rodata.str1.1' in loaded collection") } if !strMap.readOnly() { t.Fatal("Read only data maps should be frozen") } if strMap.Flags != sys.BPF_F_RDONLY_PROG { t.Fatal("Read only data maps should have the prog-read-only flag set") } coll, err := newCollection(t, spec, nil) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatalf("new collection: %s", err) } prog := coll.Programs["filter"] if prog == nil { t.Fatal("program not found") } testMap := coll.Maps["my_map"] if testMap == nil { t.Fatal("test map not found") } _, err = prog.Run(&RunOptions{ Data: internal.EmptyBPFContext, // Min size for XDP programs }) if err != nil { t.Fatalf("prog run: %s", err) } key := []byte("This string is allocated in the string section\n\x00") var value uint32 if err = testMap.Lookup(&key, &value); err != nil { t.Fatalf("test map lookup: %s", err) } if value != 1 { t.Fatal("Test map value not 1!") } } func TestLoadRawTracepoint(t *testing.T) { testutils.SkipOnOldKernel(t, "4.17", "BPF_RAW_TRACEPOINT API") file := testutils.NativeFile(t, "testdata/raw_tracepoint-%s.elf") spec, err := LoadCollectionSpec(file) if err != nil { t.Fatal("Can't parse ELF:", err) } coll, err := NewCollectionWithOptions(spec, CollectionOptions{ Programs: ProgramOptions{ LogLevel: LogLevelBranch, }, }) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal("Can't create collection:", err) } coll.Close() } func TestTailCall(t *testing.T) { file := testutils.NativeFile(t, "testdata/btf_map_init-%s.elf") spec, err := LoadCollectionSpec(file) if err != nil { t.Fatal(err) } var obj struct { TailMain *Program `ebpf:"tail_main"` ProgArray *Map `ebpf:"prog_array_init"` // Windows evicts programs from the tail call array when the last // user space reference is closed. This is not the case on Linux. Tail *Program `ebpf:"tail_1"` } err = loadAndAssign(t, spec, &obj, nil) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal(err) } defer obj.TailMain.Close() defer obj.Tail.Close() defer obj.ProgArray.Close() ret, _, err := obj.TailMain.Test(internal.EmptyBPFContext) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal(err) } // Expect the tail_1 tail call to be taken, returning value 42. if ret != 42 { t.Fatalf("Expected tail call to return value 42, got %d", ret) } } func TestKconfig(t *testing.T) { file := testutils.NativeFile(t, "testdata/kconfig-%s.elf") spec, err := LoadCollectionSpec(file) if err != nil { t.Fatal(err) } var obj struct { Main *Program `ebpf:"kconfig"` } err = spec.LoadAndAssign(&obj, nil) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal(err) } defer obj.Main.Close() ret, _, err := obj.Main.Test(internal.EmptyBPFContext) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal(err) } qt.Assert(t, qt.Equals(ret, 0), qt.Commentf("Failed assertion at line %d in testdata/kconfig.c", ret)) } func TestKsym(t *testing.T) { file := testutils.NativeFile(t, "testdata/ksym-%s.elf") spec, err := LoadCollectionSpec(file) qt.Assert(t, qt.IsNil(err)) var obj struct { Main *Program `ebpf:"ksym_test"` ArrayMap *Map `ebpf:"array_map"` } err = spec.LoadAndAssign(&obj, nil) testutils.SkipIfNotSupported(t, err) qt.Assert(t, qt.IsNil(err)) defer obj.Main.Close() defer obj.ArrayMap.Close() _, _, err = obj.Main.Test(internal.EmptyBPFContext) testutils.SkipIfNotSupported(t, err) qt.Assert(t, qt.IsNil(err)) ksyms := map[string]uint64{ "bpf_init": 0, "bpf_trace_run1": 0, } qt.Assert(t, qt.IsNil(kallsyms.AssignAddresses(ksyms))) qt.Assert(t, qt.Not(qt.Equals(ksyms["bpf_init"], 0))) qt.Assert(t, qt.Not(qt.Equals(ksyms["bpf_trace_run1"], 0))) var value uint64 qt.Assert(t, qt.IsNil(obj.ArrayMap.Lookup(uint32(0), &value))) qt.Assert(t, qt.Equals(value, ksyms["bpf_init"])) qt.Assert(t, qt.IsNil(obj.ArrayMap.Lookup(uint32(1), &value))) qt.Assert(t, qt.Equals(value, ksyms["bpf_trace_run1"])) } func TestKsymWeakMissing(t *testing.T) { file := testutils.NativeFile(t, "testdata/ksym-%s.elf") spec, err := LoadCollectionSpec(file) qt.Assert(t, qt.IsNil(err)) var obj struct { Main *Program `ebpf:"ksym_missing_test"` } err = spec.LoadAndAssign(&obj, nil) testutils.SkipIfNotSupported(t, err) qt.Assert(t, qt.IsNil(err)) defer obj.Main.Close() res, _, err := obj.Main.Test(internal.EmptyBPFContext) testutils.SkipIfNotSupported(t, err) qt.Assert(t, qt.IsNil(err)) qt.Assert(t, qt.Equals(res, 1)) } func TestKfunc(t *testing.T) { testutils.SkipOnOldKernel(t, "5.18", "kfunc support") file := testutils.NativeFile(t, "testdata/kfunc-%s.elf") spec, err := LoadCollectionSpec(file) if err != nil { t.Fatal(err) } var obj struct { Main *Program `ebpf:"call_kfunc"` } err = spec.LoadAndAssign(&obj, nil) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatalf("%+v", err) } defer obj.Main.Close() ret, _, err := obj.Main.Test(internal.EmptyBPFContext) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal(err) } if ret != 1 { t.Fatalf("Expected kfunc to return value 1, got %d", ret) } } func TestWeakKfunc(t *testing.T) { testutils.SkipOnOldKernel(t, "5.18", "kfunc support") file := testutils.NativeFile(t, "testdata/kfunc-%s.elf") spec, err := LoadCollectionSpec(file) if err != nil { t.Fatal(err) } var obj struct { Missing *Program `ebpf:"weak_kfunc_missing"` Calling *Program `ebpf:"call_weak_kfunc"` } err = spec.LoadAndAssign(&obj, nil) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatalf("%+v", err) } defer obj.Missing.Close() defer obj.Calling.Close() } func TestInvalidKfunc(t *testing.T) { testutils.SkipOnOldKernel(t, "5.18", "kfunc support") requireTestmod(t) file := testutils.NativeFile(t, "testdata/invalid-kfunc-%s.elf") coll, err := LoadCollection(file) if err == nil { coll.Close() t.Fatal("Expected an error") } var ike *incompatibleKfuncError if !errors.As(err, &ike) { t.Fatalf("Expected an error wrapping incompatibleKfuncError, got %s", err) } } func TestKfuncKmod(t *testing.T) { testutils.SkipOnOldKernel(t, "5.18", "Kernel module function calls") requireTestmod(t) file := testutils.NativeFile(t, "testdata/kfunc-kmod-%s.elf") spec, err := LoadCollectionSpec(file) if err != nil { t.Fatal(err) } var obj struct { Main *Program `ebpf:"call_kfunc"` } err = spec.LoadAndAssign(&obj, nil) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatalf("%v+", err) } defer obj.Main.Close() ret, _, err := obj.Main.Test(internal.EmptyBPFContext) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal(err) } if ret != 1 { t.Fatalf("Expected kfunc to return value 1, got %d", ret) } } func TestSubprogRelocation(t *testing.T) { testutils.SkipOnOldKernel(t, "5.13", "bpf_for_each_map_elem") file := testutils.NativeFile(t, "testdata/subprog_reloc-%s.elf") spec, err := LoadCollectionSpec(file) if err != nil { t.Fatal(err) } var obj struct { Main *Program `ebpf:"fp_relocation"` HashMap *Map `ebpf:"hash_map"` } err = loadAndAssign(t, spec, &obj, nil) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal(err) } defer obj.Main.Close() defer obj.HashMap.Close() ret, _, err := obj.Main.Test(internal.EmptyBPFContext) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal(err) } if ret != 42 { t.Fatalf("Expected subprog reloc to return value 42, got %d", ret) } } func TestUnassignedProgArray(t *testing.T) { file := testutils.NativeFile(t, "testdata/btf_map_init-%s.elf") spec, err := LoadCollectionSpec(file) if err != nil { t.Fatal(err) } // tail_main references a ProgArray that is not being assigned // to this struct. Normally, this would clear all its entries // and make any tail calls into the ProgArray result in a miss. // The library needs to explicitly refuse such operations. var obj struct { TailMain *Program `ebpf:"tail_main"` // ProgArray *Map `ebpf:"prog_array_init"` } err = loadAndAssign(t, spec, &obj, nil) testutils.SkipIfNotSupported(t, err) defer obj.TailMain.Close() qt.Assert(t, qt.IsNotNil(err)) } func TestIPRoute2Compat(t *testing.T) { file := testutils.NativeFile(t, "testdata/iproute2_map_compat-%s.elf") spec, err := LoadCollectionSpec(file) if err != nil { t.Fatal("Can't parse ELF:", err) } ms, ok := spec.Maps["hash_map"] if !ok { t.Fatal("Map hash_map not found") } var id, pinning, innerID, innerIndex uint32 if ms.Extra == nil { t.Fatal("missing extra bytes") } switch { case binary.Read(ms.Extra, spec.ByteOrder, &id) != nil: t.Fatal("missing id") case binary.Read(ms.Extra, spec.ByteOrder, &pinning) != nil: t.Fatal("missing pinning") case binary.Read(ms.Extra, spec.ByteOrder, &innerID) != nil: t.Fatal("missing inner_id") case binary.Read(ms.Extra, spec.ByteOrder, &innerIndex) != nil: t.Fatal("missing inner_idx") } if id != 0 || innerID != 0 || innerIndex != 0 { t.Fatal("expecting id, inner_id and inner_idx to be zero") } if pinning != 2 { t.Fatal("expecting pinning field to be 2 (PIN_GLOBAL_NS)") } // iproute2 (tc) pins maps in /sys/fs/bpf/tc/globals with PIN_GLOBAL_NS, // which needs to be configured in this library using MapOptions.PinPath. // For the sake of the test, we use a tempdir on bpffs below. ms.Pinning = PinByName coll, err := NewCollectionWithOptions(spec, CollectionOptions{ Maps: MapOptions{ PinPath: testutils.TempBPFFS(t), }, }) testutils.SkipIfNotSupported(t, err) if err != nil { t.Fatal("Can't create collection:", err) } coll.Close() } var ( elfPath = flag.String("elfs", os.Getenv("CI_KERNEL_SELFTESTS"), "`Path` containing libbpf-compatible ELFs (defaults to $CI_KERNEL_SELFTESTS)") elfPattern = flag.String("elf-pattern", "*.o", "Glob `pattern` for object files that should be tested") ) func TestLibBPFCompat(t *testing.T) { if *elfPath == "" { // Specify the path to the directory containing the eBPF for // the kernel's selftests if you want to run this test. // As of 5.2 that is tools/testing/selftests/bpf/ t.Skip("No path specified") } load := func(t *testing.T, spec *CollectionSpec, opts CollectionOptions, valid bool) { // Disable retrying a program load with the log enabled, it leads // to OOM kills. opts.Programs.LogDisabled = true coll, err := NewCollectionWithOptions(spec, opts) testutils.SkipIfNotSupported(t, err) var errno syscall.Errno if errors.As(err, &errno) { // This error is most likely from a syscall and caused by us not // replicating some fixups done in the selftests or the test // intentionally failing. This is expected, so skip the test // instead of failing. t.Skip("Skipping since the kernel rejected the program:", err) } if err == nil { coll.Close() } if !valid { if err == nil { t.Fatal("Expected an error during load") } } else if err != nil { t.Fatal("Error during loading:", err) } } files := testutils.Glob(t, filepath.Join(*elfPath, *elfPattern), // These files are only used as a source of btf. "btf__core_reloc_*", ) testutils.Files(t, files, func(t *testing.T, path string) { name := selftestName(path) switch name { case "test_map_in_map", "test_select_reuseport_kern": t.Skip("Skipping due to missing InnerMap in map definition") case "test_core_autosize": t.Skip("Skipping since the test generates dynamic BTF") case "test_static_linked": t.Skip("Skipping since .text contains 'subprog' twice") case "bloom_filter_map", "bloom_filter_bench": t.Skip("Skipping due to missing MapExtra field in MapSpec") case "netif_receive_skb", "local_kptr_stash", "local_kptr_stash_fail", "type_cast", "preempted_bpf_ma_op", "percpu_alloc_fail": // Error message like // fixup for CORERelocation(local_type_id, Struct:"bin_data"[0], // local_id=27): invalid immediate 31, expected 27 (fixup: local_type_id=27->1) // See https://github.com/cilium/ebpf/issues/739 t.Skip("Skipping due to bug in libbpf type deduplication") case "test_usdt", "test_urandom_usdt", "test_usdt_multispec": t.Skip("Skipping due to missing support for usdt.bpf.h") case "lsm_cgroup", "bpf_iter_ipv6_route", "test_core_extern", "profiler1", "profiler2", "profiler3": t.Skip("Skipping due to using weak CONFIG_* variables") case "linked_maps", "linked_maps1", "linked_maps2", "linked_funcs1", "linked_funcs2", "test_subskeleton", "test_subskeleton_lib": t.Skip("Skipping due to relying on cross ELF linking") case "test_log_fixup": t.Skip("Skipping due to intentionally broken CO-RE relocations") } t.Parallel() spec, err := LoadCollectionSpec(path) testutils.SkipIfNotSupported(t, err) if errors.Is(err, errUnsupportedBinding) { t.Skip(err) } if err != nil { t.Fatal(err) } switch name { case "test_sk_assign": // Test contains a legacy iproute2 bpf_elf_map definition. for _, m := range spec.Maps { if m.Extra == nil || m.Extra.Len() == 0 { t.Fatalf("Expected extra bytes in map %s", m.Name) } m.Extra = nil } case "fexit_bpf2bpf", "freplace_get_constant", "freplace_global_func": loadTargetProgram(t, spec, "test_pkt_access.bpf.o", "test_pkt_access") case "freplace_cls_redirect": loadTargetProgram(t, spec, "test_cls_redirect.bpf.o", "cls_redirect") case "test_trace_ext": loadTargetProgram(t, spec, "test_pkt_md_access.bpf.o", "test_pkt_md_access") case "freplace_progmap": loadTargetProgram(t, spec, "xdp_dummy.bpf.o", "xdp_dummy_prog") if prog := spec.Programs["xdp_cpumap_prog"]; prog.AttachTo == "" { prog.AttachTo = "xdp_dummy_prog" } case "freplace_attach_probe": // Looks like the test should have a target, but 6.6 selftests don't // seem to be using it. } var opts CollectionOptions for _, mapSpec := range spec.Maps { if mapSpec.Pinning != PinNone { opts.Maps.PinPath = testutils.TempBPFFS(t) break } } coreFiles := sourceOfBTF(t, path) if len(coreFiles) == 0 { // NB: test_core_reloc_kernel.o doesn't have dedicated BTF and // therefore goes via this code path. load(t, spec, opts, true) return } for _, coreFile := range coreFiles { name := selftestName(coreFile) t.Run(name, func(t *testing.T) { // Some files like btf__core_reloc_arrays___err_too_small.o // trigger an error on purpose. Use the name to infer whether // the test should succeed. var valid bool switch name { case "btf__core_reloc_existence___err_wrong_arr_kind", "btf__core_reloc_existence___err_wrong_arr_value_type", "btf__core_reloc_existence___err_wrong_int_kind", "btf__core_reloc_existence___err_wrong_int_sz", "btf__core_reloc_existence___err_wrong_int_type", "btf__core_reloc_existence___err_wrong_struct_type": // These tests are buggy upstream, see https://lore.kernel.org/bpf/20210420111639.155580-1-lmb@cloudflare.com/ valid = true case "btf__core_reloc_ints___err_wrong_sz_16", "btf__core_reloc_ints___err_wrong_sz_32", "btf__core_reloc_ints___err_wrong_sz_64", "btf__core_reloc_ints___err_wrong_sz_8", "btf__core_reloc_arrays___err_wrong_val_type1", "btf__core_reloc_arrays___err_wrong_val_type2": // These tests are valid according to current libbpf behaviour, // see commit 42765ede5c54ca915de5bfeab83be97207e46f68. valid = true case "btf__core_reloc_type_id___missing_targets", "btf__core_reloc_flavors__err_wrong_name": valid = false case "btf__core_reloc_ints___err_bitfield": // Bitfields are now valid. valid = true default: valid = !strings.Contains(name, "___err_") } fh, err := os.Open(coreFile) if err != nil { t.Fatal(err) } defer fh.Close() btfSpec, err := btf.LoadSpec(coreFile) if err != nil { t.Fatal(err) } opts := opts // copy opts.Programs.KernelTypes = btfSpec opts.Programs.KernelModuleTypes = map[string]*btf.Spec{} load(t, spec, opts, valid) }) } }) } func loadTargetProgram(tb testing.TB, spec *CollectionSpec, file, program string) { targetSpec, err := LoadCollectionSpec(filepath.Join(*elfPath, file)) if errors.Is(err, os.ErrNotExist) && strings.HasSuffix(file, ".bpf.o") { // Prior to v6.1 BPF ELF used a plain .o suffix. file = strings.TrimSuffix(file, ".bpf.o") + ".o" targetSpec, err = LoadCollectionSpec(filepath.Join(*elfPath, file)) } if err != nil { tb.Fatalf("Can't read %s: %s", file, err) } qt.Assert(tb, qt.IsNotNil(targetSpec.Programs[program])) coll, err := NewCollectionWithOptions(targetSpec, CollectionOptions{ Programs: ProgramOptions{LogDisabled: true}, }) if err != nil { tb.Fatalf("Can't load target: %s", err) } tb.Cleanup(func() { coll.Close() }) target := coll.Programs[program] for _, prog := range spec.Programs { if prog.Type == Extension && prog.AttachType == AttachNone { prog.AttachTarget = target continue } if prog.Type == Tracing { switch prog.AttachType { case AttachTraceFEntry, AttachTraceFExit, AttachModifyReturn: prog.AttachTarget = target continue } } } } func sourceOfBTF(tb testing.TB, path string) []string { const testPrefix = "test_core_reloc_" const btfPrefix = "btf__core_reloc_" dir, base := filepath.Split(path) if !strings.HasPrefix(base, testPrefix) { return nil } base = strings.TrimSuffix(base[len(testPrefix):], ".o") switch base { case "bitfields_direct", "bitfields_probed": base = "bitfields" } return testutils.Glob(tb, filepath.Join(dir, btfPrefix+base+"*.o")) } func TestELFSectionProgramTypes(t *testing.T) { type testcase struct { Section string ProgramType ProgramType AttachType AttachType Flags uint32 Extra string } testcases := []testcase{ {"socket", SocketFilter, AttachNone, 0, ""}, {"socket/garbage", SocketFilter, AttachNone, 0, ""}, {"sk_reuseport/migrate", SkReuseport, AttachSkReuseportSelectOrMigrate, 0, ""}, {"sk_reuseport", SkReuseport, AttachSkReuseportSelect, 0, ""}, {"kprobe/", Kprobe, AttachNone, 0, ""}, {"kprobe/func", Kprobe, AttachNone, 0, "func"}, {"uprobe/", Kprobe, AttachNone, 0, ""}, {"kretprobe/", Kprobe, AttachNone, 0, ""}, {"uretprobe/", Kprobe, AttachNone, 0, ""}, {"tc", SchedCLS, AttachNone, 0, ""}, {"classifier", SchedCLS, AttachNone, 0, ""}, {"action", SchedACT, AttachNone, 0, ""}, {"tracepoint/", TracePoint, AttachNone, 0, ""}, {"tp/", TracePoint, AttachNone, 0, ""}, {"raw_tracepoint/", RawTracepoint, AttachNone, 0, ""}, {"raw_tp/", RawTracepoint, AttachNone, 0, ""}, {"raw_tracepoint.w/", RawTracepointWritable, AttachNone, 0, ""}, {"raw_tp.w/", RawTracepointWritable, AttachNone, 0, ""}, {"tp_btf/", Tracing, AttachTraceRawTp, 0, ""}, {"fentry/", Tracing, AttachTraceFEntry, 0, ""}, {"fmod_ret/", Tracing, AttachModifyReturn, 0, ""}, {"fexit/", Tracing, AttachTraceFExit, 0, ""}, {"fentry.s/", Tracing, AttachTraceFEntry, sys.BPF_F_SLEEPABLE, ""}, {"fmod_ret.s/", Tracing, AttachModifyReturn, sys.BPF_F_SLEEPABLE, ""}, {"fexit.s/", Tracing, AttachTraceFExit, sys.BPF_F_SLEEPABLE, ""}, {"freplace/", Extension, AttachNone, 0, ""}, {"lsm/foo", LSM, AttachLSMMac, 0, "foo"}, {"lsm.s/foo", LSM, AttachLSMMac, sys.BPF_F_SLEEPABLE, "foo"}, {"iter/bpf_map", Tracing, AttachTraceIter, 0, "bpf_map"}, {"iter.s/", Tracing, AttachTraceIter, sys.BPF_F_SLEEPABLE, ""}, // Was missing sleepable. {"syscall", Syscall, AttachNone, sys.BPF_F_SLEEPABLE, ""}, {"xdp.frags_devmap/foo", XDP, AttachXDPDevMap, sys.BPF_F_XDP_HAS_FRAGS, "foo"}, {"xdp_devmap/foo", XDP, AttachXDPDevMap, 0, "foo"}, {"xdp.frags_cpumap/", XDP, AttachXDPCPUMap, sys.BPF_F_XDP_HAS_FRAGS, ""}, {"xdp_cpumap/", XDP, AttachXDPCPUMap, 0, ""}, // Used incorrect attach type. {"xdp.frags/foo", XDP, AttachXDP, sys.BPF_F_XDP_HAS_FRAGS, ""}, {"xdp/foo", XDP, AttachNone, 0, ""}, {"perf_event", PerfEvent, AttachNone, 0, ""}, {"lwt_in", LWTIn, AttachNone, 0, ""}, {"lwt_out", LWTOut, AttachNone, 0, ""}, {"lwt_xmit", LWTXmit, AttachNone, 0, ""}, {"lwt_seg6local", LWTSeg6Local, AttachNone, 0, ""}, {"cgroup_skb/ingress", CGroupSKB, AttachCGroupInetIngress, 0, ""}, {"cgroup_skb/egress", CGroupSKB, AttachCGroupInetEgress, 0, ""}, {"cgroup/skb", CGroupSKB, AttachNone, 0, ""}, {"cgroup/sock_create", CGroupSock, AttachCGroupInetSockCreate, 0, ""}, {"cgroup/sock_release", CGroupSock, AttachCgroupInetSockRelease, 0, ""}, {"cgroup/sock", CGroupSock, AttachCGroupInetSockCreate, 0, ""}, {"cgroup/post_bind4", CGroupSock, AttachCGroupInet4PostBind, 0, ""}, {"cgroup/post_bind6", CGroupSock, AttachCGroupInet6PostBind, 0, ""}, {"cgroup/dev", CGroupDevice, AttachCGroupDevice, 0, ""}, {"sockops", SockOps, AttachCGroupSockOps, 0, ""}, {"sk_skb/stream_parser", SkSKB, AttachSkSKBStreamParser, 0, ""}, {"sk_skb/stream_verdict", SkSKB, AttachSkSKBStreamVerdict, 0, ""}, {"sk_skb/stream_verdict/foo", SkSKB, AttachSkSKBStreamVerdict, 0, ""}, {"sk_skb", SkSKB, AttachNone, 0, ""}, {"sk_skb/bar", SkSKB, AttachNone, 0, ""}, {"sk_msg", SkMsg, AttachSkMsgVerdict, 0, ""}, {"lirc_mode2", LircMode2, AttachLircMode2, 0, ""}, {"flow_dissector", FlowDissector, AttachFlowDissector, 0, ""}, {"cgroup/bind4", CGroupSockAddr, AttachCGroupInet4Bind, 0, ""}, {"cgroup/bind6", CGroupSockAddr, AttachCGroupInet6Bind, 0, ""}, {"cgroup/connect4", CGroupSockAddr, AttachCGroupInet4Connect, 0, ""}, {"cgroup/connect6", CGroupSockAddr, AttachCGroupInet6Connect, 0, ""}, {"cgroup/sendmsg4", CGroupSockAddr, AttachCGroupUDP4Sendmsg, 0, ""}, {"cgroup/sendmsg6", CGroupSockAddr, AttachCGroupUDP6Sendmsg, 0, ""}, {"cgroup/recvmsg4", CGroupSockAddr, AttachCGroupUDP4Recvmsg, 0, ""}, {"cgroup/recvmsg6", CGroupSockAddr, AttachCGroupUDP6Recvmsg, 0, ""}, {"cgroup/getpeername4", CGroupSockAddr, AttachCgroupInet4GetPeername, 0, ""}, {"cgroup/getpeername6", CGroupSockAddr, AttachCgroupInet6GetPeername, 0, ""}, {"cgroup/getsockname4", CGroupSockAddr, AttachCgroupInet4GetSockname, 0, ""}, {"cgroup/getsockname6", CGroupSockAddr, AttachCgroupInet6GetSockname, 0, ""}, {"cgroup/sysctl", CGroupSysctl, AttachCGroupSysctl, 0, ""}, {"cgroup/getsockopt", CGroupSockopt, AttachCGroupGetsockopt, 0, ""}, {"cgroup/setsockopt", CGroupSockopt, AttachCGroupSetsockopt, 0, ""}, // Bogus pattern means it never matched anything. // {"struct_ops+", StructOps, AttachNone, 0, ""}, {"sk_lookup/", SkLookup, AttachSkLookup, 0, ""}, {"seccomp", SocketFilter, AttachNone, 0, ""}, {"kprobe.multi", Kprobe, AttachTraceKprobeMulti, 0, ""}, {"kretprobe.multi", Kprobe, AttachTraceKprobeMulti, 0, ""}, } for _, tc := range testcases { t.Run(tc.Section, func(t *testing.T) { pt, at, fl, extra := getProgType(tc.Section) have := testcase{tc.Section, pt, at, fl, extra} qt.Assert(t, qt.DeepEquals(have, tc)) }) } } func TestMatchSectionName(t *testing.T) { for _, testcase := range []struct { pattern string input string matches bool extra string }{ {"prefix/", "prefix/", true, ""}, {"prefix/", "prefix/a", true, "a"}, {"prefix/", "prefix/b", true, "b"}, {"prefix/", "prefix", false, ""}, {"prefix/", "junk", false, ""}, {"prefix+", "prefix/", true, ""}, {"prefix+", "prefix/a", true, "a"}, {"prefix+", "prefix/b", true, "b"}, {"prefix+", "prefix", true, ""}, {"prefix+", "junk", false, ""}, {"exact", "exact", true, ""}, {"exact", "exact/", true, ""}, {"exact", "exact/a", true, ""}, {"exact", "exactement", true, ""}, {"exact", "junk", false, ""}, } { name := fmt.Sprintf("%s:%s", testcase.pattern, testcase.input) t.Run(name, func(t *testing.T) { extra, matches := matchSectionName(testcase.input, testcase.pattern) qt.Assert(t, qt.Equals(matches, testcase.matches)) if testcase.matches { qt.Assert(t, qt.Equals(extra, testcase.extra)) } }) } } // selftestName takes a path to a file and derives a canonical name from it. // // It strips various suffixes used by the selftest build system. func selftestName(path string) string { file := filepath.Base(path) name := strings.TrimSuffix(file, ".o") // Strip various suffixes. // Various linking suffixes. name = strings.TrimSuffix(name, ".linked3") name = strings.TrimSuffix(name, ".llinked1") name = strings.TrimSuffix(name, ".llinked2") name = strings.TrimSuffix(name, ".llinked3") // v6.1 started adding .bpf to all BPF ELF. name = strings.TrimSuffix(name, ".bpf") return name }