github.com/cilium/ebpf@v0.16.0/info_test.go (about) 1 package ebpf 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "reflect" 8 "strings" 9 "testing" 10 11 "github.com/go-quicktest/qt" 12 13 "github.com/cilium/ebpf/asm" 14 "github.com/cilium/ebpf/btf" 15 "github.com/cilium/ebpf/internal" 16 "github.com/cilium/ebpf/internal/sys" 17 "github.com/cilium/ebpf/internal/testutils" 18 "github.com/cilium/ebpf/internal/unix" 19 ) 20 21 func TestMapInfoFromProc(t *testing.T) { 22 hash, err := NewMap(&MapSpec{ 23 Name: "testing", 24 Type: Hash, 25 KeySize: 4, 26 ValueSize: 5, 27 MaxEntries: 2, 28 Flags: unix.BPF_F_NO_PREALLOC, 29 }) 30 testutils.SkipIfNotSupported(t, err) 31 if err != nil { 32 t.Fatal(err) 33 } 34 defer hash.Close() 35 36 info, err := newMapInfoFromProc(hash.fd) 37 testutils.SkipIfNotSupported(t, err) 38 if err != nil { 39 t.Fatal("Can't get map info:", err) 40 } 41 42 if info.Type != Hash { 43 t.Error("Expected Hash, got", info.Type) 44 } 45 46 if info.KeySize != 4 { 47 t.Error("Expected KeySize of 4, got", info.KeySize) 48 } 49 50 if info.ValueSize != 5 { 51 t.Error("Expected ValueSize of 5, got", info.ValueSize) 52 } 53 54 if info.MaxEntries != 2 { 55 t.Error("Expected MaxEntries of 2, got", info.MaxEntries) 56 } 57 58 if info.Flags != unix.BPF_F_NO_PREALLOC { 59 t.Errorf("Expected Flags to be %d, got %d", unix.BPF_F_NO_PREALLOC, info.Flags) 60 } 61 62 if info.Name != "" && info.Name != "testing" { 63 t.Error("Expected name to be testing, got", info.Name) 64 } 65 66 if _, ok := info.ID(); ok { 67 t.Error("Expected ID to not be available") 68 } 69 70 nested, err := NewMap(&MapSpec{ 71 Type: ArrayOfMaps, 72 KeySize: 4, 73 MaxEntries: 2, 74 InnerMap: &MapSpec{ 75 Type: Array, 76 KeySize: 4, 77 ValueSize: 4, 78 MaxEntries: 2, 79 }, 80 }) 81 testutils.SkipIfNotSupported(t, err) 82 if err != nil { 83 t.Fatal(err) 84 } 85 defer nested.Close() 86 87 _, err = newMapInfoFromProc(nested.fd) 88 if err != nil { 89 t.Fatal("Can't get nested map info from /proc:", err) 90 } 91 } 92 93 func TestProgramInfo(t *testing.T) { 94 prog := mustSocketFilter(t) 95 96 for name, fn := range map[string]func(*sys.FD) (*ProgramInfo, error){ 97 "generic": newProgramInfoFromFd, 98 "proc": newProgramInfoFromProc, 99 } { 100 t.Run(name, func(t *testing.T) { 101 info, err := fn(prog.fd) 102 testutils.SkipIfNotSupported(t, err) 103 if err != nil { 104 t.Fatal("Can't get program info:", err) 105 } 106 107 if info.Type != SocketFilter { 108 t.Error("Expected Type to be SocketFilter, got", info.Type) 109 } 110 111 if info.Name != "" && info.Name != "test" { 112 t.Error("Expected Name to be test, got", info.Name) 113 } 114 115 if want := "d7edec644f05498d"; info.Tag != want { 116 t.Errorf("Expected Tag to be %s, got %s", want, info.Tag) 117 } 118 119 if id, ok := info.ID(); ok && id == 0 { 120 t.Error("Expected a valid ID:", id) 121 } else if name == "proc" && ok { 122 t.Error("Expected ID to not be available") 123 } 124 125 if name == "proc" { 126 _, ok := info.CreatedByUID() 127 qt.Assert(t, qt.IsFalse(ok)) 128 } else { 129 uid, ok := info.CreatedByUID() 130 if testutils.IsKernelLessThan(t, "4.15") { 131 qt.Assert(t, qt.IsFalse(ok)) 132 } else { 133 qt.Assert(t, qt.IsTrue(ok)) 134 qt.Assert(t, qt.Equals(uid, uint32(os.Getuid()))) 135 } 136 } 137 }) 138 } 139 } 140 141 func TestProgramInfoMapIDs(t *testing.T) { 142 arr, err := NewMap(&MapSpec{ 143 Type: Array, 144 KeySize: 4, 145 ValueSize: 4, 146 MaxEntries: 1, 147 }) 148 qt.Assert(t, qt.IsNil(err)) 149 defer arr.Close() 150 151 prog, err := NewProgram(&ProgramSpec{ 152 Type: SocketFilter, 153 Instructions: asm.Instructions{ 154 asm.LoadMapPtr(asm.R0, arr.FD()), 155 asm.LoadImm(asm.R0, 2, asm.DWord), 156 asm.Return(), 157 }, 158 License: "MIT", 159 }) 160 qt.Assert(t, qt.IsNil(err)) 161 defer prog.Close() 162 163 info, err := prog.Info() 164 testutils.SkipIfNotSupported(t, err) 165 qt.Assert(t, qt.IsNil(err)) 166 167 ids, ok := info.MapIDs() 168 switch { 169 case testutils.IsKernelLessThan(t, "4.15"): 170 qt.Assert(t, qt.IsFalse(ok)) 171 qt.Assert(t, qt.HasLen(ids, 0)) 172 173 default: 174 qt.Assert(t, qt.IsTrue(ok)) 175 176 mapInfo, err := arr.Info() 177 qt.Assert(t, qt.IsNil(err)) 178 179 mapID, ok := mapInfo.ID() 180 qt.Assert(t, qt.IsTrue(ok)) 181 qt.Assert(t, qt.ContentEquals(ids, []MapID{mapID})) 182 } 183 } 184 185 func TestProgramInfoMapIDsNoMaps(t *testing.T) { 186 prog, err := NewProgram(&ProgramSpec{ 187 Type: SocketFilter, 188 Instructions: asm.Instructions{ 189 asm.LoadImm(asm.R0, 0, asm.DWord), 190 asm.Return(), 191 }, 192 License: "MIT", 193 }) 194 qt.Assert(t, qt.IsNil(err)) 195 defer prog.Close() 196 197 info, err := prog.Info() 198 testutils.SkipIfNotSupported(t, err) 199 qt.Assert(t, qt.IsNil(err)) 200 201 ids, ok := info.MapIDs() 202 switch { 203 case testutils.IsKernelLessThan(t, "4.15"): 204 qt.Assert(t, qt.IsFalse(ok)) 205 qt.Assert(t, qt.HasLen(ids, 0)) 206 207 default: 208 qt.Assert(t, qt.IsTrue(ok)) 209 qt.Assert(t, qt.HasLen(ids, 0)) 210 } 211 } 212 213 func TestScanFdInfoReader(t *testing.T) { 214 tests := []struct { 215 fields map[string]interface{} 216 valid bool 217 }{ 218 {nil, true}, 219 {map[string]interface{}{"foo": new(string)}, true}, 220 {map[string]interface{}{"zap": new(string)}, false}, 221 {map[string]interface{}{"foo": new(int)}, false}, 222 } 223 224 for _, test := range tests { 225 err := scanFdInfoReader(strings.NewReader("foo:\tbar\n"), test.fields) 226 if test.valid { 227 if err != nil { 228 t.Errorf("fields %v returns an error: %s", test.fields, err) 229 } 230 } else { 231 if err == nil { 232 t.Errorf("fields %v doesn't return an error", test.fields) 233 } 234 } 235 } 236 } 237 238 // TestStats loads a BPF program once and executes back-to-back test runs 239 // of the program. See testStats for details. 240 func TestStats(t *testing.T) { 241 testutils.SkipOnOldKernel(t, "5.8", "BPF_ENABLE_STATS") 242 243 prog := mustSocketFilter(t) 244 245 pi, err := prog.Info() 246 if err != nil { 247 t.Errorf("failed to get ProgramInfo: %v", err) 248 } 249 250 rc, ok := pi.RunCount() 251 if !ok { 252 t.Errorf("expected run count info to be available") 253 } 254 if rc != 0 { 255 t.Errorf("expected a run count of 0 but got %d", rc) 256 } 257 258 rt, ok := pi.Runtime() 259 if !ok { 260 t.Errorf("expected runtime info to be available") 261 } 262 if rt != 0 { 263 t.Errorf("expected a runtime of 0ns but got %v", rt) 264 } 265 266 rm, ok := pi.RecursionMisses() 267 if !ok { 268 t.Errorf("expected recursion misses info to be available") 269 } 270 if rm != 0 { 271 t.Errorf("expected a recursion misses of 0 but got %v", rm) 272 } 273 274 if err := testStats(prog); err != nil { 275 t.Error(err) 276 } 277 } 278 279 // BenchmarkStats is a benchmark of TestStats. See testStats for details. 280 func BenchmarkStats(b *testing.B) { 281 testutils.SkipOnOldKernel(b, "5.8", "BPF_ENABLE_STATS") 282 283 prog := mustSocketFilter(b) 284 285 for n := 0; n < b.N; n++ { 286 if err := testStats(prog); err != nil { 287 b.Fatal(fmt.Errorf("iter %d: %w", n, err)) 288 } 289 } 290 } 291 292 // testStats implements the behaviour under test for TestStats 293 // and BenchmarkStats. First, a test run is executed with runtime statistics 294 // enabled, followed by another with runtime stats disabled. Counters are only 295 // expected to increase on the runs where runtime stats are enabled. 296 // 297 // Due to runtime behaviour on Go 1.14 and higher, the syscall backing 298 // (*Program).Test() could be invoked multiple times for each call to Test(), 299 // resulting in RunCount incrementing by more than one. Expecting RunCount to 300 // be of a specific value after a call to Test() is therefore not possible. 301 // See https://golang.org/doc/go1.14#runtime for more details. 302 func testStats(prog *Program) error { 303 in := internal.EmptyBPFContext 304 305 stats, err := EnableStats(uint32(unix.BPF_STATS_RUN_TIME)) 306 if err != nil { 307 return fmt.Errorf("failed to enable stats: %v", err) 308 } 309 defer stats.Close() 310 311 // Program execution with runtime statistics enabled. 312 // Should increase both runtime and run counter. 313 if _, _, err := prog.Test(in); err != nil { 314 return fmt.Errorf("failed to trigger program: %v", err) 315 } 316 317 pi, err := prog.Info() 318 if err != nil { 319 return fmt.Errorf("failed to get ProgramInfo: %v", err) 320 } 321 322 rc, ok := pi.RunCount() 323 if !ok { 324 return errors.New("expected run count info to be available") 325 } 326 if rc < 1 { 327 return fmt.Errorf("expected a run count of at least 1 but got %d", rc) 328 } 329 // Store the run count for the next invocation. 330 lc := rc 331 332 rt, ok := pi.Runtime() 333 if !ok { 334 return errors.New("expected runtime info to be available") 335 } 336 if rt == 0 { 337 return errors.New("expected a runtime other than 0ns") 338 } 339 // Store the runtime value for the next invocation. 340 lt := rt 341 342 if err := stats.Close(); err != nil { 343 return fmt.Errorf("failed to disable statistics: %v", err) 344 } 345 346 // Second program execution, with runtime statistics gathering disabled. 347 // Total runtime and run counters are not expected to increase. 348 if _, _, err := prog.Test(in); err != nil { 349 return fmt.Errorf("failed to trigger program: %v", err) 350 } 351 352 pi, err = prog.Info() 353 if err != nil { 354 return fmt.Errorf("failed to get ProgramInfo: %v", err) 355 } 356 357 rc, ok = pi.RunCount() 358 if !ok { 359 return errors.New("expected run count info to be available") 360 } 361 if rc != lc { 362 return fmt.Errorf("run count unexpectedly increased over previous value (current: %v, prev: %v)", rc, lc) 363 } 364 365 rt, ok = pi.Runtime() 366 if !ok { 367 return errors.New("expected runtime info to be available") 368 } 369 if rt != lt { 370 return fmt.Errorf("runtime unexpectedly increased over the previous value (current: %v, prev: %v)", rt, lt) 371 } 372 373 return nil 374 } 375 376 func TestHaveProgramInfoMapIDs(t *testing.T) { 377 testutils.CheckFeatureTest(t, haveProgramInfoMapIDs) 378 } 379 380 func TestProgInfoExtBTF(t *testing.T) { 381 testutils.SkipOnOldKernel(t, "5.0", "Program BTF (func/line_info)") 382 383 spec, err := LoadCollectionSpec(testutils.NativeFile(t, "testdata/loader-%s.elf")) 384 if err != nil { 385 t.Fatal(err) 386 } 387 388 var obj struct { 389 Main *Program `ebpf:"xdp_prog"` 390 } 391 392 err = spec.LoadAndAssign(&obj, nil) 393 testutils.SkipIfNotSupported(t, err) 394 if err != nil { 395 t.Fatal(err) 396 } 397 defer obj.Main.Close() 398 399 info, err := obj.Main.Info() 400 if err != nil { 401 t.Fatal(err) 402 } 403 404 inst, err := info.Instructions() 405 if err != nil { 406 t.Fatal(err) 407 } 408 409 expectedLineInfoCount := 26 410 expectedFuncInfo := map[string]bool{ 411 "xdp_prog": false, 412 "static_fn": false, 413 "global_fn2": false, 414 "global_fn3": false, 415 } 416 417 lineInfoCount := 0 418 419 for _, ins := range inst { 420 if ins.Source() != nil { 421 lineInfoCount++ 422 } 423 424 fn := btf.FuncMetadata(&ins) 425 if fn != nil { 426 expectedFuncInfo[fn.Name] = true 427 } 428 } 429 430 if lineInfoCount != expectedLineInfoCount { 431 t.Errorf("expected %d line info entries, got %d", expectedLineInfoCount, lineInfoCount) 432 } 433 434 for fn, found := range expectedFuncInfo { 435 if !found { 436 t.Errorf("func %q not found", fn) 437 } 438 } 439 } 440 441 func TestInfoExportedFields(t *testing.T) { 442 // It is highly unlikely that you should be adjusting the asserts below. 443 // See the comment at the top of info.go for more information. 444 445 var names []string 446 for _, field := range reflect.VisibleFields(reflect.TypeOf(MapInfo{})) { 447 if field.IsExported() { 448 names = append(names, field.Name) 449 } 450 } 451 qt.Assert(t, qt.ContentEquals(names, []string{ 452 "Type", 453 "KeySize", 454 "ValueSize", 455 "MaxEntries", 456 "Flags", 457 "Name", 458 })) 459 460 names = nil 461 for _, field := range reflect.VisibleFields(reflect.TypeOf(ProgramInfo{})) { 462 if field.IsExported() { 463 names = append(names, field.Name) 464 } 465 } 466 qt.Assert(t, qt.ContentEquals(names, []string{ 467 "Type", 468 "Tag", 469 "Name", 470 })) 471 }