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

     1  package link
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"go/build"
     7  	"os"
     8  	"os/exec"
     9  	"path"
    10  	"testing"
    11  
    12  	qt "github.com/frankban/quicktest"
    13  
    14  	"github.com/kubeshark/ebpf"
    15  	"github.com/kubeshark/ebpf/internal/testutils"
    16  	"github.com/kubeshark/ebpf/internal/unix"
    17  )
    18  
    19  var (
    20  	bashEx, _ = OpenExecutable("/bin/bash")
    21  	bashSym   = "main"
    22  )
    23  
    24  func TestExecutable(t *testing.T) {
    25  	_, err := OpenExecutable("")
    26  	if err == nil {
    27  		t.Fatal("create executable: expected error on empty path")
    28  	}
    29  
    30  	if bashEx.path != "/bin/bash" {
    31  		t.Fatalf("create executable: unexpected path '%s'", bashEx.path)
    32  	}
    33  
    34  	_, err = bashEx.offset(bashSym)
    35  	if err != nil {
    36  		t.Fatalf("find offset: %v", err)
    37  	}
    38  
    39  	_, err = bashEx.offset("bogus")
    40  	if err == nil {
    41  		t.Fatal("find symbol: expected error")
    42  	}
    43  }
    44  
    45  func TestOffsetWithOpts(t *testing.T) {
    46  	c := qt.New(t)
    47  
    48  	_ = mustLoadProgram(t, ebpf.Kprobe, 0, "")
    49  
    50  	symbolOffset, err := bashEx.offset(bashSym)
    51  	if err != nil {
    52  		t.Fatal(err)
    53  	}
    54  
    55  	offset, err := bashEx.offsetWithOpts(bashSym, nil)
    56  	if err != nil {
    57  		t.Fatal(err)
    58  	}
    59  	c.Assert(offset, qt.Equals, uint64(symbolOffset))
    60  
    61  	offset, err = bashEx.offsetWithOpts(bashSym, &UprobeOptions{})
    62  	if err != nil {
    63  		t.Fatal(err)
    64  	}
    65  	c.Assert(offset, qt.Equals, uint64(symbolOffset))
    66  
    67  	offset, err = bashEx.offsetWithOpts(bashSym, &UprobeOptions{Offset: 0x1})
    68  	if err != nil {
    69  		t.Fatal(err)
    70  	}
    71  	c.Assert(offset, qt.Equals, uint64(0x1))
    72  
    73  	offset, err = bashEx.offsetWithOpts(bashSym, &UprobeOptions{RelativeOffset: 0x2})
    74  	if err != nil {
    75  		t.Fatal(err)
    76  	}
    77  	c.Assert(offset, qt.Equals, uint64(symbolOffset+0x2))
    78  
    79  	offset, err = bashEx.offsetWithOpts(bashSym, &UprobeOptions{Offset: 0x1, RelativeOffset: 0x2})
    80  	if err != nil {
    81  		t.Fatal(err)
    82  	}
    83  	c.Assert(offset, qt.Equals, uint64(0x1))
    84  }
    85  
    86  func TestUprobe(t *testing.T) {
    87  	c := qt.New(t)
    88  
    89  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
    90  
    91  	up, err := bashEx.Uprobe(bashSym, prog, nil)
    92  	c.Assert(err, qt.IsNil)
    93  	defer up.Close()
    94  
    95  	testLink(t, up, prog)
    96  }
    97  
    98  func TestUprobeExtNotFound(t *testing.T) {
    99  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
   100  
   101  	// This symbol will not be present in Executable (elf.SHN_UNDEF).
   102  	_, err := bashEx.Uprobe("open", prog, nil)
   103  	if err == nil {
   104  		t.Fatal("expected error")
   105  	}
   106  }
   107  
   108  func TestUprobeExtWithOpts(t *testing.T) {
   109  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
   110  
   111  	// This Uprobe is broken and will not work because the offset is not
   112  	// correct. This is expected since the offset is provided by the user.
   113  	up, err := bashEx.Uprobe("open", prog, &UprobeOptions{Offset: 0x1})
   114  	if err != nil {
   115  		t.Fatal(err)
   116  	}
   117  	defer up.Close()
   118  }
   119  
   120  func TestUprobeWithPID(t *testing.T) {
   121  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
   122  
   123  	up, err := bashEx.Uprobe(bashSym, prog, &UprobeOptions{PID: os.Getpid()})
   124  	if err != nil {
   125  		t.Fatal(err)
   126  	}
   127  	defer up.Close()
   128  }
   129  
   130  func TestUprobeWithNonExistentPID(t *testing.T) {
   131  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
   132  
   133  	// trying to open a perf event on a non-existent PID will return ESRCH.
   134  	_, err := bashEx.Uprobe(bashSym, prog, &UprobeOptions{PID: -2})
   135  	if !errors.Is(err, unix.ESRCH) {
   136  		t.Fatalf("expected ESRCH, got %v", err)
   137  	}
   138  }
   139  
   140  func TestUretprobe(t *testing.T) {
   141  	c := qt.New(t)
   142  
   143  	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
   144  
   145  	up, err := bashEx.Uretprobe(bashSym, prog, nil)
   146  	c.Assert(err, qt.IsNil)
   147  	defer up.Close()
   148  
   149  	testLink(t, up, prog)
   150  }
   151  
   152  // Test u(ret)probe creation using perf_uprobe PMU.
   153  func TestUprobeCreatePMU(t *testing.T) {
   154  	// Requires at least 4.17 (e12f03d7031a "perf/core: Implement the 'perf_kprobe' PMU")
   155  	testutils.SkipOnOldKernel(t, "4.17", "perf_kprobe PMU")
   156  
   157  	c := qt.New(t)
   158  
   159  	// Fetch the offset from the /bin/bash Executable already defined.
   160  	off, err := bashEx.offset(bashSym)
   161  	c.Assert(err, qt.IsNil)
   162  
   163  	// Prepare probe args.
   164  	args := probeArgs{
   165  		symbol: bashSym,
   166  		path:   bashEx.path,
   167  		offset: off,
   168  		pid:    perfAllThreads,
   169  	}
   170  
   171  	// uprobe PMU
   172  	pu, err := pmuUprobe(args)
   173  	c.Assert(err, qt.IsNil)
   174  	defer pu.Close()
   175  
   176  	c.Assert(pu.typ, qt.Equals, uprobeEvent)
   177  
   178  	// uretprobe PMU
   179  	args.ret = true
   180  	pr, err := pmuUprobe(args)
   181  	c.Assert(err, qt.IsNil)
   182  	defer pr.Close()
   183  
   184  	c.Assert(pr.typ, qt.Equals, uretprobeEvent)
   185  }
   186  
   187  // Test fallback behaviour on kernels without perf_uprobe PMU available.
   188  func TestUprobePMUUnavailable(t *testing.T) {
   189  	c := qt.New(t)
   190  
   191  	// Fetch the offset from the /bin/bash Executable already defined.
   192  	off, err := bashEx.offset(bashSym)
   193  	c.Assert(err, qt.IsNil)
   194  
   195  	// Prepare probe args.
   196  	args := probeArgs{
   197  		symbol: bashSym,
   198  		path:   bashEx.path,
   199  		offset: off,
   200  		pid:    perfAllThreads,
   201  	}
   202  
   203  	pk, err := pmuUprobe(args)
   204  	if err == nil {
   205  		pk.Close()
   206  		t.Skipf("Kernel supports perf_uprobe PMU, not asserting error.")
   207  	}
   208  
   209  	// Expect ErrNotSupported.
   210  	c.Assert(errors.Is(err, ErrNotSupported), qt.IsTrue, qt.Commentf("got error: %s", err))
   211  }
   212  
   213  // Test tracefs u(ret)probe creation on all kernel versions.
   214  func TestUprobeTraceFS(t *testing.T) {
   215  	c := qt.New(t)
   216  
   217  	// Fetch the offset from the /bin/bash Executable already defined.
   218  	off, err := bashEx.offset(bashSym)
   219  	c.Assert(err, qt.IsNil)
   220  
   221  	// Prepare probe args.
   222  	args := probeArgs{
   223  		symbol: sanitizeSymbol(bashSym),
   224  		path:   bashEx.path,
   225  		offset: off,
   226  		pid:    perfAllThreads,
   227  	}
   228  
   229  	// Open and close tracefs u(ret)probes, checking all errors.
   230  	up, err := tracefsUprobe(args)
   231  	c.Assert(err, qt.IsNil)
   232  	c.Assert(up.Close(), qt.IsNil)
   233  	c.Assert(up.typ, qt.Equals, uprobeEvent)
   234  
   235  	args.ret = true
   236  	up, err = tracefsUprobe(args)
   237  	c.Assert(err, qt.IsNil)
   238  	c.Assert(up.Close(), qt.IsNil)
   239  	c.Assert(up.typ, qt.Equals, uretprobeEvent)
   240  
   241  	// Create two identical trace events, ensure their IDs differ.
   242  	args.ret = false
   243  	u1, err := tracefsUprobe(args)
   244  	c.Assert(err, qt.IsNil)
   245  	defer u1.Close()
   246  	c.Assert(u1.tracefsID, qt.Not(qt.Equals), 0)
   247  
   248  	u2, err := tracefsUprobe(args)
   249  	c.Assert(err, qt.IsNil)
   250  	defer u2.Close()
   251  	c.Assert(u2.tracefsID, qt.Not(qt.Equals), 0)
   252  
   253  	// Compare the uprobes' tracefs IDs.
   254  	c.Assert(u1.tracefsID, qt.Not(qt.CmpEquals()), u2.tracefsID)
   255  }
   256  
   257  // Test u(ret)probe creation writing directly to <tracefs>/uprobe_events.
   258  // Only runs on 5.0 and over. Earlier versions ignored writes of duplicate
   259  // events, while 5.0 started returning -EEXIST when a uprobe event already
   260  // exists.
   261  func TestUprobeCreateTraceFS(t *testing.T) {
   262  	testutils.SkipOnOldKernel(t, "5.0", "<tracefs>/uprobe_events doesn't reject duplicate events")
   263  
   264  	c := qt.New(t)
   265  
   266  	// Fetch the offset from the /bin/bash Executable already defined.
   267  	off, err := bashEx.offset(bashSym)
   268  	c.Assert(err, qt.IsNil)
   269  
   270  	// Sanitize the symbol in order to be used in tracefs API.
   271  	ssym := sanitizeSymbol(bashSym)
   272  
   273  	pg, _ := randomGroup("ebpftest")
   274  	rg, _ := randomGroup("ebpftest")
   275  
   276  	// Tee up cleanups in case any of the Asserts abort the function.
   277  	defer func() {
   278  		_ = closeTraceFSProbeEvent(uprobeType, pg, ssym)
   279  		_ = closeTraceFSProbeEvent(uprobeType, rg, ssym)
   280  	}()
   281  
   282  	// Prepare probe args.
   283  	args := probeArgs{
   284  		group:  pg,
   285  		symbol: ssym,
   286  		path:   bashEx.path,
   287  		offset: off,
   288  	}
   289  
   290  	// Create a uprobe.
   291  	err = createTraceFSProbeEvent(uprobeType, args)
   292  	c.Assert(err, qt.IsNil)
   293  
   294  	// Attempt to create an identical uprobe using tracefs,
   295  	// expect it to fail with os.ErrExist.
   296  	err = createTraceFSProbeEvent(uprobeType, args)
   297  	c.Assert(errors.Is(err, os.ErrExist), qt.IsTrue,
   298  		qt.Commentf("expected consecutive uprobe creation to contain os.ErrExist, got: %v", err))
   299  
   300  	// Expect a successful close of the kprobe.
   301  	c.Assert(closeTraceFSProbeEvent(uprobeType, pg, ssym), qt.IsNil)
   302  
   303  	args.group = rg
   304  	args.ret = true
   305  
   306  	// Same test for a kretprobe.
   307  	err = createTraceFSProbeEvent(uprobeType, args)
   308  	c.Assert(err, qt.IsNil)
   309  
   310  	err = createTraceFSProbeEvent(uprobeType, args)
   311  	c.Assert(os.IsExist(err), qt.IsFalse,
   312  		qt.Commentf("expected consecutive uretprobe creation to contain os.ErrExist, got: %v", err))
   313  
   314  	// Expect a successful close of the uretprobe.
   315  	c.Assert(closeTraceFSProbeEvent(uprobeType, rg, ssym), qt.IsNil)
   316  }
   317  
   318  func TestUprobeSanitizedSymbol(t *testing.T) {
   319  	tests := []struct {
   320  		symbol   string
   321  		expected string
   322  	}{
   323  		{"readline", "readline"},
   324  		{"main.Func123", "main_Func123"},
   325  		{"a.....a", "a_a"},
   326  		{"./;'{}[]a", "_a"},
   327  		{"***xx**xx###", "_xx_xx_"},
   328  		{`@P#r$i%v^3*+t)i&k++--`, "_P_r_i_v_3_t_i_k_"},
   329  	}
   330  
   331  	for i, tt := range tests {
   332  		t.Run(fmt.Sprint(i), func(t *testing.T) {
   333  			sanitized := sanitizeSymbol(tt.symbol)
   334  			if tt.expected != sanitized {
   335  				t.Errorf("Expected sanitized symbol to be '%s', got '%s'", tt.expected, sanitized)
   336  			}
   337  		})
   338  	}
   339  }
   340  
   341  func TestUprobeToken(t *testing.T) {
   342  	tests := []struct {
   343  		args     probeArgs
   344  		expected string
   345  	}{
   346  		{probeArgs{path: "/bin/bash"}, "/bin/bash:0x0"},
   347  		{probeArgs{path: "/bin/bash", offset: 1}, "/bin/bash:0x1"},
   348  		{probeArgs{path: "/bin/bash", offset: 65535}, "/bin/bash:0xffff"},
   349  		{probeArgs{path: "/bin/bash", offset: 65536}, "/bin/bash:0x10000"},
   350  		{probeArgs{path: "/bin/bash", offset: 1, refCtrOffset: 1}, "/bin/bash:0x1(0x1)"},
   351  		{probeArgs{path: "/bin/bash", offset: 1, refCtrOffset: 65535}, "/bin/bash:0x1(0xffff)"},
   352  	}
   353  
   354  	for i, tt := range tests {
   355  		t.Run(fmt.Sprint(i), func(t *testing.T) {
   356  			po := uprobeToken(tt.args)
   357  			if tt.expected != po {
   358  				t.Errorf("Expected path:offset to be '%s', got '%s'", tt.expected, po)
   359  			}
   360  		})
   361  	}
   362  }
   363  
   364  func TestUprobeProgramCall(t *testing.T) {
   365  	tests := []struct {
   366  		name string
   367  		elf  string
   368  		args []string
   369  		sym  string
   370  	}{
   371  		{
   372  			"bash",
   373  			"/bin/bash",
   374  			[]string{"--help"},
   375  			"main",
   376  		},
   377  		{
   378  			"go-binary",
   379  			path.Join(build.Default.GOROOT, "bin/go"),
   380  			[]string{"version"},
   381  			"main.main",
   382  		},
   383  	}
   384  
   385  	for _, tt := range tests {
   386  		t.Run(tt.name, func(t *testing.T) {
   387  			if tt.name == "go-binary" {
   388  				// https://github.com/kubeshark/ebpf/issues/406
   389  				testutils.SkipOnOldKernel(t, "4.14", "uprobes on Go binaries silently fail on kernel < 4.14")
   390  			}
   391  
   392  			m, p := newUpdaterMapProg(t, ebpf.Kprobe)
   393  
   394  			// Load the executable.
   395  			ex, err := OpenExecutable(tt.elf)
   396  			if err != nil {
   397  				t.Fatal(err)
   398  			}
   399  
   400  			// Open Uprobe on the executable for the given symbol
   401  			// and attach it to the ebpf program created above.
   402  			u, err := ex.Uprobe(tt.sym, p, nil)
   403  			if errors.Is(err, ErrNoSymbol) {
   404  				// Assume bash::main and go::main.main always exists
   405  				// and skip the test if the symbol can't be found as
   406  				// certain OS (eg. Debian) strip binaries.
   407  				t.Skipf("executable %s appear to be stripped, skipping", tt.elf)
   408  			}
   409  			if err != nil {
   410  				t.Fatal(err)
   411  			}
   412  
   413  			// Trigger ebpf program call.
   414  			trigger := func(t *testing.T) {
   415  				if err := exec.Command(tt.elf, tt.args...).Run(); err != nil {
   416  					t.Fatal(err)
   417  				}
   418  			}
   419  			trigger(t)
   420  
   421  			// Assert that the value at index 0 has been updated to 1.
   422  			assertMapValue(t, m, 0, 1)
   423  
   424  			// Detach the Uprobe.
   425  			if err := u.Close(); err != nil {
   426  				t.Fatal(err)
   427  			}
   428  
   429  			// Reset map value to 0 at index 0.
   430  			if err := m.Update(uint32(0), uint32(0), ebpf.UpdateExist); err != nil {
   431  				t.Fatal(err)
   432  			}
   433  
   434  			// Retrigger the ebpf program call.
   435  			trigger(t)
   436  
   437  			// Assert that this time the value has not been updated.
   438  			assertMapValue(t, m, 0, 0)
   439  		})
   440  	}
   441  }
   442  
   443  func TestUprobeProgramWrongPID(t *testing.T) {
   444  	m, p := newUpdaterMapProg(t, ebpf.Kprobe)
   445  
   446  	// Load the '/bin/bash' executable.
   447  	ex, err := OpenExecutable("/bin/bash")
   448  	if err != nil {
   449  		t.Fatal(err)
   450  	}
   451  
   452  	// Open Uprobe on '/bin/bash' for the symbol 'main'
   453  	// and attach it to the ebpf program created above.
   454  	// Create the perf-event with the current process' PID
   455  	// to make sure the event is not fired when we will try
   456  	// to trigger the program execution via exec.
   457  	u, err := ex.Uprobe("main", p, &UprobeOptions{PID: os.Getpid()})
   458  	if err != nil {
   459  		t.Fatal(err)
   460  	}
   461  	defer u.Close()
   462  
   463  	// Trigger ebpf program call.
   464  	if err := exec.Command("/bin/bash", "--help").Run(); err != nil {
   465  		t.Fatal(err)
   466  	}
   467  
   468  	// Assert that the value at index 0 is still 0.
   469  	assertMapValue(t, m, 0, 0)
   470  }
   471  
   472  func TestHaveRefCtrOffsetPMU(t *testing.T) {
   473  	testutils.CheckFeatureTest(t, haveRefCtrOffsetPMU)
   474  }