Repository: cilium/ebpf
Pull request #673 (2022-05-13 17:12:58)

features: add HaveMapFlag API
`HaveMapFlag(flag uint32) error` would allow a user to probe running kernel for support for map flags.
The main target of this would be to probe for availability of mmapable array maps.

This PR could also be replaced by just exporting https://github.com/cilium/ebpf/blob/314c7732ecc76e7587edf487d1048fbc165983e3/syscalls.go#L91

rgo3: Thanks for the PR @paulcacheux, I'll give it a closer look tomorrow. Back when we added the `HaveMapType` API there was some discussion around adding a `HaveMapFlags` API, roughly starting at this [message](https://github.com/cilium/ebpf/issues/235#issuecomment-871386930) on #235. Maybe you can give it a look in the meantime. But the gist of it was that having `HaveMapFlag(MapType, Flag)` opens up a lot of possible combinations that we would need to test for.
ti-mo: @paulcacheux Feel free to swing by `#ebpf-go-dev` on our community Slack if you want to discuss this more interactively. We have a few questions around this proposal:

1. What is the practical use case? Please include this in any future contributions so we get a better idea of what you're trying to achieve. (also, commit message :pray:)
2. Would an API that only takes a `flag` (not a map type) be sufficient? We could probe using a map type that is known to support the flag.
3. Building on the previous point, are there known cases in the kernel's development history where a flag was introduced on one map type, and later implemented on more map types? This is important to know if we can get away with (2).

Something else to take into account is the caller can probe for map type A with flag B, but the kernel doesn't necessarily support map type A. Of course, this means that the (A, B) combo is definitely not supported, but this overlaps a bit too much with `HaveMapType`. Knowing more about the use case would really help here.

Thanks!

Patch:
diff --git a/btf/btf_types.go b/btf/btf_types.go
index 4810180..1ab0917 100644
--- a/btf/btf_types.go
+++ b/btf/btf_types.go
@@ -6,33 +6,36 @@ import (
 	"io"
 )
 
-//go:generate stringer -linecomment -output=btf_types_string.go -type=FuncLinkage,VarLinkage
+//go:generate stringer -linecomment -output=btf_types_string.go -type=FuncLinkage,VarLinkage,btfKind
 
 // btfKind describes a Type.
 type btfKind uint8
 
 // Equivalents of the BTF_KIND_* constants.
 const (
-	kindUnknown btfKind = iota
-	kindInt
-	kindPointer
-	kindArray
-	kindStruct
-	kindUnion
-	kindEnum
-	kindForward
-	kindTypedef
-	kindVolatile
-	kindConst
-	kindRestrict
+	kindUnknown  btfKind = iota // Unknown
+	kindInt                     // Int
+	kindPointer                 // Pointer
+	kindArray                   // Array
+	kindStruct                  // Struct
+	kindUnion                   // Union
+	kindEnum                    // Enum
+	kindForward                 // Forward
+	kindTypedef                 // Typedef
+	kindVolatile                // Volatile
+	kindConst                   // Const
+	kindRestrict                // Restrict
 	// Added ~4.20
-	kindFunc
-	kindFuncProto
+	kindFunc      // Func
+	kindFuncProto // FuncProto
 	// Added ~5.1
-	kindVar
-	kindDatasec
+	kindVar     // Var
+	kindDatasec // Datasec
 	// Added ~5.13
-	kindFloat
+	kindFloat // Float
+	// Added 5.16
+	kindDeclTag // DeclTag
+	kindTypeTag // TypeTag
 )
 
 // FuncLinkage describes BTF function linkage metadata.
@@ -85,47 +88,6 @@ type btfType struct {
 	SizeType uint32
 }
 
