Repository: cilium/ebpf
Pull request #1319 (2024-01-26 21:03:18)

add support for reading auxv from Go runtime
Fixes https://github.com/cilium/ebpf/issues/1166

This PR extracts auxv related code to its own file. It also creates a common interface to read auxv pairs. The new go 1.21 runtime function to fetch auxv is then used to create a new auxv provider, and the old file based one is kept around for tests.

This PR also adds a test ensuring the file and runtime based auxv reader actually read the same pairs.

If we decide we can remove `TestAuxvVDSOMemoryAddress` and `TestAuxvNoVDSO`, I would be open to simplifying drastically this PR and keeping only the runtime auxv provider. Open to feedback especially on this.

lmb: @paulcacheux you could rebase this now if you want.
paulcacheux: Yes good idea, the first version of this PR kept support for older version of Go, meaning that the file provider was actually used. But now that we support only go 1.21 and up I will move it to a test file

Patch:
diff --git a/internal/auxv.go b/internal/auxv.go
new file mode 100644
index 0000000..45fd0d3
--- /dev/null
+++ b/internal/auxv.go
@@ -0,0 +1,60 @@
+package internal
+
+import (
+	"errors"
+	"io"
+	_ "unsafe"
+)
+
+type auxvPairReader interface {
+	Close() error
+	ReadAuxvPair() (uint64, uint64, error)
+}
+
+// See https://elixir.bootlin.com/linux/v6.5.5/source/include/uapi/linux/auxvec.h
+const (
+	_AT_NULL         = 0  // End of vector
+	_AT_SYSINFO_EHDR = 33 // Offset to vDSO blob in process image
+)
+
+//go:linkname runtime_getAuxv runtime.getAuxv
+func runtime_getAuxv() []uintptr
+
+type auxvRuntimeReader struct {
+	data  []uintptr
+	index int
+}
+
+func (r *auxvRuntimeReader) Close() error {
+	return nil
+}
+
+func (r *auxvRuntimeReader) ReadAuxvPair() (uint64, uint64, error) {
+	if r.index >= len(r.data)+2 {
+		return 0, 0, io.EOF
+	}
+
+	// we manually add the (_AT_NULL, _AT_NULL) pair at the end
+	// that is not provided by the go runtime
+	var tag, value uintptr
+	if r.index+1 < len(r.data) {
+		tag, value = r.data[r.index], r.data[r.index+1]
+	} else {
+		tag, value = _AT_NULL, _AT_NULL
+	}
+	r.index += 2
+	return uint64(tag), uint64(value), nil
+}
+
+func newAuxvRuntimeReader() (auxvPairReader, error) {
+	data := runtime_getAuxv()
+
+	if len(data)%2 != 0 {
+		return nil, errors.New("malformed auxv passed from runtime")
+	}
+
+	return &auxvRuntimeReader{
+		data:  data,
+		index: 0,
+	}, nil
+}
diff --git a/internal/vdso.go b/internal/vdso.go
index c444a41..1049278 100644
--- a/internal/vdso.go
+++ b/internal/vdso.go
@@ -8,7 +8,6 @@ import (
 	"io"
 	"math"
 	"os"
-	"unsafe"
 
 	"github.com/cilium/ebpf/internal/unix"
 )
