diff --git a/elf_reader_test.go b/elf_reader_test.go index b98b555e8..f37659a8a 100644 --- a/elf_reader_test.go +++ b/elf_reader_test.go @@ -1065,9 +1065,6 @@ func TestLibBPFCompat(t *testing.T) { t.Fatal("Expected an error during load") } } else if err != nil { - if errors.Is(err, errUnknownStructOps) { - t.Skip("Skipping since the struct_ops target doesn't exist in kernel") - } t.Fatal("Error during loading:", err) } } diff --git a/link/link_other.go b/link/link_other.go index cd9452fd8..de9fd2cb8 100644 --- a/link/link_other.go +++ b/link/link_other.go @@ -25,6 +25,7 @@ const ( UprobeMultiType = sys.BPF_LINK_TYPE_UPROBE_MULTI NetfilterType = sys.BPF_LINK_TYPE_NETFILTER NetkitType = sys.BPF_LINK_TYPE_NETKIT + StructOpsType = sys.BPF_LINK_TYPE_STRUCT_OPS ) // AttachRawLink creates a raw link. @@ -102,6 +103,8 @@ func wrapRawLink(raw *RawLink) (_ Link, err error) { return &netkitLink{*raw}, nil case XDPType: return &xdpLink{*raw}, nil + case StructOpsType: + return &structOpsLink{*raw}, nil default: return raw, nil } diff --git a/link/struct_ops.go b/link/struct_ops.go new file mode 100644 index 000000000..d1b7f2517 --- /dev/null +++ b/link/struct_ops.go @@ -0,0 +1,51 @@ +package link + +import ( + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal/sys" +) + +type structOpsLink struct { + RawLink +} + +func (*structOpsLink) Update(*ebpf.Program) error { + return fmt.Errorf("update struct_ops link: %w", ErrNotSupported) +} + +type StructOpsOptions struct { + Map *ebpf.Map +} + +// AttachStructOps attaches a struct_ops map (created from a ".struct_ops.link" +// section) to its kernel subsystem via a BPF link. +func AttachStructOps(opts StructOpsOptions) (Link, error) { + m := opts.Map + + if m == nil { + return nil, fmt.Errorf("map cannot be nil") + } + + if t := m.Type(); t != ebpf.StructOpsMap { + return nil, fmt.Errorf("can't attach non-struct_ops map") + } + + mapFD := m.FD() + if mapFD <= 0 { + return nil, fmt.Errorf("invalid map: %s", sys.ErrClosedFd) + } + + fd, err := sys.LinkCreate(&sys.LinkCreateAttr{ + // For struct_ops links, the mapFD must be passed as ProgFd. + ProgFd: uint32(mapFD), + AttachType: sys.AttachType(ebpf.AttachStructOps), + TargetFd: 0, + }) + if err != nil { + return nil, fmt.Errorf("attach StructOps: create link: %w", err) + } + + return &structOpsLink{RawLink{fd: fd}}, nil +} diff --git a/link/struct_ops_test.go b/link/struct_ops_test.go new file mode 100644 index 000000000..ebb1ea809 --- /dev/null +++ b/link/struct_ops_test.go @@ -0,0 +1,118 @@ +//go:build !windows + +package link + +import ( + "testing" + + "github.com/go-quicktest/qt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/btf" + "github.com/cilium/ebpf/internal/sys" + "github.com/cilium/ebpf/internal/testutils" +) + +func TestStructOps(t *testing.T) { + testutils.SkipOnOldKernel(t, "6.12", "bpf_testmod_ops") + + m := mustStructOpsFixtures(t) + l, err := AttachStructOps(StructOpsOptions{Map: m}) + qt.Assert(t, qt.IsNil(err)) + + testLink(t, l, nil) +} + +func mustStructOpsFixtures(tb testing.TB) *ebpf.Map { + tb.Helper() + + testutils.SkipIfNotSupported(tb, haveBPFLink()) + + userData := []byte{ + // test_1 func ptr (8B) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // test_2 func ptr (8B) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // data (4B) + padding (4B) + 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, 0x00, 0x00, + } + + spec := &ebpf.CollectionSpec{ + Maps: map[string]*ebpf.MapSpec{ + "testmod_ops": { + Name: "testmod_ops", + Type: ebpf.StructOpsMap, + MaxEntries: 1, + Flags: sys.BPF_F_LINK, + Key: &btf.Int{Size: 4}, + KeySize: 4, + ValueSize: 24, + Value: &btf.Struct{ + Name: "bpf_testmod_ops", + Size: 24, + Members: []btf.Member{ + { + Name: "test_1", + Type: &btf.Pointer{ + Target: &btf.FuncProto{ + Params: []btf.FuncParam{}, + Return: &btf.Int{Name: "int", Size: 4, Encoding: btf.Signed}}}, + Offset: 0, + }, + { + Name: "test_2", + Type: &btf.Pointer{ + Target: &btf.FuncProto{ + Params: []btf.FuncParam{ + {Type: &btf.Int{Name: "int", Size: 4, Encoding: btf.Signed}}, + {Type: &btf.Int{Name: "int", Size: 4, Encoding: btf.Signed}}, + }, + Return: (*btf.Void)(nil), + }, + }, + Offset: 64, + }, + { + Name: "data", + Type: &btf.Int{Name: "int", Size: 4, Encoding: btf.Signed}, + Offset: 128, // bits + }, + }, + }, + Contents: []ebpf.MapKV{ + { + Key: uint32(0), + Value: userData, + }, + }, + }, + }, + Programs: map[string]*ebpf.ProgramSpec{ + "test_1": { + Name: "test_1", + Type: ebpf.StructOps, + AttachTo: "bpf_testmod_ops:test_1", + License: "GPL", + SectionName: "struct_ops/test_1", + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + }, + }, + }, + Variables: map[string]*ebpf.VariableSpec{}, + } + + coll, err := ebpf.NewCollection(spec) + testutils.SkipIfNotSupported(tb, err) + qt.Assert(tb, qt.IsNil(err)) + tb.Cleanup(func() { + coll.Close() + }) + + m := coll.Maps["testmod_ops"] + qt.Assert(tb, qt.IsNotNil(m)) + + return m +} diff --git a/prog.go b/prog.go index a6ba888c6..3e724234d 100644 --- a/prog.go +++ b/prog.go @@ -37,9 +37,6 @@ var errBadRelocation = errors.New("bad CO-RE relocation") // This error is detected based on heuristics and therefore may not be reliable. var errUnknownKfunc = errors.New("unknown kfunc") -// errUnknownStructOps is returned when the struct_ops target doesn't exist in kernel -var errUnknownStructOps = errors.New("unknown struct_ops target") - // ProgramID represents the unique ID of an eBPF program. type ProgramID = sys.ProgramID diff --git a/struct_ops.go b/struct_ops.go index 0e676893e..3b70d56d2 100644 --- a/struct_ops.go +++ b/struct_ops.go @@ -49,7 +49,7 @@ func structOpsFindTarget(userType *btf.Struct, cache *btf.Cache) (vType *btf.Str target := btf.Type((*btf.Struct)(nil)) spec, module, err := findTargetInKernel(vTypeName, &target, cache) if errors.Is(err, btf.ErrNotFound) { - return nil, 0, nil, fmt.Errorf("%q doesn't exist in kernel: %w", vTypeName, errUnknownStructOps) + return nil, 0, nil, fmt.Errorf("%q doesn't exist in kernel: %w", vTypeName, ErrNotSupported) } if err != nil { return nil, 0, nil, fmt.Errorf("lookup value type %q: %w", vTypeName, err)