github.com/cilium/cilium@v1.16.2/bpf/tests/bpftest/bpf_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 //go:generate protoc --go_out=. trf.proto 5 package bpftests 6 7 import ( 8 "bufio" 9 "bytes" 10 "encoding/binary" 11 "encoding/hex" 12 "errors" 13 "flag" 14 "fmt" 15 "io" 16 "io/fs" 17 "os" 18 "path" 19 "regexp" 20 "sort" 21 "strings" 22 "testing" 23 "time" 24 25 "github.com/cilium/coverbee" 26 "github.com/cilium/ebpf" 27 "github.com/cilium/ebpf/perf" 28 "github.com/cilium/ebpf/rlimit" 29 "github.com/davecgh/go-spew/spew" 30 "github.com/vishvananda/netlink/nl" 31 "golang.org/x/tools/cover" 32 "google.golang.org/protobuf/encoding/protowire" 33 "google.golang.org/protobuf/proto" 34 35 "github.com/cilium/cilium/pkg/bpf" 36 "github.com/cilium/cilium/pkg/byteorder" 37 "github.com/cilium/cilium/pkg/datapath/link" 38 "github.com/cilium/cilium/pkg/monitor" 39 ) 40 41 var ( 42 testPath = flag.String("bpf-test-path", "", "Path to the eBPF tests") 43 testCoverageReport = flag.String("coverage-report", "", "Specify a path for the coverage report") 44 testCoverageFormat = flag.String("coverage-format", "html", "Specify the format of the coverage report") 45 noTestCoverage = flag.String("no-test-coverage", "", "Don't collect coverages for the file matches to the given regex") 46 testInstrumentationLog = flag.String("instrumentation-log", "", "Path to a log file containing details about"+ 47 " code coverage instrumentation, needed if code coverage breaks the verifier") 48 testFilePrefix = flag.String("test", "", "Single test file to run (without file extension)") 49 50 dumpCtx = flag.Bool("dump-ctx", false, "If set, the program context will be dumped after a CHECK and SETUP run.") 51 ) 52 53 func TestBPF(t *testing.T) { 54 if testPath == nil || *testPath == "" { 55 t.Skip("Set -bpf-test-path to run BPF tests") 56 } 57 58 if err := rlimit.RemoveMemlock(); err != nil { 59 t.Log(err) 60 } 61 62 entries, err := os.ReadDir(*testPath) 63 if err != nil { 64 t.Fatal("os readdir: ", err) 65 } 66 67 var instrLog io.Writer 68 if *testInstrumentationLog != "" { 69 instrLogFile, err := os.Create(*testInstrumentationLog) 70 if err != nil { 71 t.Fatal("os create instrumentation log: ", err) 72 } 73 defer instrLogFile.Close() 74 75 buf := bufio.NewWriter(instrLogFile) 76 instrLog = buf 77 defer buf.Flush() 78 } 79 80 mergedProfiles := make([]*cover.Profile, 0) 81 82 for _, entry := range entries { 83 if entry.IsDir() { 84 continue 85 } 86 87 if !strings.HasSuffix(entry.Name(), ".o") { 88 continue 89 } 90 91 if *testFilePrefix != "" && !strings.HasPrefix(entry.Name(), *testFilePrefix) { 92 continue 93 } 94 95 t.Run(entry.Name(), func(t *testing.T) { 96 profiles := loadAndRunSpec(t, entry, instrLog) 97 for _, profile := range profiles { 98 if len(profile.Blocks) > 0 { 99 mergedProfiles = addProfile(mergedProfiles, profile) 100 } 101 } 102 }) 103 } 104 105 if *testCoverageReport != "" { 106 coverReport, err := os.Create(*testCoverageReport) 107 if err != nil { 108 t.Fatalf("create coverage report: %s", err.Error()) 109 } 110 defer coverReport.Close() 111 112 switch *testCoverageFormat { 113 case "html": 114 if err = coverbee.HTMLOutput(mergedProfiles, coverReport); err != nil { 115 t.Fatalf("create HTML coverage report: %s", err.Error()) 116 } 117 case "go-cover", "cover": 118 coverbee.ProfilesToGoCover(mergedProfiles, coverReport, "count") 119 default: 120 t.Fatal("unknown output format") 121 } 122 } 123 } 124 125 func loadAndRunSpec(t *testing.T, entry fs.DirEntry, instrLog io.Writer) []*cover.Profile { 126 elfPath := path.Join(*testPath, entry.Name()) 127 128 if instrLog != nil { 129 fmt.Fprintln(instrLog, "===", elfPath, "===") 130 } 131 132 spec := loadAndPrepSpec(t, elfPath) 133 134 var ( 135 coll *ebpf.Collection 136 cfg []*coverbee.BasicBlock 137 err error 138 collectCoverage bool 139 ) 140 141 if *testCoverageReport != "" { 142 if *noTestCoverage != "" { 143 matched, err := regexp.MatchString(*noTestCoverage, entry.Name()) 144 if err != nil { 145 t.Fatal("test file regex matching failed:", err) 146 } 147 148 if matched { 149 t.Logf("Disabling coverage report for %s", entry.Name()) 150 } 151 152 collectCoverage = !matched 153 } else { 154 collectCoverage = true 155 } 156 } 157 158 if !collectCoverage { 159 coll, _, err = bpf.LoadCollection(spec, nil) 160 } else { 161 coll, cfg, err = coverbee.InstrumentAndLoadCollection(spec, ebpf.CollectionOptions{ 162 Programs: ebpf.ProgramOptions{ 163 // 64 MiB, not needed in most cases, except when running instrumented code. 164 LogSize: 64 << 20, 165 }, 166 }, instrLog) 167 } 168 169 var ve *ebpf.VerifierError 170 if errors.As(err, &ve) { 171 if ve.Truncated { 172 t.Fatal("Verifier log exceeds 64MiB, increase LogSize passed to coverbee.InstrumentAndLoadCollection") 173 } 174 t.Fatalf("verifier error: %+v", ve) 175 } 176 if err != nil { 177 t.Fatal("loading collection:", err) 178 } 179 defer coll.Close() 180 181 testNameToPrograms := make(map[string]programSet) 182 183 for progName, spec := range spec.Programs { 184 match := checkProgRegex.FindStringSubmatch(spec.SectionName) 185 if len(match) == 0 { 186 continue 187 } 188 189 progs := testNameToPrograms[match[1]] 190 if match[2] == "pktgen" { 191 progs.pktgenProg = coll.Programs[progName] 192 } 193 if match[2] == "setup" { 194 progs.setupProg = coll.Programs[progName] 195 } 196 if match[2] == "check" { 197 progs.checkProg = coll.Programs[progName] 198 } 199 testNameToPrograms[match[1]] = progs 200 } 201 202 for progName, set := range testNameToPrograms { 203 if set.checkProg == nil { 204 t.Fatalf( 205 "File '%s' contains a setup program in section '%s' but no check program.", 206 elfPath, 207 spec.Programs[progName].SectionName, 208 ) 209 } 210 } 211 212 // Collect debug events and add them as logs of the main test 213 var globalLogReader *perf.Reader 214 if m := coll.Maps["test_cilium_events"]; m != nil { 215 globalLogReader, err = perf.NewReader(m, os.Getpagesize()*16) 216 if err != nil { 217 t.Fatalf("new global log reader: %s", err.Error()) 218 } 219 defer globalLogReader.Close() 220 221 linkCache := link.NewLinkCache() 222 223 go func() { 224 for { 225 rec, err := globalLogReader.Read() 226 if err != nil { 227 return 228 } 229 230 dm := monitor.DebugMsg{} 231 reader := bytes.NewReader(rec.RawSample) 232 if err := binary.Read(reader, byteorder.Native, &dm); err != nil { 233 return 234 } 235 236 t.Log(dm.Message(linkCache)) 237 } 238 }() 239 } 240 241 // Make sure sub-tests are executed in alphabetic order, to make test results repeatable if programs rely on 242 // the order of execution. 243 testNames := make([]string, 0, len(testNameToPrograms)) 244 for name := range testNameToPrograms { 245 testNames = append(testNames, name) 246 } 247 sort.Strings(testNames) 248 249 // Get maps used for common mocking facilities 250 skbMdMap := coll.Maps[mockSkbMetaMap] 251 252 for _, name := range testNames { 253 t.Run(name, subTest(testNameToPrograms[name], coll.Maps[suiteResultMap], skbMdMap)) 254 } 255 256 if globalLogReader != nil { 257 // Give the global log buf some time to empty 258 // TODO: replace with flush on the buffer, as soon as cilium/ebpf supports that 259 time.Sleep(50 * time.Millisecond) 260 } 261 262 if !collectCoverage { 263 return nil 264 } 265 266 blocklist := coverbee.CFGToBlockList(cfg) 267 if err = coverbee.ApplyCoverMapToBlockList(coll.Maps["coverbee_covermap"], blocklist); err != nil { 268 t.Fatalf("apply covermap to blocklist: %s", err.Error()) 269 } 270 271 outBlocks, err := coverbee.SourceCodeInterpolation(blocklist, nil) 272 if err != nil { 273 t.Fatalf("error while interpolating using source files: %s", err) 274 } 275 276 var buf bytes.Buffer 277 coverbee.BlockListToGoCover(outBlocks, &buf, "count") 278 profiles, err := cover.ParseProfilesFromReader(&buf) 279 if err != nil { 280 t.Fatalf("parse profiles: %s", err.Error()) 281 } 282 283 return profiles 284 } 285 286 func loadAndPrepSpec(t *testing.T, elfPath string) *ebpf.CollectionSpec { 287 spec, err := bpf.LoadCollectionSpec(elfPath) 288 if err != nil { 289 t.Fatalf("load spec %s: %v", elfPath, err) 290 } 291 292 for _, m := range spec.Maps { 293 m.Pinning = ebpf.PinNone 294 } 295 296 for n, p := range spec.Programs { 297 switch p.Type { 298 case ebpf.XDP, ebpf.SchedACT, ebpf.SchedCLS: 299 continue 300 } 301 302 t.Logf("Skipping program '%s' of type '%s': BPF_PROG_RUN not supported", p.Name, p.Type) 303 delete(spec.Programs, n) 304 } 305 306 return spec 307 } 308 309 type programSet struct { 310 pktgenProg *ebpf.Program 311 setupProg *ebpf.Program 312 checkProg *ebpf.Program 313 } 314 315 var checkProgRegex = regexp.MustCompile(`[^/]+/test/([^/]+)/((?:check)|(?:setup)|(?:pktgen))`) 316 317 const ( 318 ResultSuccess = 1 319 320 suiteResultMap = "suite_result_map" 321 mockSkbMetaMap = "mock_skb_meta_map" 322 ) 323 324 func subTest(progSet programSet, resultMap *ebpf.Map, skbMdMap *ebpf.Map) func(t *testing.T) { 325 return func(t *testing.T) { 326 // create ctx with the max allowed size(4k - head room - tailroom) 327 data := make([]byte, 4096-256-320) 328 329 // ctx is only used for tc programs 330 // non-empty ctx passed to non-tc programs will cause error: invalid argument 331 ctx := make([]byte, 0) 332 if progSet.checkProg.Type() == ebpf.SchedCLS { 333 // sizeof(struct __sk_buff) < 256, let's make it 256 334 ctx = make([]byte, 256) 335 } 336 337 var ( 338 statusCode uint32 339 err error 340 ) 341 if progSet.pktgenProg != nil { 342 if statusCode, data, ctx, err = runBpfProgram(progSet.pktgenProg, data, ctx); err != nil { 343 t.Fatalf("error while running pktgen prog: %s", err) 344 } 345 346 if *dumpCtx { 347 t.Log("Pktgen returned status: ") 348 t.Log(statusCode) 349 t.Log("data after pktgen: ") 350 t.Log(spew.Sdump(data)) 351 t.Log("ctx after pktgen: ") 352 t.Log(spew.Sdump(ctx)) 353 } 354 } 355 356 if progSet.setupProg != nil { 357 if statusCode, data, ctx, err = runBpfProgram(progSet.setupProg, data, ctx); err != nil { 358 t.Fatalf("error while running setup prog: %s", err) 359 } 360 361 if *dumpCtx { 362 t.Log("Setup returned status: ") 363 t.Log(statusCode) 364 t.Log("data after setup: ") 365 t.Log(spew.Sdump(data)) 366 t.Log("ctx after setup: ") 367 t.Log(spew.Sdump(ctx)) 368 } 369 370 status := make([]byte, 4) 371 nl.NativeEndian().PutUint32(status, statusCode) 372 data = append(status, data...) 373 } 374 375 // Run test, input a 376 if statusCode, data, ctx, err = runBpfProgram(progSet.checkProg, data, ctx); err != nil { 377 t.Fatal("error while running check program:", err) 378 } 379 380 if *dumpCtx { 381 t.Log("Check returned status: ") 382 t.Log(statusCode) 383 t.Logf("data after check: %d", len(data)) 384 t.Log(spew.Sdump(data)) 385 t.Log("ctx after check: ") 386 t.Log(spew.Sdump(ctx)) 387 } 388 389 // Clear map value after each test 390 defer func() { 391 for _, m := range []*ebpf.Map{resultMap, skbMdMap} { 392 if m == nil { 393 continue 394 } 395 396 var key int32 397 value := make([]byte, m.ValueSize()) 398 m.Lookup(&key, &value) 399 for i := 0; i < len(value); i++ { 400 value[i] = 0 401 } 402 m.Update(&key, &value, ebpf.UpdateAny) 403 } 404 }() 405 406 var key int32 407 value := make([]byte, resultMap.ValueSize()) 408 err = resultMap.Lookup(&key, &value) 409 if err != nil { 410 t.Fatal("error while getting suite result:", err) 411 } 412 413 // Detect the length of the result, since the proto.Unmarshal doesn't like trailing zeros. 414 valueLen := 0 415 valueC := value 416 for { 417 _, _, len := protowire.ConsumeField(valueC) 418 if len <= 0 { 419 break 420 } 421 valueLen += len 422 valueC = valueC[len:] 423 } 424 425 result := &SuiteResult{} 426 err = proto.Unmarshal(value[:valueLen], result) 427 if err != nil { 428 t.Fatal("error while unmarshalling suite result:", err) 429 } 430 431 for _, testResult := range result.Results { 432 // Remove the C-string, null-terminator. 433 name := strings.TrimSuffix(testResult.Name, "\x00") 434 t.Run(name, func(tt *testing.T) { 435 if len(testResult.TestLog) > 0 && testing.Verbose() || testResult.Status != SuiteResult_TestResult_PASS { 436 for _, log := range testResult.TestLog { 437 tt.Logf("%s", log.FmtString()) 438 } 439 } 440 441 switch testResult.Status { 442 case SuiteResult_TestResult_ERROR: 443 tt.Fatal("Test failed due to unknown error in test framework") 444 case SuiteResult_TestResult_FAIL: 445 tt.Fail() 446 case SuiteResult_TestResult_SKIP: 447 tt.Skip() 448 } 449 }) 450 } 451 452 if len(result.SuiteLog) > 0 && testing.Verbose() || 453 SuiteResult_TestResult_TestStatus(statusCode) != SuiteResult_TestResult_PASS { 454 for _, log := range result.SuiteLog { 455 t.Logf("%s", log.FmtString()) 456 } 457 } 458 459 switch SuiteResult_TestResult_TestStatus(statusCode) { 460 case SuiteResult_TestResult_ERROR: 461 t.Fatal("Test failed due to unknown error in test framework") 462 case SuiteResult_TestResult_FAIL: 463 t.Fail() 464 case SuiteResult_TestResult_SKIP: 465 t.SkipNow() 466 } 467 } 468 } 469 470 // A simplified version of fmt.Printf logic, the meaning of % specifiers changed to match the kernels printk specifiers. 471 // In the eBPF code a user can for example call `test_log("expected 123, got %llu", some_val)` the %llu meaning 472 // long-long-unsigned translates into a uint64, the rendered out would for example be -> 'expected 123, got 234'. 473 // https://www.kernel.org/doc/Documentation/printk-formats.txt 474 // https://github.com/libbpf/libbpf/blob/4eb6485c08867edaa5a0a81c64ddb23580420340/src/bpf_helper_defs.h#L152 475 func (l *Log) FmtString() string { 476 var sb strings.Builder 477 478 end := len(l.Fmt) 479 argNum := 0 480 481 for i := 0; i < end; { 482 lasti := i 483 for i < end && l.Fmt[i] != '%' { 484 i++ 485 } 486 if i > lasti { 487 sb.WriteString(strings.TrimSuffix(l.Fmt[lasti:i], "\x00")) 488 } 489 if i >= end { 490 // done processing format string 491 break 492 } 493 494 // Process one verb 495 i++ 496 497 var spec []byte 498 loop: 499 for ; i < end; i++ { 500 c := l.Fmt[i] 501 switch c { 502 case 'd', 'i', 'u', 'x', 's': 503 spec = append(spec, c) 504 break loop 505 case 'l': 506 spec = append(spec, c) 507 default: 508 break loop 509 } 510 } 511 // Advance to to next char 512 i++ 513 514 // No argument left over to print for the current verb. 515 if argNum >= len(l.Args) { 516 sb.WriteString("%!") 517 sb.WriteString(string(spec)) 518 sb.WriteString("(MISSING)") 519 continue 520 } 521 522 switch string(spec) { 523 case "u": 524 fmt.Fprint(&sb, uint16(l.Args[argNum])) 525 case "d", "i", "s": 526 fmt.Fprint(&sb, int16(l.Args[argNum])) 527 case "x": 528 hb := make([]byte, 2) 529 binary.BigEndian.PutUint16(hb, uint16(l.Args[argNum])) 530 fmt.Fprint(&sb, hex.EncodeToString(hb)) 531 532 case "lu": 533 fmt.Fprint(&sb, uint32(l.Args[argNum])) 534 case "ld", "li", "ls": 535 fmt.Fprint(&sb, int32(l.Args[argNum])) 536 case "lx": 537 hb := make([]byte, 4) 538 binary.BigEndian.PutUint32(hb, uint32(l.Args[argNum])) 539 fmt.Fprint(&sb, hex.EncodeToString(hb)) 540 541 case "llu": 542 fmt.Fprint(&sb, uint64(l.Args[argNum])) 543 case "lld", "lli", "lls": 544 fmt.Fprint(&sb, int64(l.Args[argNum])) 545 case "llx": 546 hb := make([]byte, 8) 547 binary.BigEndian.PutUint64(hb, uint64(l.Args[argNum])) 548 fmt.Fprint(&sb, hex.EncodeToString(hb)) 549 550 default: 551 sb.WriteString("%!") 552 sb.WriteString(string(spec)) 553 sb.WriteString("(INVALID)") 554 continue 555 } 556 557 argNum++ 558 } 559 560 return sb.String() 561 } 562 563 func runBpfProgram(prog *ebpf.Program, data, ctx []byte) (statusCode uint32, dataOut, ctxOut []byte, err error) { 564 dataOut = make([]byte, len(data)) 565 if len(dataOut) > 0 { 566 // See comments at https://github.com/cilium/ebpf/blob/20c4d8896bdde990ce6b80d59a4262aa3ccb891d/prog.go#L563-L567 567 dataOut = make([]byte, len(data)+256+2) 568 } 569 ctxOut = make([]byte, len(ctx)) 570 opts := &ebpf.RunOptions{ 571 Data: data, 572 DataOut: dataOut, 573 Context: ctx, 574 ContextOut: ctxOut, 575 Repeat: 1, 576 } 577 ret, err := prog.Run(opts) 578 return ret, opts.DataOut, ctxOut, err 579 }