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  }