github.com/kubeshark/ebpf@v0.9.2/link/uprobe.go (about)

     1  package link
     2  
     3  import (
     4  	"debug/elf"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/kubeshark/ebpf"
    13  	"github.com/kubeshark/ebpf/internal"
    14  )
    15  
    16  var (
    17  	uprobeEventsPath = filepath.Join(tracefsPath, "uprobe_events")
    18  
    19  	uprobeRetprobeBit = struct {
    20  		once  sync.Once
    21  		value uint64
    22  		err   error
    23  	}{}
    24  
    25  	uprobeRefCtrOffsetPMUPath = "/sys/bus/event_source/devices/uprobe/format/ref_ctr_offset"
    26  	// elixir.bootlin.com/linux/v5.15-rc7/source/kernel/events/core.c#L9799
    27  	uprobeRefCtrOffsetShift = 32
    28  	haveRefCtrOffsetPMU     = internal.FeatureTest("RefCtrOffsetPMU", "4.20", func() error {
    29  		_, err := os.Stat(uprobeRefCtrOffsetPMUPath)
    30  		if err != nil {
    31  			return internal.ErrNotSupported
    32  		}
    33  		return nil
    34  	})
    35  
    36  	// ErrNoSymbol indicates that the given symbol was not found
    37  	// in the ELF symbols table.
    38  	ErrNoSymbol = errors.New("not found")
    39  )
    40  
    41  // Executable defines an executable program on the filesystem.
    42  type Executable struct {
    43  	// Path of the executable on the filesystem.
    44  	path string
    45  	// Parsed ELF symbols and dynamic symbols offsets.
    46  	offsets map[string]uint64
    47  }
    48  
    49  // UprobeOptions defines additional parameters that will be used
    50  // when loading Uprobes.
    51  type UprobeOptions struct {
    52  	// Symbol offset. Must be provided in case of external symbols (shared libs).
    53  	// If set, overrides the offset eventually parsed from the executable.
    54  	Offset uint64
    55  	// The offset relative to given symbol. Useful when tracing an arbitrary point
    56  	// inside the frame of given symbol and eliminates the need of recalculating
    57  	// the absolute offset.
    58  	RelativeOffset uint64
    59  	// Only set the uprobe on the given process ID. Useful when tracing
    60  	// shared library calls or programs that have many running instances.
    61  	PID int
    62  	// Automatically manage SDT reference counts (semaphores).
    63  	//
    64  	// If this field is set, the Kernel will increment/decrement the
    65  	// semaphore located in the process memory at the provided address on
    66  	// probe attach/detach.
    67  	//
    68  	// See also:
    69  	// sourceware.org/systemtap/wiki/UserSpaceProbeImplementation (Semaphore Handling)
    70  	// github.com/torvalds/linux/commit/1cc33161a83d
    71  	// github.com/torvalds/linux/commit/a6ca88b241d5
    72  	RefCtrOffset uint64
    73  	// Arbitrary value that can be fetched from an eBPF program
    74  	// via `bpf_get_attach_cookie()`.
    75  	//
    76  	// Needs kernel 5.15+.
    77  	Cookie uint64
    78  }
    79  
    80  // To open a new Executable, use:
    81  //
    82  //  OpenExecutable("/bin/bash")
    83  //
    84  // The returned value can then be used to open Uprobe(s).
    85  func OpenExecutable(path string) (*Executable, error) {
    86  	if path == "" {
    87  		return nil, fmt.Errorf("path cannot be empty")
    88  	}
    89  
    90  	f, err := os.Open(path)
    91  	if err != nil {
    92  		return nil, fmt.Errorf("open file '%s': %w", path, err)
    93  	}
    94  	defer f.Close()
    95  
    96  	se, err := internal.NewSafeELFFile(f)
    97  	if err != nil {
    98  		return nil, fmt.Errorf("parse ELF file: %w", err)
    99  	}
   100  
   101  	if se.Type != elf.ET_EXEC && se.Type != elf.ET_DYN {
   102  		// ELF is not an executable or a shared object.
   103  		return nil, errors.New("the given file is not an executable or a shared object")
   104  	}
   105  
   106  	ex := Executable{
   107  		path:    path,
   108  		offsets: make(map[string]uint64),
   109  	}
   110  
   111  	if err := ex.load(se); err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	return &ex, nil
   116  }
   117  
   118  func (ex *Executable) load(f *internal.SafeELFFile) error {
   119  	syms, err := f.Symbols()
   120  	if err != nil && !errors.Is(err, elf.ErrNoSymbols) {
   121  		return err
   122  	}
   123  
   124  	dynsyms, err := f.DynamicSymbols()
   125  	if err != nil && !errors.Is(err, elf.ErrNoSymbols) {
   126  		return err
   127  	}
   128  
   129  	syms = append(syms, dynsyms...)
   130  
   131  	for _, s := range syms {
   132  		if elf.ST_TYPE(s.Info) != elf.STT_FUNC {
   133  			// Symbol not associated with a function or other executable code.
   134  			continue
   135  		}
   136  
   137  		off := s.Value
   138  
   139  		// Loop over ELF segments.
   140  		for _, prog := range f.Progs {
   141  			// Skip uninteresting segments.
   142  			if prog.Type != elf.PT_LOAD || (prog.Flags&elf.PF_X) == 0 {
   143  				continue
   144  			}
   145  
   146  			if prog.Vaddr <= s.Value && s.Value < (prog.Vaddr+prog.Memsz) {
   147  				// If the symbol value is contained in the segment, calculate
   148  				// the symbol offset.
   149  				//
   150  				// fn symbol offset = fn symbol VA - .text VA + .text offset
   151  				//
   152  				// stackoverflow.com/a/40249502
   153  				off = s.Value - prog.Vaddr + prog.Off
   154  				break
   155  			}
   156  		}
   157  
   158  		ex.offsets[s.Name] = off
   159  	}
   160  
   161  	return nil
   162  }
   163  
   164  func (ex *Executable) offset(symbol string) (uint64, error) {
   165  	if off, ok := ex.offsets[symbol]; ok {
   166  		// Symbols with location 0 from section undef are shared library calls and
   167  		// are relocated before the binary is executed. Dynamic linking is not
   168  		// implemented by the library, so mark this as unsupported for now.
   169  		//
   170  		// Since only offset values are stored and not elf.Symbol, if the value is 0,
   171  		// assume it's an external symbol.
   172  		if off == 0 {
   173  			return 0, fmt.Errorf("cannot resolve %s library call '%s', "+
   174  				"consider providing the offset via options: %w", ex.path, symbol, ErrNotSupported)
   175  		}
   176  		return off, nil
   177  	}
   178  	return 0, fmt.Errorf("symbol %s: %w", symbol, ErrNoSymbol)
   179  }
   180  
   181  // offsetWithOpts adds opts.RelativeOffset to symbol's offset
   182  // if opts.Offset is not set.
   183  func (ex *Executable) offsetWithOpts(symbol string, opts *UprobeOptions) (uint64, error) {
   184  	if opts == nil {
   185  		return ex.offset(symbol)
   186  	}
   187  
   188  	offset := opts.Offset
   189  	if offset == 0 {
   190  		offset = opts.RelativeOffset
   191  		off, err := ex.offset(symbol)
   192  		if err != nil {
   193  			return 0, err
   194  		}
   195  		offset += off
   196  	}
   197  	return offset, nil
   198  }
   199  
   200  // Uprobe attaches the given eBPF program to a perf event that fires when the
   201  // given symbol starts executing in the given Executable.
   202  // For example, /bin/bash::main():
   203  //
   204  //  ex, _ = OpenExecutable("/bin/bash")
   205  //  ex.Uprobe("main", prog, nil)
   206  //
   207  // When using symbols which belongs to shared libraries,
   208  // an offset must be provided via options:
   209  //
   210  //  up, err := ex.Uprobe("main", prog, &UprobeOptions{Offset: 0x123})
   211  //
   212  // Note: Setting the Offset field in the options supersedes the symbol's offset.
   213  //
   214  // Losing the reference to the resulting Link (up) will close the Uprobe
   215  // and prevent further execution of prog. The Link must be Closed during
   216  // program shutdown to avoid leaking system resources.
   217  //
   218  // Functions provided by shared libraries can currently not be traced and
   219  // will result in an ErrNotSupported.
   220  func (ex *Executable) Uprobe(symbol string, prog *ebpf.Program, opts *UprobeOptions) (Link, error) {
   221  	u, err := ex.uprobe(symbol, prog, opts, false)
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	lnk, err := attachPerfEvent(u, prog)
   227  	if err != nil {
   228  		u.Close()
   229  		return nil, err
   230  	}
   231  
   232  	return lnk, nil
   233  }
   234  
   235  // Uretprobe attaches the given eBPF program to a perf event that fires right
   236  // before the given symbol exits. For example, /bin/bash::main():
   237  //
   238  //  ex, _ = OpenExecutable("/bin/bash")
   239  //  ex.Uretprobe("main", prog, nil)
   240  //
   241  // When using symbols which belongs to shared libraries,
   242  // an offset must be provided via options:
   243  //
   244  //  up, err := ex.Uretprobe("main", prog, &UprobeOptions{Offset: 0x123})
   245  //
   246  // Note: Setting the Offset field in the options supersedes the symbol's offset.
   247  //
   248  // Losing the reference to the resulting Link (up) will close the Uprobe
   249  // and prevent further execution of prog. The Link must be Closed during
   250  // program shutdown to avoid leaking system resources.
   251  //
   252  // Functions provided by shared libraries can currently not be traced and
   253  // will result in an ErrNotSupported.
   254  func (ex *Executable) Uretprobe(symbol string, prog *ebpf.Program, opts *UprobeOptions) (Link, error) {
   255  	u, err := ex.uprobe(symbol, prog, opts, true)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	lnk, err := attachPerfEvent(u, prog)
   261  	if err != nil {
   262  		u.Close()
   263  		return nil, err
   264  	}
   265  
   266  	return lnk, nil
   267  }
   268  
   269  // uprobe opens a perf event for the given binary/symbol and attaches prog to it.
   270  // If ret is true, create a uretprobe.
   271  func (ex *Executable) uprobe(symbol string, prog *ebpf.Program, opts *UprobeOptions, ret bool) (*perfEvent, error) {
   272  	if prog == nil {
   273  		return nil, fmt.Errorf("prog cannot be nil: %w", errInvalidInput)
   274  	}
   275  	if prog.Type() != ebpf.Kprobe {
   276  		return nil, fmt.Errorf("eBPF program type %s is not Kprobe: %w", prog.Type(), errInvalidInput)
   277  	}
   278  	if opts == nil {
   279  		opts = &UprobeOptions{}
   280  	}
   281  
   282  	offset, err := ex.offsetWithOpts(symbol, opts)
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  
   287  	pid := opts.PID
   288  	if pid == 0 {
   289  		pid = perfAllThreads
   290  	}
   291  
   292  	if opts.RefCtrOffset != 0 {
   293  		if err := haveRefCtrOffsetPMU(); err != nil {
   294  			return nil, fmt.Errorf("uprobe ref_ctr_offset: %w", err)
   295  		}
   296  	}
   297  
   298  	args := probeArgs{
   299  		symbol:       symbol,
   300  		path:         ex.path,
   301  		offset:       offset,
   302  		pid:          pid,
   303  		refCtrOffset: opts.RefCtrOffset,
   304  		ret:          ret,
   305  		cookie:       opts.Cookie,
   306  	}
   307  
   308  	// Use uprobe PMU if the kernel has it available.
   309  	tp, err := pmuUprobe(args)
   310  	if err == nil {
   311  		return tp, nil
   312  	}
   313  	if err != nil && !errors.Is(err, ErrNotSupported) {
   314  		return nil, fmt.Errorf("creating perf_uprobe PMU: %w", err)
   315  	}
   316  
   317  	// Use tracefs if uprobe PMU is missing.
   318  	args.symbol = sanitizeSymbol(symbol)
   319  	tp, err = tracefsUprobe(args)
   320  	if err != nil {
   321  		return nil, fmt.Errorf("creating trace event '%s:%s' in tracefs: %w", ex.path, symbol, err)
   322  	}
   323  
   324  	return tp, nil
   325  }
   326  
   327  // pmuUprobe opens a perf event based on the uprobe PMU.
   328  func pmuUprobe(args probeArgs) (*perfEvent, error) {
   329  	return pmuProbe(uprobeType, args)
   330  }
   331  
   332  // tracefsUprobe creates a Uprobe tracefs entry.
   333  func tracefsUprobe(args probeArgs) (*perfEvent, error) {
   334  	return tracefsProbe(uprobeType, args)
   335  }
   336  
   337  // sanitizeSymbol replaces every invalid character for the tracefs api with an underscore.
   338  // It is equivalent to calling regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString("_").
   339  func sanitizeSymbol(s string) string {
   340  	var b strings.Builder
   341  	b.Grow(len(s))
   342  	var skip bool
   343  	for _, c := range []byte(s) {
   344  		switch {
   345  		case c >= 'a' && c <= 'z',
   346  			c >= 'A' && c <= 'Z',
   347  			c >= '0' && c <= '9':
   348  			skip = false
   349  			b.WriteByte(c)
   350  
   351  		default:
   352  			if !skip {
   353  				b.WriteByte('_')
   354  				skip = true
   355  			}
   356  		}
   357  	}
   358  
   359  	return b.String()
   360  }
   361  
   362  // uprobeToken creates the PATH:OFFSET(REF_CTR_OFFSET) token for the tracefs api.
   363  func uprobeToken(args probeArgs) string {
   364  	po := fmt.Sprintf("%s:%#x", args.path, args.offset)
   365  
   366  	if args.refCtrOffset != 0 {
   367  		// This is not documented in Documentation/trace/uprobetracer.txt.
   368  		// elixir.bootlin.com/linux/v5.15-rc7/source/kernel/trace/trace.c#L5564
   369  		po += fmt.Sprintf("(%#x)", args.refCtrOffset)
   370  	}
   371  
   372  	return po
   373  }
   374  
   375  func uretprobeBit() (uint64, error) {
   376  	uprobeRetprobeBit.once.Do(func() {
   377  		uprobeRetprobeBit.value, uprobeRetprobeBit.err = determineRetprobeBit(uprobeType)
   378  	})
   379  	return uprobeRetprobeBit.value, uprobeRetprobeBit.err
   380  }