github.com/cilium/ebpf@v0.15.1-0.20240517100537-8079b37aa138/link/uprobe_test.go (about)

     1  package link
     2  
     3  import (
     4  	"errors"
     5  	"go/build"
     6  	"os"
     7  	"os/exec"
     8  	"path"
     9  	"testing"
    10  
    11  	"github.com/go-quicktest/qt"
    12  
    13  	"github.com/cilium/ebpf"
    14  	"github.com/cilium/ebpf/internal/testutils"
    15  	"github.com/cilium/ebpf/internal/tracefs"
    16  	"github.com/cilium/ebpf/internal/unix"
    17  )
    18  
    19  var (
    20  	bashEx, _ = OpenExecutable("/bin/bash")
    21  	bashSyms  = []string{"main", "_start", "check_dev_tty"}
    22  	bashSym   = bashSyms[0]
    23  )
    24  
    25  func TestExecutable(t *testing.T) {
    26  	_, err := OpenExecutable("")
    27  	if err == nil {
    28  		t.Fatal("create executable: expected error on empty path")
    29  	}
    30  
    31  	if bashEx.path != "/bin/bash" {
    32  		t.Fatalf("create executable: unexpected path '%s'", bashEx.path)
    33  	}
    34  
    35  	_, err = bashEx.address(bashSym, 0, 0)
    36  	if err != nil {
    37  		t.Fatalf("find offset: %v", err)
    38  	}
    39  
    40  	_, err = bashEx.address("bogus", 0, 0)
    41  	if err == nil {
    42  		t.Fatal("find symbol: expected error")
    43  	}
    44  }
    45  
    46  func TestExecutableOffset(t *testing.T) {
    47  	symbolOffset, err := bashEx.address(bashSym, 0, 0)
    48  	if err != nil {
    49  		t.Fatal(err)
    50  	}
    51  
    52  	offset, err := bashEx.address(bashSym, 0x1, 0)
    53  	if err != nil {
    54  		t.Fatal(err)
    55  	}
    56  	qt.Assert(t, qt.Equals(offset, 0x1))
    57  
    58  	offset, err = bashEx.address(bashSym, 0, 0x2)
    59  	if err != nil {
    60  		t.Fatal(err)
    61  	}
    62  	qt.Assert(t, qt.Equals(offset, symbolOffset+0x2))
    63  
    64  	offset, err = bashEx.address(bashSym, 0x1, 0x2)
    65  	if err != nil {
    66  		t.Fatal(err)
    67  	}
    68  	qt.Assert(t, qt.Equals(offset, 0x1+0x2))
    69  }
    70  
    71  func TestExecutableLazyLoadSymbols(t *testing.T) {
    72  	testutils.SkipOnOldKernel(t, "4.14", "uprobe on v4.9 returns EIO on vimto")
    73  
    74  	ex, err := OpenExecutable("/bin/bash")
    75  	qt.Assert(t, qt.IsNil(err))
    76  	// Addresses must be empty, will be lazy loaded.
    77  	qt.Assert(t, qt.HasLen(ex.cachedAddresses, 0))
    78  
    79  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
    80  	// Address must be a multiple of 4 on arm64, see
    81  	// https://elixir.bootlin.com/linux/v6.6.4/source/arch/arm64/kernel/probes/uprobes.c#L42
    82  	up, err := ex.Uprobe(bashSym, prog, &UprobeOptions{Address: 124})
    83  	qt.Assert(t, qt.IsNil(err))
    84  	up.Close()
    85  
    86  	// Addresses must still be empty as Address has been provided via options.
    87  	qt.Assert(t, qt.HasLen(ex.cachedAddresses, 0))
    88  
    89  	up, err = ex.Uprobe(bashSym, prog, nil)
    90  	qt.Assert(t, qt.IsNil(err))
    91  	up.Close()
    92  
    93  	// Symbol table should be loaded.
    94  	qt.Assert(t, qt.Not(qt.HasLen(ex.cachedAddresses, 0)))
    95  }
    96  
    97  func TestUprobe(t *testing.T) {
    98  	testutils.SkipOnOldKernel(t, "4.14", "uprobe on v4.9 returns EIO on vimto")
    99  
   100  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
   101  
   102  	up, err := bashEx.Uprobe(bashSym, prog, nil)
   103  	qt.Assert(t, qt.IsNil(err))
   104  	defer up.Close()
   105  
   106  	testLink(t, up, prog)
   107  }
   108  
   109  func TestUprobeExtNotFound(t *testing.T) {
   110  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
   111  
   112  	// This symbol will not be present in Executable (elf.SHN_UNDEF).
   113  	_, err := bashEx.Uprobe("open", prog, nil)
   114  	if err == nil {
   115  		t.Fatal("expected error")
   116  	}
   117  }
   118  
   119  func TestUprobeExtWithOpts(t *testing.T) {
   120  	testutils.SkipOnOldKernel(t, "4.14", "uprobe on v4.9 returns EIO on vimto")
   121  
   122  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
   123  
   124  	// NB: It's not possible to invoke the uprobe since we use an arbitrary
   125  	// address.
   126  	up, err := bashEx.Uprobe("open", prog, &UprobeOptions{
   127  		// arm64 requires the addresses to be aligned (a multiple of 4)
   128  		Address: 0x4,
   129  	})
   130  	if err != nil {
   131  		t.Fatal(err)
   132  	}
   133  	defer up.Close()
   134  }
   135  
   136  func TestUprobeWithPID(t *testing.T) {
   137  	testutils.SkipOnOldKernel(t, "4.14", "uprobe on v4.9 returns EIO on vimto")
   138  
   139  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
   140  
   141  	up, err := bashEx.Uprobe(bashSym, prog, &UprobeOptions{PID: os.Getpid()})
   142  	if err != nil {
   143  		t.Fatal(err)
   144  	}
   145  	defer up.Close()
   146  }
   147  
   148  func TestUprobeWithNonExistentPID(t *testing.T) {
   149  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
   150  
   151  	// trying to open a perf event on a non-existent PID will return ESRCH.
   152  	_, err := bashEx.Uprobe(bashSym, prog, &UprobeOptions{PID: -2})
   153  	if !errors.Is(err, unix.ESRCH) {
   154  		t.Fatalf("expected ESRCH, got %v", err)
   155  	}
   156  }
   157  
   158  func TestUretprobe(t *testing.T) {
   159  	testutils.SkipOnOldKernel(t, "4.14", "uprobe on v4.9 returns EIO on vimto")
   160  
   161  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
   162  
   163  	up, err := bashEx.Uretprobe(bashSym, prog, nil)
   164  	qt.Assert(t, qt.IsNil(err))
   165  	defer up.Close()
   166  
   167  	testLink(t, up, prog)
   168  }
   169  
   170  // Test u(ret)probe creation using perf_uprobe PMU.
   171  func TestUprobeCreatePMU(t *testing.T) {
   172  	// Requires at least 4.17 (e12f03d7031a "perf/core: Implement the 'perf_kprobe' PMU")
   173  	testutils.SkipOnOldKernel(t, "4.17", "perf_kprobe PMU")
   174  
   175  	// Fetch the offset from the /bin/bash Executable already defined.
   176  	off, err := bashEx.address(bashSym, 0, 0)
   177  	qt.Assert(t, qt.IsNil(err))
   178  
   179  	// Prepare probe args.
   180  	args := tracefs.ProbeArgs{
   181  		Type:   tracefs.Uprobe,
   182  		Symbol: bashSym,
   183  		Path:   bashEx.path,
   184  		Offset: off,
   185  		Pid:    perfAllThreads,
   186  	}
   187  
   188  	// uprobe PMU
   189  	pu, err := pmuProbe(args)
   190  	qt.Assert(t, qt.IsNil(err))
   191  	defer pu.Close()
   192  
   193  	// uretprobe PMU
   194  	args.Ret = true
   195  	pr, err := pmuProbe(args)
   196  	qt.Assert(t, qt.IsNil(err))
   197  	defer pr.Close()
   198  }
   199  
   200  // Test fallback behaviour on kernels without perf_uprobe PMU available.
   201  func TestUprobePMUUnavailable(t *testing.T) {
   202  	// Fetch the offset from the /bin/bash Executable already defined.
   203  	off, err := bashEx.address(bashSym, 0, 0)
   204  	qt.Assert(t, qt.IsNil(err))
   205  
   206  	// Prepare probe args.
   207  	args := tracefs.ProbeArgs{
   208  		Type:   tracefs.Uprobe,
   209  		Symbol: bashSym,
   210  		Path:   bashEx.path,
   211  		Offset: off,
   212  		Pid:    perfAllThreads,
   213  	}
   214  
   215  	pk, err := pmuProbe(args)
   216  	if err == nil {
   217  		pk.Close()
   218  		t.Skipf("Kernel supports perf_uprobe PMU, not asserting error.")
   219  	}
   220  
   221  	// Expect ErrNotSupported.
   222  	qt.Assert(t, qt.ErrorIs(err, ErrNotSupported), qt.Commentf("got error: %s", err))
   223  }
   224  
   225  // Test tracefs u(ret)probe creation on all kernel versions.
   226  func TestUprobeTraceFS(t *testing.T) {
   227  	testutils.SkipOnOldKernel(t, "4.14", "uprobe on v4.9 returns EIO on vimto")
   228  
   229  	// Fetch the offset from the /bin/bash Executable already defined.
   230  	off, err := bashEx.address(bashSym, 0, 0)
   231  	qt.Assert(t, qt.IsNil(err))
   232  
   233  	// Prepare probe args.
   234  	args := tracefs.ProbeArgs{
   235  		Type:   tracefs.Uprobe,
   236  		Symbol: bashSym,
   237  		Path:   bashEx.path,
   238  		Offset: off,
   239  		Pid:    perfAllThreads,
   240  	}
   241  
   242  	// Open and close tracefs u(ret)probes, checking all errors.
   243  	up, err := tracefsProbe(args)
   244  	qt.Assert(t, qt.IsNil(err))
   245  	qt.Assert(t, qt.IsNil(up.Close()))
   246  
   247  	args.Ret = true
   248  	up, err = tracefsProbe(args)
   249  	qt.Assert(t, qt.IsNil(err))
   250  	qt.Assert(t, qt.IsNil(up.Close()))
   251  
   252  	// Create two identical trace events, ensure their IDs differ.
   253  	args.Ret = false
   254  	u1, err := tracefsProbe(args)
   255  	qt.Assert(t, qt.IsNil(err))
   256  	defer u1.Close()
   257  	qt.Assert(t, qt.IsNotNil(u1.tracefsEvent))
   258  
   259  	u2, err := tracefsProbe(args)
   260  	qt.Assert(t, qt.IsNil(err))
   261  	defer u2.Close()
   262  	qt.Assert(t, qt.IsNotNil(u2.tracefsEvent))
   263  
   264  	// Compare the uprobes' tracefs IDs.
   265  	qt.Assert(t, qt.Not(qt.Equals(u1.tracefsEvent.ID(), u2.tracefsEvent.ID())))
   266  
   267  	// Expect an error when supplying an invalid custom group name
   268  	args.Group = "/"
   269  	_, err = tracefsProbe(args)
   270  	qt.Assert(t, qt.Not(qt.IsNil(err)))
   271  
   272  	args.Group = "customgroup"
   273  	u3, err := tracefsProbe(args)
   274  	qt.Assert(t, qt.IsNil(err))
   275  	defer u3.Close()
   276  	qt.Assert(t, qt.Matches(u3.tracefsEvent.Group(), `customgroup_[a-f0-9]{16}`))
   277  }
   278  
   279  func TestUprobeProgramCall(t *testing.T) {
   280  	testutils.SkipOnOldKernel(t, "4.14", "uprobe on v4.9 returns EIO on vimto")
   281  
   282  	tests := []struct {
   283  		name string
   284  		elf  string
   285  		args []string
   286  		sym  string
   287  	}{
   288  		{
   289  			"bash",
   290  			"/bin/bash",
   291  			[]string{"--help"},
   292  			"main",
   293  		},
   294  		{
   295  			"go-binary",
   296  			path.Join(build.Default.GOROOT, "bin/go"),
   297  			[]string{"version"},
   298  			"main.main",
   299  		},
   300  	}
   301  
   302  	for _, tt := range tests {
   303  		t.Run(tt.name, func(t *testing.T) {
   304  			if tt.name == "go-binary" {
   305  				// https://github.com/cilium/ebpf/issues/406
   306  				testutils.SkipOnOldKernel(t, "4.14", "uprobes on Go binaries silently fail on kernel < 4.14")
   307  			}
   308  
   309  			m, p := newUpdaterMapProg(t, ebpf.Kprobe, 0)
   310  
   311  			// Load the executable.
   312  			ex, err := OpenExecutable(tt.elf)
   313  			if err != nil {
   314  				t.Fatal(err)
   315  			}
   316  
   317  			// Open Uprobe on the executable for the given symbol
   318  			// and attach it to the ebpf program created above.
   319  			u, err := ex.Uprobe(tt.sym, p, nil)
   320  			if errors.Is(err, ErrNoSymbol) {
   321  				// Assume bash::main and go::main.main always exists
   322  				// and skip the test if the symbol can't be found as
   323  				// certain OS (eg. Debian) strip binaries.
   324  				t.Skipf("executable %s appear to be stripped, skipping", tt.elf)
   325  			}
   326  			if err != nil {
   327  				t.Fatal(err)
   328  			}
   329  
   330  			// Trigger ebpf program call.
   331  			trigger := func(t *testing.T) {
   332  				if err := exec.Command(tt.elf, tt.args...).Run(); err != nil {
   333  					t.Fatal(err)
   334  				}
   335  			}
   336  			trigger(t)
   337  
   338  			// Assert that the value got incremented to at least 1, while allowing
   339  			// for bigger values, because we could race with other bash execution.
   340  			assertMapValueGE(t, m, 0, 1)
   341  
   342  			// Detach the Uprobe.
   343  			if err := u.Close(); err != nil {
   344  				t.Fatal(err)
   345  			}
   346  
   347  			// Reset map value to 0 at index 0.
   348  			if err := m.Update(uint32(0), uint32(0), ebpf.UpdateExist); err != nil {
   349  				t.Fatal(err)
   350  			}
   351  
   352  			// Retrigger the ebpf program call.
   353  			trigger(t)
   354  
   355  			// Assert that this time the value has not been updated.
   356  			assertMapValue(t, m, 0, 0)
   357  		})
   358  	}
   359  }
   360  
   361  func TestUprobeProgramWrongPID(t *testing.T) {
   362  	testutils.SkipOnOldKernel(t, "4.14", "uprobe on v4.9 returns EIO on vimto")
   363  
   364  	m, p := newUpdaterMapProg(t, ebpf.Kprobe, 0)
   365  
   366  	// Load the '/bin/bash' executable.
   367  	ex, err := OpenExecutable("/bin/bash")
   368  	if err != nil {
   369  		t.Fatal(err)
   370  	}
   371  
   372  	// Open Uprobe on '/bin/bash' for the symbol 'main'
   373  	// and attach it to the ebpf program created above.
   374  	// Create the perf-event with the current process' PID
   375  	// to make sure the event is not fired when we will try
   376  	// to trigger the program execution via exec.
   377  	u, err := ex.Uprobe("main", p, &UprobeOptions{PID: os.Getpid()})
   378  	if err != nil {
   379  		t.Fatal(err)
   380  	}
   381  	defer u.Close()
   382  
   383  	// Trigger ebpf program call.
   384  	if err := exec.Command("/bin/bash", "--help").Run(); err != nil {
   385  		t.Fatal(err)
   386  	}
   387  
   388  	// Assert that the value at index 0 is still 0.
   389  	assertMapValue(t, m, 0, 0)
   390  }
   391  
   392  func TestHaveRefCtrOffsetPMU(t *testing.T) {
   393  	testutils.CheckFeatureTest(t, haveRefCtrOffsetPMU)
   394  }