diff --git a/elf_reader.go b/elf_reader.go index 0469244b5..2c0cbc79b 100644 --- a/elf_reader.go +++ b/elf_reader.go @@ -183,12 +183,13 @@ func (ec *elfCode) loadPrograms(progSections map[elf.SectionIndex]*elf.Section, return nil, xerrors.Errorf("program %s: can't unmarshal instructions: %w", funcSym.Name, err) } - progType, attachType := getProgType(sec.Name) + progType, attachType, attachTo := getProgType(sec.Name) spec := &ProgramSpec{ Name: funcSym.Name, Type: progType, AttachType: attachType, + AttachTo: attachTo, License: ec.license, KernelVersion: ec.version, Instructions: insns, @@ -567,7 +568,7 @@ func (ec *elfCode) loadDataSections(maps map[string]*MapSpec, dataSections map[e return nil } -func getProgType(sectionName string) (ProgramType, AttachType) { +func getProgType(sectionName string) (ProgramType, AttachType, string) { types := map[string]struct { progType ProgramType attachType AttachType @@ -593,6 +594,7 @@ func getProgType(sectionName string) (ProgramType, AttachType) { "sk_msg": {SkMsg, AttachSkSKBStreamVerdict}, "lirc_mode2": {LircMode2, AttachLircMode2}, "flow_dissector": {FlowDissector, AttachFlowDissector}, + "iter/": {Tracing, AttachTraceIter}, "cgroup_skb/ingress": {CGroupSKB, AttachCGroupInetIngress}, "cgroup_skb/egress": {CGroupSKB, AttachCGroupInetEgress}, @@ -617,12 +619,18 @@ func getProgType(sectionName string) (ProgramType, AttachType) { } for prefix, t := range types { - if strings.HasPrefix(sectionName, prefix) { - return t.progType, t.attachType + if !strings.HasPrefix(sectionName, prefix) { + continue + } + + if !strings.HasSuffix(prefix, "/") { + return t.progType, t.attachType, "" } + + return t.progType, t.attachType, sectionName[len(prefix):] } - return UnspecifiedProgram, AttachNone + return UnspecifiedProgram, AttachNone, "" } func (ec *elfCode) loadRelocations(sections map[elf.SectionIndex]*elf.Section) (map[elf.SectionIndex]map[uint64]elf.Symbol, error) { diff --git a/elf_reader_test.go b/elf_reader_test.go index 0136dc167..9f81d6b31 100644 --- a/elf_reader_test.go +++ b/elf_reader_test.go @@ -258,15 +258,17 @@ func TestGetProgType(t *testing.T) { section string pt ProgramType at AttachType + to string }{ - {"socket/garbage", SocketFilter, AttachNone}, - {"kprobe/func", Kprobe, AttachNone}, - {"xdp/foo", XDP, AttachNone}, - {"cgroup_skb/ingress", CGroupSKB, AttachCGroupInetIngress}, + {"socket/garbage", SocketFilter, AttachNone, ""}, + {"kprobe/func", Kprobe, AttachNone, "func"}, + {"xdp/foo", XDP, AttachNone, ""}, + {"cgroup_skb/ingress", CGroupSKB, AttachCGroupInetIngress, ""}, + {"iter/bpf_map", Tracing, AttachTraceIter, "bpf_map"}, } for _, tc := range testcases { - pt, at := getProgType(tc.section) + pt, at, to := getProgType(tc.section) if pt != tc.pt { t.Errorf("section %s: expected type %s, got %s", tc.section, tc.pt, pt) } @@ -274,5 +276,9 @@ func TestGetProgType(t *testing.T) { if at != tc.at { t.Errorf("section %s: expected attach type %s, got %s", tc.section, tc.at, at) } + + if to != tc.to { + t.Errorf("section %s: expected attachment to be %q, got %q", tc.section, tc.to, to) + } } } diff --git a/internal/fd.go b/internal/fd.go index f27a1f260..499f1ec0d 100644 --- a/internal/fd.go +++ b/internal/fd.go @@ -1,6 +1,7 @@ package internal import ( + "os" "runtime" "strconv" @@ -61,3 +62,8 @@ func (fd *FD) Dup() (*FD, error) { return NewFD(uint32(dup)), nil } + +func (fd *FD) File(name string) *os.File { + fd.Forget() + return os.NewFile(uintptr(fd.raw), name) +} diff --git a/link/cgroup.go b/link/cgroup.go new file mode 100644 index 000000000..96a5cec7f --- /dev/null +++ b/link/cgroup.go @@ -0,0 +1,169 @@ +package link + +import ( + "os" + + "github.com/cilium/ebpf" + + "golang.org/x/xerrors" +) + +type cgroupAttachFlags uint32 + +// cgroup attach flags +const ( + flagAllowOverride cgroupAttachFlags = 1 << iota + flagAllowMulti + flagReplace +) + +type CgroupOptions struct { + // Path to a cgroupv2 folder. + Path string + // One of the AttachCgroup* constants + Attach ebpf.AttachType + // Program must be of type CGroup*, and the attach type must match Attach. + Program *ebpf.Program +} + +// AttachCgroup links a BPF program to a cgroup. +func AttachCgroup(opts CgroupOptions) (Link, error) { + cgroup, err := os.Open(opts.Path) + if err != nil { + return nil, xerrors.Errorf("can't open cgroup: %s", err) + } + + clone, err := opts.Program.Clone() + if err != nil { + cgroup.Close() + return nil, err + } + + var cg Link + cg, err = newLinkCgroup(cgroup, opts.Attach, clone) + if xerrors.Is(err, ErrNotSupported) { + cg, err = newProgAttachCgroup(cgroup, opts.Attach, clone, flagAllowMulti) + } + if xerrors.Is(err, ErrNotSupported) { + cg, err = newProgAttachCgroup(cgroup, opts.Attach, clone, flagAllowOverride) + } + if err != nil { + cgroup.Close() + clone.Close() + return nil, err + } + + return cg, nil +} + +// LoadPinnedCgroup loads a pinned cgroup from a bpffs. +func LoadPinnedCgroup(fileName string) (Link, error) { + link, err := LoadPinnedRawLink(fileName) + if err != nil { + return nil, err + } + + return &linkCgroup{link}, nil +} + +type progAttachCgroup struct { + cgroup *os.File + current *ebpf.Program + attachType ebpf.AttachType + flags cgroupAttachFlags +} + +var _ Link = (*progAttachCgroup)(nil) + +func (cg *progAttachCgroup) isLink() {} + +func newProgAttachCgroup(cgroup *os.File, attach ebpf.AttachType, prog *ebpf.Program, flags cgroupAttachFlags) (*progAttachCgroup, error) { + if flags&flagAllowMulti > 0 { + if err := haveProgAttachReplace(); err != nil { + return nil, xerrors.Errorf("can't support multiple programs: %w", err) + } + } + + err := RawAttachProgram(RawAttachProgramOptions{ + Target: int(cgroup.Fd()), + Program: prog, + Flags: uint32(flags), + Attach: attach, + }) + if err != nil { + return nil, xerrors.Errorf("cgroup: %w", err) + } + + return &progAttachCgroup{cgroup, prog, attach, flags}, nil +} + +func (cg *progAttachCgroup) Close() error { + defer cg.cgroup.Close() + defer cg.current.Close() + + err := RawDetachProgram(RawDetachProgramOptions{ + Target: int(cg.cgroup.Fd()), + Program: cg.current, + Attach: cg.attachType, + }) + if err != nil { + return xerrors.Errorf("close cgroup: %s", err) + } + return nil +} + +func (cg *progAttachCgroup) Update(prog *ebpf.Program) error { + new, err := prog.Clone() + if err != nil { + return err + } + + args := RawAttachProgramOptions{ + Target: int(cg.cgroup.Fd()), + Program: prog, + Attach: cg.attachType, + Flags: uint32(cg.flags), + } + + if cg.flags&flagAllowMulti > 0 { + // Atomically replacing multiple programs requires at least + // 5.5 (commit 7dd68b3279f17921 "bpf: Support replacing cgroup-bpf + // program in MULTI mode") + args.Flags |= uint32(flagReplace) + args.Replace = cg.current + } + + if err := RawAttachProgram(args); err != nil { + new.Close() + return xerrors.Errorf("can't update cgroup: %s", err) + } + + cg.current.Close() + cg.current = new + return nil +} + +func (cg *progAttachCgroup) Pin(string) error { + return xerrors.Errorf("can't pin cgroup: %w", ErrNotSupported) +} + +type linkCgroup struct { + *RawLink +} + +var _ Link = (*linkCgroup)(nil) + +func (cg *linkCgroup) isLink() {} + +func newLinkCgroup(cgroup *os.File, attach ebpf.AttachType, prog *ebpf.Program) (*linkCgroup, error) { + link, err := AttachRawLink(RawLinkOptions{ + Target: int(cgroup.Fd()), + Program: prog, + Attach: attach, + }) + if err != nil { + return nil, err + } + + return &linkCgroup{link}, err +} diff --git a/link/cgroup_test.go b/link/cgroup_test.go new file mode 100644 index 000000000..d9358a4ac --- /dev/null +++ b/link/cgroup_test.go @@ -0,0 +1,81 @@ +package link + +import ( + "testing" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal/testutils" +) + +func TestAttachCgroup(t *testing.T) { + cgroup, prog, cleanup := mustCgroupFixtures(t) + defer cleanup() + + link, err := AttachCgroup(CgroupOptions{ + Path: cgroup.Name(), + Attach: ebpf.AttachCGroupInetEgress, + Program: prog, + }) + testutils.SkipIfNotSupported(t, err) + if err != nil { + t.Fatal(err) + } + + if haveBPFLink() == nil { + if _, ok := link.(*linkCgroup); !ok { + t.Fatalf("Have support for bpf_link, but got %T instead of linkCgroup", link) + } + } else { + if _, ok := link.(*progAttachCgroup); !ok { + t.Fatalf("Expected progAttachCgroup, got %T instead", link) + } + } +} + +func TestProgAttachCgroup(t *testing.T) { + cgroup, prog, cleanup := mustCgroupFixtures(t) + defer cleanup() + + link, err := newProgAttachCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog, 0) + if err != nil { + t.Fatal("Can't create link:", err) + } + + testLink(t, link, testLinkOptions{ + prog: prog, + }) +} + +func TestProgAttachCgroupAllowMulti(t *testing.T) { + cgroup, prog, cleanup := mustCgroupFixtures(t) + defer cleanup() + + link, err := newProgAttachCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog, flagAllowMulti) + testutils.SkipIfNotSupported(t, err) + if err != nil { + t.Fatal("Can't create link:", err) + } + + // It's currently not possible for a program to replace + // itself. + prog2 := mustCgroupEgressProgram(t) + testLink(t, link, testLinkOptions{ + prog: prog2, + }) +} + +func TestLinkCgroup(t *testing.T) { + cgroup, prog, cleanup := mustCgroupFixtures(t) + defer cleanup() + + link, err := newLinkCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog) + testutils.SkipIfNotSupported(t, err) + if err != nil { + t.Fatal("Can't create link:", err) + } + + testLink(t, link, testLinkOptions{ + prog: prog, + loadPinned: LoadPinnedCgroup, + }) +} diff --git a/link/iter.go b/link/iter.go new file mode 100644 index 000000000..2aa6af1ac --- /dev/null +++ b/link/iter.go @@ -0,0 +1,86 @@ +package link + +import ( + "io" + + "github.com/cilium/ebpf" + "golang.org/x/xerrors" +) + +type IterOptions struct { + // Program must be of type Tracing with attach type + // AttachTraceIter. The kind of iterator to attach to is + // determined at load time via the AttachTo field. + // + // AttachTo requires the kernel to include BTF of itself, + // and it to be compiled with a recent pahole (>= 1.16). + Program *ebpf.Program +} + +// AttachIter attaches a BPF seq_file iterator. +func AttachIter(opts IterOptions) (*Iter, error) { + link, err := AttachRawLink(RawLinkOptions{ + Program: opts.Program, + Attach: ebpf.AttachTraceIter, + }) + if err != nil { + return nil, xerrors.Errorf("can't link iterator: %w", err) + } + + return &Iter{link}, err +} + +// LoadPinnedIter loads a pinned iterator from a bpffs. +func LoadPinnedIter(fileName string) (*Iter, error) { + link, err := LoadPinnedRawLink(fileName) + if err != nil { + return nil, err + } + + return &Iter{link}, err +} + +// Iter represents an attached bpf_iter. +type Iter struct { + link *RawLink +} + +var _ Link = (*Iter)(nil) + +func (it *Iter) isLink() {} + +// Close implements Link. +func (it *Iter) Close() error { + return it.link.Close() +} + +// Pin implements Link. +func (it *Iter) Pin(fileName string) error { + return it.link.Pin(fileName) +} + +// Update implements Link. +func (it *Iter) Update(new *ebpf.Program) error { + return it.link.Update(new) +} + +// Open creates a new instance of the iterator. +// +// Reading from the returned reader triggers the BPF program. +func (it *Iter) Open() (io.ReadCloser, error) { + linkFd, err := it.link.fd.Value() + if err != nil { + return nil, err + } + + attr := &bpfIterCreateAttr{ + linkFd: linkFd, + } + + fd, err := bpfIterCreate(attr) + if err != nil { + return nil, xerrors.Errorf("can't create iterator: %w", err) + } + + return fd.File("bpf_iter"), nil +} diff --git a/link/iter_test.go b/link/iter_test.go new file mode 100644 index 000000000..5e1bac04c --- /dev/null +++ b/link/iter_test.go @@ -0,0 +1,61 @@ +package link + +import ( + "io/ioutil" + "testing" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/internal/btf" + + "golang.org/x/xerrors" +) + +func TestIter(t *testing.T) { + prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{ + Type: ebpf.Tracing, + AttachType: ebpf.AttachTraceIter, + AttachTo: "bpf_map", + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + }, + License: "MIT", + }) + if xerrors.Is(err, btf.ErrNotFound) { + t.Skip("Kernel doesn't support iter:", err) + } + if err != nil { + t.Fatal("Can't load program:", err) + } + defer prog.Close() + + it, err := AttachIter(IterOptions{ + Program: prog, + }) + if err != nil { + t.Fatal("Can't create iter:", err) + } + + file, err := it.Open() + if err != nil { + t.Fatal("Can't open iter instance:", err) + } + defer file.Close() + + contents, err := ioutil.ReadAll(file) + if err != nil { + t.Fatal(err) + } + + if len(contents) != 0 { + t.Error("Non-empty output from no-op iterator:", string(contents)) + } + + testLink(t, it, testLinkOptions{ + prog: prog, + loadPinned: func(s string) (Link, error) { + return LoadPinnedIter(s) + }, + }) +} diff --git a/link/syscalls.go b/link/syscalls.go index 0c40750c2..ac14839c5 100644 --- a/link/syscalls.go +++ b/link/syscalls.go @@ -32,6 +32,45 @@ var haveProgAttach = internal.FeatureTest("BPF_PROG_ATTACH", "4.10", func() (boo return true, nil }) +var haveProgAttachReplace = internal.FeatureTest("BPF_PROG_ATTACH atomic replacement", "5.5", func() (bool, error) { + if err := haveProgAttach(); err != nil { + return false, err + } + + prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{ + Type: ebpf.CGroupSKB, + AttachType: ebpf.AttachCGroupInetIngress, + License: "MIT", + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + }, + }) + if err != nil { + return false, nil + } + defer prog.Close() + + // We know that we have BPF_PROG_ATTACH since we can load CGroupSKB programs. + // If passing BPF_F_REPLACE gives us EINVAL we know that the feature isn't + // present. + attr := internal.BPFProgAttachAttr{ + // We rely on this being checked after attachFlags. + TargetFd: ^uint32(0), + AttachBpfFd: uint32(prog.FD()), + AttachType: uint32(ebpf.AttachCGroupInetIngress), + AttachFlags: uint32(flagReplace), + } + + err = internal.BPFProgAttach(&attr) + if xerrors.Is(err, unix.EPERM) { + // We don't have enough permissions, so we never get to the point + // where flags are checked. + return false, err + } + return !xerrors.Is(err, unix.EINVAL), nil +}) + type bpfLinkCreateAttr struct { progFd uint32 targetFd uint32 @@ -83,3 +122,16 @@ var haveBPFLink = internal.FeatureTest("bpf_link", "5.7", func() (bool, error) { _, err = bpfLinkCreate(&attr) return !xerrors.Is(err, unix.EINVAL), nil }) + +type bpfIterCreateAttr struct { + linkFd uint32 + flags uint32 +} + +func bpfIterCreate(attr *bpfIterCreateAttr) (*internal.FD, error) { + ptr, err := internal.BPF(internal.BPF_ITER_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + if err == nil { + return internal.NewFD(uint32(ptr)), nil + } + return nil, err +} diff --git a/link/syscalls_test.go b/link/syscalls_test.go index aabad551c..361dc47a2 100644 --- a/link/syscalls_test.go +++ b/link/syscalls_test.go @@ -10,6 +10,10 @@ func TestHaveProgAttach(t *testing.T) { testutils.CheckFeatureTest(t, haveProgAttach) } +func TestHaveProgAttachReplace(t *testing.T) { + testutils.CheckFeatureTest(t, haveProgAttachReplace) +} + func TestHaveBPFLink(t *testing.T) { testutils.CheckFeatureTest(t, haveBPFLink) } diff --git a/prog.go b/prog.go index 279992a8f..fd4430b62 100644 --- a/prog.go +++ b/prog.go @@ -47,11 +47,24 @@ type ProgramOptions struct { type ProgramSpec struct { // Name is passed to the kernel as a debug aid. Must only contain // alpha numeric and '_' characters. - Name string - Type ProgramType - AttachType AttachType - Instructions asm.Instructions - License string + Name string + // Type determines at which hook in the kernel a program will run. + Type ProgramType + AttachType AttachType + // Name of a kernel data structure to attach to. It's interpretation + // depends on Type and AttachType. + AttachTo string + Instructions asm.Instructions + + // License of the program. Some helpers are only available if + // the license is deemed compatible with the GPL. + // + // See https://www.kernel.org/doc/html/latest/process/license-rules.html#id1 + License string + + // Version used by tracing programs. + // + // Deprecated: superseded by BTF. KernelVersion uint32 // The BTF associated with this program. Changing Instructions @@ -83,9 +96,10 @@ type Program struct { // otherwise it is empty. VerifierLog string - fd *internal.FD - name string - abi ProgramABI + fd *internal.FD + name string + abi ProgramABI + attachType AttachType } // NewProgram creates a new Program. @@ -236,6 +250,16 @@ func convertProgramSpec(spec *ProgramSpec, handle *btf.Handle) (*bpfProgLoadAttr attr.funcInfo = internal.NewSlicePointer(bytes) } + if spec.AttachTo != "" { + target, err := resolveBTFType(spec.AttachTo, spec.Type, spec.AttachType) + if err != nil { + return nil, err + } + if target != nil { + attr.attachBTFID = target.ID() + } + } + return attr, nil } @@ -571,3 +595,29 @@ func (p *Program) ID() (ProgramID, error) { } return ProgramID(info.id), nil } + +func resolveBTFType(name string, progType ProgramType, attachType AttachType) (btf.Type, error) { + kernel, err := btf.LoadKernelSpec() + if err != nil { + return nil, xerrors.Errorf("can't resolve BTF type %s: %w", name, err) + } + + type match struct { + p ProgramType + a AttachType + } + + target := match{progType, attachType} + switch target { + case match{Tracing, AttachTraceIter}: + var target btf.Func + if err := kernel.FindType("bpf_iter_"+name, &target); err != nil { + return nil, xerrors.Errorf("can't resolve BTF for iterator %s: %w", name, err) + } + + return &target, nil + + default: + return nil, nil + } +} diff --git a/syscalls.go b/syscalls.go index 399501c92..ff98cc403 100644 --- a/syscalls.go +++ b/syscalls.go @@ -99,6 +99,8 @@ type bpfProgLoadAttr struct { lineInfoRecSize uint32 lineInfo internal.Pointer lineInfoCnt uint32 + attachBTFID btf.TypeID + attachProgFd uint32 } type bpfProgInfo struct {