-func (k btfKind) String() string {
-	switch k {
-	case kindUnknown:
-		return "Unknown"
-	case kindInt:
-		return "Integer"
-	case kindPointer:
-		return "Pointer"
-	case kindArray:
-		return "Array"
-	case kindStruct:
-		return "Struct"
-	case kindUnion:
-		return "Union"
-	case kindEnum:
-		return "Enumeration"
-	case kindForward:
-		return "Forward"
-	case kindTypedef:
-		return "Typedef"
-	case kindVolatile:
-		return "Volatile"
-	case kindConst:
-		return "Const"
-	case kindRestrict:
-		return "Restrict"
-	case kindFunc:
-		return "Function"
-	case kindFuncProto:
-		return "Function Proto"
-	case kindVar:
-		return "Variable"
-	case kindDatasec:
-		return "Section"
-	case kindFloat:
-		return "Float"
-	default:
-		return fmt.Sprintf("Unknown (%d)", k)
-	}
-}
-
 func mask(len uint32) uint32 {
 	return (1 << len) - 1
 }
@@ -209,11 +171,11 @@ func (rt *rawType) Marshal(w io.Writer, bo binary.ByteOrder) error {
 
 // btfInt encodes additional data for integers.
 //
-//    ? ? ? ? e e e e o o o o o o o o ? ? ? ? ? ? ? ? b b b b b b b b
-//    ? = undefined
-//    e = encoding
-//    o = offset (bitfields?)
-//    b = bits (bitfields)
+//	? ? ? ? e e e e o o o o o o o o ? ? ? ? ? ? ? ? b b b b b b b b
+//	? = undefined
+//	e = encoding
+//	o = offset (bitfields?)
+//	b = bits (bitfields)
 type btfInt struct {
 	Raw uint32
 }
@@ -283,6 +245,10 @@ type btfParam struct {
 	Type    TypeID
 }
 
+type btfDeclTag struct {
+	ComponentIdx uint32
+}
+
 func readTypes(r io.Reader, bo binary.ByteOrder, typeLen uint32) ([]rawType, error) {
 	var header btfType
 	// because of the interleaving between types and struct members it is difficult to
@@ -325,6 +291,9 @@ func readTypes(r io.Reader, bo binary.ByteOrder, typeLen uint32) ([]rawType, err
 		case kindDatasec:
 			data = make([]btfVarSecinfo, header.Vlen())
 		case kindFloat:
+		case kindDeclTag:
+			data = new(btfDeclTag)
+		case kindTypeTag:
 		default:
 			return nil, fmt.Errorf("type id %v: unknown kind: %v", id, header.Kind())
 		}
diff --git a/btf/btf_types_string.go b/btf/btf_types_string.go
index 0e0c17d..fdb2662 100644
--- a/btf/btf_types_string.go
+++ b/btf/btf_types_string.go
@@ -1,4 +1,4 @@
-// Code generated by "stringer -linecomment -output=btf_types_string.go -type=FuncLinkage,VarLinkage"; DO NOT EDIT.
+// Code generated by "stringer -linecomment -output=btf_types_string.go -type=FuncLinkage,VarLinkage,btfKind"; DO NOT EDIT.
 
 package btf
 
@@ -42,3 +42,38 @@ func (i VarLinkage) String() string {
 	}
 	return _VarLinkage_name[_VarLinkage_index[i]:_VarLinkage_index[i+1]]
 }
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[kindUnknown-0]
+	_ = x[kindInt-1]
+	_ = x[kindPointer-2]
+	_ = x[kindArray-3]
+	_ = x[kindStruct-4]
+	_ = x[kindUnion-5]
+	_ = x[kindEnum-6]
+	_ = x[kindForward-7]
+	_ = x[kindTypedef-8]
+	_ = x[kindVolatile-9]
+	_ = x[kindConst-10]
+	_ = x[kindRestrict-11]
+	_ = x[kindFunc-12]
+	_ = x[kindFuncProto-13]
+	_ = x[kindVar-14]
+	_ = x[kindDatasec-15]
+	_ = x[kindFloat-16]
+	_ = x[kindDeclTag-17]
+	_ = x[kindTypeTag-18]
+}
+
+const _btfKind_name = "UnknownIntPointerArrayStructUnionEnumForwardTypedefVolatileConstRestrictFuncFuncProtoVarDatasecFloatDeclTagTypeTag"
+
+var _btfKind_index = [...]uint8{0, 7, 10, 17, 22, 28, 33, 37, 44, 51, 59, 64, 72, 76, 85, 88, 95, 100, 107, 114}
+
+func (i btfKind) String() string {
+	if i >= btfKind(len(_btfKind_index)-1) {
+		return "btfKind(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+	return _btfKind_name[_btfKind_index[i]:_btfKind_index[i+1]]
+}
diff --git a/btf/types.go b/btf/types.go
index 402a363..38ad3dd 100644
--- a/btf/types.go
+++ b/btf/types.go
@@ -541,6 +541,45 @@ func (f *Float) copy() Type {
 	return &cpy
 }
 
+// declTag associates metadata with a declaration.
+type declTag struct {
+	Type  Type
+	Value string
+	// The index this tag refers to in the target type. For composite types,
+	// a value of -1 indicates that the tag refers to the whole type. Otherwise
+	// it indicates which member or argument the tag applies to.
+	Index int
+}
+
+func (dt *declTag) Format(fs fmt.State, verb rune) {
+	formatType(fs, verb, dt, "type=", dt.Type, "value=", dt.Value, "index=", dt.Index)
+}
+
+func (dt *declTag) TypeName() string   { return "" }
+func (dt *declTag) walk(td *typeDeque) { td.push(&dt.Type) }
+func (dt *declTag) copy() Type {
+	cpy := *dt
+	return &cpy
+}
+
+// typeTag associates metadata with a type.
+type typeTag struct {
+	Type  Type
+	Value string
+}
+
+func (tt *typeTag) Format(fs fmt.State, verb rune) {
+	formatType(fs, verb, tt, "type=", tt.Type, "value=", tt.Value)
+}
+
+func (tt *typeTag) TypeName() string   { return "" }
+func (tt *typeTag) qualify() Type      { return tt.Type }
+func (tt *typeTag) walk(td *typeDeque) { td.push(&tt.Type) }
+func (tt *typeTag) copy() Type {
+	cpy := *tt
+	return &cpy
+}
+
 // cycle is a type which had to be elided since it exceeded maxTypeDepth.
 type cycle struct {
 	root Type
@@ -576,6 +615,7 @@ var (
 	_ qualifier = (*Const)(nil)
 	_ qualifier = (*Restrict)(nil)
 	_ qualifier = (*Volatile)(nil)
+	_ qualifier = (*typeTag)(nil)
 )
 
 // Sizeof returns the size of a type in bytes.
@@ -903,6 +943,7 @@ func inflateRawTypes(rawTypes []rawType, baseTypes types, rawStrings *stringTabl
 		return members, nil
 	}
 
+	var declTags []*declTag
 	for i, raw := range rawTypes {
 		var (
 			id  = typeIDOffset + TypeID(i)
@@ -1045,6 +1086,28 @@ func inflateRawTypes(rawTypes []rawType, baseTypes types, rawStrings *stringTabl
 		case kindFloat:
 			typ = &Float{name, raw.Size()}
 
+		case kindDeclTag:
+			btfIndex := raw.data.(*btfDeclTag).ComponentIdx
+			if uint64(btfIndex) > math.MaxInt {
+				return nil, fmt.Errorf("type id %d: index exceeds int", id)
+			}
+
+			index := int(btfIndex)
+			if btfIndex == math.MaxUint32 {
+				index = -1
+			}
+
+			dt := &declTag{nil, name, index}
+			fixup(raw.Type(), &dt.Type)
+			typ = dt
+
+			declTags = append(declTags, dt)
+
+		case kindTypeTag:
+			tt := &typeTag{nil, name}
+			fixup(raw.Type(), &tt.Type)
+			typ = tt
+
 		default:
 			return nil, fmt.Errorf("type id %d: unknown kind: %v", id, raw.Kind())
 		}
@@ -1083,6 +1146,28 @@ func inflateRawTypes(rawTypes []rawType, baseTypes types, rawStrings *stringTabl
 		}
 	}
 
+	for _, dt := range declTags {
+		switch t := dt.Type.(type) {
+		case *Var, *Typedef:
+			if dt.Index != -1 {
+				return nil, fmt.Errorf("type %s: index %d is not -1", dt, dt.Index)
+			}
+
+		case composite:
+			if dt.Index >= len(t.members()) {
+				return nil, fmt.Errorf("type %s: index %d exceeds members of %s", dt, dt.Index, t)
+			}
+
+		case *Func:
+			if dt.Index >= len(t.Type.(*FuncProto).Params) {
+				return nil, fmt.Errorf("type %s: index %d exceeds params of %s", dt, dt.Index, t)
+			}
+
+		default:
+			return nil, fmt.Errorf("type %s: decl tag for type %s is not supported", dt, t)
+		}
+	}
+
 	return types, nil
 }
 
diff --git a/cmd/bpf2go/output.go b/cmd/bpf2go/output.go
index 18b9e0f..577e741 100644
--- a/cmd/bpf2go/output.go
+++ b/cmd/bpf2go/output.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"go/token"
 	"io"
-	"io/ioutil"
+	"os"
 	"path/filepath"
 	"sort"
 	"strings"
@@ -228,7 +228,7 @@ type outputArgs struct {
 }
 
 func output(args outputArgs) error {
-	obj, err := ioutil.ReadFile(args.obj)
+	obj, err := os.ReadFile(args.obj)
 	if err != nil {
 		return fmt.Errorf("read object file contents: %s", err)
 	}
diff --git a/features/map.go b/features/map.go
index 7e5db4d..e83e48c 100644
--- a/features/map.go
+++ b/features/map.go
@@ -13,6 +13,7 @@ import (
 
 func init() {
 	mc.mapTypes = make(map[ebpf.MapType]error)
+	mc.mapFlags = make(map[uint32]error)
 }
 
 var (
@@ -22,6 +23,7 @@ var (
 type mapCache struct {
 	sync.Mutex
 	mapTypes map[ebpf.MapType]error
+	mapFlags map[uint32]error
 }
 
 func createMapTypeAttr(mt ebpf.MapType) *sys.MapCreateAttr {
@@ -159,6 +161,70 @@ func isStorageMap(mt ebpf.MapType) bool {
 	case ebpf.SkStorage, ebpf.InodeStorage, ebpf.TaskStorage:
 		return true
 	}
-
 	return false
 }
+
+// HaveMapFlag probes the running kernel for the availability of the specified map flag.
+//
+// See the package documentation for the meaning of the error return value.
+func HaveMapFlag(flag uint32) (err error) {
+	defer func() {
+		// This closure modifies a named return variable.
+		err = wrapProbeErrors(err)
+	}()
+
+	return haveMapFlag(flag)
+}
+
+func haveMapFlag(flag uint32) error {
+	mc.Lock()
+	defer mc.Unlock()
+	err, ok := mc.mapFlags[flag]
+	if ok {
+		return err
+	}
+
+	attr, err := createMapFlagTypeAttr(flag)
+	if err != nil {
+		return err
+	}
+
+	fd, err := sys.MapCreate(attr)
+	if err == nil {
+		fd.Close()
+	}
+
+	// EINVAL occurs when attempting to create a map with an unknown type or an unknown flag.
+	if errors.Is(err, unix.EINVAL) {
+		err = ebpf.ErrNotSupported
+	}
+
+	mc.mapFlags[flag] = err
+
+	return err
+}
+
+func createMapFlagTypeAttr(flag uint32) (*sys.MapCreateAttr, error) {
+	a := &sys.MapCreateAttr{
+		KeySize:    4,
+		ValueSize:  4,
+		MaxEntries: 1,
+		MapFlags:   flag,
+	}
+
+	// For now, we do not check if the map type is supported because we only support
+	// probing for flags defined on arrays and hashs that are always supported.
+	// In the future, if we allow probing on flags defined on newer types, checking for map type
+	// support will be required.
+
+	switch flag {
+	case unix.BPF_F_MMAPABLE, unix.BPF_F_INNER_MAP, unix.BPF_F_RDONLY_PROG, unix.BPF_F_WRONLY_PROG:
+		a.MapType = sys.MapType(ebpf.Array)
+		return a, nil
+	case unix.BPF_F_NO_PREALLOC:
+		a.MapType = sys.MapType(ebpf.Hash)
+		return a, nil
+	}
+
+	return nil, errors.New("probe not implemented")
+}
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/internal/unix/types_other.go b/internal/unix/types_other.go
index b1318f6..1bc6c87 100644
--- a/internal/unix/types_other.go
+++ b/internal/unix/types_other.go
@@ -27,14 +27,14 @@ const (
 	EACCES = syscall.Errno(0)
 	EILSEQ = syscall.Errno(0)
 
-	BPF_F_NO_PREALLOC        = 0
+	BPF_F_NO_PREALLOC        = 1 << 0
 	BPF_F_NUMA_NODE          = 0
 	BPF_F_RDONLY             = 0
 	BPF_F_WRONLY             = 0
-	BPF_F_RDONLY_PROG        = 0
-	BPF_F_WRONLY_PROG        = 0
+	BPF_F_RDONLY_PROG        = 1 << 7
+	BPF_F_WRONLY_PROG        = 1 << 8
 	BPF_F_SLEEPABLE          = 0
-	BPF_F_MMAPABLE           = 0
+	BPF_F_MMAPABLE           = 1 << 10
 	BPF_F_INNER_MAP          = 0
 	BPF_OBJ_NAME_LEN         = 0x10
 	BPF_TAG_SIZE             = 0x8
diff --git a/prog.go b/prog.go
index a0ed434..1f5bdb4 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
@@ -73,7 +102,7 @@ type ProgramSpec struct {
 	// The program to attach to. Must be provided manually.
 	AttachTarget *Program
 
-	// The name of the ELF section this program orininated from.
+	// The name of the ELF section this program originated from.
 	SectionName string
 
 	Instructions asm.Instructions
@@ -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/btf/types_test.go b/btf/types_test.go
index 5a80db4..c143753 100644
--- a/btf/types_test.go
+++ b/btf/types_test.go
@@ -134,6 +134,9 @@ func TestType(t *testing.T) {
 				Vars: []VarSecinfo{{Type: &Void{}}},
 			}
 		},
+		func() Type { return &Float{} },
+		func() Type { return &declTag{Type: &Void{}} },
+		func() Type { return &typeTag{Type: &Void{}} },
 		func() Type { return &cycle{&Void{}} },
 	}
 
@@ -331,6 +334,7 @@ func TestUnderlyingType(t *testing.T) {
 		{"volatile", func(t Type) Type { return &Volatile{Type: t} }},
 		{"restrict", func(t Type) Type { return &Restrict{Type: t} }},
 		{"typedef", func(t Type) Type { return &Typedef{Type: t} }},
+		{"type tag", func(t Type) Type { return &typeTag{Type: t} }},
 	}
 
 	for _, test := range wrappers {
diff --git a/elf_reader_test.go b/elf_reader_test.go
index 960e562..385b645 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 {
@@ -740,9 +740,6 @@ func TestLibBPFCompat(t *testing.T) {
 		case "bloom_filter_map.o", "bloom_filter_map.linked3.o",
 			"bloom_filter_bench.o", "bloom_filter_bench.linked3.o":
 			t.Skip("Skipping due to missing MapExtra field in MapSpec")
-		case "btf_type_tag.o", "btf_type_tag.linked3.o", "test_btf_decl_tag.o",
-			"test_btf_decl_tag.linked3.o":
-			t.Skip("Skipping due to missing support for BTF_KIND_TYPE_TAG and BTF_KIND_DECL_TAG")
 		case "netif_receive_skb.linked3.o":
 			t.Skip("Skipping due to possible bug in upstream CO-RE generation")
 		}
diff --git a/features/map_test.go b/features/map_test.go
index 5654761..d87237f 100644
--- a/features/map_test.go
+++ b/features/map_test.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/internal/testutils"
+	"github.com/cilium/ebpf/internal/unix"
 )
 
 var mapTypeMinVersion = map[ebpf.MapType]string{
@@ -61,9 +62,38 @@ func TestHaveMapType(t *testing.T) {
 				t.Fatalf("Map type %s isn't supported even though kernel is at least %s: %v", mt.String(), minVersion, err)
 			}
 		})
+	}
+}
+
+type haveMapFlagsTestEntry struct {
+	flags            uint32
+	minKernelVersion string
+	description      string
+}
 
+func TestHaveMapFlag(t *testing.T) {
+	mapFlagTestEntries := []haveMapFlagsTestEntry{
+		{unix.BPF_F_RDONLY_PROG, "5.2", "read_only_array_map"},
+		{unix.BPF_F_WRONLY_PROG, "5.2", "write_only_array_map"},
+		{unix.BPF_F_MMAPABLE, "5.5", "mmapable_array_map"},
+		{unix.BPF_F_INNER_MAP, "5.10", "inner_map_array_map"},
+		{unix.BPF_F_NO_PREALLOC, "4.6", "no_prealloc_hash_map"},
 	}
 
+	for _, entry := range mapFlagTestEntries {
+		t.Run(entry.description, func(t *testing.T) {
+			err := HaveMapFlag(entry.flags)
+			if testutils.IsKernelLessThan(t, entry.minKernelVersion) {
+				if err == nil {
+					t.Fatalf("Map flag %d is supported on this kernel even though kernel is less than %s", entry.flags, entry.minKernelVersion)
+				}
+			} else {
+				if err != nil {
+					t.Fatalf("Map flag %d isn't supported even though kernel is at least %s: %v", entry.flags, entry.minKernelVersion, err)
+				}
+			}
+		})
+	}
 }
 
 func TestHaveMapTypeUnsupported(t *testing.T) {
diff --git a/internal/testutils/feature.go b/internal/testutils/feature.go
index 86092c4..efece22 100644
--- a/internal/testutils/feature.go
+++ b/internal/testutils/feature.go
@@ -65,6 +65,14 @@ func checkKernelVersion(tb testing.TB, ufe *internal.UnsupportedFeatureError) {
 func SkipOnOldKernel(tb testing.TB, minVersion, feature string) {
 	tb.Helper()
 
+	if IsKernelLessThan(tb, minVersion) {
+		tb.Skipf("Test requires at least kernel %s (due to missing %s)", minVersion, feature)
+	}
+}
+
+func IsKernelLessThan(tb testing.TB, minVersion string) bool {
+	tb.Helper()
+
 	minv, err := internal.NewVersion(minVersion)
 	if err != nil {
 		tb.Fatalf("Invalid version %s: %s", minVersion, err)
@@ -81,7 +89,5 @@ func SkipOnOldKernel(tb testing.TB, minVersion, feature string) {
 		}
 	}
 
-	if MustKernelVersion().Less(minv) {
-		tb.Skipf("Test requires at least kernel %s (due to missing %s)", minv, feature)
-	}
+	return MustKernelVersion().Less(minv)
 }
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 {