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 }