Repository: cilium/ebpf
Pull request #913 (2023-01-22 19:48:27)

link: make KprobeMultiOptions.Addresses a []uintptr
The kernel expects kprobe_multi.addrs to point at an array of unsigned long. The size of unsigned long is compiler or platform dependent, so using uint64 is incorrect on 32-bit arches.

uintptr seems like the closest analog to C's unsigned long, although I've not been able to figure out how uintptr is really defined.

See https://elixir.bootlin.com/linux/v6.1.7/source/kernel/trace/bpf_trace.c#L2671

Signed-off-by: Lorenz Bauer <oss@lmb.io>


Patch:
diff --git a/info.go b/info.go
index 8f42173..e0168cd 100644
--- a/info.go
+++ b/info.go
@@ -130,6 +130,12 @@ func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
 		pi.maps = make([]MapID, info.NrMapIds)
 		info2.NrMapIds = info.NrMapIds
 		info2.MapIds = sys.NewPointer(unsafe.Pointer(&pi.maps[0]))
+	} else if haveProgramInfoMapIDs() == nil {
+		// This program really has no associated maps.
+		pi.maps = make([]MapID, 0)
+	} else {
+		// The kernel doesn't report associated maps.
+		pi.maps = nil
 	}
 
 	if info.XlatedProgLen > 0 {
@@ -321,3 +327,30 @@ func EnableStats(which uint32) (io.Closer, error) {
 	}
 	return fd, nil
 }
+
+var haveProgramInfoMapIDs = internal.NewFeatureTest("map IDs in program info", "4.15", func() error {
+	prog, err := progLoad(asm.Instructions{
+		asm.LoadImm(asm.R0, 0, asm.DWord),
+		asm.Return(),
+	}, SocketFilter, "MIT")
+	if err != nil {
+		return err
+	}
+	defer prog.Close()
+
+	err = sys.ObjInfo(prog, &sys.ProgInfo{
+		// NB: Don't need to allocate MapIds since the program isn't using
+		// any maps.
+		NrMapIds: 1,
+	})
+	if errors.Is(err, unix.EINVAL) {
+		// Most likely the syscall doesn't exist.
+		return internal.ErrNotSupported
+	}
+	if errors.Is(err, unix.E2BIG) {
+		// We've hit check_uarg_tail_zero on older kernels.
+		return internal.ErrNotSupported
+	}
+
+	return err
+})
diff --git a/internal/cpu.go b/internal/cpu.go
index 3affa1e..9e908b6 100644
--- a/internal/cpu.go
+++ b/internal/cpu.go
@@ -4,24 +4,13 @@ import (
 	"fmt"
 	"os"
 	"strings"
-	"sync"
 )
 
-var sysCPU struct {
-	once sync.Once
-	err  error
-	num  int
-}
-
 // PossibleCPUs returns the max number of CPUs a system may possibly have
 // Logical CPU numbers must be of the form 0-n
-func PossibleCPUs() (int, error) {
-	sysCPU.once.Do(func() {
-		sysCPU.num, sysCPU.err = parseCPUsFromFile("/sys/devices/system/cpu/possible")
-	})
-
-	return sysCPU.num, sysCPU.err
-}
+var PossibleCPUs = Memoize(func() (int, error) {
+	return parseCPUsFromFile("/sys/devices/system/cpu/possible")
+})
 
 func parseCPUsFromFile(path string) (int, error) {
 	spec, err := os.ReadFile(path)
diff --git a/internal/memoize.go b/internal/memoize.go
new file mode 100644
index 0000000..3de0a3f
--- /dev/null
+++ b/internal/memoize.go
@@ -0,0 +1,26 @@
+package internal
+
+import (
+	"sync"
+)
+
+type memoizedFunc[T any] struct {
+	once   sync.Once
+	fn     func() (T, error)
+	result T
+	err    error
+}
+
+func (mf *memoizedFunc[T]) do() (T, error) {
+	mf.once.Do(func() {
+		mf.result, mf.err = mf.fn()
+	})
+	return mf.result, mf.err
+}
+
+// Memoize the result of a function call.
+//
+// fn is only ever called once, even if it returns an error.
+func Memoize[T any](fn func() (T, error)) func() (T, error) {
+	return (&memoizedFunc[T]{fn: fn}).do
+}
diff --git a/internal/version.go b/internal/version.go
index 370e01e..9b17ffb 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"fmt"
-	"sync"
 
 	"github.com/cilium/ebpf/internal/unix"
 )