@@ -20,21 +19,14 @@ var (
 // vdsoVersion returns the LINUX_VERSION_CODE embedded in the vDSO library
 // linked into the current process image.
 func vdsoVersion() (uint32, error) {
-	const uintptrIs32bits = unsafe.Sizeof((uintptr)(0)) == 4
-
-	// Read data from the auxiliary vector, which is normally passed directly
-	// to the process. Go does not expose that data, so we must read it from procfs.
-	// https://man7.org/linux/man-pages/man3/getauxval.3.html
-	av, err := os.Open("/proc/self/auxv")
-	if errors.Is(err, unix.EACCES) {
-		return 0, fmt.Errorf("opening auxv: %w (process may not be dumpable due to file capabilities)", err)
-	}
+	av, err := newAuxvRuntimeReader()
 	if err != nil {
-		return 0, fmt.Errorf("opening auxv: %w", err)
+		return 0, err
 	}
+
 	defer av.Close()
 
-	vdsoAddr, err := vdsoMemoryAddress(av, NativeEndian, uintptrIs32bits)
+	vdsoAddr, err := vdsoMemoryAddress(av)
 	if err != nil {
 		return 0, fmt.Errorf("finding vDSO memory address: %w", err)
 	}
@@ -55,43 +47,13 @@ func vdsoVersion() (uint32, error) {
 	return c, nil
 }
 
-type auxvPair32 struct {
-	Tag, Value uint32
-}
-
-type auxvPair64 struct {
-	Tag, Value uint64
-}
-
-func readAuxvPair(r io.Reader, order binary.ByteOrder, uintptrIs32bits bool) (tag, value uint64, _ error) {
-	if uintptrIs32bits {
-		var aux auxvPair32
-		if err := binary.Read(r, order, &aux); err != nil {
-			return 0, 0, fmt.Errorf("reading auxv entry: %w", err)
-		}
-		return uint64(aux.Tag), uint64(aux.Value), nil
-	}
-
-	var aux auxvPair64
-	if err := binary.Read(r, order, &aux); err != nil {
-		return 0, 0, fmt.Errorf("reading auxv entry: %w", err)
-	}
-	return aux.Tag, aux.Value, nil
-}
-
 // vdsoMemoryAddress returns the memory address of the vDSO library
 // linked into the current process image. r is an io.Reader into an auxv blob.
-func vdsoMemoryAddress(r io.Reader, order binary.ByteOrder, uintptrIs32bits bool) (uintptr, error) {
-	// See https://elixir.bootlin.com/linux/v6.5.5/source/include/uapi/linux/auxvec.h
-	const (
-		_AT_NULL         = 0  // End of vector
-		_AT_SYSINFO_EHDR = 33 // Offset to vDSO blob in process image
-	)
-
+func vdsoMemoryAddress(r auxvPairReader) (uintptr, error) {
 	// Loop through all tag/value pairs in auxv until we find `AT_SYSINFO_EHDR`,
 	// the address of a page containing the virtual Dynamic Shared Object (vDSO).
 	for {
-		tag, value, err := readAuxvPair(r, order, uintptrIs32bits)
+		tag, value, err := r.ReadAuxvPair()
 		if err != nil {
 			return 0, err
 		}


Test patch:
diff --git a/internal/auxv_test.go b/internal/auxv_test.go
new file mode 100644
index 0000000..ac9d043
--- /dev/null
+++ b/internal/auxv_test.go
@@ -0,0 +1,105 @@
+package internal
+
+import (
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"os"
+	"testing"
+	"unsafe"
+
+	"github.com/cilium/ebpf/internal/unix"
+)
+
+type auxvFileReader struct {
+	file            *os.File
+	order           binary.ByteOrder
+	uintptrIs32bits bool
+}
+
+func (r *auxvFileReader) Close() error {
+	return r.file.Close()
+}
+
+type auxvPair32 struct {
+	Tag, Value uint32
+}
+
+type auxvPair64 struct {
+	Tag, Value uint64
+}
+
+func (r *auxvFileReader) ReadAuxvPair() (tag, value uint64, _ error) {
+	if r.uintptrIs32bits {
+		var aux auxvPair32
+		if err := binary.Read(r.file, r.order, &aux); err != nil {
+			return 0, 0, fmt.Errorf("reading auxv entry: %w", err)
+		}
+		return uint64(aux.Tag), uint64(aux.Value), nil
+	}
+
+	var aux auxvPair64
+	if err := binary.Read(r.file, r.order, &aux); err != nil {
+		return 0, 0, fmt.Errorf("reading auxv entry: %w", err)
+	}
+	return aux.Tag, aux.Value, nil
+}
+
+func newAuxFileReader(path string, order binary.ByteOrder, uintptrIs32bits bool) (auxvPairReader, error) {
+	// Read data from the auxiliary vector, which is normally passed directly
+	// to the process. Go does not expose that data before go 1.21, so we must read it from procfs.
+	// https://man7.org/linux/man-pages/man3/getauxval.3.html
+	av, err := os.Open(path)
+	if errors.Is(err, unix.EACCES) {
+		return nil, fmt.Errorf("opening auxv: %w (process may not be dumpable due to file capabilities)", err)
+	}
+	if err != nil {
+		return nil, fmt.Errorf("opening auxv: %w", err)
+	}
+
+	return &auxvFileReader{
+		file:            av,
+		order:           order,
+		uintptrIs32bits: uintptrIs32bits,
+	}, nil
+}
+
+func newDefaultAuxvFileReader() (auxvPairReader, error) {
+	const uintptrIs32bits = unsafe.Sizeof((uintptr)(0)) == 4
+	return newAuxFileReader("/proc/self/auxv", NativeEndian, uintptrIs32bits)
+}
+
+func TestAuxvBothSourcesEqual(t *testing.T) {
+	runtimeBased, err := newAuxvRuntimeReader()
+	if err != nil {
+		t.Fatal(err)
+	}
+	fileBased, err := newDefaultAuxvFileReader()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	for {
+		runtimeTag, runtimeValue, err := runtimeBased.ReadAuxvPair()
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		fileTag, fileValue, err := fileBased.ReadAuxvPair()
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if runtimeTag != fileTag {
+			t.Errorf("mismatching tags: runtime=%v, file=%v", runtimeTag, fileTag)
+		}
+
+		if runtimeValue != fileValue {
+			t.Errorf("mismatching values: runtime=%v, file=%v", runtimeValue, fileValue)
+		}
+
+		if runtimeTag == _AT_NULL {
+			break
+		}
+	}
+}
diff --git a/internal/vdso_test.go b/internal/vdso_test.go
index d500a61..bb6f73b 100644
--- a/internal/vdso_test.go
+++ b/internal/vdso_test.go
@@ -19,13 +19,13 @@ func TestAuxvVDSOMemoryAddress(t *testing.T) {
 		{"auxv32le.bin", true, 0xb7fc3000},
 	} {
 		t.Run(testcase.source, func(t *testing.T) {
-			av, err := os.Open("testdata/" + testcase.source)
+			av, err := newAuxFileReader("testdata/"+testcase.source, binary.LittleEndian, testcase.is32bit)
 			if err != nil {
 				t.Fatal(err)
 			}
 			t.Cleanup(func() { av.Close() })
 
-			addr, err := vdsoMemoryAddress(av, binary.LittleEndian, testcase.is32bit)
+			addr, err := vdsoMemoryAddress(av)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -39,13 +39,13 @@ func TestAuxvVDSOMemoryAddress(t *testing.T) {
 
 func TestAuxvNoVDSO(t *testing.T) {
 	// Copy of auxv.bin with the vDSO pointer removed.
-	av, err := os.Open("testdata/auxv64le_no_vdso.bin")
+	av, err := newAuxFileReader("testdata/auxv64le_no_vdso.bin", binary.LittleEndian, false)
 	if err != nil {
 		t.Fatal(err)
 	}
 	t.Cleanup(func() { av.Close() })
 
-	_, err = vdsoMemoryAddress(av, binary.LittleEndian, false)
+	_, err = vdsoMemoryAddress(av)
 	if want, got := errAuxvNoVDSO, err; !errors.Is(got, want) {
 		t.Fatalf("expected error '%v', got: %v", want, got)
 	}