github.com/cilium/ebpf@v0.15.1-0.20240517100537-8079b37aa138/info_test.go (about)

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