github.com/cilium/ebpf@v0.16.0/info_test.go (about)

     1  package ebpf
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"reflect"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/go-quicktest/qt"
    12  
    13  	"github.com/cilium/ebpf/asm"
    14  	"github.com/cilium/ebpf/btf"
    15  	"github.com/cilium/ebpf/internal"
    16  	"github.com/cilium/ebpf/internal/sys"
    17  	"github.com/cilium/ebpf/internal/testutils"
    18  	"github.com/cilium/ebpf/internal/unix"
    19  )
    20  
    21  func TestMapInfoFromProc(t *testing.T) {
    22  	hash, err := NewMap(&MapSpec{
    23  		Name:       "testing",
    24  		Type:       Hash,
    25  		KeySize:    4,
    26  		ValueSize:  5,
    27  		MaxEntries: 2,
    28  		Flags:      unix.BPF_F_NO_PREALLOC,
    29  	})
    30  	testutils.SkipIfNotSupported(t, err)
    31  	if err != nil {
    32  		t.Fatal(err)
    33  	}
    34  	defer hash.Close()
    35  
    36  	info, err := newMapInfoFromProc(hash.fd)
    37  	testutils.SkipIfNotSupported(t, err)
    38  	if err != nil {
    39  		t.Fatal("Can't get map info:", err)
    40  	}
    41  
    42  	if info.Type != Hash {
    43  		t.Error("Expected Hash, got", info.Type)
    44  	}
    45  
    46  	if info.KeySize != 4 {
    47  		t.Error("Expected KeySize of 4, got", info.KeySize)
    48  	}
    49  
    50  	if info.ValueSize != 5 {
    51  		t.Error("Expected ValueSize of 5, got", info.ValueSize)
    52  	}
    53  
    54  	if info.MaxEntries != 2 {
    55  		t.Error("Expected MaxEntries of 2, got", info.MaxEntries)
    56  	}
    57  
    58  	if info.Flags != unix.BPF_F_NO_PREALLOC {
    59  		t.Errorf("Expected Flags to be %d, got %d", unix.BPF_F_NO_PREALLOC, info.Flags)
    60  	}
    61  
    62  	if info.Name != "" && info.Name != "testing" {
    63  		t.Error("Expected name to be testing, got", info.Name)
    64  	}
    65  
    66  	if _, ok := info.ID(); ok {
    67  		t.Error("Expected ID to not be available")
    68  	}
    69  
    70  	nested, err := NewMap(&MapSpec{
    71  		Type:       ArrayOfMaps,
    72  		KeySize:    4,
    73  		MaxEntries: 2,
    74  		InnerMap: &MapSpec{
    75  			Type:       Array,
    76  			KeySize:    4,
    77  			ValueSize:  4,
    78  			MaxEntries: 2,
    79  		},
    80  	})
    81  	testutils.SkipIfNotSupported(t, err)
    82  	if err != nil {
    83  		t.Fatal(err)
    84  	}
    85  	defer nested.Close()
    86  
    87  	_, err = newMapInfoFromProc(nested.fd)
    88  	if err != nil {
    89  		t.Fatal("Can't get nested map info from /proc:", err)
    90  	}
    91  }
    92  
    93  func TestProgramInfo(t *testing.T) {
    94  	prog := mustSocketFilter(t)
    95  
    96  	for name, fn := range map[string]func(*sys.FD) (*ProgramInfo, error){
    97  		"generic": newProgramInfoFromFd,
    98  		"proc":    newProgramInfoFromProc,
    99  	} {
   100  		t.Run(name, func(t *testing.T) {
   101  			info, err := fn(prog.fd)
   102  			testutils.SkipIfNotSupported(t, err)
   103  			if err != nil {
   104  				t.Fatal("Can't get program info:", err)
   105  			}
   106  
   107  			if info.Type != SocketFilter {
   108  				t.Error("Expected Type to be SocketFilter, got", info.Type)
   109  			}
   110  
   111  			if info.Name != "" && info.Name != "test" {
   112  				t.Error("Expected Name to be test, got", info.Name)
   113  			}
   114  
   115  			if want := "d7edec644f05498d"; info.Tag != want {
   116  				t.Errorf("Expected Tag to be %s, got %s", want, info.Tag)
   117  			}
   118  
   119  			if id, ok := info.ID(); ok && id == 0 {
   120  				t.Error("Expected a valid ID:", id)
   121  			} else if name == "proc" && ok {
   122  				t.Error("Expected ID to not be available")
   123  			}
   124  
   125  			if name == "proc" {
   126  				_, ok := info.CreatedByUID()
   127  				qt.Assert(t, qt.IsFalse(ok))
   128  			} else {
   129  				uid, ok := info.CreatedByUID()
   130  				if testutils.IsKernelLessThan(t, "4.15") {
   131  					qt.Assert(t, qt.IsFalse(ok))
   132  				} else {
   133  					qt.Assert(t, qt.IsTrue(ok))
   134  					qt.Assert(t, qt.Equals(uid, uint32(os.Getuid())))
   135  				}
   136  			}
   137  		})
   138  	}
   139  }
   140  
   141  func TestProgramInfoMapIDs(t *testing.T) {
   142  	arr, err := NewMap(&MapSpec{
   143  		Type:       Array,
   144  		KeySize:    4,
   145  		ValueSize:  4,
   146  		MaxEntries: 1,
   147  	})
   148  	qt.Assert(t, qt.IsNil(err))
   149  	defer arr.Close()
   150  
   151  	prog, err := NewProgram(&ProgramSpec{
   152  		Type: SocketFilter,
   153  		Instructions: asm.Instructions{
   154  			asm.LoadMapPtr(asm.R0, arr.FD()),
   155  			asm.LoadImm(asm.R0, 2, asm.DWord),
   156  			asm.Return(),
   157  		},
   158  		License: "MIT",
   159  	})
   160  	qt.Assert(t, qt.IsNil(err))
   161  	defer prog.Close()
   162  
   163  	info, err := prog.Info()
   164  	testutils.SkipIfNotSupported(t, err)
   165  	qt.Assert(t, qt.IsNil(err))
   166  
   167  	ids, ok := info.MapIDs()
   168  	switch {
   169  	case testutils.IsKernelLessThan(t, "4.15"):
   170  		qt.Assert(t, qt.IsFalse(ok))
   171  		qt.Assert(t, qt.HasLen(ids, 0))
   172  
   173  	default:
   174  		qt.Assert(t, qt.IsTrue(ok))
   175  
   176  		mapInfo, err := arr.Info()
   177  		qt.Assert(t, qt.IsNil(err))
   178  
   179  		mapID, ok := mapInfo.ID()
   180  		qt.Assert(t, qt.IsTrue(ok))
   181  		qt.Assert(t, qt.ContentEquals(ids, []MapID{mapID}))
   182  	}
   183  }
   184  
   185  func TestProgramInfoMapIDsNoMaps(t *testing.T) {
   186  	prog, err := NewProgram(&ProgramSpec{
   187  		Type: SocketFilter,
   188  		Instructions: asm.Instructions{
   189  			asm.LoadImm(asm.R0, 0, asm.DWord),
   190  			asm.Return(),
   191  		},
   192  		License: "MIT",
   193  	})
   194  	qt.Assert(t, qt.IsNil(err))
   195  	defer prog.Close()
   196  
   197  	info, err := prog.Info()
   198  	testutils.SkipIfNotSupported(t, err)
   199  	qt.Assert(t, qt.IsNil(err))
   200  
   201  	ids, ok := info.MapIDs()
   202  	switch {
   203  	case testutils.IsKernelLessThan(t, "4.15"):
   204  		qt.Assert(t, qt.IsFalse(ok))
   205  		qt.Assert(t, qt.HasLen(ids, 0))
   206  
   207  	default:
   208  		qt.Assert(t, qt.IsTrue(ok))
   209  		qt.Assert(t, qt.HasLen(ids, 0))
   210  	}
   211  }
   212  
   213  func TestScanFdInfoReader(t *testing.T) {
   214  	tests := []struct {
   215  		fields map[string]interface{}
   216  		valid  bool
   217  	}{
   218  		{nil, true},
   219  		{map[string]interface{}{"foo": new(string)}, true},
   220  		{map[string]interface{}{"zap": new(string)}, false},
   221  		{map[string]interface{}{"foo": new(int)}, false},
   222  	}
   223  
   224  	for _, test := range tests {
   225  		err := scanFdInfoReader(strings.NewReader("foo:\tbar\n"), test.fields)
   226  		if test.valid {
   227  			if err != nil {
   228  				t.Errorf("fields %v returns an error: %s", test.fields, err)
   229  			}
   230  		} else {
   231  			if err == nil {
   232  				t.Errorf("fields %v doesn't return an error", test.fields)
   233  			}
   234  		}
   235  	}
   236  }
   237  
   238  // TestStats loads a BPF program once and executes back-to-back test runs
   239  // of the program. See testStats for details.
   240  func TestStats(t *testing.T) {
   241  	testutils.SkipOnOldKernel(t, "5.8", "BPF_ENABLE_STATS")
   242  
   243  	prog := mustSocketFilter(t)
   244  
   245  	pi, err := prog.Info()
   246  	if err != nil {
   247  		t.Errorf("failed to get ProgramInfo: %v", err)
   248  	}
   249  
   250  	rc, ok := pi.RunCount()
   251  	if !ok {
   252  		t.Errorf("expected run count info to be available")
   253  	}
   254  	if rc != 0 {
   255  		t.Errorf("expected a run count of 0 but got %d", rc)
   256  	}
   257  
   258  	rt, ok := pi.Runtime()
   259  	if !ok {
   260  		t.Errorf("expected runtime info to be available")
   261  	}
   262  	if rt != 0 {
   263  		t.Errorf("expected a runtime of 0ns but got %v", rt)
   264  	}
   265  
   266  	rm, ok := pi.RecursionMisses()
   267  	if !ok {
   268  		t.Errorf("expected recursion misses info to be available")
   269  	}
   270  	if rm != 0 {
   271  		t.Errorf("expected a recursion misses of 0 but got %v", rm)
   272  	}
   273  
   274  	if err := testStats(prog); err != nil {
   275  		t.Error(err)
   276  	}
   277  }
   278  
   279  // BenchmarkStats is a benchmark of TestStats. See testStats for details.
   280  func BenchmarkStats(b *testing.B) {
   281  	testutils.SkipOnOldKernel(b, "5.8", "BPF_ENABLE_STATS")
   282  
   283  	prog := mustSocketFilter(b)
   284  
   285  	for n := 0; n < b.N; n++ {
   286  		if err := testStats(prog); err != nil {
   287  			b.Fatal(fmt.Errorf("iter %d: %w", n, err))
   288  		}
   289  	}
   290  }
   291  
   292  // testStats implements the behaviour under test for TestStats
   293  // and BenchmarkStats. First, a test run is executed with runtime statistics
   294  // enabled, followed by another with runtime stats disabled. Counters are only
   295  // expected to increase on the runs where runtime stats are enabled.
   296  //
   297  // Due to runtime behaviour on Go 1.14 and higher, the syscall backing
   298  // (*Program).Test() could be invoked multiple times for each call to Test(),
   299  // resulting in RunCount incrementing by more than one. Expecting RunCount to
   300  // be of a specific value after a call to Test() is therefore not possible.
   301  // See https://golang.org/doc/go1.14#runtime for more details.
   302  func testStats(prog *Program) error {
   303  	in := internal.EmptyBPFContext
   304  
   305  	stats, err := EnableStats(uint32(unix.BPF_STATS_RUN_TIME))
   306  	if err != nil {
   307  		return fmt.Errorf("failed to enable stats: %v", err)
   308  	}
   309  	defer stats.Close()
   310  
   311  	// Program execution with runtime statistics enabled.
   312  	// Should increase both runtime and run counter.
   313  	if _, _, err := prog.Test(in); err != nil {
   314  		return fmt.Errorf("failed to trigger program: %v", err)
   315  	}
   316  
   317  	pi, err := prog.Info()
   318  	if err != nil {
   319  		return fmt.Errorf("failed to get ProgramInfo: %v", err)
   320  	}
   321  
   322  	rc, ok := pi.RunCount()
   323  	if !ok {
   324  		return errors.New("expected run count info to be available")
   325  	}
   326  	if rc < 1 {
   327  		return fmt.Errorf("expected a run count of at least 1 but got %d", rc)
   328  	}
   329  	// Store the run count for the next invocation.
   330  	lc := rc
   331  
   332  	rt, ok := pi.Runtime()
   333  	if !ok {
   334  		return errors.New("expected runtime info to be available")
   335  	}
   336  	if rt == 0 {
   337  		return errors.New("expected a runtime other than 0ns")
   338  	}
   339  	// Store the runtime value for the next invocation.
   340  	lt := rt
   341  
   342  	if err := stats.Close(); err != nil {
   343  		return fmt.Errorf("failed to disable statistics: %v", err)
   344  	}
   345  
   346  	// Second program execution, with runtime statistics gathering disabled.
   347  	// Total runtime and run counters are not expected to increase.
   348  	if _, _, err := prog.Test(in); err != nil {
   349  		return fmt.Errorf("failed to trigger program: %v", err)
   350  	}
   351  
   352  	pi, err = prog.Info()
   353  	if err != nil {
   354  		return fmt.Errorf("failed to get ProgramInfo: %v", err)
   355  	}
   356  
   357  	rc, ok = pi.RunCount()
   358  	if !ok {
   359  		return errors.New("expected run count info to be available")
   360  	}
   361  	if rc != lc {
   362  		return fmt.Errorf("run count unexpectedly increased over previous value (current: %v, prev: %v)", rc, lc)
   363  	}
   364  
   365  	rt, ok = pi.Runtime()
   366  	if !ok {
   367  		return errors.New("expected runtime info to be available")
   368  	}
   369  	if rt != lt {
   370  		return fmt.Errorf("runtime unexpectedly increased over the previous value (current: %v, prev: %v)", rt, lt)
   371  	}
   372  
   373  	return nil
   374  }
   375  
   376  func TestHaveProgramInfoMapIDs(t *testing.T) {
   377  	testutils.CheckFeatureTest(t, haveProgramInfoMapIDs)
   378  }
   379  
   380  func TestProgInfoExtBTF(t *testing.T) {
   381  	testutils.SkipOnOldKernel(t, "5.0", "Program BTF (func/line_info)")
   382  
   383  	spec, err := LoadCollectionSpec(testutils.NativeFile(t, "testdata/loader-%s.elf"))
   384  	if err != nil {
   385  		t.Fatal(err)
   386  	}
   387  
   388  	var obj struct {
   389  		Main *Program `ebpf:"xdp_prog"`
   390  	}
   391  
   392  	err = spec.LoadAndAssign(&obj, nil)
   393  	testutils.SkipIfNotSupported(t, err)
   394  	if err != nil {
   395  		t.Fatal(err)
   396  	}
   397  	defer obj.Main.Close()
   398  
   399  	info, err := obj.Main.Info()
   400  	if err != nil {
   401  		t.Fatal(err)
   402  	}
   403  
   404  	inst, err := info.Instructions()
   405  	if err != nil {
   406  		t.Fatal(err)
   407  	}
   408  
   409  	expectedLineInfoCount := 26
   410  	expectedFuncInfo := map[string]bool{
   411  		"xdp_prog":   false,
   412  		"static_fn":  false,
   413  		"global_fn2": false,
   414  		"global_fn3": false,
   415  	}
   416  
   417  	lineInfoCount := 0
   418  
   419  	for _, ins := range inst {
   420  		if ins.Source() != nil {
   421  			lineInfoCount++
   422  		}
   423  
   424  		fn := btf.FuncMetadata(&ins)
   425  		if fn != nil {
   426  			expectedFuncInfo[fn.Name] = true
   427  		}
   428  	}
   429  
   430  	if lineInfoCount != expectedLineInfoCount {
   431  		t.Errorf("expected %d line info entries, got %d", expectedLineInfoCount, lineInfoCount)
   432  	}
   433  
   434  	for fn, found := range expectedFuncInfo {
   435  		if !found {
   436  			t.Errorf("func %q not found", fn)
   437  		}
   438  	}
   439  }
   440  
   441  func TestInfoExportedFields(t *testing.T) {
   442  	// It is highly unlikely that you should be adjusting the asserts below.
   443  	// See the comment at the top of info.go for more information.
   444  
   445  	var names []string
   446  	for _, field := range reflect.VisibleFields(reflect.TypeOf(MapInfo{})) {
   447  		if field.IsExported() {
   448  			names = append(names, field.Name)
   449  		}
   450  	}
   451  	qt.Assert(t, qt.ContentEquals(names, []string{
   452  		"Type",
   453  		"KeySize",
   454  		"ValueSize",
   455  		"MaxEntries",
   456  		"Flags",
   457  		"Name",
   458  	}))
   459  
   460  	names = nil
   461  	for _, field := range reflect.VisibleFields(reflect.TypeOf(ProgramInfo{})) {
   462  		if field.IsExported() {
   463  			names = append(names, field.Name)
   464  		}
   465  	}
   466  	qt.Assert(t, qt.ContentEquals(names, []string{
   467  		"Type",
   468  		"Tag",
   469  		"Name",
   470  	}))
   471  }