Repository: cilium/ebpf
Pull request #754 (2022-08-01 14:50:46)

prog: unconditionally enable verifier retries, add consts, document LogLevel
With the recent addition of verifier error summaries, there are fewer reasons for disabling verifier output altogether. The existing behaviour is somewhat underspecified and underdocumented. Logging behaviour hinges on both LogLevel and LogSize, with an undocumented -1 value for LogSize to disable the verifier log. This gets rather complex with the retry mechanism involved.

This patch removes the local logSize variable, sets opts.LogSize directly to the default value, and log behaviour is now controlled by LogLevel only. Disabling the verifier log altogether can be done by setting the LogDisabled ProgramOption.

A type LogLevel was added to reflect the kernel's own constants for requesting logs on either branch or instruction level, as well as the stats summary of the current verifier pass on supported kernels. (v5.2 and up)

Documented all behaviour on the LogLevel and LogSize fields and constants.

This patch introduces one behavioural change: when ProgramOptions.LogLevel is left to its default value, a failed prog load will always result in a follow-up load with the verifier log level enabled (set to 1). The existing behaviour is to only request a verifier log if at least one of LogLevel or LogSize are set.


Patch:
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index 376d405..98786be 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -65,21 +65,24 @@ func generateTypes(spec *btf.Spec) ([]byte, error) {
 	linkID := &btf.Int{Size: 4}
 	btfID := &btf.Int{Size: 4}
 	pointer := &btf.Int{Size: 8}
+	logLevel := &btf.Int{Size: 4}
 
-	// Pre-declare handwritten types sys.ObjName and sys.Pointer so that
-	// generated types can refer to them.
+	// Pre-declare handwritten types so that generated types can refer to them.
 	var (
-		_ sys.Pointer
 		_ sys.ObjName
 		_ sys.LinkID
+		_ sys.BTFID
+		_ sys.Pointer
+		_ sys.LogLevel
 	)
 
 	gf := &btf.GoFormatter{
 		Names: map[btf.Type]string{
-			objName: "ObjName",
-			linkID:  "LinkID",
-			btfID:   "BTFID",
-			pointer: "Pointer",
+			objName:  "ObjName",
+			linkID:   "LinkID",
+			btfID:    "BTFID",
+			pointer:  "Pointer",
+			logLevel: "LogLevel",
 		},
 		Identifier: internal.Identifier,
 		EnumIdentifier: func(name, element string) string {
@@ -276,6 +279,7 @@ import (
 				replace(objName, "prog_name"),
 				replace(enumTypes["ProgType"], "prog_type"),
 				replace(enumTypes["AttachType"], "expected_attach_type"),
+				replace(logLevel, "log_level"),
 				replace(pointer,
 					"insns",
 					"license",
diff --git a/internal/sys/syscall.go b/internal/sys/syscall.go
index cfadedf..c3da832 100644
--- a/internal/sys/syscall.go
+++ b/internal/sys/syscall.go
@@ -95,6 +95,15 @@ func NewObjName(name string) ObjName {
 	return result
 }
 
+// LogLevel controls the verbosity of the kernel's eBPF program verifier.
+type LogLevel uint32
+
+const (
+	BPF_LOG_LEVEL1 LogLevel = 1 << iota
+	BPF_LOG_LEVEL2
+	BPF_LOG_STATS
+)
+
 // LinkID uniquely identifies a bpf_link.
 type LinkID uint32
 
diff --git a/internal/sys/types.go b/internal/sys/types.go
index a5f90e7..f1fde4e 100644
--- a/internal/sys/types.go
+++ b/internal/sys/types.go
@@ -951,7 +951,7 @@ type ProgLoadAttr struct {
 	InsnCnt            uint32
 	Insns              Pointer
 	License            Pointer
-	LogLevel           uint32
+	LogLevel           LogLevel
 	LogSize            uint32
 	LogBuf             Pointer
 	KernVersion        uint32
diff --git a/prog.go b/prog.go
index a0ed434..b359137 100644
--- a/prog.go
+++ b/prog.go
@@ -35,14 +35,43 @@ const (
 // verifier log.
 const DefaultVerifierLogSize = 64 * 1024
 
+// maxVerifierLogSize is the maximum size of verifier log buffer the kernel
+// will accept before returning EINVAL.
+const maxVerifierLogSize = math.MaxUint32 >> 2
+
 // ProgramOptions control loading a program into the kernel.
 type ProgramOptions struct {
-	// Controls the detail emitted by the kernel verifier. Set to non-zero
-	// to enable logging.
-	LogLevel uint32
-	// Controls the output buffer size for the verifier. Defaults to
-	// DefaultVerifierLogSize.
+	// Bitmap controlling the detail emitted by the kernel's eBPF verifier log.
+	// LogLevel-type values can be ORed together to request specific kinds of
+	// verifier output. See the documentation on [ebpf.LogLevel] for details.
+	//
+	//  opts.LogLevel = (ebpf.LogLevelBranch | ebpf.LogLevelStats)
+	//
+	// If left to its default value, the program will first be loaded without
+	// verifier output enabled. Upon error, the program load will be repeated
+	// with LogLevelBranch and the given (or default) LogSize value.
+	//
+	// Setting this to a non-zero value will unconditionally enable the verifier
+	// log, populating the [ebpf.Program.VerifierLog] field on successful loads
+	// and including detailed verifier errors if the program is rejected. This
+	// will always allocate an output buffer, but will result in only a single
+	// attempt at loading the program.
+	LogLevel LogLevel
+
+	// Controls the output buffer size for the verifier log, in bytes. See the
+	// documentation on ProgramOptions.LogLevel for details about how this value
+	// is used.
+	//
+	// If this value is set too low to fit the verifier log, the resulting
+	// [ebpf.VerifierError]'s Truncated flag will be true, and the error string
+	// will also contain a hint to that effect.
+	//
+	// Defaults to DefaultVerifierLogSize.
 	LogSize int
+
+	// Disables the verifier log completely, regardless of other options.
+	LogDisabled bool
+
 	// Type information used for CO-RE relocations.
 	//
 	// This is useful in environments where the kernel BTF is not available
@@ -180,6 +209,10 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand
 		return nil, fmt.Errorf("can't load %s program on %s", spec.ByteOrder, internal.NativeEndian)
 	}
 
+	if opts.LogSize < 0 {
+		return nil, errors.New("ProgramOptions.LogSize must be a positive value; disable verifier logs using ProgramOptions.LogDisabled")
+	}
+
 	// Kernels before 5.0 (6c4fc209fcf9 "bpf: remove useless version check for prog load")
 	// require the version field to be set to the value of the KERNEL_VERSION
 	// macro for kprobe-type programs.
@@ -276,14 +309,16 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand
 		}
 	}
 
-	logSize := DefaultVerifierLogSize
-	if opts.LogSize > 0 {
-		logSize = opts.LogSize
+	if opts.LogSize == 0 {
+		opts.LogSize = DefaultVerifierLogSize
 	}
 
+	// The caller provided a specific verifier log level. Immediately load
+	// the program with the given log level and buffer size, and skip retrying
+	// with a different level / size later.
 	var logBuf []byte
-	if opts.LogLevel > 0 {
-		logBuf = make([]byte, logSize)
+	if !opts.LogDisabled && opts.LogLevel != 0 {
+		logBuf = make([]byte, opts.LogSize)
 		attr.LogLevel = opts.LogLevel
 		attr.LogSize = uint32(len(logBuf))
 		attr.LogBuf = sys.NewSlicePointer(logBuf)
@@ -294,10 +329,11 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand
 		return &Program{unix.ByteSliceToString(logBuf), fd, spec.Name, "", spec.Type}, nil
 	}
 
-	if opts.LogLevel == 0 && opts.LogSize >= 0 {
-		// Re-run with the verifier enabled to get better error messages.
-		logBuf = make([]byte, logSize)
-		attr.LogLevel = 1
+	// If the caller did not specify a log level,
+	// re-run with branch-level verifier logs enabled.
+	if !opts.LogDisabled && opts.LogLevel == 0 {
+		logBuf = make([]byte, opts.LogSize)
+		attr.LogLevel = LogLevelBranch
 		attr.LogSize = uint32(len(logBuf))
 		attr.LogBuf = sys.NewSlicePointer(logBuf)
 		_, _ = sys.ProgLoad(attr)
@@ -319,11 +355,15 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand
 				return nil, fmt.Errorf("load program: %w", err)
 			}
 		}
+
+		if opts.LogSize > maxVerifierLogSize {
+			return nil, fmt.Errorf("load program: %w (ProgramOptions.LogSize exceeds maximum value of %d)", err, maxVerifierLogSize)
+		}
 	}
 
 	err = internal.ErrorWithLog(err, logBuf)
 	if btfDisabled {
-		return nil, fmt.Errorf("load program: %w (BTF disabled)", err)
+		return nil, fmt.Errorf("load program: %w (kernel without BTF support)", err)
 	}
 	return nil, fmt.Errorf("load program: %w", err)
 }
diff --git a/types.go b/types.go
index a27b442..4659f1b 100644
--- a/types.go
+++ b/types.go
@@ -1,6 +1,7 @@
 package ebpf
 
 import (
+	"github.com/cilium/ebpf/internal/sys"
 	"github.com/cilium/ebpf/internal/unix"
 )
 
@@ -282,3 +283,20 @@ type BatchOptions struct {
 	ElemFlags uint64
 	Flags     uint64
 }
+
+// LogLevel controls the verbosity of the kernel's eBPF program verifier.
+// These constants can be used for the ProgramOptions.LogLevel field.
+type LogLevel = sys.LogLevel
+
+const (
+	// Print verifier state at branch points.
+	LogLevelBranch = sys.BPF_LOG_LEVEL1
+
+	// Print verifier state for every instruction.
+	// Available since Linux v5.2.
+	LogLevelInstruction = sys.BPF_LOG_LEVEL2
+
+	// Print verifier errors and stats at the end of the verification process.
+	// Available since Linux v5.2.
+	LogLevelStats = sys.BPF_LOG_STATS
+)


Test patch:
diff --git a/elf_reader_test.go b/elf_reader_test.go
index 960e562..f9edc99 100644
--- a/elf_reader_test.go
+++ b/elf_reader_test.go
@@ -207,7 +207,7 @@ func TestLoadCollectionSpec(t *testing.T) {
 				PinPath: testutils.TempBPFFS(t),
 			},
 			Programs: ProgramOptions{
-				LogLevel: 1,
+				LogLevel: LogLevelBranch,
 			},
 		})
 		testutils.SkipIfNotSupported(t, err)
@@ -494,7 +494,7 @@ func TestLoadRawTracepoint(t *testing.T) {
 
 		coll, err := NewCollectionWithOptions(spec, CollectionOptions{
 			Programs: ProgramOptions{
-				LogLevel: 1,
+				LogLevel: LogLevelBranch,
 			},
 		})
 		testutils.SkipIfNotSupported(t, err)
@@ -686,7 +686,7 @@ func TestLibBPFCompat(t *testing.T) {
 	load := func(t *testing.T, spec *CollectionSpec, opts CollectionOptions, valid bool) {
 		// Disable retrying a program load with the log enabled, it leads
 		// to OOM kills.
-		opts.Programs.LogSize = -1
+		opts.Programs.LogDisabled = true
 
 		for name, p := range spec.Programs {
 			if p.Type != Extension {
diff --git a/prog_test.go b/prog_test.go
index f200635..722d7f5 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -390,7 +390,7 @@ func TestProgramKernelVersion(t *testing.T) {
 
 func TestProgramVerifierOutput(t *testing.T) {
 	prog, err := NewProgramWithOptions(socketFilterSpec, ProgramOptions{
-		LogLevel: 2,
+		LogLevel: LogLevelInstruction,
 	})
 	if err != nil {
 		t.Fatal(err)
@@ -409,7 +409,7 @@ func TestProgramVerifierOutput(t *testing.T) {
 		},
 		License: "MIT",
 	}, ProgramOptions{
-		LogLevel: 2,
+		LogLevel: LogLevelInstruction,
 	})
 
 	if err == nil {
@@ -906,7 +906,7 @@ func ExampleProgram_retrieveVerifierOutput() {
 	}
 
 	prog, err := NewProgramWithOptions(spec, ProgramOptions{
-		LogLevel: 2,
+		LogLevel: LogLevelInstruction,
 		LogSize:  1024,
 	})
 	if err != nil {