Skip to content

Commit

Permalink
Simplify kprobe/uprobe attach to progLink pattern (#183)
Browse files Browse the repository at this point in the history
  • Loading branch information
brycekahle authored May 13, 2024
1 parent 478a9c8 commit 623f5d0
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 325 deletions.
3 changes: 1 addition & 2 deletions err.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ var (
ErrProbeRunning = errors.New("the probe is already running")
ErrSectionFormat = errors.New("invalid section format")
ErrSymbolNotFound = errors.New("symbol not found")
ErrKprobeIDNotExist = errors.New("kprobe id file doesn't exist")
ErrUprobeIDNotExist = errors.New("uprobe id file doesn't exist")
ErrProbeIDNotExist = errors.New("probe id file doesn't exist")
ErrKProbeHookPointNotExist = errors.New("the kprobe hook point doesn't exist")
ErrCloneProbeRequired = errors.New("use CloneProbe to load 2 instances of the same program")
ErrInterfaceNotSet = errors.New("interface not provided: at least one of IfIndex and IfName must be set")
Expand Down
180 changes: 46 additions & 134 deletions kprobe.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,6 @@ import (
"errors"
"fmt"
"os"
"strconv"
"strings"

"github.com/DataDog/ebpf-manager/tracefs"
)

type probeType uint8

const (
kprobe probeType = iota
uprobe
)

type KprobeAttachMethod = AttachMethod
Expand All @@ -26,162 +15,85 @@ const (
)

func (p *Probe) prefix() string {
if p.isReturnProbe {
return "r"
}
return "p"
return tracefsPrefix(p.isReturnProbe)
}

type attachFunc func() (*tracefsLink, error)

// attachKprobe - Attaches the probe to its kprobe
func (p *Probe) attachKprobe() error {
var err error

if len(p.HookFuncName) == 0 {
return errors.New("HookFuncName, MatchFuncName or SyscallFuncName is required")
return fmt.Errorf("HookFuncName, MatchFuncName or SyscallFuncName is required")
}

// currently the perf event open ABI doesn't allow to specify the max active parameter
if p.KProbeMaxActive > 0 && p.isReturnProbe {
if err = p.attachWithKprobeEvents(); err != nil {
if p.perfEventFD, err = perfEventOpenPMU(p.HookFuncName, 0, -1, "kprobe", p.isReturnProbe, 0); err != nil {
return err
}
}
} else if p.KprobeAttachMethod == AttachKprobeWithPerfEventOpen {
if p.perfEventFD, err = perfEventOpenPMU(p.HookFuncName, 0, -1, "kprobe", p.isReturnProbe, 0); err != nil {
if err = p.attachWithKprobeEvents(); err != nil {
return err
}
var eventsFunc attachFunc = p.attachWithKprobeEvents
var pmuFunc attachFunc = func() (*tracefsLink, error) {
pfd, err := perfEventOpenPMU(p.HookFuncName, 0, -1, kprobe, p.isReturnProbe, 0)
if err != nil {
return nil, err
}
} else if p.KprobeAttachMethod == AttachKprobeWithKprobeEvents {
if err = p.attachWithKprobeEvents(); err != nil {
if p.perfEventFD, err = perfEventOpenPMU(p.HookFuncName, 0, -1, "kprobe", p.isReturnProbe, 0); err != nil {
return err
}
}
} else {
return fmt.Errorf("invalid kprobe attach method: %d", p.KprobeAttachMethod)
return &tracefsLink{perfEventLink: newPerfEventLink(pfd), Type: kprobe}, nil
}

// enable perf event
if err = ioctlPerfEventSetBPF(p.perfEventFD, p.program.FD()); err != nil {
return fmt.Errorf("couldn't set perf event bpf %s: %w", p.ProbeIdentificationPair, err)
startFunc, fallbackFunc := pmuFunc, eventsFunc
// currently the perf event open ABI doesn't allow to specify the max active parameter
if (p.KProbeMaxActive > 0 && p.isReturnProbe) || p.KprobeAttachMethod == AttachKprobeWithKprobeEvents {
startFunc, fallbackFunc = eventsFunc, pmuFunc
}
if err = ioctlPerfEventEnable(p.perfEventFD); err != nil {
return fmt.Errorf("couldn't enable perf event %s: %w", p.ProbeIdentificationPair, err)

var startErr, fallbackErr error
var tl *tracefsLink
if tl, startErr = startFunc(); startErr != nil {
if tl, fallbackErr = fallbackFunc(); fallbackErr != nil {
return errors.Join(startErr, fallbackErr)
}
}

if err := attachPerfEvent(tl.perfEventLink, p.program); err != nil {
_ = tl.Close()
return fmt.Errorf("attach %s: %w", p.ProbeIdentificationPair, err)
}
p.progLink = tl
return nil
}

// attachWithKprobeEvents attaches the kprobe using the kprobes_events ABI
func (p *Probe) attachWithKprobeEvents() error {
func (p *Probe) attachWithKprobeEvents() (*tracefsLink, error) {
if p.kprobeHookPointNotExist {
return ErrKProbeHookPointNotExist
return nil, ErrKProbeHookPointNotExist
}

// Prepare kprobe_events line parameters
var maxActiveStr string
if p.isReturnProbe {
if p.KProbeMaxActive > 0 {
maxActiveStr = fmt.Sprintf("%d", p.KProbeMaxActive)
}
args := traceFsEventArgs{
Type: kprobe,
ReturnProbe: p.isReturnProbe,
Symbol: p.HookFuncName,
UID: p.UID,
MaxActive: p.KProbeMaxActive,
AttachingPID: p.attachPID,
}

// Fallback to debugfs, write kprobe_events line to register kprobe
var kprobeID int
kprobeID, err := registerKprobeEvent(p.prefix(), p.HookFuncName, p.UID, maxActiveStr, p.attachPID)
if errors.Is(err, ErrKprobeIDNotExist) {
var eventName string
kprobeID, eventName, err := registerTraceFSEvent(args)
if errors.Is(err, ErrProbeIDNotExist) {
// The probe might have been loaded under a kernel generated event name. Clean up just in case.
_ = unregisterTraceFSEvent("kprobe_events", getKernelGeneratedEventName(p.prefix(), p.HookFuncName))
_ = unregisterTraceFSEvent(kprobe.eventsFilename(), getKernelGeneratedEventName(p.prefix(), p.HookFuncName))
// fallback without KProbeMaxActive
kprobeID, err = registerKprobeEvent(p.prefix(), p.HookFuncName, p.UID, "", p.attachPID)
args.MaxActive = 0
kprobeID, eventName, err = registerTraceFSEvent(args)
}

if err != nil {
if errors.Is(err, os.ErrNotExist) {
p.kprobeHookPointNotExist = true
}
return fmt.Errorf("couldn't enable kprobe %s: %w", p.ProbeIdentificationPair, err)
return nil, fmt.Errorf("couldn't enable kprobe %s: %w", p.ProbeIdentificationPair, err)
}

// create perf event fd
p.perfEventFD, err = perfEventOpenTracingEvent(kprobeID, -1)
if err != nil {
return fmt.Errorf("couldn't open perf event fd for %s: %w", p.ProbeIdentificationPair, err)
}
p.attachedWithDebugFS = true

return nil
}

// detachKprobe - Detaches the probe from its kprobe
func (p *Probe) detachKprobe() error {
if !p.attachedWithDebugFS {
// nothing to do
return nil
}

// Write kprobe_events line to remove hook point
return unregisterKprobeEvent(p.prefix(), p.HookFuncName, p.UID, p.attachPID)
}

func (p *Probe) pauseKprobe() error {
if err := ioctlPerfEventDisable(p.perfEventFD); err != nil {
return fmt.Errorf("pause kprobe: %w", err)
}
return nil
}

func (p *Probe) resumeKprobe() error {
if err := ioctlPerfEventEnable(p.perfEventFD); err != nil {
return fmt.Errorf("resume kprobe: %w", err)
}
return nil
}

// registerKprobeEvent - Writes a new kprobe in kprobe_events with the provided parameters. Call DisableKprobeEvent
// to remove the kprobe.
func registerKprobeEvent(probeType, funcName, UID, maxActiveStr string, kprobeAttachPID int) (int, error) {
// Generate event name
eventName, err := generateEventName(probeType, funcName, UID, kprobeAttachPID)
if err != nil {
return -1, err
}

// Write line to kprobe_events
f, err := tracefs.OpenFile("kprobe_events", os.O_APPEND|os.O_WRONLY, 0666)
if err != nil {
return -1, fmt.Errorf("cannot open kprobe_events: %w", err)
}
defer f.Close()
cmd := fmt.Sprintf("%s%s:%s %s\n", probeType, maxActiveStr, eventName, funcName)
if _, err = f.WriteString(cmd); err != nil && !os.IsExist(err) {
return -1, fmt.Errorf("cannot write %q to kprobe_events: %w", cmd, err)
}

// Retrieve kprobe ID
kprobeIDFile := fmt.Sprintf("events/kprobes/%s/id", eventName)
kprobeIDBytes, err := tracefs.ReadFile(kprobeIDFile)
if err != nil {
if os.IsNotExist(err) {
return -1, ErrKprobeIDNotExist
}
return -1, fmt.Errorf("cannot read kprobe id: %w", err)
}
id := strings.TrimSpace(string(kprobeIDBytes))
kprobeID, err := strconv.Atoi(id)
if err != nil {
return -1, fmt.Errorf("invalid kprobe id: '%s': %w", id, err)
}
return kprobeID, nil
}

// unregisterKprobeEvent - Removes a kprobe from kprobe_events
func unregisterKprobeEvent(probeType, funcName, UID string, kprobeAttachPID int) error {
// Generate event name
eventName, err := generateEventName(probeType, funcName, UID, kprobeAttachPID)
pfd, err := perfEventOpenTracingEvent(kprobeID, -1)
if err != nil {
return err
return nil, fmt.Errorf("couldn't open perf event fd for %s: %w", p.ProbeIdentificationPair, err)
}
return unregisterTraceFSEvent("kprobe_events", eventName)
return &tracefsLink{perfEventLink: newPerfEventLink(pfd), Type: kprobe, EventName: eventName}, nil
}
6 changes: 6 additions & 0 deletions pauser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package manager

type pauser interface {
Pause() error
Resume() error
}
79 changes: 63 additions & 16 deletions perf_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"syscall"
"unsafe"

"github.com/cilium/ebpf"
"golang.org/x/sys/unix"
)

Expand Down Expand Up @@ -37,37 +38,75 @@ func (p *Probe) attachPerfEvent() error {
pid = -1
}

link := &perfEventProgLink{
perfEventCPUFDs: make([]*perfEventLink, 0, p.PerfEventCPUCount),
}
for cpu := 0; cpu < p.PerfEventCPUCount; cpu++ {
fd, err := perfEventOpenRaw(&attr, pid, cpu, -1, 0)
if err != nil {
return fmt.Errorf("couldn't attach perf_event program %s on pid %d and CPU %d: %v", p.ProbeIdentificationPair, pid, cpu, err)
}
p.perfEventCPUFDs = append(p.perfEventCPUFDs, fd)

if err = ioctlPerfEventSetBPF(fd, p.program.FD()); err != nil {
return fmt.Errorf("couldn't set perf event bpf %s: %w", p.ProbeIdentificationPair, err)
}
if err = ioctlPerfEventEnable(fd); err != nil {
return fmt.Errorf("couldn't enable perf event %s for pid %d and CPU %d: %w", p.ProbeIdentificationPair, pid, cpu, err)
pfd := newPerfEventLink(fd)
link.perfEventCPUFDs = append(link.perfEventCPUFDs, pfd)
if err := attachPerfEvent(pfd, p.program); err != nil {
_ = link.Close()
return fmt.Errorf("attach: %w", err)
}
}
p.progLink = link
return nil
}

// detachPerfEvent - Detaches the perf_event program
func (p *Probe) detachPerfEvent() error {
type perfEventProgLink struct {
perfEventCPUFDs []*perfEventLink
}

func (p *perfEventProgLink) Close() error {
var errs []error
for _, fd := range p.perfEventCPUFDs {
errs = append(errs, fd.Close())
}
p.perfEventCPUFDs = []*fd{}
p.perfEventCPUFDs = []*perfEventLink{}
return errors.Join(errs...)
}

type perfEventLink struct {
fd *fd
}

func newPerfEventLink(fd *fd) *perfEventLink {
pe := &perfEventLink{fd}
runtime.SetFinalizer(pe, (*perfEventLink).Close)
return pe
}

func (pe *perfEventLink) Close() error {
runtime.SetFinalizer(pe, nil)
return pe.fd.Close()
}

func (pe *perfEventLink) Pause() error {
return ioctlPerfEventDisable(pe.fd)
}

func (pe *perfEventLink) Resume() error {
return ioctlPerfEventEnable(pe.fd)
}

func attachPerfEvent(pe *perfEventLink, prog *ebpf.Program) error {
if err := ioctlPerfEventSetBPF(pe.fd, prog.FD()); err != nil {
return fmt.Errorf("set perf event bpf: %w", err)
}
if err := ioctlPerfEventEnable(pe.fd); err != nil {
return fmt.Errorf("enable perf event: %w", err)
}
return nil
}

// perfEventOpenPMU - Kernel API with e12f03d ("perf/core: Implement the 'perf_kprobe' PMU") allows
// creating [k,u]probe with perf_event_open, which makes it easier to clean up
// the [k,u]probe. This function tries to create pfd with the perf_kprobe PMU.
func perfEventOpenPMU(name string, offset, pid int, eventType string, retProbe bool, referenceCounterOffset uint64) (*fd, error) {
func perfEventOpenPMU(name string, offset, pid int, eventType probeType, retProbe bool, referenceCounterOffset uint64) (*fd, error) {
var err error
var attr unix.PerfEventAttr

Expand Down Expand Up @@ -98,10 +137,10 @@ func perfEventOpenPMU(name string, offset, pid int, eventType string, retProbe b
}

switch eventType {
case "kprobe":
case kprobe:
attr.Ext1 = uint64(uintptr(unsafe.Pointer(namePtr))) // Kernel symbol to trace
pid = 0
case "uprobe":
pid = -1
case uprobe:
// The minimum size required for PMU uprobes is PERF_ATTR_SIZE_VER1,
// since it added the config2 (Ext2) field. The Size field controls the
// size of the internal buffer the kernel allocates for reading the
Expand All @@ -115,8 +154,12 @@ func perfEventOpenPMU(name string, offset, pid int, eventType string, retProbe b
}
}

cpu := 0
if pid != -1 {
cpu = -1
}
var efd int
efd, err = unix.PerfEventOpen(&attr, pid, 0, -1, unix.PERF_FLAG_FD_CLOEXEC)
efd, err = unix.PerfEventOpen(&attr, pid, cpu, -1, unix.PERF_FLAG_FD_CLOEXEC)

// Since commit 97c753e62e6c, ENOENT is correctly returned instead of EINVAL
// when trying to create a kretprobe for a missing symbol. Make sure ENOENT
Expand All @@ -138,6 +181,10 @@ func perfEventOpenTracingEvent(probeID int, pid int) (*fd, error) {
if pid <= 0 {
pid = -1
}
cpu := 0
if pid != -1 {
cpu = -1
}
attr := unix.PerfEventAttr{
Type: unix.PERF_TYPE_TRACEPOINT,
Sample_type: unix.PERF_SAMPLE_RAW,
Expand All @@ -146,7 +193,7 @@ func perfEventOpenTracingEvent(probeID int, pid int) (*fd, error) {
Config: uint64(probeID),
}
attr.Size = uint32(unsafe.Sizeof(attr))
return perfEventOpenRaw(&attr, pid, 0, -1, unix.PERF_FLAG_FD_CLOEXEC)
return perfEventOpenRaw(&attr, pid, cpu, -1, unix.PERF_FLAG_FD_CLOEXEC)
}

func perfEventOpenRaw(attr *unix.PerfEventAttr, pid int, cpu int, groupFd int, flags int) (*fd, error) {
Expand Down
Loading

0 comments on commit 623f5d0

Please sign in to comment.