Repository: cilium/ebpf
Pull request #279 (2021-04-10 16:40:22)

link: implement uprobe/uretprobe
Closes https://github.com/cilium/ebpf/issues/277


TODO

- [x]  tests (I've already tested manually (both pmu and tracefs))
- [ ]  adapt uretprobe example - https://github.com/cilium/ebpf/pull/280


mmat11: I've added unit tests; tests on v4.9 are failing because I think uprobes weren't available at all before v4.17:

https://github.com/torvalds/linux/blob/v4.16/Documentation/trace/uprobetracer.rst
https://github.com/torvalds/linux/blob/v4.17/Documentation/trace/uprobetracer.rst
ti-mo: `uprobetracer.txt` was renamed to `uprobetracer.rst` in 4.17, so this is expected. The commit `f3f096cfedf8` that introduced `/uprobe_events` landed in kernel 3.5, so should not be an issue. This test does pass on our 4.14 CI kernel. I think something must've changed in the kernel config defaults between 4.9 and 4.14, since we don't enable uprobes explicitly.

It's probably okay to skip the test if `tracefsProbe()` returns ENOENT there.
mmat11: https://elixir.bootlin.com/linux/v4.9/source/kernel/trace/Kconfig#L448
https://elixir.bootlin.com/linux/v4.19/source/kernel/trace/Kconfig#L496

I think we need to set this explicitly here -> https://github.com/cilium/ci-kernels/blob/master/config
ti-mo: @mmat11 Tracking this in https://github.com/cilium/ci-kernels/issues/11, feel free to add a conditional skip.
lmb: Some last minute drive by comments, looks great!
lmb: > @mmat11 Tracking this in [cilium/ci-kernels#11](https://github.com/cilium/ci-kernels/issues/11), feel free to add a conditional skip.

`BRANCH=uprobes ./run-tests.sh 5.10` should work now.

Patch:
diff --git a/internal/elf.go b/internal/elf.go
index c3f9ea0..54a4313 100644
--- a/internal/elf.go
+++ b/internal/elf.go
@@ -50,3 +50,19 @@ func (se *SafeELFFile) Symbols() (syms []elf.Symbol, err error) {
 	syms, err = se.File.Symbols()
 	return
 }
+
+// DynamicSymbols is the safe version of elf.File.DynamicSymbols.
+func (se *SafeELFFile) DynamicSymbols() (syms []elf.Symbol, err error) {
+	defer func() {
+		r := recover()
+		if r == nil {
+			return
+		}
+
+		syms = nil
+		err = fmt.Errorf("reading ELF dynamic symbols panicked: %s", r)
+	}()
+
+	syms, err = se.File.DynamicSymbols()
+	return
+}
diff --git a/link/kprobe.go b/link/kprobe.go
index 3c1d6bd..70e0f48 100644
--- a/link/kprobe.go
+++ b/link/kprobe.go
@@ -10,6 +10,7 @@ import (
 	"path/filepath"
 	"runtime"
 	"sync"
+	"unsafe"
 
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/internal"
@@ -26,6 +27,47 @@ var (
 	}{}
 )
 
+type probeType uint8
+
+const (
+	kprobeType probeType = iota
+	uprobeType
+)
+
+func (pt probeType) String() string {
+	if pt == kprobeType {
+		return "kprobe"
+	}
+	return "uprobe"
+}
+
+func (pt probeType) EventsPath() string {
+	if pt == kprobeType {
+		return kprobeEventsPath
+	}
+	return uprobeEventsPath
+}
+
+func (pt probeType) PerfEventType(ret bool) perfEventType {
+	if pt == kprobeType {
+		if ret {
+			return kretprobeEvent
+		}
+		return kprobeEvent
+	}
+	if ret {
+		return uretprobeEvent
+	}
+	return uprobeEvent
+}
+
+func (pt probeType) RetprobeBit() (uint64, error) {
+	if pt == kprobeType {
+		return kretprobeBit()
+	}
+	return uretprobeBit()
+}
+
 // Kprobe attaches the given eBPF program to a perf event that fires when the
 // given kernel symbol starts executing. See /proc/kallsyms for available
 // symbols. For example, printk():
@@ -106,39 +148,61 @@ func kprobe(symbol string, prog *ebpf.Program, ret bool) (*perfEvent, error) {
 	return tp, nil
 }
 
-// pmuKprobe opens a perf event based on a Performance Monitoring Unit.
-// Requires at least 4.17 (e12f03d7031a "perf/core: Implement the
-// 'perf_kprobe' PMU").
-// Returns ErrNotSupported if the kernel doesn't support perf_kprobe PMU,
-// or os.ErrNotExist if the given symbol does not exist in the kernel.
+// pmuKprobe opens a perf event based on the kprobe PMU.
+// Returns os.ErrNotExist if the given symbol does not exist in the kernel.
 func pmuKprobe(symbol string, ret bool) (*perfEvent, error) {
+	return pmuProbe(kprobeType, symbol, "", 0, ret)
+}
 
+// pmuProbe opens a perf event based on a Performance Monitoring Unit.
+//
+// Requires at least a 4.17 kernel.
+// e12f03d7031a "perf/core: Implement the 'perf_kprobe' PMU"
+// 33ea4b24277b "perf/core: Implement the 'perf_uprobe' PMU"
+//
+// Returns ErrNotSupported if the kernel doesn't support perf_[k,u]probe PMU
+func pmuProbe(typ probeType, symbol, path string, offset uint64, ret bool) (*perfEvent, error) {
 	// Getting the PMU type will fail if the kernel doesn't support
-	// the perf_kprobe PMU.
-	et, err := getPMUEventType("kprobe")
+	// the perf_[k,u]probe PMU.
+	et, err := getPMUEventType(typ)
 	if err != nil {
 		return nil, err
 	}
 
-	// Create a pointer to a NUL-terminated string for the kernel.
-	sp, err := unsafeStringPtr(symbol)
+	config, err := typ.RetprobeBit()
 	if err != nil {
 		return nil, err
 	}
 
-	var config uint64
-	if ret {
-		retprobeBit, err := kretprobeBit()
+	var (
+		attr unix.PerfEventAttr
+		sp   unsafe.Pointer
+	)
+	switch typ {
+	case kprobeType:
+		// Create a pointer to a NUL-terminated string for the kernel.
+		sp, err := unsafeStringPtr(symbol)
 		if err != nil {
-			return nil, fmt.Errorf("determine kretprobe bit: %w", err)
+			return nil, err
+		}
+
+		attr = unix.PerfEventAttr{
+			Type:   uint32(et),          // PMU event type read from sysfs
+			Ext1:   uint64(uintptr(sp)), // Kernel symbol to trace
+			Config: config,              // Retprobe flag
+		}
+	case uprobeType:
+		sp, err := unsafeStringPtr(path)
+		if err != nil {
+			return nil, err
 		}
-		config |= 1 << retprobeBit
-	}
 
-	attr := unix.PerfEventAttr{
-		Type:   uint32(et),          // PMU event type read from sysfs
-		Ext1:   uint64(uintptr(sp)), // Kernel symbol to trace
-		Config: config,              // Set retprobe for k,uretprobes
+		attr = unix.PerfEventAttr{
+			Type:   uint32(et),              // PMU event type read from sysfs
+			Ext1:   uint64(uintptr(sp)),     // Uprobe path
+			Ext2:   uint64(uintptr(offset)), // Uprobe offset
+			Config: config,                  // Retprobe flag
+		}
 	}
 
 	fd, err := unix.PerfEventOpen(&attr, perfAllThreads, 0, -1, unix.PERF_FLAG_FD_CLOEXEC)
@@ -156,21 +220,27 @@ func pmuKprobe(symbol string, ret bool) (*perfEvent, error) {
 	// Ensure the string pointer is not collected before PerfEventOpen returns.
 	runtime.KeepAlive(sp)
 
-	// Kernel has perf_kprobe PMU available, initialize perf event.
+	// Kernel has perf_[k,u]probe PMU available, initialize perf event.
 	return &perfEvent{
 		fd:    internal.NewFD(uint32(fd)),
 		pmuID: et,
 		name:  symbol,
-		typ:   kprobeEventType(ret),
+		typ:   typ.PerfEventType(ret),
 	}, nil
 }
 
-// tracefsKprobe creates a trace event by writing an entry to <tracefs>/kprobe_events.
-// A new trace event group name is generated on every call to support creating
-// multiple trace events for the same kernel symbol. A perf event is then opened
-// on the newly-created trace event and returned to the caller.
+// tracefsKprobe creates a Kprobe tracefs entry.
 func tracefsKprobe(symbol string, ret bool) (*perfEvent, error) {
+	return tracefsProbe(kprobeType, symbol, "", 0, ret)
+}
 
+// tracefsProbe creates a trace event by writing an entry to <tracefs>/[k,u]probe_events.
+// A new trace event group name is generated on every call to support creating
+// multiple trace events for the same kernel or userspace symbol.
+// Path and offset are only set in the case of uprobe(s) and are used to set
+// the executable/library path on the filesystem and the offset where the probe is inserted.
+// A perf event is then opened on the newly-created trace event and returned to the caller.
+func tracefsProbe(typ probeType, symbol, path string, offset uint64, ret bool) (*perfEvent, error) {
 	// Generate a random string for each trace event we attempt to create.
 	// This value is used as the 'group' token in tracefs to allow creating
 	// multiple kprobe trace events with the same name.
@@ -192,9 +262,9 @@ func tracefsKprobe(symbol string, ret bool) (*perfEvent, error) {
 		return nil, fmt.Errorf("checking trace event %s/%s: %w", group, symbol, err)
 	}
 
-	// Create the kprobe trace event using tracefs.
-	if err := createTraceFSKprobeEvent(group, symbol, ret); err != nil {
-		return nil, fmt.Errorf("creating kprobe event on tracefs: %w", err)
+	// Create the [k,u]probe trace event using tracefs.
+	if err := createTraceFSProbeEvent(typ, group, symbol, path, offset, ret); err != nil {
+		return nil, fmt.Errorf("creating probe entry on tracefs: %w", err)
 	}
 
 	// Get the newly-created trace event's id.
@@ -214,35 +284,53 @@ func tracefsKprobe(symbol string, ret bool) (*perfEvent, error) {
 		group:     group,
 		name:      symbol,
 		tracefsID: tid,
-		typ:       kprobeEventType(ret),
+		typ:       typ.PerfEventType(ret),
 	}, nil
 }
 
-// createTraceFSKprobeEvent creates a new ephemeral trace event by writing to
-// <tracefs>/kprobe_events. Returns ErrNotSupported if symbol is not a valid
+// createTraceFSProbeEvent creates a new ephemeral trace event by writing to
+// <tracefs>/[k,u]probe_events. Returns ErrNotSupported if symbol is not a valid
 // kernel symbol, or if it is not traceable with kprobes.
-func createTraceFSKprobeEvent(group, symbol string, ret bool) error {
+func createTraceFSProbeEvent(typ probeType, group, symbol, path string, offset uint64, ret bool) error {
 	// Open the kprobe_events file in tracefs.
-	f, err := os.OpenFile(kprobeEventsPath, os.O_APPEND|os.O_WRONLY, 0666)
+	f, err := os.OpenFile(typ.EventsPath(), os.O_APPEND|os.O_WRONLY, 0666)
 	if err != nil {
-		return fmt.Errorf("error opening kprobe_events: %w", err)
+		return fmt.Errorf("error opening '%s': %w", typ.EventsPath(), err)
 	}
 	defer f.Close()
 
-	// The kprobe_events syntax is as follows (see Documentation/trace/kprobetrace.txt):
-	// p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS] : Set a probe
-	// r[MAXACTIVE][:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS] : Set a return probe
-	// -:[GRP/]EVENT                                        : Clear a probe
-	//
-	// Some examples:
-	// r:ebpf_1234/r_my_kretprobe nf_conntrack_destroy
-	// p:ebpf_5678/p_my_kprobe __x64_sys_execve
-	//
-	// Leaving the kretprobe's MAXACTIVE set to 0 (or absent) will make the
-	// kernel default to NR_CPUS. This is desired in most eBPF cases since
-	// subsampling or rate limiting logic can be more accurately implemented in
-	// the eBPF program itself. See Documentation/kprobes.txt for more details.
-	pe := fmt.Sprintf("%s:%s/%s %s", kprobePrefix(ret), group, symbol, symbol)
+	var pe string
+	switch typ {
+	case kprobeType:
+		// The kprobe_events syntax is as follows (see Documentation/trace/kprobetrace.txt):
+		// p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS] : Set a probe
+		// r[MAXACTIVE][:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS] : Set a return probe
+		// -:[GRP/]EVENT                                        : Clear a probe
+		//
+		// Some examples:
+		// r:ebpf_1234/r_my_kretprobe nf_conntrack_destroy
+		// p:ebpf_5678/p_my_kprobe __x64_sys_execve
+		//
+		// Leaving the kretprobe's MAXACTIVE set to 0 (or absent) will make the
+		// kernel default to NR_CPUS. This is desired in most eBPF cases since
+		// subsampling or rate limiting logic can be more accurately implemented in
+		// the eBPF program itself.
+		// See Documentation/kprobes.txt for more details.
+		pe = fmt.Sprintf("%s:%s/%s %s", probePrefix(ret), group, symbol, symbol)
+	case uprobeType:
+		// The uprobe_events syntax is as follows:
+		// p[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a probe
+		// r[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a return probe
+		// -:[GRP/]EVENT                           : Clear a probe
+		//
+		// Some examples:
+		// r:ebpf_1234/readline /bin/bash:0x12345
+		// p:ebpf_5678/main_mySymbol /bin/mybin:0x12345
+		//
+		// See Documentation/trace/uprobetracer.txt for more details.
+		pathOffset := uprobePathOffset(path, offset)
+		pe = fmt.Sprintf("%s:%s/%s %s", probePrefix(ret), group, symbol, pathOffset)
+	}
 	_, err = f.WriteString(pe)
 	// Since commit 97c753e62e6c, ENOENT is correctly returned instead of EINVAL
 	// when trying to create a kretprobe for a missing symbol. Make sure ENOENT
@@ -251,26 +339,26 @@ func createTraceFSKprobeEvent(group, symbol string, ret bool) error {
 		return fmt.Errorf("kernel symbol %s not found: %w", symbol, os.ErrNotExist)
 	}
 	if err != nil {
-		return fmt.Errorf("writing '%s' to kprobe_events: %w", pe, err)
+		return fmt.Errorf("writing '%s' to '%s': %w", pe, typ.EventsPath(), err)
 	}
 
 	return nil
 }
 
-// closeTraceFSKprobeEvent removes the kprobe with the given group, symbol and kind
-// from <tracefs>/kprobe_events.
-func closeTraceFSKprobeEvent(group, symbol string) error {
-	f, err := os.OpenFile(kprobeEventsPath, os.O_APPEND|os.O_WRONLY, 0666)
+// closeTraceFSProbeEvent removes the [k,u]probe with the given type, group and symbol
+// from <tracefs>/[k,u]probe_events.
+func closeTraceFSProbeEvent(typ probeType, group, symbol string) error {
+	f, err := os.OpenFile(typ.EventsPath(), os.O_APPEND|os.O_WRONLY, 0666)
 	if err != nil {
-		return fmt.Errorf("error opening kprobe_events: %w", err)
+		return fmt.Errorf("error opening %s: %w", typ.EventsPath(), err)
 	}
 	defer f.Close()
 
-	// See kprobe_events syntax above. Kprobe type does not need to be specified
+	// See [k,u]probe_events syntax above. The probe type does not need to be specified
 	// for removals.
 	pe := fmt.Sprintf("-:%s/%s", group, symbol)
 	if _, err = f.WriteString(pe); err != nil {
-		return fmt.Errorf("writing '%s' to kprobe_events: %w", pe, err)
+		return fmt.Errorf("writing '%s' to '%s': %w", pe, typ.EventsPath(), err)
 	}
 
 	return nil
@@ -298,7 +386,7 @@ func randomGroup(prefix string) (string, error) {
 	return group, nil
 }
 
-func kprobePrefix(ret bool) string {
+func probePrefix(ret bool) string {
 	if ret {
 		return "r"
 	}
@@ -307,8 +395,8 @@ func kprobePrefix(ret bool) string {
 
 // determineRetprobeBit reads a Performance Monitoring Unit's retprobe bit
 // from /sys/bus/event_source/devices/<pmu>/format/retprobe.
-func determineRetprobeBit(pmu string) (uint64, error) {
-	p := filepath.Join("/sys/bus/event_source/devices/", pmu, "/format/retprobe")
+func determineRetprobeBit(typ probeType) (uint64, error) {
+	p := filepath.Join("/sys/bus/event_source/devices/", typ.String(), "/format/retprobe")
 
 	data, err := ioutil.ReadFile(p)
 	if err != nil {
@@ -329,14 +417,7 @@ func determineRetprobeBit(pmu string) (uint64, error) {
 
 func kretprobeBit() (uint64, error) {
 	kprobeRetprobeBit.once.Do(func() {
-		kprobeRetprobeBit.value, kprobeRetprobeBit.err = determineRetprobeBit("kprobe")
+		kprobeRetprobeBit.value, kprobeRetprobeBit.err = determineRetprobeBit(kprobeType)
 	})
 	return kprobeRetprobeBit.value, kprobeRetprobeBit.err
 }
-
-func kprobeEventType(ret bool) perfEventType {
-	if ret {
-		return kretprobeEvent
-	}
-	return kprobeEvent
-}
diff --git a/link/perf_event.go b/link/perf_event.go
index 1855d1a..10a2c88 100644
--- a/link/perf_event.go
+++ b/link/perf_event.go
@@ -31,6 +31,10 @@ import (
 //   exported kernel symbols. kprobe-based (tracefs) trace events can be
 //   created system-wide by writing to the <tracefs>/kprobe_events file, or
 //   they can be scoped to the current process by creating PMU perf events.
+// - u(ret)probe: Ephemeral trace events based on user provides ELF binaries
+//   and offsets. uprobe-based (tracefs) trace events can be
+//   created system-wide by writing to the <tracefs>/uprobe_events file, or
+//   they can be scoped to the current process by creating PMU perf events.
 // - perf event: An object instantiated based on an existing trace event or
 //   kernel symbol. Referred to by fd in userspace.
 //   Exactly one eBPF program can be attached to a perf event. Multiple perf
@@ -58,6 +62,8 @@ const (
 	tracepointEvent perfEventType = iota
 	kprobeEvent
 	kretprobeEvent
+	uprobeEvent
+	uretprobeEvent
 )
 
 // A perfEvent represents a perf event kernel object. Exactly one eBPF program
@@ -126,9 +132,14 @@ func (pe *perfEvent) Close() error {
 
 	switch pe.typ {
 	case kprobeEvent, kretprobeEvent:
-		// For kprobes created using tracefs, clean up the <tracefs>/kprobe_events entry.
+		// Clean up kprobe tracefs entry.
+		if pe.tracefsID != 0 {
+			return closeTraceFSProbeEvent(kprobeType, pe.group, pe.name)
+		}
+	case uprobeEvent, uretprobeEvent:
+		// Clean up uprobe tracefs entry.
 		if pe.tracefsID != 0 {
-			return closeTraceFSKprobeEvent(pe.group, pe.name)
+			return closeTraceFSProbeEvent(uprobeType, pe.group, pe.name)
 		}
 	case tracepointEvent:
 		// Tracepoint trace events don't hold any extra resources.
@@ -152,7 +163,7 @@ func (pe *perfEvent) attach(prog *ebpf.Program) error {
 		return fmt.Errorf("invalid program: %w", internal.ErrClosedFd)
 	}
 	switch pe.typ {
-	case kprobeEvent, kretprobeEvent:
+	case kprobeEvent, kretprobeEvent, uprobeEvent, uretprobeEvent:
 		if t := prog.Type(); t != ebpf.Kprobe {
 			return fmt.Errorf("invalid program type (expected %s): %s", ebpf.Kprobe, t)
 		}
@@ -208,20 +219,20 @@ func getTraceEventID(group, name string) (uint64, error) {
 
 // getPMUEventType reads a Performance Monitoring Unit's type (numeric identifier)
 // from /sys/bus/event_source/devices/<pmu>/type.
-func getPMUEventType(pmu string) (uint64, error) {
-	et, err := uint64FromFile("/sys/bus/event_source/devices", pmu, "type")
+func getPMUEventType(typ probeType) (uint64, error) {
+	et, err := uint64FromFile("/sys/bus/event_source/devices", typ.String(), "type")
 	if errors.Is(err, ErrNotSupported) {
-		return 0, fmt.Errorf("pmu type %s: %w", pmu, ErrNotSupported)
+		return 0, fmt.Errorf("pmu type %s: %w", typ, ErrNotSupported)
 	}
 	if err != nil {
-		return 0, fmt.Errorf("reading pmu type %s: %w", pmu, err)
+		return 0, fmt.Errorf("reading pmu type %s: %w", typ, err)
 	}
 
 	return et, nil
 }
 
 // openTracepointPerfEvent opens a tracepoint-type perf event. System-wide
-// kprobes created by writing to <tracefs>/kprobe_events are tracepoints
+// [k,u]probes created by writing to <tracefs>/[k,u]probe_events are tracepoints
 // behind the scenes, and can be attached to using these perf events.
 func openTracepointPerfEvent(tid uint64) (*internal.FD, error) {
 	attr := unix.PerfEventAttr{
diff --git a/link/uprobe.go b/link/uprobe.go
new file mode 100644
index 0000000..6f0a0a7
--- /dev/null
+++ b/link/uprobe.go
@@ -0,0 +1,195 @@
+package link
+
+import (
+	"debug/elf"
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"regexp"
+	"sync"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/internal"
+)
+
+var (
+	uprobeEventsPath = filepath.Join(tracefsPath, "uprobe_events")
+
+	// rgxUprobeSymbol is used to strip invalid characters from the uprobe symbol
+	// as they are not allowed to be used as the EVENT token in tracefs.
+	rgxUprobeSymbol = regexp.MustCompile("[^a-zA-Z0-9]+")
+
+	uprobeRetprobeBit = struct {
+		once  sync.Once
+		value uint64
+		err   error
+	}{}
+)
+
+// Executable defines an executable program on the filesystem.
+type Executable struct {
+	// Path of the executable on the filesystem.
+	path string
+	// Parsed ELF symbols and dynamic symbols.
+	symbols map[string]elf.Symbol
+}
+
+// To open a new Executable, use:
+//
+//	OpenExecutable("/bin/bash")
+//
+// The returned value can then be used to open Uprobe(s).
+func OpenExecutable(path string) (*Executable, error) {
+	if path == "" {
+		return nil, fmt.Errorf("path cannot be empty")
+	}
+
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, fmt.Errorf("open file '%s': %w", path, err)
+	}
+	defer f.Close()
+
+	se, err := internal.NewSafeELFFile(f)
+	if err != nil {
+		return nil, fmt.Errorf("parse ELF file: %w", err)
+	}
+
+	var ex = Executable{
+		path:    path,
+		symbols: make(map[string]elf.Symbol),
+	}
+	if err := ex.addSymbols(se.Symbols); err != nil {
+		return nil, err
+	}
+
+	if err := ex.addSymbols(se.DynamicSymbols); err != nil {
+		return nil, err
+	}
+
+	return &ex, nil
+}
+
+func (ex *Executable) addSymbols(f func() ([]elf.Symbol, error)) error {
+	// elf.Symbols and elf.DynamicSymbols return ErrNoSymbols if the section is not found.
+	syms, err := f()
+	if err != nil && !errors.Is(err, elf.ErrNoSymbols) {
+		return err
+	}
+	for _, s := range syms {
+		ex.symbols[s.Name] = s
+	}
+	return nil
+}
+
+func (ex *Executable) symbol(symbol string) (*elf.Symbol, error) {
+	if s, ok := ex.symbols[symbol]; ok {
+		return &s, nil
+	}
+	return nil, fmt.Errorf("symbol %s not found", symbol)
+}
+
+// Uprobe attaches the given eBPF program to a perf event that fires when the
+// given symbol starts executing in the given Executable.
+// For example, /bin/bash::readline():
+//
+//  ex, _ = OpenExecutable("/bin/bash")
+//  ex.Uprobe("readline", prog)
+//
+// The resulting Link must be Closed during program shutdown to avoid leaking
+// system resources.
+func (ex *Executable) Uprobe(symbol string, prog *ebpf.Program) (Link, error) {
+	u, err := ex.uprobe(symbol, prog, false)
+	if err != nil {
+		return nil, err
+	}
+
+	err = u.attach(prog)
+	if err != nil {
+		u.Close()
+		return nil, err
+	}
+
+	return u, nil
+}
+
+// Uretprobe attaches the given eBPF program to a perf event that fires right
+// before the given symbol exits.
+//
+// The resulting Link must be Closed during program shutdown to avoid leaking
+// system resources.
+func (ex *Executable) Uretprobe(symbol string, prog *ebpf.Program) (Link, error) {
+	u, err := ex.uprobe(symbol, prog, true)
+	if err != nil {
+		return nil, err
+	}
+
+	err = u.attach(prog)
+	if err != nil {
+		u.Close()
+		return nil, err
+	}
+
+	return u, nil
+}
+
+// uprobe opens a perf event for the given binary/symbol and attaches prog to it.
+// If ret is true, create a uretprobe.
+func (ex *Executable) uprobe(symbol string, prog *ebpf.Program, ret bool) (*perfEvent, error) {
+	if prog == nil {
+		return nil, fmt.Errorf("prog cannot be nil: %w", errInvalidInput)
+	}
+	if prog.Type() != ebpf.Kprobe {
+		return nil, fmt.Errorf("eBPF program type %s is not Kprobe: %w", prog.Type(), errInvalidInput)
+	}
+
+	sym, err := ex.symbol(symbol)
+	if err != nil {
+		return nil, fmt.Errorf("symbol '%s' not found in '%s': %w", symbol, ex.path, err)
+	}
+
+	// Use uprobe PMU if the kernel has it available.
+	tp, err := pmuUprobe(sym.Name, ex.path, sym.Value, ret)
+	if err == nil {
+		return tp, nil
+	}
+	if err != nil && !errors.Is(err, ErrNotSupported) {
+		return nil, fmt.Errorf("creating perf_uprobe PMU: %w", err)
+	}
+
+	// Use tracefs if uprobe PMU is missing.
+	tp, err = tracefsUprobe(uprobeSanitizedSymbol(sym.Name), ex.path, sym.Value, ret)
+	if err != nil {
+		return nil, fmt.Errorf("creating trace event '%s:%s' in tracefs: %w", ex.path, symbol, err)
+	}
+
+	return tp, nil
+}
+
+// pmuUprobe opens a perf event based on the uprobe PMU.
+func pmuUprobe(symbol, path string, offset uint64, ret bool) (*perfEvent, error) {
+	return pmuProbe(uprobeType, symbol, path, offset, ret)
+}
+
+// tracefsUprobe creates a Uprobe tracefs entry.
+func tracefsUprobe(symbol, path string, offset uint64, ret bool) (*perfEvent, error) {
+	return tracefsProbe(uprobeType, symbol, path, offset, ret)
+}
+
+// uprobeSanitizedSymbol replaces every invalid characted for the tracefs api with an underscore.
+func uprobeSanitizedSymbol(symbol string) string {
+	return rgxUprobeSymbol.ReplaceAllString(symbol, "_")
+}
+
+// uprobePathOffset creates the PATH:OFFSET token for the tracefs api.
+func uprobePathOffset(path string, offset uint64) string {
+	return fmt.Sprintf("%s:%#x", path, offset)
+}
+
+func uretprobeBit() (uint64, error) {
+	uprobeRetprobeBit.once.Do(func() {
+		uprobeRetprobeBit.value, uprobeRetprobeBit.err = determineRetprobeBit(uprobeType)
+	})
+	return uprobeRetprobeBit.value, uprobeRetprobeBit.err
+}


Test patch:
diff --git a/link/kprobe_test.go b/link/kprobe_test.go
index 255e3d7..595c24d 100644
--- a/link/kprobe_test.go
+++ b/link/kprobe_test.go
@@ -182,12 +182,12 @@ func TestKprobeTraceFS(t *testing.T) {
 	c.Assert(k1.tracefsID, qt.Not(qt.CmpEquals()), k2.tracefsID)
 
 	// Write a k(ret)probe event for a non-existing symbol.
-	err = createTraceFSKprobeEvent("testgroup", "bogus", false)
+	err = createTraceFSProbeEvent(kprobeType, "testgroup", "bogus", "", 0, false)
 	c.Assert(errors.Is(err, os.ErrNotExist), qt.IsTrue, qt.Commentf("got error: %s", err))
 
 	// A kernel bug was fixed in 97c753e62e6c where EINVAL was returned instead
 	// of ENOENT, but only for kretprobes.
-	err = createTraceFSKprobeEvent("testgroup", "bogus", true)
+	err = createTraceFSProbeEvent(kprobeType, "testgroup", "bogus", "", 0, true)
 	c.Assert(errors.Is(err, os.ErrNotExist), qt.IsTrue, qt.Commentf("got error: %s", err))
 }
 
@@ -221,34 +221,33 @@ func TestKprobeCreateTraceFS(t *testing.T) {
 
 	// Tee up cleanups in case any of the Asserts abort the function.
 	defer func() {
-		_ = closeTraceFSKprobeEvent(pg, symbol)
-		_ = closeTraceFSKprobeEvent(rg, symbol)
+		_ = closeTraceFSProbeEvent(kprobeType, pg, symbol)
+		_ = closeTraceFSProbeEvent(kprobeType, rg, symbol)
 	}()
 
 	// Create a kprobe.
-	err := createTraceFSKprobeEvent(pg, symbol, false)
+	err := createTraceFSProbeEvent(kprobeType, pg, symbol, "", 0, false)
 	c.Assert(err, qt.IsNil)
 
 	// Attempt to create an identical kprobe using tracefs,
 	// expect it to fail with os.ErrExist.
-	err = createTraceFSKprobeEvent(pg, symbol, false)
+	err = createTraceFSProbeEvent(kprobeType, pg, symbol, "", 0, false)
 	c.Assert(errors.Is(err, os.ErrExist), qt.IsTrue,
 		qt.Commentf("expected consecutive kprobe creation to contain os.ErrExist, got: %v", err))
 
 	// Expect a successful close of the kprobe.
-	c.Assert(closeTraceFSKprobeEvent(pg, symbol), qt.IsNil)
+	c.Assert(closeTraceFSProbeEvent(kprobeType, pg, symbol), qt.IsNil)
 
 	// Same test for a kretprobe.
-	err = createTraceFSKprobeEvent(rg, symbol, true)
+	err = createTraceFSProbeEvent(kprobeType, rg, symbol, "", 0, true)
 	c.Assert(err, qt.IsNil)
 
-	err = createTraceFSKprobeEvent(rg, symbol, true)
+	err = createTraceFSProbeEvent(kprobeType, rg, symbol, "", 0, true)
 	c.Assert(os.IsExist(err), qt.IsFalse,
 		qt.Commentf("expected consecutive kretprobe creation to contain os.ErrExist, got: %v", err))
 
 	// Expect a successful close of the kretprobe.
-	c.Assert(closeTraceFSKprobeEvent(rg, symbol), qt.IsNil)
-
+	c.Assert(closeTraceFSProbeEvent(kprobeType, rg, symbol), qt.IsNil)
 }
 
 func TestKprobeTraceFSGroup(t *testing.T) {
@@ -276,10 +275,11 @@ func TestDetermineRetprobeBit(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "4.17", "perf_kprobe PMU")
 	c := qt.New(t)
 
-	rp, err := kretprobeBit()
+	rpk, err := kretprobeBit()
 	c.Assert(err, qt.IsNil)
-	c.Assert(rp, qt.Equals, uint64(0))
+	c.Assert(rpk, qt.Equals, uint64(0))
 
-	_, err = determineRetprobeBit("bogus")
-	c.Assert(errors.Is(err, os.ErrNotExist), qt.IsTrue, qt.Commentf("got error: %s", err))
+	rpu, err := uretprobeBit()
+	c.Assert(err, qt.IsNil)
+	c.Assert(rpu, qt.Equals, uint64(0))
 }
diff --git a/link/perf_event_test.go b/link/perf_event_test.go
index 3f7d252..7e6b77f 100644
--- a/link/perf_event_test.go
+++ b/link/perf_event_test.go
@@ -14,7 +14,11 @@ func TestTraceEventTypePMU(t *testing.T) {
 
 	c := qt.New(t)
 
-	et, err := getPMUEventType("kprobe")
+	et, err := getPMUEventType(kprobeType)
+	c.Assert(err, qt.IsNil)
+	c.Assert(et, qt.Not(qt.Equals), 0)
+
+	et, err = getPMUEventType(uprobeType)
 	c.Assert(err, qt.IsNil)
 	c.Assert(et, qt.Not(qt.Equals), 0)
 }
diff --git a/link/uprobe_test.go b/link/uprobe_test.go
new file mode 100644
index 0000000..f4d3612
--- /dev/null
+++ b/link/uprobe_test.go
@@ -0,0 +1,249 @@
+package link
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/internal/testutils"
+)
+
+var bashEx, _ = OpenExecutable("/bin/bash")
+
+func TestExecutable(t *testing.T) {
+	_, err := OpenExecutable("")
+	if err == nil {
+		t.Fatal("create executable: expected error on empty path")
+	}
+
+	if bashEx.path != "/bin/bash" {
+		t.Fatalf("create executable: unexpected path '%s'", bashEx.path)
+	}
+
+	sym, err := bashEx.symbol("readline")
+	if err != nil {
+		t.Fatalf("find symbol: %v", err)
+	}
+	if sym.Name != "readline" {
+		t.Fatalf("find symbol: unexpected symbol '%s'", sym.Name)
+	}
+
+	_, err = bashEx.symbol("bogus")
+	if err == nil {
+		t.Fatal("find symbol: expected error")
+	}
+}
+
+func TestUprobe(t *testing.T) {
+	c := qt.New(t)
+
+	prog, err := ebpf.NewProgram(&kprobeSpec)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer prog.Close()
+
+	up, err := bashEx.Uprobe("readline", prog)
+	c.Assert(err, qt.IsNil)
+	defer up.Close()
+
+	testLink(t, up, testLinkOptions{
+		prog: prog,
+	})
+}
+
+func TestUretprobe(t *testing.T) {
+	c := qt.New(t)
+
+	prog, err := ebpf.NewProgram(&kprobeSpec)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer prog.Close()
+
+	up, err := bashEx.Uretprobe("readline", prog)
+	c.Assert(err, qt.IsNil)
+	defer up.Close()
+
+	testLink(t, up, testLinkOptions{
+		prog: prog,
+	})
+}
+
+// Test u(ret)probe creation using perf_uprobe PMU.
+func TestUprobeCreatePMU(t *testing.T) {
+	// Requires at least 4.17 (e12f03d7031a "perf/core: Implement the 'perf_kprobe' PMU")
+	testutils.SkipOnOldKernel(t, "4.17", "perf_kprobe PMU")
+
+	c := qt.New(t)
+
+	// Fetch the elf.Symbol from the /bin/bash Executable already defined.
+	sym, err := bashEx.symbol("readline")
+	c.Assert(err, qt.IsNil)
+
+	// uprobe PMU /bin/bash::readline()
+	pu, err := pmuUprobe(sym.Name, bashEx.path, sym.Value, false)
+	c.Assert(err, qt.IsNil)
+	defer pu.Close()
+
+	c.Assert(pu.typ, qt.Equals, uprobeEvent)
+
+	// uretprobe PMU /bin/bash::readline()
+	pr, err := pmuUprobe(sym.Name, bashEx.path, sym.Value, true)
+	c.Assert(err, qt.IsNil)
+	defer pr.Close()
+
+	c.Assert(pr.typ, qt.Equals, uretprobeEvent)
+}
+
+// Test fallback behaviour on kernels without perf_uprobe PMU available.
+func TestUprobePMUUnavailable(t *testing.T) {
+	c := qt.New(t)
+
+	// Fetch the elf.Symbol from the /bin/bash Executable already defined.
+	sym, err := bashEx.symbol("readline")
+	c.Assert(err, qt.IsNil)
+
+	pk, err := pmuUprobe(sym.Name, bashEx.path, sym.Value, false)
+	if err == nil {
+		pk.Close()
+		t.Skipf("Kernel supports perf_uprobe PMU, not asserting error.")
+	}
+
+	// Expect ErrNotSupported.
+	c.Assert(errors.Is(err, ErrNotSupported), qt.IsTrue, qt.Commentf("got error: %s", err))
+}
+
+// Test tracefs u(ret)probe creation on all kernel versions.
+func TestUprobeTraceFS(t *testing.T) {
+	c := qt.New(t)
+
+	// Fetch the elf.Symbol from the /bin/bash Executable already defined.
+	sym, err := bashEx.symbol("readline")
+	c.Assert(err, qt.IsNil)
+
+	// Sanitize the symbol in order to be used in tracefs API.
+	ssym := uprobeSanitizedSymbol(sym.Name)
+
+	// Open and close tracefs u(ret)probes, checking all errors.
+	up, err := tracefsUprobe(ssym, bashEx.path, sym.Value, false)
+	c.Assert(err, qt.IsNil)
+	c.Assert(up.Close(), qt.IsNil)
+	c.Assert(up.typ, qt.Equals, uprobeEvent)
+
+	up, err = tracefsUprobe(ssym, bashEx.path, sym.Value, true)
+	c.Assert(err, qt.IsNil)
+	c.Assert(up.Close(), qt.IsNil)
+	c.Assert(up.typ, qt.Equals, uretprobeEvent)
+
+	// Create two identical trace events, ensure their IDs differ.
+	u1, err := tracefsUprobe(ssym, bashEx.path, sym.Value, false)
+	c.Assert(err, qt.IsNil)
+	defer u1.Close()
+	c.Assert(u1.tracefsID, qt.Not(qt.Equals), 0)
+
+	u2, err := tracefsUprobe(ssym, bashEx.path, sym.Value, false)
+	c.Assert(err, qt.IsNil)
+	defer u2.Close()
+	c.Assert(u2.tracefsID, qt.Not(qt.Equals), 0)
+
+	// Compare the uprobes' tracefs IDs.
+	c.Assert(u1.tracefsID, qt.Not(qt.CmpEquals()), u2.tracefsID)
+}
+
+// Test u(ret)probe creation writing directly to <tracefs>/uprobe_events.
+// Only runs on 5.0 and over. Earlier versions ignored writes of duplicate
+// events, while 5.0 started returning -EEXIST when a uprobe event already
+// exists.
+func TestUprobeCreateTraceFS(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "5.0", "<tracefs>/uprobe_events doesn't reject duplicate events")
+
+	c := qt.New(t)
+
+	// Fetch the elf.Symbol from the /bin/bash Executable already defined.
+	sym, err := bashEx.symbol("readline")
+	c.Assert(err, qt.IsNil)
+
+	// Sanitize the symbol in order to be used in tracefs API.
+	ssym := uprobeSanitizedSymbol(sym.Name)
+
+	pg, _ := randomGroup("ebpftest")
+	rg, _ := randomGroup("ebpftest")
+
+	// Tee up cleanups in case any of the Asserts abort the function.
+	defer func() {
+		_ = closeTraceFSProbeEvent(uprobeType, pg, ssym)
+		_ = closeTraceFSProbeEvent(uprobeType, rg, ssym)
+	}()
+
+	// Create a uprobe.
+	err = createTraceFSProbeEvent(uprobeType, pg, ssym, bashEx.path, sym.Value, false)
+	c.Assert(err, qt.IsNil)
+
+	// Attempt to create an identical uprobe using tracefs,
+	// expect it to fail with os.ErrExist.
+	err = createTraceFSProbeEvent(uprobeType, pg, ssym, bashEx.path, sym.Value, false)
+	c.Assert(errors.Is(err, os.ErrExist), qt.IsTrue,
+		qt.Commentf("expected consecutive uprobe creation to contain os.ErrExist, got: %v", err))
+
+	// Expect a successful close of the kprobe.
+	c.Assert(closeTraceFSProbeEvent(uprobeType, pg, ssym), qt.IsNil)
+
+	// Same test for a kretprobe.
+	err = createTraceFSProbeEvent(uprobeType, rg, ssym, bashEx.path, sym.Value, true)
+	c.Assert(err, qt.IsNil)
+
+	err = createTraceFSProbeEvent(uprobeType, rg, ssym, bashEx.path, sym.Value, true)
+	c.Assert(os.IsExist(err), qt.IsFalse,
+		qt.Commentf("expected consecutive uretprobe creation to contain os.ErrExist, got: %v", err))
+
+	// Expect a successful close of the uretprobe.
+	c.Assert(closeTraceFSProbeEvent(uprobeType, rg, ssym), qt.IsNil)
+}
+
+func TestUprobeSanitizedSymbol(t *testing.T) {
+	var tests = []struct {
+		symbol   string
+		expected string
+	}{
+		{"readline", "readline"},
+		{"main.Func", "main_Func"},
+		{"a.....a", "a_a"},
+		{"./;'{}[]a", "_a"},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprint(i), func(t *testing.T) {
+			sanitized := uprobeSanitizedSymbol(tt.symbol)
+			if tt.expected != sanitized {
+				t.Errorf("Expected sanitized symbol to be '%s', got '%s'", tt.expected, sanitized)
+			}
+		})
+	}
+}
+
+func TestUprobePathOffset(t *testing.T) {
+	var tests = []struct {
+		path     string
+		offset   uint64
+		expected string
+	}{
+		{"/bin/bash", 0, "/bin/bash:0x0"},
+		{"/bin/bash", 1, "/bin/bash:0x1"},
+		{"/bin/bash", 65535, "/bin/bash:0xffff"},
+		{"/bin/bash", 65536, "/bin/bash:0x10000"},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprint(i), func(t *testing.T) {
+			po := uprobePathOffset(tt.path, tt.offset)
+			if tt.expected != po {
+				t.Errorf("Expected path:offset to be '%s', got '%s'", tt.expected, po)
+			}
+		})
+	}
+}