github.com/cilium/cilium@v1.16.2/pkg/datapath/loader/compile.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package loader
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"path"
    15  	"path/filepath"
    16  	"strings"
    17  	"sync"
    18  	"syscall"
    19  
    20  	"github.com/cilium/ebpf"
    21  	"github.com/cilium/ebpf/asm"
    22  	"github.com/sirupsen/logrus"
    23  
    24  	"github.com/cilium/cilium/pkg/command/exec"
    25  	"github.com/cilium/cilium/pkg/datapath/linux/probes"
    26  	"github.com/cilium/cilium/pkg/datapath/types"
    27  	"github.com/cilium/cilium/pkg/lock"
    28  	"github.com/cilium/cilium/pkg/logging/logfields"
    29  	"github.com/cilium/cilium/pkg/option"
    30  )
    31  
    32  // outputType determines the type to be generated by the compilation steps.
    33  type outputType string
    34  
    35  const (
    36  	outputObject = outputType("obj")
    37  	outputSource = outputType("c")
    38  
    39  	compiler = "clang"
    40  
    41  	endpointPrefix = "bpf_lxc"
    42  	endpointProg   = endpointPrefix + "." + string(outputSource)
    43  	endpointObj    = endpointPrefix + ".o"
    44  
    45  	hostEndpointPrefix       = "bpf_host"
    46  	hostEndpointNetdevPrefix = "bpf_netdev_"
    47  	hostEndpointProg         = hostEndpointPrefix + "." + string(outputSource)
    48  	hostEndpointObj          = hostEndpointPrefix + ".o"
    49  
    50  	networkPrefix = "bpf_network"
    51  	networkProg   = networkPrefix + "." + string(outputSource)
    52  	networkObj    = networkPrefix + ".o"
    53  
    54  	xdpPrefix = "bpf_xdp"
    55  	xdpProg   = xdpPrefix + "." + string(outputSource)
    56  	xdpObj    = xdpPrefix + ".o"
    57  
    58  	overlayPrefix = "bpf_overlay"
    59  	overlayProg   = overlayPrefix + "." + string(outputSource)
    60  	overlayObj    = overlayPrefix + ".o"
    61  
    62  	wireguardPrefix = "bpf_wireguard"
    63  	wireguardProg   = wireguardPrefix + "." + string(outputSource)
    64  	wireguardObj    = wireguardPrefix + ".o"
    65  )
    66  
    67  var (
    68  	probeCPUOnce sync.Once
    69  
    70  	// default fallback
    71  	nameBPFCPU = "v1"
    72  )
    73  
    74  // progInfo describes a program to be compiled with the expected output format
    75  type progInfo struct {
    76  	// Source is the program source (base) filename to be compiled
    77  	Source string
    78  	// Output is the expected (base) filename produced from the source
    79  	Output string
    80  	// OutputType to be created by LLVM
    81  	OutputType outputType
    82  	// Options are passed directly to LLVM as individual parameters
    83  	Options []string
    84  }
    85  
    86  func (pi *progInfo) AbsoluteOutput(dir *directoryInfo) string {
    87  	return filepath.Join(dir.Output, pi.Output)
    88  }
    89  
    90  // directoryInfo includes relevant directories for compilation and linking
    91  type directoryInfo struct {
    92  	// Library contains the library code to be used for compilation
    93  	Library string
    94  	// Runtime contains headers for compilation
    95  	Runtime string
    96  	// State contains node, lxc, and features headers for templatization
    97  	State string
    98  	// Output is the directory where the files will be stored
    99  	Output string
   100  }
   101  
   102  var (
   103  	standardCFlags = []string{"-O2", "--target=bpf", "-std=gnu89",
   104  		"-nostdinc",
   105  		"-Wall", "-Wextra", "-Werror", "-Wshadow",
   106  		"-Wno-address-of-packed-member",
   107  		"-Wno-unknown-warning-option",
   108  		"-Wno-gnu-variable-sized-type-not-at-end",
   109  		"-Wdeclaration-after-statement",
   110  		"-Wimplicit-int-conversion",
   111  		"-Wenum-conversion"}
   112  
   113  	// testIncludes allows the unit tests to inject additional include
   114  	// paths into the compile command at test time. It is usually nil.
   115  	testIncludes []string
   116  
   117  	epProg = &progInfo{
   118  		Source:     endpointProg,
   119  		Output:     endpointObj,
   120  		OutputType: outputObject,
   121  	}
   122  	hostEpProg = &progInfo{
   123  		Source:     hostEndpointProg,
   124  		Output:     hostEndpointObj,
   125  		OutputType: outputObject,
   126  	}
   127  	networkTcProg = &progInfo{
   128  		Source:     networkProg,
   129  		Output:     networkObj,
   130  		OutputType: outputObject,
   131  	}
   132  )
   133  
   134  // getBPFCPU returns the BPF CPU for this host.
   135  func getBPFCPU() string {
   136  	probeCPUOnce.Do(func() {
   137  		if !option.Config.DryMode {
   138  			// We can probe the availability of BPF instructions indirectly
   139  			// based on what kernel helpers are available when both were
   140  			// added in the same release.
   141  			// We want to enable v3 only on kernels 5.10+ where we have
   142  			// tested it and need it to work around complexity issues.
   143  			if probes.HaveV3ISA() == nil {
   144  				if probes.HaveProgramHelper(ebpf.SchedCLS, asm.FnRedirectNeigh) == nil {
   145  					nameBPFCPU = "v3"
   146  					return
   147  				}
   148  			}
   149  			// We want to enable v2 on all kernels that support it, that is,
   150  			// kernels 4.14+.
   151  			if probes.HaveV2ISA() == nil {
   152  				nameBPFCPU = "v2"
   153  			}
   154  		}
   155  	})
   156  	return nameBPFCPU
   157  }
   158  
   159  func pidFromProcess(proc *os.Process) string {
   160  	result := "not-started"
   161  	if proc != nil {
   162  		result = fmt.Sprintf("%d", proc.Pid)
   163  	}
   164  	return result
   165  }
   166  
   167  // compile and optionally link a program.
   168  //
   169  // May output assembly or source code after prepocessing.
   170  func compile(ctx context.Context, prog *progInfo, dir *directoryInfo) (string, error) {
   171  	possibleCPUs, err := ebpf.PossibleCPU()
   172  	if err != nil {
   173  		return "", fmt.Errorf("failed to get number of possible CPUs: %w", err)
   174  	}
   175  
   176  	compileArgs := append(testIncludes,
   177  		fmt.Sprintf("-I%s", path.Join(dir.Runtime, "globals")),
   178  		fmt.Sprintf("-I%s", dir.State),
   179  		fmt.Sprintf("-I%s", dir.Library),
   180  		fmt.Sprintf("-I%s", path.Join(dir.Library, "include")),
   181  	)
   182  
   183  	switch prog.OutputType {
   184  	case outputSource:
   185  		compileArgs = append(compileArgs, "-E") // Preprocessor
   186  	case outputObject:
   187  		compileArgs = append(compileArgs, "-g")
   188  	}
   189  
   190  	compileArgs = append(compileArgs, standardCFlags...)
   191  	compileArgs = append(compileArgs, fmt.Sprintf("-D__NR_CPUS__=%d", possibleCPUs))
   192  	compileArgs = append(compileArgs, "-mcpu="+getBPFCPU())
   193  	compileArgs = append(compileArgs, prog.Options...)
   194  	compileArgs = append(compileArgs,
   195  		"-c", path.Join(dir.Library, prog.Source),
   196  		"-o", "-", // Always output to stdout
   197  	)
   198  
   199  	log.WithFields(logrus.Fields{
   200  		"target": compiler,
   201  		"args":   compileArgs,
   202  	}).Debug("Launching compiler")
   203  
   204  	compileCmd, cancelCompile := exec.WithCancel(ctx, compiler, compileArgs...)
   205  	defer cancelCompile()
   206  
   207  	output, err := os.Create(prog.AbsoluteOutput(dir))
   208  	if err != nil {
   209  		return "", err
   210  	}
   211  	defer output.Close()
   212  	compileCmd.Stdout = output
   213  
   214  	var compilerStderr bytes.Buffer
   215  	compileCmd.Stderr = &compilerStderr
   216  
   217  	if err := compileCmd.Run(); err != nil {
   218  		err = fmt.Errorf("Failed to compile %s: %w", prog.Output, err)
   219  
   220  		// In linux/unix based implementations, cancelling the context for a cmd.Run() will
   221  		// return errors: "context cancelled" if the context is cancelled prior to the process
   222  		// starting and "signal: killed" if it is already running.
   223  		// This can mess up calling logging logic which expects the returned error to have
   224  		// context.Cancelled so we join this error in to fix that.
   225  		if errors.Is(ctx.Err(), context.Canceled) &&
   226  			compileCmd.ProcessState != nil &&
   227  			!compileCmd.ProcessState.Exited() &&
   228  			strings.HasSuffix(err.Error(), syscall.SIGKILL.String()) {
   229  			err = errors.Join(err, ctx.Err())
   230  		}
   231  
   232  		if !errors.Is(err, context.Canceled) {
   233  			log.WithFields(logrus.Fields{
   234  				"compiler-pid": pidFromProcess(compileCmd.Process),
   235  			}).Error(err)
   236  		}
   237  
   238  		scanner := bufio.NewScanner(io.LimitReader(&compilerStderr, 1_000_000))
   239  		for scanner.Scan() {
   240  			log.Warn(scanner.Text())
   241  		}
   242  
   243  		return "", err
   244  	}
   245  
   246  	// Cmd.ProcessState is populated by Cmd.Wait(). Cmd.Run() bails out if
   247  	// Cmd.Start() fails, which will leave Cmd.ProcessState nil. Only log peak
   248  	// RSS if the compilation succeeded, which will be the majority of cases.
   249  	if usage, ok := compileCmd.ProcessState.SysUsage().(*syscall.Rusage); ok {
   250  		log.WithFields(logrus.Fields{
   251  			"compiler-pid": compileCmd.Process.Pid,
   252  			"output":       output.Name(),
   253  		}).Debugf("Compilation had peak RSS of %d bytes", usage.Maxrss)
   254  	}
   255  
   256  	return output.Name(), nil
   257  }
   258  
   259  // compileDatapath invokes the compiler and linker to create all state files for
   260  // the BPF datapath, with the primary target being the BPF ELF binary.
   261  //
   262  // It also creates the following output files:
   263  // * Preprocessed C
   264  // * Assembly
   265  // * Object compiled with debug symbols
   266  func compileDatapath(ctx context.Context, dirs *directoryInfo, isHost bool, logger *logrus.Entry) error {
   267  	scopedLog := logger.WithField(logfields.Debug, true)
   268  
   269  	versionCmd := exec.CommandContext(ctx, compiler, "--version")
   270  	compilerVersion, err := versionCmd.CombinedOutput(scopedLog, true)
   271  	if err != nil {
   272  		return err
   273  	}
   274  	scopedLog.WithFields(logrus.Fields{
   275  		compiler: string(compilerVersion),
   276  	}).Debug("Compiling datapath")
   277  
   278  	prog := epProg
   279  	if isHost {
   280  		prog = hostEpProg
   281  	}
   282  
   283  	if option.Config.Debug && prog.OutputType == outputObject {
   284  		// Write out preprocessing files for debugging purposes
   285  		debugProg := *prog
   286  		debugProg.Output = debugProg.Source
   287  		debugProg.OutputType = outputSource
   288  
   289  		if _, err := compile(ctx, &debugProg, dirs); err != nil {
   290  			// Only log an error here if the context was not canceled. This log message
   291  			// should only represent failures with respect to compiling the program.
   292  			if !errors.Is(err, context.Canceled) {
   293  				scopedLog.WithField(logfields.Params, logfields.Repr(debugProg)).WithError(err).Debug("JoinEP: Failed to compile")
   294  			}
   295  			return err
   296  		}
   297  	}
   298  
   299  	if _, err := compile(ctx, prog, dirs); err != nil {
   300  		// Only log an error here if the context was not canceled. This log message
   301  		// should only represent failures with respect to compiling the program.
   302  		if !errors.Is(err, context.Canceled) {
   303  			scopedLog.WithField(logfields.Params, logfields.Repr(prog)).WithError(err).Warn("JoinEP: Failed to compile")
   304  		}
   305  		return err
   306  	}
   307  
   308  	return nil
   309  }
   310  
   311  // compileWithOptions compiles a BPF program generating an object file,
   312  // using a set of provided compiler options.
   313  func compileWithOptions(ctx context.Context, src string, out string, opts []string) error {
   314  	prog := progInfo{
   315  		Source:     src,
   316  		Options:    opts,
   317  		Output:     out,
   318  		OutputType: outputObject,
   319  	}
   320  	dirs := directoryInfo{
   321  		Library: option.Config.BpfDir,
   322  		Runtime: option.Config.StateDir,
   323  		Output:  option.Config.StateDir,
   324  		State:   option.Config.StateDir,
   325  	}
   326  	_, err := compile(ctx, &prog, &dirs)
   327  	return err
   328  }
   329  
   330  // compileDefault compiles a BPF program generating an object file with default options.
   331  func compileDefault(ctx context.Context, src string, out string) error {
   332  	return compileWithOptions(ctx, src, out, nil)
   333  }
   334  
   335  // compileNetwork compiles a BPF program attached to network
   336  func compileNetwork(ctx context.Context) error {
   337  	dirs := directoryInfo{
   338  		Library: option.Config.BpfDir,
   339  		Runtime: option.Config.StateDir,
   340  		Output:  option.Config.StateDir,
   341  		State:   option.Config.StateDir,
   342  	}
   343  	scopedLog := log.WithField(logfields.Debug, true)
   344  
   345  	versionCmd := exec.CommandContext(ctx, compiler, "--version")
   346  	compilerVersion, err := versionCmd.CombinedOutput(scopedLog, true)
   347  	if err != nil {
   348  		return err
   349  	}
   350  	scopedLog.WithFields(logrus.Fields{
   351  		compiler: string(compilerVersion),
   352  	}).Debug("Compiling network programs")
   353  
   354  	// Write out assembly and preprocessing files for debugging purposes
   355  	if _, err := compile(ctx, networkTcProg, &dirs); err != nil {
   356  		scopedLog.WithField(logfields.Params, logfields.Repr(networkTcProg)).
   357  			WithError(err).Warn("Failed to compile")
   358  		return err
   359  	}
   360  	return nil
   361  }
   362  
   363  // compileOverlay compiles BPF programs in bpf_overlay.c.
   364  func compileOverlay(ctx context.Context, opts []string) error {
   365  	dirs := &directoryInfo{
   366  		Library: option.Config.BpfDir,
   367  		Runtime: option.Config.StateDir,
   368  		Output:  option.Config.StateDir,
   369  		State:   option.Config.StateDir,
   370  	}
   371  	scopedLog := log.WithField(logfields.Debug, true)
   372  
   373  	versionCmd := exec.CommandContext(ctx, compiler, "--version")
   374  	compilerVersion, err := versionCmd.CombinedOutput(scopedLog, true)
   375  	if err != nil {
   376  		return err
   377  	}
   378  	scopedLog.WithFields(logrus.Fields{
   379  		compiler: string(compilerVersion),
   380  	}).Debug("Compiling overlay programs")
   381  
   382  	prog := &progInfo{
   383  		Source:     overlayProg,
   384  		Output:     overlayObj,
   385  		OutputType: outputObject,
   386  		Options:    opts,
   387  	}
   388  	// Write out assembly and preprocessing files for debugging purposes
   389  	if _, err := compile(ctx, prog, dirs); err != nil {
   390  		scopedLog.WithField(logfields.Params, logfields.Repr(prog)).
   391  			WithError(err).Warn("Failed to compile")
   392  		return err
   393  	}
   394  	return nil
   395  }
   396  
   397  func compileWireguard(ctx context.Context, opts []string) (err error) {
   398  	dirs := &directoryInfo{
   399  		Library: option.Config.BpfDir,
   400  		Runtime: option.Config.StateDir,
   401  		Output:  option.Config.StateDir,
   402  		State:   option.Config.StateDir,
   403  	}
   404  	scopedLog := log.WithField(logfields.Debug, true)
   405  
   406  	versionCmd := exec.CommandContext(ctx, compiler, "--version")
   407  	compilerVersion, err := versionCmd.CombinedOutput(scopedLog, true)
   408  	if err != nil {
   409  		return err
   410  	}
   411  	scopedLog.WithFields(logrus.Fields{
   412  		compiler: string(compilerVersion),
   413  	}).Debug("Compiling wireguard programs")
   414  
   415  	prog := &progInfo{
   416  		Source:     wireguardProg,
   417  		Output:     wireguardObj,
   418  		OutputType: outputObject,
   419  		Options:    opts,
   420  	}
   421  	// Write out assembly and preprocessing files for debugging purposes
   422  	if _, err := compile(ctx, prog, dirs); err != nil {
   423  		scopedLog.WithField(logfields.Params, logfields.Repr(prog)).
   424  			WithError(err).Warn("Failed to compile")
   425  		return err
   426  	}
   427  	return nil
   428  }
   429  
   430  type compilationLock struct {
   431  	lock.RWMutex
   432  }
   433  
   434  func NewCompilationLock() types.CompilationLock {
   435  	return &compilationLock{}
   436  }