Files
LearnGO/go/pkg/mod/github.com/cilium/ebpf@v0.11.0/btf/btf_test.go
T
2024-09-19 21:38:24 -04:00

526 lines
12 KiB
Go

package btf
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"testing"
"github.com/cilium/ebpf/internal"
"github.com/cilium/ebpf/internal/testutils"
qt "github.com/frankban/quicktest"
)
func vmlinuxSpec(tb testing.TB) *Spec {
tb.Helper()
// /sys/kernel/btf was introduced in 341dfcf8d78e ("btf: expose BTF info
// through sysfs"), which shipped in Linux 5.4.
testutils.SkipOnOldKernel(tb, "5.4", "vmlinux BTF in sysfs")
spec, fallback, err := kernelSpec()
if err != nil {
tb.Fatal(err)
}
if fallback {
tb.Fatal("/sys/kernel/btf/vmlinux is not available")
}
return spec.Copy()
}
type specAndRawBTF struct {
raw []byte
spec *Spec
}
var vmlinuxTestdata = internal.Memoize(func() (specAndRawBTF, error) {
b, err := internal.ReadAllCompressed("testdata/vmlinux.btf.gz")
if err != nil {
return specAndRawBTF{}, err
}
spec, err := loadRawSpec(bytes.NewReader(b), binary.LittleEndian, nil)
if err != nil {
return specAndRawBTF{}, err
}
return specAndRawBTF{b, spec}, nil
})
func vmlinuxTestdataReader(tb testing.TB) *bytes.Reader {
tb.Helper()
td, err := vmlinuxTestdata()
if err != nil {
tb.Fatal(err)
}
return bytes.NewReader(td.raw)
}
func vmlinuxTestdataSpec(tb testing.TB) *Spec {
tb.Helper()
td, err := vmlinuxTestdata()
if err != nil {
tb.Fatal(err)
}
return td.spec.Copy()
}
func parseELFBTF(tb testing.TB, file string) *Spec {
tb.Helper()
spec, err := LoadSpec(file)
if err != nil {
tb.Fatal("Can't load BTF:", err)
}
return spec
}
func TestAnyTypesByName(t *testing.T) {
testutils.Files(t, testutils.Glob(t, "testdata/relocs-*.elf"), func(t *testing.T, file string) {
spec := parseELFBTF(t, file)
types, err := spec.AnyTypesByName("ambiguous")
if err != nil {
t.Fatal(err)
}
if len(types) != 1 {
t.Fatalf("expected to receive exactly 1 types from querying ambiguous type, got: %v", types)
}
types, err = spec.AnyTypesByName("ambiguous___flavour")
if err != nil {
t.Fatal(err)
}
if len(types) != 1 {
t.Fatalf("expected to receive exactly 1 type from querying ambiguous flavour, got: %v", types)
}
})
}
func TestTypeByNameAmbiguous(t *testing.T) {
testutils.Files(t, testutils.Glob(t, "testdata/relocs-*.elf"), func(t *testing.T, file string) {
spec := parseELFBTF(t, file)
var typ *Struct
if err := spec.TypeByName("ambiguous", &typ); err != nil {
t.Fatal(err)
}
if name := typ.TypeName(); name != "ambiguous" {
t.Fatal("expected type name 'ambiguous', got:", name)
}
if err := spec.TypeByName("ambiguous___flavour", &typ); err != nil {
t.Fatal(err)
}
if name := typ.TypeName(); name != "ambiguous___flavour" {
t.Fatal("expected type name 'ambiguous___flavour', got:", name)
}
})
}
func TestTypeByName(t *testing.T) {
spec := vmlinuxTestdataSpec(t)
for _, typ := range []interface{}{
nil,
Struct{},
&Struct{},
[]Struct{},
&[]Struct{},
map[int]Struct{},
&map[int]Struct{},
int(0),
new(int),
} {
t.Run(fmt.Sprintf("%T", typ), func(t *testing.T) {
// spec.TypeByName MUST fail if typ is a nil btf.Type.
if err := spec.TypeByName("iphdr", typ); err == nil {
t.Fatalf("TypeByName does not fail with type %T", typ)
}
})
}
// spec.TypeByName MUST return the same address for multiple calls with the same type name.
var iphdr1, iphdr2 *Struct
if err := spec.TypeByName("iphdr", &iphdr1); err != nil {
t.Fatal(err)
}
if err := spec.TypeByName("iphdr", &iphdr2); err != nil {
t.Fatal(err)
}
if iphdr1 != iphdr2 {
t.Fatal("multiple TypeByName calls for `iphdr` name do not return the same addresses")
}
// It's valid to pass a *Type to TypeByName.
typ := Type(iphdr2)
if err := spec.TypeByName("iphdr", &typ); err != nil {
t.Fatal("Can't look up using *Type:", err)
}
// Excerpt from linux/ip.h, https://elixir.bootlin.com/linux/latest/A/ident/iphdr
//
// struct iphdr {
// #if defined(__LITTLE_ENDIAN_BITFIELD)
// __u8 ihl:4, version:4;
// #elif defined (__BIG_ENDIAN_BITFIELD)
// __u8 version:4, ihl:4;
// #else
// ...
// }
//
// The BTF we test against is for little endian.
m := iphdr1.Members[1]
if m.Name != "version" {
t.Fatal("Expected version as the second member, got", m.Name)
}
td, ok := m.Type.(*Typedef)
if !ok {
t.Fatalf("version member of iphdr should be a __u8 typedef: actual: %T", m.Type)
}
u8, ok := td.Type.(*Int)
if !ok {
t.Fatalf("__u8 typedef should point to an Int type: actual: %T", td.Type)
}
if m.BitfieldSize != 4 {
t.Fatalf("incorrect bitfield size: expected: 4 actual: %d", m.BitfieldSize)
}
if u8.Encoding != 0 {
t.Fatalf("incorrect encoding of an __u8 int: expected: 0 actual: %x", u8.Encoding)
}
if m.Offset != 4 {
t.Fatalf("incorrect bitfield offset: expected: 4 actual: %d", m.Offset)
}
}
func BenchmarkParseVmlinux(b *testing.B) {
rd := vmlinuxTestdataReader(b)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
if _, err := rd.Seek(0, io.SeekStart); err != nil {
b.Fatal(err)
}
if _, err := loadRawSpec(rd, binary.LittleEndian, nil); err != nil {
b.Fatal("Can't load BTF:", err)
}
}
}
func TestParseCurrentKernelBTF(t *testing.T) {
spec := vmlinuxSpec(t)
if len(spec.namedTypes) == 0 {
t.Fatal("Empty kernel BTF")
}
totalBytes := 0
distinct := 0
seen := make(map[string]bool)
for _, str := range spec.strings.strings {
totalBytes += len(str)
if !seen[str] {
distinct++
seen[str] = true
}
}
t.Logf("%d strings total, %d distinct", len(spec.strings.strings), distinct)
t.Logf("Average string size: %.0f", float64(totalBytes)/float64(len(spec.strings.strings)))
}
func TestFindVMLinux(t *testing.T) {
file, err := findVMLinux()
testutils.SkipIfNotSupported(t, err)
if err != nil {
t.Fatal("Can't find vmlinux:", err)
}
defer file.Close()
spec, err := loadSpecFromELF(file)
if err != nil {
t.Fatal("Can't load BTF:", err)
}
if len(spec.namedTypes) == 0 {
t.Fatal("Empty kernel BTF")
}
}
func TestLoadSpecFromElf(t *testing.T) {
testutils.Files(t, testutils.Glob(t, "../testdata/loader-e*.elf"), func(t *testing.T, file string) {
spec := parseELFBTF(t, file)
vt, err := spec.TypeByID(0)
if err != nil {
t.Error("Can't retrieve void type by ID:", err)
}
if _, ok := vt.(*Void); !ok {
t.Errorf("Expected Void for type id 0, but got: %T", vt)
}
var bpfMapDef *Struct
if err := spec.TypeByName("bpf_map_def", &bpfMapDef); err != nil {
t.Error("Can't find bpf_map_def:", err)
}
var tmp *Void
if err := spec.TypeByName("totally_bogus_type", &tmp); !errors.Is(err, ErrNotFound) {
t.Error("TypeByName doesn't return ErrNotFound:", err)
}
var fn *Func
if err := spec.TypeByName("global_fn", &fn); err != nil {
t.Error("Can't find global_fn():", err)
} else {
if fn.Linkage != GlobalFunc {
t.Error("Expected global linkage:", fn)
}
}
var v *Var
if err := spec.TypeByName("key3", &v); err != nil {
t.Error("Cant find key3:", err)
} else {
if v.Linkage != GlobalVar {
t.Error("Expected global linkage:", v)
}
}
})
}
func TestVerifierError(t *testing.T) {
_, err := NewHandle(&Builder{})
testutils.SkipIfNotSupported(t, err)
var ve *internal.VerifierError
if !errors.As(err, &ve) {
t.Fatalf("expected a VerifierError, got: %v", err)
}
if ve.Truncated {
t.Fatalf("expected non-truncated verifier log: %v", err)
}
}
func TestLoadKernelSpec(t *testing.T) {
if _, err := os.Stat("/sys/kernel/btf/vmlinux"); os.IsNotExist(err) {
t.Skip("/sys/kernel/btf/vmlinux not present")
}
_, err := LoadKernelSpec()
if err != nil {
t.Fatal("Can't load kernel spec:", err)
}
}
func TestGuessBTFByteOrder(t *testing.T) {
bo := guessRawBTFByteOrder(vmlinuxTestdataReader(t))
if bo != binary.LittleEndian {
t.Fatalf("Guessed %s instead of %s", bo, binary.LittleEndian)
}
}
func TestSpecCopy(t *testing.T) {
spec := parseELFBTF(t, "../testdata/loader-el.elf")
if len(spec.types) < 1 {
t.Fatal("Not enough types")
}
cpy := spec.Copy()
for i := range cpy.types {
if _, ok := cpy.types[i].(*Void); ok {
// Since Void is an empty struct, a Type interface value containing
// &Void{} stores (*Void, nil). Since interface equality first compares
// the type and then the concrete value, Void is always equal.
continue
}
if cpy.types[i] == spec.types[i] {
t.Fatalf("Type at index %d is not a copy: %T == %T", i, cpy.types[i], spec.types[i])
}
}
}
func TestSpecTypeByID(t *testing.T) {
_, err := newSpec().TypeByID(0)
qt.Assert(t, err, qt.IsNil)
_, err = newSpec().TypeByID(1)
qt.Assert(t, err, qt.ErrorIs, ErrNotFound)
}
func TestHaveBTF(t *testing.T) {
testutils.CheckFeatureTest(t, haveBTF)
}
func TestHaveMapBTF(t *testing.T) {
testutils.CheckFeatureTest(t, haveMapBTF)
}
func TestHaveProgBTF(t *testing.T) {
testutils.CheckFeatureTest(t, haveProgBTF)
}
func TestHaveFuncLinkage(t *testing.T) {
testutils.CheckFeatureTest(t, haveFuncLinkage)
}
func ExampleSpec_TypeByName() {
// Acquire a Spec via one of its constructors.
spec := new(Spec)
// Declare a variable of the desired type
var foo *Struct
if err := spec.TypeByName("foo", &foo); err != nil {
// There is no struct with name foo, or there
// are multiple possibilities.
}
// We've found struct foo
fmt.Println(foo.Name)
}
func TestTypesIterator(t *testing.T) {
types := []Type{(*Void)(nil), &Int{Size: 4}, &Int{Size: 2}}
b, err := NewBuilder(types[1:])
if err != nil {
t.Fatal(err)
}
raw, err := b.Marshal(nil, nil)
if err != nil {
t.Fatal(err)
}
spec, err := LoadSpecFromReader(bytes.NewReader(raw))
if err != nil {
t.Fatal(err)
}
iter := spec.Iterate()
for i, typ := range types {
if !iter.Next() {
t.Fatal("Iterator ended early at item", i)
}
qt.Assert(t, iter.Type, qt.DeepEquals, typ)
}
if iter.Next() {
t.Fatalf("Iterator yielded too many items: %p (%[1]T)", iter.Type)
}
}
func TestLoadSplitSpecFromReader(t *testing.T) {
spec := vmlinuxTestdataSpec(t)
f, err := os.Open("testdata/btf_testmod.btf")
if err != nil {
t.Fatal(err)
}
defer f.Close()
splitSpec, err := LoadSplitSpecFromReader(f, spec)
if err != nil {
t.Fatal(err)
}
typ, err := splitSpec.AnyTypeByName("bpf_testmod_init")
if err != nil {
t.Fatal(err)
}
typeID, err := splitSpec.TypeID(typ)
if err != nil {
t.Fatal(err)
}
typeByID, err := splitSpec.TypeByID(typeID)
qt.Assert(t, err, qt.IsNil)
qt.Assert(t, typeByID, qt.Equals, typ)
fnType := typ.(*Func)
fnProto := fnType.Type.(*FuncProto)
// 'int' is defined in the base BTF...
intType, err := spec.AnyTypeByName("int")
if err != nil {
t.Fatal(err)
}
// ... but not in the split BTF
_, err = splitSpec.AnyTypeByName("int")
if err == nil {
t.Fatal("'int' is not supposed to be found in the split BTF")
}
if fnProto.Return != intType {
t.Fatalf("Return type of 'bpf_testmod_init()' (%s) does not match 'int' type (%s)",
fnProto.Return, intType)
}
// Check that copied split-BTF's spec has correct type indexing
splitSpecCopy := splitSpec.Copy()
copyType, err := splitSpecCopy.AnyTypeByName("bpf_testmod_init")
if err != nil {
t.Fatal(err)
}
copyTypeID, err := splitSpecCopy.TypeID(copyType)
if err != nil {
t.Fatal(err)
}
if copyTypeID != typeID {
t.Fatalf("'bpf_testmod_init` type ID (%d) does not match copied spec's (%d)",
typeID, copyTypeID)
}
}
func TestFixupDatasecLayout(t *testing.T) {
ds := &Datasec{
Size: 0, // Populated by fixup.
Vars: []VarSecinfo{
{Type: &Var{Type: &Int{Size: 4}}},
{Type: &Var{Type: &Int{Size: 1}}},
{Type: &Var{Type: &Int{Size: 1}}},
{Type: &Var{Type: &Int{Size: 2}}},
{Type: &Var{Type: &Int{Size: 16}}},
{Type: &Var{Type: &Int{Size: 8}}},
},
}
qt.Assert(t, fixupDatasecLayout(ds), qt.IsNil)
qt.Assert(t, ds.Size, qt.Equals, uint32(40))
qt.Assert(t, ds.Vars[0].Offset, qt.Equals, uint32(0))
qt.Assert(t, ds.Vars[1].Offset, qt.Equals, uint32(4))
qt.Assert(t, ds.Vars[2].Offset, qt.Equals, uint32(5))
qt.Assert(t, ds.Vars[3].Offset, qt.Equals, uint32(6))
qt.Assert(t, ds.Vars[4].Offset, qt.Equals, uint32(16))
qt.Assert(t, ds.Vars[5].Offset, qt.Equals, uint32(32))
}
func BenchmarkSpecCopy(b *testing.B) {
spec := vmlinuxTestdataSpec(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
spec.Copy()
}
}