diff --git a/err.go b/err.go index d384dec..383da4d 100644 --- a/err.go +++ b/err.go @@ -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") diff --git a/kprobe.go b/kprobe.go index d55a925..54f01c9 100644 --- a/kprobe.go +++ b/kprobe.go @@ -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 @@ -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 } diff --git a/pauser.go b/pauser.go new file mode 100644 index 0000000..2cec01f --- /dev/null +++ b/pauser.go @@ -0,0 +1,6 @@ +package manager + +type pauser interface { + Pause() error + Resume() error +} diff --git a/perf_event.go b/perf_event.go index 76e23e0..0116f10 100644 --- a/perf_event.go +++ b/perf_event.go @@ -8,6 +8,7 @@ import ( "syscall" "unsafe" + "github.com/cilium/ebpf" "golang.org/x/sys/unix" ) @@ -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 @@ -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 @@ -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 @@ -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, @@ -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) { diff --git a/probe.go b/probe.go index 9a40126..068f283 100644 --- a/probe.go +++ b/probe.go @@ -30,14 +30,12 @@ type Probe struct { netlinkSocketCache *netlinkSocketCache program *ebpf.Program programSpec *ebpf.ProgramSpec - perfEventFD *fd state state stateLock sync.RWMutex manualLoadNeeded bool checkPin bool attachPID int attachRetryAttempt uint - attachedWithDebugFS bool kprobeHookPointNotExist bool systemWideID int programTag string @@ -202,9 +200,6 @@ type Probe struct { // runtime.NumCPU(). Disclaimer: in containerized environment and depending on the CPU affinity of the program // holding the manager, runtime.NumCPU might not return the real CPU count of the host. PerfEventCPUCount int - - // perfEventCPUFDs - (Perf event) holds the fd of the perf_event program per CPU - perfEventCPUFDs []*fd } // GetEBPFFuncName - Returns EBPFFuncName with the UID as a postfix if the Probe was copied @@ -463,9 +458,7 @@ func (p *Probe) internalInit(manager *Manager) error { // set default kprobe attach method if p.KprobeAttachMethod == AttachKprobeMethodNotSet { - if manager != nil { - p.KprobeAttachMethod = manager.options.DefaultKprobeAttachMethod - } + p.KprobeAttachMethod = manager.options.DefaultKprobeAttachMethod if p.KprobeAttachMethod == AttachKprobeMethodNotSet { p.KprobeAttachMethod = AttachKprobeWithPerfEventOpen } @@ -473,9 +466,7 @@ func (p *Probe) internalInit(manager *Manager) error { // set default uprobe attach method if p.UprobeAttachMethod == AttachMethodNotSet { - if manager != nil { - p.UprobeAttachMethod = manager.options.DefaultUprobeAttachMethod - } + p.UprobeAttachMethod = manager.options.DefaultUprobeAttachMethod if p.UprobeAttachMethod == AttachMethodNotSet { p.UprobeAttachMethod = AttachWithPerfEventOpen } @@ -591,18 +582,12 @@ func (p *Probe) pause() error { return nil } - var err error - switch p.programSpec.Type { - case ebpf.Kprobe: - err = p.pauseKprobe() - case ebpf.SocketFilter: - err = p.detachSocket() - case ebpf.TracePoint: - err = p.pauseTracepoint() - default: + v, ok := p.progLink.(pauser) + if !ok { return fmt.Errorf("pause not supported for program type %s", p.programSpec.Type) } - if err != nil { + + if err := v.Pause(); err != nil { p.lastError = err return fmt.Errorf("error pausing probe %s: %w", p.ProbeIdentificationPair, err) } @@ -618,18 +603,12 @@ func (p *Probe) resume() error { return nil } - var err error - switch p.programSpec.Type { - case ebpf.Kprobe: - err = p.resumeKprobe() - case ebpf.SocketFilter: - err = p.attachSocket() - case ebpf.TracePoint: - err = p.resumeTracepoint() - default: + v, ok := p.progLink.(pauser) + if !ok { return fmt.Errorf("resume not supported for program type %s", p.programSpec.Type) } - if err != nil { + + if err := v.Resume(); err != nil { p.lastError = err return fmt.Errorf("error resuming probe %s: %w", p.ProbeIdentificationPair, err) } @@ -668,29 +647,14 @@ func (p *Probe) detachRetry() error { // detach - Thread unsafe version of Detach. func (p *Probe) detach() error { err := p.program.Unpin() - // Shared with all probes: close the perf event file descriptor - if p.perfEventFD != nil { - err = p.perfEventFD.Close() - } // Per program type cleanup switch p.programSpec.Type { case ebpf.UnspecifiedProgram: // nothing to do break - case ebpf.Kprobe: - switch p.kprobeType { - case kprobe: - err = errors.Join(err, p.detachKprobe()) - case uprobe: - err = errors.Join(err, p.detachUprobe()) - } - case ebpf.SocketFilter: - err = errors.Join(err, p.detachSocket()) case ebpf.SchedCLS: err = errors.Join(err, p.detachTCCLS()) - case ebpf.PerfEvent: - err = errors.Join(err, p.detachPerfEvent()) default: if p.progLink != nil { err = errors.Join(err, p.progLink.Close()) @@ -741,13 +705,12 @@ func (p *Probe) reset() { p.netlinkSocketCache = nil p.program = nil p.programSpec = nil - p.perfEventFD = nil + p.progLink = nil p.state = reset p.manualLoadNeeded = false p.checkPin = false p.attachPID = 0 p.attachRetryAttempt = 0 - p.attachedWithDebugFS = false p.kprobeHookPointNotExist = false p.systemWideID = 0 p.programTag = "" diff --git a/socket.go b/socket.go index b9ccbc9..93cd598 100644 --- a/socket.go +++ b/socket.go @@ -8,12 +8,28 @@ import ( // attachSocket - Attaches the probe to the provided socket func (p *Probe) attachSocket() error { - return sockAttach(p.SocketFD, p.program.FD()) + if err := sockAttach(p.SocketFD, p.program.FD()); err != nil { + return err + } + p.progLink = &socketLink{p.SocketFD, p.program.FD()} + return nil } -// detachSocket - Detaches the probe from its socket -func (p *Probe) detachSocket() error { - return sockDetach(p.SocketFD, p.program.FD()) +type socketLink struct { + sockFD int + progFD int +} + +func (s *socketLink) Close() error { + return sockDetach(s.sockFD, s.progFD) +} + +func (s *socketLink) Pause() error { + return sockDetach(s.sockFD, s.progFD) +} + +func (s *socketLink) Resume() error { + return sockAttach(s.sockFD, s.progFD) } func sockAttach(sockFd int, progFd int) error { diff --git a/sysfs.go b/sysfs.go index 3157409..152a0fd 100644 --- a/sysfs.go +++ b/sysfs.go @@ -24,7 +24,7 @@ var ( }{} ) -func parsePMUEventType(eventType string) (uint32, error) { +func parsePMUEventType(eventType probeType) (uint32, error) { PMUTypeFile := fmt.Sprintf("/sys/bus/event_source/devices/%s/type", eventType) f, err := os.Open(PMUTypeFile) if err != nil { @@ -44,14 +44,14 @@ func parsePMUEventType(eventType string) (uint32, error) { // getPMUEventType reads a Performance Monitoring Unit's type (numeric identifier) // from /sys/bus/event_source/devices//type. -func getPMUEventType(eventType string) (uint32, error) { +func getPMUEventType(eventType probeType) (uint32, error) { switch eventType { - case "kprobe": + case kprobe: kprobePMUType.once.Do(func() { kprobePMUType.value, kprobePMUType.err = parsePMUEventType(eventType) }) return kprobePMUType.value, kprobePMUType.err - case "uprobe": + case uprobe: uprobePMUType.once.Do(func() { uprobePMUType.value, uprobePMUType.err = parsePMUEventType(eventType) }) @@ -78,8 +78,8 @@ var ( // parseRetProbeBit reads a Performance Monitoring Unit's retprobe bit // from /sys/bus/event_source/devices//format/retprobe. -func parseRetProbeBit(eventType string) (uint64, error) { - p := filepath.Join("/sys/bus/event_source/devices/", eventType, "/format/retprobe") +func parseRetProbeBit(eventType probeType) (uint64, error) { + p := filepath.Join("/sys/bus/event_source/devices/", eventType.String(), "/format/retprobe") data, err := os.ReadFile(p) if err != nil { @@ -98,14 +98,14 @@ func parseRetProbeBit(eventType string) (uint64, error) { return rp, nil } -func getRetProbeBit(eventType string) (uint64, error) { +func getRetProbeBit(eventType probeType) (uint64, error) { switch eventType { - case "kprobe": + case kprobe: kprobeRetProbeBit.once.Do(func() { kprobeRetProbeBit.value, kprobeRetProbeBit.err = parseRetProbeBit(eventType) }) return kprobeRetProbeBit.value, kprobeRetProbeBit.err - case "uprobe": + case uprobe: uprobeRetProbeBit.once.Do(func() { uprobeRetProbeBit.value, uprobeRetProbeBit.err = parseRetProbeBit(eventType) }) diff --git a/tracefs.go b/tracefs.go index 5fd7832..221e5e7 100644 --- a/tracefs.go +++ b/tracefs.go @@ -23,6 +23,35 @@ const ( minFunctionNameLen = 10 ) +type probeType uint8 + +const ( + kprobe probeType = iota + uprobe +) + +func (p probeType) String() string { + switch p { + case kprobe: + return "kprobe" + case uprobe: + return "uprobe" + default: + return "" + } +} + +func (p probeType) eventsFilename() string { + switch p { + case kprobe: + return "kprobe_events" + case uprobe: + return "uprobe_events" + default: + return "" + } +} + // getUIDSet - Returns the list of UIDs used by this manager. func (m *Manager) getUIDSet() []string { var uidSet []string @@ -76,7 +105,7 @@ func (m *Manager) cleanupTraceFS() error { var cleanUpErrors error pidMask := map[int]bool{Getpid(): true} - eventFiles := []string{"kprobe_events", "uprobe_events"} + eventFiles := []string{kprobe.eventsFilename(), uprobe.eventsFilename()} for _, eventFile := range eventFiles { events, err := tracefs.ReadFile(eventFile) if err != nil { @@ -174,6 +203,86 @@ func getKernelGeneratedEventName(probeType, funcName string) string { return fmt.Sprintf("%s_%s_0", probeType, funcName) } +type traceFsEventArgs struct { + Type probeType + ReturnProbe bool + Symbol, Path string + Offset uint64 + UID string + MaxActive int + AttachingPID int +} + +func tracefsPrefix(ret bool) string { + if ret { + return "r" + } + return "p" +} + +func registerTraceFSEvent(args traceFsEventArgs) (int, string, error) { + prefix := tracefsPrefix(args.ReturnProbe) + eventName, err := generateEventName(prefix, args.Symbol, args.UID, args.AttachingPID) + if err != nil { + return -1, "", err + } + + f, err := tracefs.OpenFile(args.Type.eventsFilename(), os.O_APPEND|os.O_WRONLY, 0666) + if err != nil { + return -1, "", fmt.Errorf("cannot open %s: %w", args.Type.eventsFilename(), err) + } + defer f.Close() + // precompute strings of unknown length + maxActiveStr := strconv.Itoa(args.MaxActive) + offsetStr := fmt.Sprintf("%#x", args.Offset) + token := args.Symbol + if args.Type == uprobe { + token = args.Path + } + + var sb strings.Builder + // may be larger than necessary, but never smaller + sb.Grow(len(prefix) + len(maxActiveStr) + 1 + len(eventName) + 1 + len(token) + 1 + len(offsetStr) + 1) + sb.WriteString(prefix) + if args.Type == kprobe && args.ReturnProbe && args.MaxActive > 0 { + sb.WriteString(maxActiveStr) + } + sb.WriteRune(':') + sb.WriteString(eventName) + sb.WriteRune(' ') + sb.WriteString(token) + if args.Type == kprobe { + if args.Offset > 0 { + sb.WriteRune('+') + sb.WriteString(offsetStr) + } + } else if args.Type == uprobe { + sb.WriteRune(':') + sb.WriteString(offsetStr) + } + sb.WriteRune('\n') + cmd := sb.String() + if _, err = f.WriteString(cmd); err != nil && !os.IsExist(err) { + return -1, "", fmt.Errorf("cannot write %q to %s: %w", cmd, args.Type.eventsFilename(), err) + } + + // Retrieve probe ID + probeIDFile := fmt.Sprintf("events/%ss/%s/id", args.Type.String(), eventName) + probeIDBytes, err := tracefs.ReadFile(probeIDFile) + if err != nil { + if os.IsNotExist(err) { + return -1, "", ErrProbeIDNotExist + } + return -1, "", fmt.Errorf("cannot read probe id: %w", err) + } + id := strings.TrimSpace(string(probeIDBytes)) + probeID, err := strconv.Atoi(id) + if err != nil { + return -1, "", fmt.Errorf("invalid probe id: '%s': %w", id, err) + } + return probeID, eventName, nil +} + func unregisterTraceFSEvent(eventsFile string, name string) error { f, err := tracefs.OpenFile(eventsFile, os.O_APPEND|os.O_WRONLY, 0666) if err != nil { @@ -208,3 +317,17 @@ func GetTracepointID(category, name string) (int, error) { } return tracepointID, nil } + +type tracefsLink struct { + *perfEventLink + Type probeType + EventName string +} + +func (l *tracefsLink) Close() error { + err := l.perfEventLink.Close() + if l.EventName != "" { + err = errors.Join(err, unregisterTraceFSEvent(l.Type.eventsFilename(), l.EventName)) + } + return err +} diff --git a/tracepoint.go b/tracepoint.go index dc9286c..dd29bd7 100644 --- a/tracepoint.go +++ b/tracepoint.go @@ -24,29 +24,15 @@ func (p *Probe) attachTracepoint() error { } // Hook the eBPF program to the tracepoint - p.perfEventFD, err = perfEventOpenTracingEvent(tracepointID, -1) + fd, err := perfEventOpenTracingEvent(tracepointID, -1) if err != nil { return fmt.Errorf("couldn't enable tracepoint %s: %w", p.ProbeIdentificationPair, err) } - if err = ioctlPerfEventSetBPF(p.perfEventFD, p.program.FD()); err != nil { - return fmt.Errorf("couldn't set perf event bpf %s: %w", p.ProbeIdentificationPair, err) - } - if err = ioctlPerfEventEnable(p.perfEventFD); err != nil { - return fmt.Errorf("couldn't enable perf event %s: %w", p.ProbeIdentificationPair, err) - } - return nil -} - -func (p *Probe) pauseTracepoint() error { - if err := ioctlPerfEventDisable(p.perfEventFD); err != nil { - return fmt.Errorf("pause tracepoint: %w", err) - } - return nil -} - -func (p *Probe) resumeTracepoint() error { - if err := ioctlPerfEventEnable(p.perfEventFD); err != nil { - return fmt.Errorf("resume tracepoint: %w", err) + pe := newPerfEventLink(fd) + if err := attachPerfEvent(pe, p.program); err != nil { + _ = pe.Close() + return fmt.Errorf("attach %s: %w", p.ProbeIdentificationPair, err) } + p.progLink = pe return nil } diff --git a/uprobe.go b/uprobe.go index dbb1930..80ee8c7 100644 --- a/uprobe.go +++ b/uprobe.go @@ -2,13 +2,9 @@ package manager import ( "debug/elf" + "errors" "fmt" - "os" "regexp" - "strconv" - "strings" - - "github.com/DataDog/ebpf-manager/tracefs" ) // SanitizeUprobeAddresses - sanitizes the addresses of the provided symbols @@ -82,27 +78,33 @@ func findSymbolOffsets(path string, pattern *regexp.Regexp) ([]elf.Symbol, error } // attachWithUprobeEvents attaches the uprobe using the uprobes_events ABI -func (p *Probe) attachWithUprobeEvents() error { - // fallback to debugfs +func (p *Probe) attachWithUprobeEvents() (*tracefsLink, error) { + args := traceFsEventArgs{ + Type: uprobe, + ReturnProbe: p.isReturnProbe, + Symbol: p.HookFuncName, // only used for event naming + Path: p.BinaryPath, + Offset: p.UprobeOffset, + UID: p.UID, + AttachingPID: p.attachPID, + } + var uprobeID int - uprobeID, err := registerUprobeEvent(p.prefix(), p.HookFuncName, p.BinaryPath, p.UID, p.attachPID, p.UprobeOffset) + var eventName string + uprobeID, eventName, err := registerTraceFSEvent(args) if err != nil { - return fmt.Errorf("couldn't enable uprobe %s: %w", p.ProbeIdentificationPair, err) + return nil, fmt.Errorf("couldn't enable uprobe %s: %w", p.ProbeIdentificationPair, err) } - // Activate perf event - p.perfEventFD, err = perfEventOpenTracingEvent(uprobeID, p.PerfEventPID) + pfd, err := perfEventOpenTracingEvent(uprobeID, p.PerfEventPID) if err != nil { - return fmt.Errorf("couldn't open perf event fd for %s: %w", p.ProbeIdentificationPair, err) + return nil, fmt.Errorf("couldn't open perf event fd for %s: %w", p.ProbeIdentificationPair, err) } - p.attachedWithDebugFS = true - return nil + return &tracefsLink{perfEventLink: newPerfEventLink(pfd), Type: uprobe, EventName: eventName}, nil } // attachUprobe - Attaches the probe to its Uprobe func (p *Probe) attachUprobe() error { - var err error - // compute the offset if it was not provided if p.UprobeOffset == 0 { var funcPattern string @@ -127,88 +129,32 @@ func (p *Probe) attachUprobe() error { p.HookFuncName = offsets[0].Name } - if p.UprobeAttachMethod == AttachWithPerfEventOpen { - if p.perfEventFD, err = perfEventOpenPMU(p.BinaryPath, int(p.UprobeOffset), p.PerfEventPID, "uprobe", p.isReturnProbe, 0); err != nil { - if err = p.attachWithUprobeEvents(); err != nil { - return err - } - } - } else if p.UprobeAttachMethod == AttachWithProbeEvents { - if err = p.attachWithUprobeEvents(); err != nil { - if p.perfEventFD, err = perfEventOpenPMU(p.BinaryPath, int(p.UprobeOffset), p.PerfEventPID, "uprobe", p.isReturnProbe, 0); err != nil { - return err - } + var eventsFunc attachFunc = p.attachWithUprobeEvents + var pmuFunc attachFunc = func() (*tracefsLink, error) { + pfd, err := perfEventOpenPMU(p.BinaryPath, int(p.UprobeOffset), p.PerfEventPID, uprobe, p.isReturnProbe, 0) + if err != nil { + return nil, err } - } else { - return fmt.Errorf("invalid uprobe attach method: %d", p.UprobeAttachMethod) + return &tracefsLink{perfEventLink: newPerfEventLink(pfd), Type: uprobe}, 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 + if p.UprobeAttachMethod == AttachWithProbeEvents { + startFunc, fallbackFunc = eventsFunc, pmuFunc } - if err = ioctlPerfEventEnable(p.perfEventFD); err != nil { - return fmt.Errorf("couldn't enable perf event %s: %w", p.ProbeIdentificationPair, err) - } - return nil -} - -// detachUprobe - Detaches the probe from its Uprobe -func (p *Probe) detachUprobe() error { - if !p.attachedWithDebugFS { - // nothing to do - return nil - } - - // Write uprobe_events line to remove hook point - return unregisterUprobeEvent(p.prefix(), p.HookFuncName, p.UID, p.attachPID) -} -// registerUprobeEvent - Writes a new Uprobe in uprobe_events with the provided parameters. Call DisableUprobeEvent -// to remove the kprobe. -func registerUprobeEvent(probeType string, funcName, path, UID string, uprobeAttachPID int, offset uint64) (int, error) { - // Generate event name - eventName, err := generateEventName(probeType, funcName, UID, uprobeAttachPID) - if err != nil { - return -1, err - } - - // Write line to uprobe_events, only eventName is tested to max MAX_EVENT_NAME_LEN (linux/kernel/trace/trace.h) - - f, err := tracefs.OpenFile("uprobe_events", os.O_APPEND|os.O_WRONLY, 0666) - if err != nil { - return -1, fmt.Errorf("cannot open uprobe_events: %w", err) - } - defer f.Close() - - cmd := fmt.Sprintf("%s:%s %s:%#x\n", probeType, eventName, path, offset) - if _, err = f.WriteString(cmd); err != nil && !os.IsExist(err) { - return -1, fmt.Errorf("cannot write %q to uprobe_events: %w", cmd, err) - } - - // Retrieve Uprobe ID - uprobeIDFile := fmt.Sprintf("events/uprobes/%s/id", eventName) - uprobeIDBytes, err := tracefs.ReadFile(uprobeIDFile) - if err != nil { - if os.IsNotExist(err) { - return -1, ErrUprobeIDNotExist + var startErr, fallbackErr error + var tl *tracefsLink + if tl, startErr = startFunc(); startErr != nil { + if tl, fallbackErr = fallbackFunc(); fallbackErr != nil { + return errors.Join(startErr, fallbackErr) } - return -1, fmt.Errorf("cannot read uprobe id: %w", err) - } - uprobeID, err := strconv.Atoi(strings.TrimSpace(string(uprobeIDBytes))) - if err != nil { - return -1, fmt.Errorf("invalid uprobe id: %w", err) } - return uprobeID, nil -} - -// unregisterUprobeEvent - Removes a uprobe from uprobe_events -func unregisterUprobeEvent(probeType string, funcName string, UID string, uprobeAttachPID int) error { - // Generate event name - eventName, err := generateEventName(probeType, funcName, UID, uprobeAttachPID) - if err != nil { - return err + if err := attachPerfEvent(tl.perfEventLink, p.program); err != nil { + _ = tl.Close() + return fmt.Errorf("attach %s: %w", p.ProbeIdentificationPair, err) } - return unregisterTraceFSEvent("uprobe_events", eventName) + p.progLink = tl + return nil }