Raw File
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package verifier_test

import (
	"bufio"
	"bytes"
	"errors"
	"flag"
	"fmt"
	"io"
	"math"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"testing"

	"github.com/cilium/ebpf"
	"github.com/cilium/ebpf/rlimit"
	"github.com/sirupsen/logrus"
	"golang.org/x/sys/unix"

	"github.com/cilium/cilium/pkg/bpf"
	"github.com/cilium/cilium/pkg/datapath/linux/probes"
	"github.com/cilium/cilium/pkg/logging"
)

var (
	ciliumBasePath  = flag.String("cilium-base-path", "", "Cilium checkout base path")
	ciKernelVersion = flag.String("ci-kernel-version", "", "CI kernel version to assume for verifier tests (supported values: 54, 510, 61, netnext)")
)

func getCIKernelVersion(t *testing.T) (string, string) {
	t.Helper()

	var uts unix.Utsname
	if err := unix.Uname(&uts); err != nil {
		t.Fatalf("uname: %v", err)
	}
	release := unix.ByteSliceToString(uts.Release[:])
	t.Logf("Running kernel version: %s", release)

	if ciKernelVersion != nil && *ciKernelVersion != "" {
		return *ciKernelVersion, "cli"
	}

	var ciKernel string
	switch {
	case strings.HasPrefix(release, "5.4"):
		ciKernel = "54"
	case strings.HasPrefix(release, "5.10"):
		ciKernel = "510"
	case strings.HasPrefix(release, "6.1"):
		ciKernel = "61"
	case strings.HasPrefix(release, "bpf-next"):
		ciKernel = "netnext"
	default:
		t.Fatalf("detected kernel version %s not supported by verifier complexity tests, specify using -ci-kernel-version", release)
	}

	return ciKernel, "detected"
}

func getDatapathConfigFiles(t *testing.T, ciKernelVersion, bpfProgram string) []string {
	t.Helper()

	pattern := filepath.Join("bpf", "complexity-tests", ciKernelVersion, bpfProgram, "*.txt")
	files, err := filepath.Glob(filepath.Join(*ciliumBasePath, pattern))
	if err != nil {
		t.Fatal(err)
	}

	if len(files) == 0 {
		t.Fatal("No files match", pattern)
	}

	return files
}

// readDatapathConfig turns each line in a reader into a single line with each
// element separated by spaces.
func readDatapathConfig(t *testing.T, r io.Reader) string {
	scanner := bufio.NewScanner(r)
	var lines []string
	for scanner.Scan() {
		lines = append(lines, strings.TrimSpace(scanner.Text()))
	}

	if err := scanner.Err(); err != nil {
		t.Fatal(err)
	}

	return strings.Join(lines, " ")
}