@@ -15,14 +14,6 @@ const (
 	MagicKernelVersion = 0xFFFFFFFE
 )
 
-var (
-	kernelVersion = struct {
-		once    sync.Once
-		version Version
-		err     error
-	}{}
-)
-
 // A Version in the form Major.Minor.Patch.
 type Version [3]uint16
 
@@ -88,16 +79,9 @@ func (v Version) Kernel() uint32 {
 }
 
 // KernelVersion returns the version of the currently running kernel.
-func KernelVersion() (Version, error) {
-	kernelVersion.once.Do(func() {
-		kernelVersion.version, kernelVersion.err = detectKernelVersion()
-	})
-
-	if kernelVersion.err != nil {
-		return Version{}, kernelVersion.err
-	}
-	return kernelVersion.version, nil
-}
+var KernelVersion = Memoize(func() (Version, error) {
+	return detectKernelVersion()
+})
 
 // detectKernelVersion returns the version of the running kernel.
 func detectKernelVersion() (Version, error) {
diff --git a/link/kprobe_multi.go b/link/kprobe_multi.go
index 151f47d..697c6d7 100644
--- a/link/kprobe_multi.go
+++ b/link/kprobe_multi.go
@@ -28,7 +28,7 @@ type KprobeMultiOptions struct {
 	// limits the attach point to the function entry or return.
 	//
 	// Mutually exclusive with Symbols.
-	Addresses []uint64
+	Addresses []uintptr
 
 	// Cookies specifies arbitrary values that can be fetched from an eBPF
 	// program via `bpf_get_attach_cookie()`.


Test patch:
diff --git a/btf/btf_test.go b/btf/btf_test.go
index d681a16..0fc3fd1 100644
--- a/btf/btf_test.go
+++ b/btf/btf_test.go
@@ -7,7 +7,6 @@ import (
 	"fmt"
 	"io"
 	"os"
-	"sync"
 	"testing"
 
 	"github.com/cilium/ebpf/internal"
@@ -32,53 +31,45 @@ func vmlinuxSpec(tb testing.TB) *Spec {
 	return spec
 }
 
-// vmlinuxTestdata caches the result of reading and parsing a BTF blob from
-// testdata.
-var vmlinuxTestdata struct {
-	sync.Once
+type specAndRawBTF struct {
 	raw  []byte
 	spec *Spec
-	err  error
 }
 
-func doVMLinuxTestdata() {
+var vmlinuxTestdata = internal.Memoize(func() (specAndRawBTF, error) {
 	b, err := internal.ReadAllCompressed("testdata/vmlinux.btf.gz")
 	if err != nil {
-		vmlinuxTestdata.err = fmt.Errorf("uncompressing vmlinux testdata: %w", err)
-		return
+		return specAndRawBTF{}, err
 	}
-	vmlinuxTestdata.raw = b
 
-	s, err := loadRawSpec(bytes.NewReader(b), binary.LittleEndian, nil, nil)
+	spec, err := loadRawSpec(bytes.NewReader(b), binary.LittleEndian, nil, nil)
 	if err != nil {
-		vmlinuxTestdata.err = fmt.Errorf("parsing vmlinux testdata types: %w", err)
-		return
+		return specAndRawBTF{}, err
 	}
-	vmlinuxTestdata.spec = s
-}
+
+	return specAndRawBTF{b, spec}, nil
+})
 
 func vmlinuxTestdataReader(tb testing.TB) *bytes.Reader {
 	tb.Helper()
 
-	vmlinuxTestdata.Do(doVMLinuxTestdata)
-
-	if err := vmlinuxTestdata.err; err != nil {
+	td, err := vmlinuxTestdata()
+	if err != nil {
 		tb.Fatal(err)
 	}
 
-	return bytes.NewReader(vmlinuxTestdata.raw)
+	return bytes.NewReader(td.raw)
 }
 
 func vmlinuxTestdataSpec(tb testing.TB) *Spec {
 	tb.Helper()
 
-	vmlinuxTestdata.Do(doVMLinuxTestdata)
-
-	if err := vmlinuxTestdata.err; err != nil {
+	td, err := vmlinuxTestdata()
+	if err != nil {
 		tb.Fatal(err)
 	}
 
-	return vmlinuxTestdata.spec.Copy()
+	return td.spec.Copy()
 }
 
 func parseELFBTF(tb testing.TB, file string) *Spec {
@@ -140,10 +131,7 @@ func TestTypeByNameAmbiguous(t *testing.T) {
 }
 
 func TestTypeByName(t *testing.T) {
-	spec, err := LoadSpecFromReader(vmlinuxTestdataReader(t))
-	if err != nil {
-		t.Fatal(err)
-	}
+	spec := vmlinuxTestdataSpec(t)
 
 	for _, typ := range []interface{}{
 		nil,
@@ -415,17 +403,14 @@ func ExampleSpec_TypeByName() {
 }
 
 func TestTypesIterator(t *testing.T) {
-	spec, err := LoadSpecFromReader(vmlinuxTestdataReader(t))
-	if err != nil {
-		t.Fatal(err)
-	}
+	spec := vmlinuxTestdataSpec(t)
 
 	if len(spec.types) < 1 {
 		t.Fatal("Not enough types")
 	}
 
 	// Assertion that 'iphdr' type exists within the spec
-	_, err = spec.AnyTypeByName("iphdr")
+	_, err := spec.AnyTypeByName("iphdr")
 	if err != nil {
 		t.Fatalf("Failed to find 'iphdr' type by name: %s", err)
 	}
@@ -450,10 +435,7 @@ func TestTypesIterator(t *testing.T) {
 }
 
 func TestLoadSplitSpecFromReader(t *testing.T) {
-	spec, err := LoadSpecFromReader(vmlinuxTestdataReader(t))
-	if err != nil {
-		t.Fatal(err)
-	}
+	spec := vmlinuxTestdataSpec(t)
 
 	f, err := os.Open("testdata/btf_testmod.btf")
 	if err != nil {
@@ -512,5 +494,4 @@ func TestLoadSplitSpecFromReader(t *testing.T) {
 		t.Fatalf("'bpf_testmod_init` type ID (%d) does not match copied spec's (%d)",
 			typeID, copyTypeID)
 	}
-
 }
diff --git a/info_test.go b/info_test.go
index 9655387..c50e18d 100644
--- a/info_test.go
+++ b/info_test.go
@@ -122,8 +122,6 @@ func TestProgramInfo(t *testing.T) {
 }
 
 func TestProgramInfoMapIDs(t *testing.T) {
-	testutils.SkipOnOldKernel(t, "4.10", "reading program info")
-
 	arr, err := NewMap(&MapSpec{
 		Type:       Array,
 		KeySize:    4,
@@ -146,21 +144,52 @@ func TestProgramInfoMapIDs(t *testing.T) {
 	defer prog.Close()
 
 	info, err := prog.Info()
+	testutils.SkipIfNotSupported(t, err)
 	qt.Assert(t, err, qt.IsNil)
 
 	ids, ok := info.MapIDs()
-	if testutils.MustKernelVersion().Less(internal.Version{4, 15, 0}) {
+	switch {
+	case testutils.IsKernelLessThan(t, "4.15"):
 		qt.Assert(t, ok, qt.IsFalse)
 		qt.Assert(t, ids, qt.HasLen, 0)
-	} else {
+
+	default:
 		qt.Assert(t, ok, qt.IsTrue)
-		qt.Assert(t, ids, qt.HasLen, 1)
 
 		mapInfo, err := arr.Info()
 		qt.Assert(t, err, qt.IsNil)
+
 		mapID, ok := mapInfo.ID()
 		qt.Assert(t, ok, qt.IsTrue)
-		qt.Assert(t, ids[0], qt.Equals, mapID)
+		qt.Assert(t, ids, qt.ContentEquals, []MapID{mapID})
+	}
+}
+
+func TestProgramInfoMapIDsNoMaps(t *testing.T) {
+	prog, err := NewProgram(&ProgramSpec{
+		Type: SocketFilter,
+		Instructions: asm.Instructions{
+			asm.LoadImm(asm.R0, 0, asm.DWord),
+			asm.Return(),
+		},
+		License: "MIT",
+	})
+	qt.Assert(t, err, qt.IsNil)
+	defer prog.Close()
+
+	info, err := prog.Info()
+	testutils.SkipIfNotSupported(t, err)
+	qt.Assert(t, err, qt.IsNil)
+
+	ids, ok := info.MapIDs()
+	switch {
+	case testutils.IsKernelLessThan(t, "4.15"):
+		qt.Assert(t, ok, qt.IsFalse)
+		qt.Assert(t, ids, qt.HasLen, 0)
+
+	default:
+		qt.Assert(t, ok, qt.IsTrue)
+		qt.Assert(t, ids, qt.HasLen, 0)
 	}
 }
 
@@ -318,3 +347,7 @@ func testStats(prog *Program) error {
 
 	return nil
 }
+
+func TestHaveProgramInfoMapIDs(t *testing.T) {
+	testutils.CheckFeatureTest(t, haveProgramInfoMapIDs)
+}
diff --git a/internal/testutils/cgroup.go b/internal/testutils/cgroup.go
index 9b2af5f..b3c6020 100644
--- a/internal/testutils/cgroup.go
+++ b/internal/testutils/cgroup.go
@@ -4,45 +4,29 @@ import (
 	"errors"
 	"os"
 	"strings"
-	"sync"
 	"testing"
 
+	"github.com/cilium/ebpf/internal"
 	"golang.org/x/sys/unix"
 )
 
-var cgroup2 = struct {
-	once sync.Once
-	path string
-	err  error
-}{}
-
-func cgroup2Path() (string, error) {
-	cgroup2.once.Do(func() {
-		mounts, err := os.ReadFile("/proc/mounts")
-		if err != nil {
-			cgroup2.err = err
-			return
-		}
-
-		for _, line := range strings.Split(string(mounts), "\n") {
-			mount := strings.SplitN(line, " ", 3)
-			if mount[0] == "cgroup2" {
-				cgroup2.path = mount[1]
-				return
-			}
+var cgroup2Path = internal.Memoize(func() (string, error) {
+	mounts, err := os.ReadFile("/proc/mounts")
+	if err != nil {
+		return "", err
+	}
 
-			continue
+	for _, line := range strings.Split(string(mounts), "\n") {
+		mount := strings.SplitN(line, " ", 3)
+		if mount[0] == "cgroup2" {
+			return mount[1], nil
 		}
 
-		cgroup2.err = errors.New("cgroup2 not mounted")
-	})
-
-	if cgroup2.err != nil {
-		return "", cgroup2.err
+		continue
 	}
 
-	return cgroup2.path, nil
-}
+	return "", errors.New("cgroup2 not mounted")
+})
 
 func CreateCgroup(tb testing.TB) *os.File {
 	tb.Helper()
diff --git a/internal/testutils/feature.go b/internal/testutils/feature.go
index 0db609e..a8397b2 100644
--- a/internal/testutils/feature.go
+++ b/internal/testutils/feature.go
@@ -8,14 +8,6 @@ import (
 	"github.com/cilium/ebpf/internal"
 )
 
-func MustKernelVersion() internal.Version {
-	v, err := internal.KernelVersion()
-	if err != nil {
-		panic(err)
-	}
-	return v
-}
-
 func CheckFeatureTest(t *testing.T, fn func() error) {
 	checkFeatureTestError(t, fn())
 }
@@ -65,11 +57,10 @@ func checkKernelVersion(tb testing.TB, ufe *internal.UnsupportedFeatureError) {
 		return
 	}
 
-	kernelVersion := MustKernelVersion()
-	if ufe.MinimumVersion.Less(kernelVersion) {
+	if !isKernelLessThan(tb, ufe.MinimumVersion) {
 		tb.Helper()
-		tb.Fatalf("Feature '%s' isn't supported even though kernel %s is newer than %s",
-			ufe.Name, kernelVersion, ufe.MinimumVersion)
+		tb.Fatalf("Feature '%s' isn't supported even though kernel is newer than %s",
+			ufe.Name, ufe.MinimumVersion)
 	}
 }
 
@@ -89,6 +80,12 @@ func IsKernelLessThan(tb testing.TB, minVersion string) bool {
 		tb.Fatalf("Invalid version %s: %s", minVersion, err)
 	}
 
+	return isKernelLessThan(tb, minv)
+}
+
+func isKernelLessThan(tb testing.TB, minv internal.Version) bool {
+	tb.Helper()
+
 	if max := os.Getenv("CI_MAX_KERNEL_VERSION"); max != "" {
 		maxv, err := internal.NewVersion(max)
 		if err != nil {
@@ -100,5 +97,15 @@ func IsKernelLessThan(tb testing.TB, minVersion string) bool {
 		}
 	}
 
-	return MustKernelVersion().Less(minv)
+	return kernelVersion(tb).Less(minv)
+}
+
+func kernelVersion(tb testing.TB) internal.Version {
+	tb.Helper()
+
+	v, err := internal.KernelVersion()
+	if err != nil {
+		tb.Fatal(err)
+	}
+	return v
 }
diff --git a/link/kprobe_multi_test.go b/link/kprobe_multi_test.go
index 57c91a2..e6b64bc 100644
--- a/link/kprobe_multi_test.go
+++ b/link/kprobe_multi_test.go
@@ -2,7 +2,6 @@ package link
 
 import (
 	"errors"
-	"math"
 	"os"
 	"testing"
 
@@ -40,7 +39,7 @@ func TestKprobeMultiInput(t *testing.T) {
 	// Symbols and Addresses are mutually exclusive.
 	_, err = KprobeMulti(prog, KprobeMultiOptions{
 		Symbols:   []string{"foo"},
-		Addresses: []uint64{1},
+		Addresses: []uintptr{1},
 	})
 	if !errors.Is(err, errInvalidInput) {
 		t.Fatalf("expected errInvalidInput, got: %v", err)
@@ -70,7 +69,7 @@ func TestKprobeMultiErrors(t *testing.T) {
 	// Only have a negative test for addresses as it would be hard to maintain a
 	// proper one.
 	if _, err := KprobeMulti(prog, KprobeMultiOptions{
-		Addresses: []uint64{math.MaxUint64},
+		Addresses: []uintptr{^uintptr(0)},
 	}); !errors.Is(err, unix.EINVAL) {
 		t.Fatalf("expected EINVAL, got: %s", err)
 	}
diff --git a/link/kprobe_test.go b/link/kprobe_test.go
index 9c01e43..a80f99d 100644
--- a/link/kprobe_test.go
+++ b/link/kprobe_test.go
@@ -10,7 +10,6 @@ import (
 
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/asm"
-	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/testutils"
 	"github.com/cilium/ebpf/internal/unix"
 )
@@ -83,7 +82,7 @@ func TestKretprobeMaxActive(t *testing.T) {
 	}
 
 	k, err = Kretprobe("__put_task_struct", prog, &KprobeOptions{RetprobeMaxActive: 4096})
-	if testutils.MustKernelVersion().Less(internal.Version{4, 12, 0}) {
+	if testutils.IsKernelLessThan(t, "4.12") {
 		if !errors.Is(err, ErrNotSupported) {
 			t.Fatal("Expected ErrNotSupported, got", err)
 		}
diff --git a/prog_test.go b/prog_test.go
index 583349f..31466df 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -749,7 +749,7 @@ func TestProgramAttachToKernel(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "5.5", "attach_btf_id")
 
 	haveTestmod := false
-	if !testutils.MustKernelVersion().Less(internal.Version{5, 11}) {
+	if !testutils.IsKernelLessThan(t, "5.11") {
 		// See https://github.com/torvalds/linux/commit/290248a5b7d829871b3ea3c62578613a580a1744
 		testmod, err := btf.FindHandle(func(info *btf.HandleInfo) bool {
 			return info.IsModule() && info.Name == "bpf_testmod"