// This test tries to compile BPF programs with a set of options that maximize
// size & complexity (as defined in bpf/complexity-tests). Programs are then
// loaded into the kernel to detect complexity & other verifier-related
// regressions.
func TestVerifier(t *testing.T) {
	flag.Parse()

	logging.DefaultLogger.SetLevel(logrus.DebugLevel)

	if ciliumBasePath == nil || *ciliumBasePath == "" {
		t.Skip("Please set -cilium-base-path to run verifier tests")
	}
	t.Logf("Cilium checkout base path: %s", *ciliumBasePath)

	if err := rlimit.RemoveMemlock(); err != nil {
		t.Fatal(err)
	}

	kernelVersion, source := getCIKernelVersion(t)
	t.Logf("CI kernel version: %s (%s)", kernelVersion, source)

	for _, bpfProgram := range []struct {
		name      string
		macroName string
	}{
		{
			name:      "bpf_lxc",
			macroName: "MAX_LXC_OPTIONS",
		},
		{
			name:      "bpf_host",
			macroName: "MAX_HOST_OPTIONS",
		},
		{
			name:      "bpf_xdp",
			macroName: "MAX_XDP_OPTIONS",
		},
		{
			name:      "bpf_overlay",
			macroName: "MAX_OVERLAY_OPTIONS",
		},
		{
			name:      "bpf_sock",
			macroName: "MAX_LB_OPTIONS",
		},
		{
			name:      "bpf_network",
			macroName: "BPF_SIMPLE_OPTIONS",
		},
	} {
		t.Run(bpfProgram.name, func(t *testing.T) {
			initObjFile := path.Join(*ciliumBasePath, "bpf", fmt.Sprintf("%s.o", bpfProgram.name))

			fileNames := getDatapathConfigFiles(t, kernelVersion, bpfProgram.name)
			for _, fileName := range fileNames {
				configName := strings.TrimSuffix(filepath.Base(fileName), filepath.Ext(fileName))
				t.Run(configName, func(t *testing.T) {
					file, err := os.Open(fileName)
					if err != nil {
						t.Fatalf("Unable to open configuration: %v", err)
					}
					defer file.Close()

					datapathConfig := readDatapathConfig(t, file)

					name := fmt.Sprintf("%s_%s", bpfProgram.name, configName)
					cmd := exec.Command("make", "-C", "bpf", "clean", fmt.Sprintf("%s.o", bpfProgram.name))
					cmd.Dir = *ciliumBasePath
					cmd.Env = append(os.Environ(),
						fmt.Sprintf("%s=%s", bpfProgram.macroName, datapathConfig),
						fmt.Sprintf("KERNEL=%s", kernelVersion),
					)
					t.Logf("Compiling with %q", cmd.Args)
					t.Logf("Env is %q", cmd.Env)
					if out, err := cmd.CombinedOutput(); err != nil {
						t.Logf("Command output:\n%s", string(out))
						t.Fatalf("Failed to compile bpf objects: %v", err)
					}

					objFile := filepath.Join("./", name+".o")
					// Rename object file to avoid subsequent runs to overwrite it,
					// so we can keep it for CI's artifact upload.
					if err = os.Rename(initObjFile, objFile); err != nil {
						t.Fatalf("Failed to rename %s to %s: %v", initObjFile, objFile, err)
					}

					// Parse the compiled object into a CollectionSpec.
					spec, err := bpf.LoadCollectionSpec(objFile)
					if err != nil {
						t.Fatal(err)
					}

					// Delete unsupported programs from the spec.
					for n, p := range spec.Programs {
						err := probes.HaveAttachType(p.Type, p.AttachType)
						if errors.Is(err, ebpf.ErrNotSupported) {
							t.Logf("%s: skipped unsupported program/attach type (%s/%s)", n, p.Type, p.AttachType)
							delete(spec.Programs, n)
							continue
						}
						if err != nil {
							t.Fatal(err)
						}
					}

					// Strip all pinning flags so we don't need to specify a pin path.
					// This creates new maps for every Collection.
					for _, m := range spec.Maps {
						m.Pinning = ebpf.PinNone
					}

					coll, _, err := bpf.LoadCollection(spec, &bpf.CollectionOptions{
						CollectionOptions: ebpf.CollectionOptions{
							// Enable verifier logs for successful loads.
							// Use log level 1 since it's known by all target kernels.
							Programs: ebpf.ProgramOptions{
								// Maximum log size for kernels <5.2. Some programs generate a
								// verifier log of over 8MiB, so avoid retries due to the initial
								// size being too small. This saves a lot of time as retrying means
								// reloading all maps and progs in the collection.
								LogSize:  (math.MaxUint32 >> 8), // 16MiB
								LogLevel: ebpf.LogLevelBranch,
							},
						},
					})
					var ve *ebpf.VerifierError
					if errors.As(err, &ve) {
						// Write full verifier log to a path on disk for offline analysis.
						var buf bytes.Buffer
						fmt.Fprintf(&buf, "%+v", ve)
						fullLogFile := name + "_verifier.log"
						_ = os.WriteFile(fullLogFile, buf.Bytes(), 0444)
						t.Log("Full verifier log at", fullLogFile)

						// Print unverified instruction count.
						t.Log("BPF unverified instruction count per program:")
						for n, p := range spec.Programs {
							t.Logf("\t%s: %d insns", n, len(p.Instructions))
						}

						// Include the original err in the output since it contains the name
						// of the program that triggered the verifier error.
						// ebpf.VerifierError only contains the return code and verifier log
						// buffer.
						t.Fatalf("Error: %v\nVerifier error tail: %-10v", err, ve)
					}
					if err != nil {
						t.Fatal(err)
					}
					defer coll.Close()

					// Print verifier stats appearing on the last line of the log, e.g.
					// 'processed 12248 insns (limit 1000000) ...'.
					// Sort by program names for stable output.
					names := make([]string, 0, len(coll.Programs))
					for n := range coll.Programs {
						names = append(names, n)
					}
					sort.Strings(names)
					for _, n := range names {
						p := coll.Programs[n]
						p.VerifierLog = strings.TrimRight(p.VerifierLog, "\n")
						// Offset points at the last newline, increment by 1 to skip it.
						// Turn a -1 into a 0 if there are no newlines in the log.
						lastOff := strings.LastIndex(p.VerifierLog, "\n") + 1
						t.Logf("%s: %v (%d unverified insns)", n, p.VerifierLog[lastOff:], len(spec.Programs[n].Instructions))
					}
				})
			}
		})
	}
}
back to top