Repository: cilium/ebpf
Pull request #500 (2021-11-24 09:07:10)

Read LINUX_VERSION_CODE from vDSO ELF header instead of uname
Continuation of #489 with a few changes:

- moved the `_AT_SYSINFO_EHDR` tag to the end of the auxv test before `_AT_NULL` to test unmatched/fallthrough cases (ehdr used to be the first element in the vector)
- read auxv unbuffered without `ReadAll()` since we don't have any control over its size. Avoids most allocs at the cost of more reads but auxv is already an in-memory structure in practice. Also being a bit more liberal regarding alignment, removing the double-buffering and reading it using an anon struct with Tag and Val fields.
- removed one of the buffers in parsing ELF notes
- using `unix.ByteSliceToString` for reading nul-terminated strings
- added a test for an auxv blob without a vdso pointer

Thanks @brycekahle!


Patch:
diff --git a/features/prog.go b/features/prog.go
index 04b1a88..6cfd091 100644
--- a/features/prog.go
+++ b/features/prog.go
@@ -66,7 +66,6 @@ func createProgLoadAttr(pt ebpf.ProgramType) (*sys.ProgLoadAttr, error) {
 	if err != nil {
 		return nil, fmt.Errorf("detecting kernel version: %w", err)
 	}
-	kv := v.Kernel()
 
 	return &sys.ProgLoadAttr{
 		ProgType:           sys.ProgType(pt),
@@ -75,7 +74,7 @@ func createProgLoadAttr(pt ebpf.ProgramType) (*sys.ProgLoadAttr, error) {
 		ProgFlags:          progFlags,
 		ExpectedAttachType: sys.AttachType(expectedAttachType),
 		License:            sys.NewStringPointer("GPL"),
-		KernVersion:        kv,
+		KernVersion:        v.Kernel(),
 	}, nil
 }
 
diff --git a/internal/vdso.go b/internal/vdso.go
new file mode 100644
index 0000000..aba8601
--- /dev/null
+++ b/internal/vdso.go
@@ -0,0 +1,145 @@
+package internal
+
+import (
+	"debug/elf"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+	"math"
+	"os"
+
+	"github.com/cilium/ebpf/internal/unix"
+)
+
+var (
+	errAuxvNoVDSO = errors.New("no vdso address found in auxv")
+)
+
+// vdsoVersion returns the LINUX_VERSION_CODE embedded in the vDSO library
+// linked into the current process image.
+func vdsoVersion() (uint32, error) {
+	// 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 err != nil {
+		return 0, fmt.Errorf("opening auxv: %w", err)
+	}
+	defer av.Close()
+
+	vdsoAddr, err := vdsoMemoryAddress(av)
+	if err != nil {
+		return 0, fmt.Errorf("finding vDSO memory address: %w", err)
+	}
+
+	// Use /proc/self/mem rather than unsafe.Pointer tricks.
+	mem, err := os.Open("/proc/self/mem")
+	if err != nil {
+		return 0, fmt.Errorf("opening mem: %w", err)
+	}
+	defer mem.Close()
+
+	// Open ELF at provided memory address, as offset into /proc/self/mem.
+	c, err := vdsoLinuxVersionCode(io.NewSectionReader(mem, int64(vdsoAddr), math.MaxInt64))
+	if err != nil {
+		return 0, fmt.Errorf("reading linux version code: %w", err)
+	}
+
+	return c, 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) (uint64, error) {
+	const (
+		_AT_NULL         = 0  // End of vector
+		_AT_SYSINFO_EHDR = 33 // Offset to vDSO blob in process image
+	)
+
+	// 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).
+	aux := struct{ Tag, Val uint64 }{}
+	for {
+		if err := binary.Read(r, NativeEndian, &aux); err != nil {
+			return 0, fmt.Errorf("reading auxv entry: %w", err)
+		}
+
+		switch aux.Tag {
+		case _AT_SYSINFO_EHDR:
+			if aux.Val != 0 {
+				return aux.Val, nil
+			}
+			return 0, fmt.Errorf("invalid vDSO address in auxv")
+		// _AT_NULL is always the last tag/val pair in the aux vector
+		// and can be treated like EOF.
+		case _AT_NULL:
+			return 0, errAuxvNoVDSO
+		}
+	}
+}
+
+// format described at https://www.man7.org/linux/man-pages/man5/elf.5.html in section 'Notes (Nhdr)'
+type elfNoteHeader struct {
+	NameSize int32
+	DescSize int32
+	Type     int32
+}
+
+// vdsoLinuxVersionCode returns the LINUX_VERSION_CODE embedded in
+// the ELF notes section of the binary provided by the reader.
+func vdsoLinuxVersionCode(r io.ReaderAt) (uint32, error) {
+	hdr, err := NewSafeELFFile(r)
+	if err != nil {
+		return 0, fmt.Errorf("reading vDSO ELF: %w", err)
+	}
+
+	sec := hdr.SectionByType(elf.SHT_NOTE)
+	if sec == nil {
+		return 0, fmt.Errorf("no note section found in vDSO ELF")
+	}
+
+	sr := sec.Open()
+	var n elfNoteHeader
+
+	// Read notes until we find one named 'Linux'.
+	for {
+		if err := binary.Read(sr, hdr.ByteOrder, &n); err != nil {
+			if errors.Is(err, io.EOF) {
+				return 0, fmt.Errorf("no Linux note in ELF")
+			}
+			return 0, fmt.Errorf("reading note header: %w", err)
+		}
+
+		// If a note name is defined, it follows the note header.
+		var name string
+		if n.NameSize > 0 {
+			// Read the note name, aligned to 4 bytes.
+			buf := make([]byte, Align(int(n.NameSize), 4))
+			if err := binary.Read(sr, hdr.ByteOrder, &buf); err != nil {
+				return 0, fmt.Errorf("reading note name: %w", err)
+			}
+
+			// Read nul-terminated string.
+			name = unix.ByteSliceToString(buf[:n.NameSize])
+		}
+
+		// If a note descriptor is defined, it follows the name.
+		// It is possible for a note to have a descriptor but not a name.
+		if n.DescSize > 0 {
+			// LINUX_VERSION_CODE is a uint32 value.
+			if name == "Linux" && n.DescSize == 4 && n.Type == 0 {
+				var version uint32
+				if err := binary.Read(sr, hdr.ByteOrder, &version); err != nil {
+					return 0, fmt.Errorf("reading note descriptor: %w", err)
+				}
+				return version, nil
+			}
+
+			// Discard the note descriptor if it exists but we're not interested in it.
+			if _, err := io.CopyN(io.Discard, sr, int64(Align(int(n.DescSize), 4))); err != nil {
+				return 0, err
+			}
+		}
+	}
+}
diff --git a/internal/version.go b/internal/version.go
index 4915e58..690c552 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,11 +2,7 @@ package internal
 
 import (
 	"fmt"
-	"os"
-	"regexp"
 	"sync"
-
-	"github.com/cilium/ebpf/internal/unix"
 )
 
 const (
@@ -18,12 +14,6 @@ const (
 )
 
 var (
-	// Match between one and three decimals separated by dots, with the last
-	// segment (patch level) being optional on some kernels.
-	// The x.y.z string must appear at the start of a string or right after
-	// whitespace to prevent sequences like 'x.y.z-a.b.c' from matching 'a.b.c'.
-	rgxKernelVersion = regexp.MustCompile(`(?:\A|\s)\d{1,3}\.\d{1,3}(?:\.\d{1,3})?`)
-
 	kernelVersion = struct {
 		once    sync.Once
 		version Version
@@ -46,6 +36,15 @@ func NewVersion(ver string) (Version, error) {
 	return Version{major, minor, patch}, nil
 }
 
+// NewVersionFromCode creates a version from a LINUX_VERSION_CODE.
+func NewVersionFromCode(code uint32) Version {
+	return Version{
+		uint16(uint8(code >> 16)),
+		uint16(uint8(code >> 8)),
+		uint16(uint8(code)),
+	}
+}
+
 func (v Version) String() string {
 	if v[2] == 0 {
 		return fmt.Sprintf("v%d.%d", v[0], v[1])
@@ -98,66 +97,11 @@ func KernelVersion() (Version, error) {
 	return kernelVersion.version, nil
 }
 
-// detectKernelVersion returns the version of the running kernel. It scans the
-// following sources in order: /proc/version_signature, uname -v, uname -r.
-// In each of those locations, the last-appearing x.y(.z) value is selected
-// for parsing. The first location that yields a usable version number is
-// returned.
+// detectKernelVersion returns the version of the running kernel.
 func detectKernelVersion() (Version, error) {
-
-	// Try reading /proc/version_signature for Ubuntu compatibility.
-	// Example format: Ubuntu 4.15.0-91.92-generic 4.15.18
-	// This method exists in the kernel itself, see d18acd15c
-	// ("perf tools: Fix kernel version error in ubuntu").
-	if pvs, err := os.ReadFile("/proc/version_signature"); err == nil {
-		// If /proc/version_signature exists, failing to parse it is an error.
-		// It only exists on Ubuntu, where the real patch level is not obtainable
-		// through any other method.
-		v, err := findKernelVersion(string(pvs))
-		if err != nil {
-			return Version{}, err
-		}
-		return v, nil
-	}
-
-	var uname unix.Utsname
-	if err := unix.Uname(&uname); err != nil {
-		return Version{}, fmt.Errorf("calling uname: %w", err)
-	}
-
-	// Debian puts the version including the patch level in uname.Version.
-	// It is not an error if there's no version number in uname.Version,
-	// as most distributions don't use it. Parsing can continue on uname.Release.
-	// Example format: #1 SMP Debian 4.19.37-5+deb10u2 (2019-08-08)
-	if v, err := findKernelVersion(unix.ByteSliceToString(uname.Version[:])); err == nil {
-		return v, nil
-	}
-
-	// Most other distributions have the full kernel version including patch
-	// level in uname.Release.
-	// Example format: 4.19.0-5-amd64, 5.5.10-arch1-1
-	v, err := findKernelVersion(unix.ByteSliceToString(uname.Release[:]))
+	vc, err := vdsoVersion()
 	if err != nil {
 		return Version{}, err
 	}
-
-	return v, nil
-}
-
-// findKernelVersion matches s against rgxKernelVersion and parses the result
-// into a Version. If s contains multiple matches, the last entry is selected.
-func findKernelVersion(s string) (Version, error) {
-	m := rgxKernelVersion.FindAllString(s, -1)
-	if m == nil {
-		return Version{}, fmt.Errorf("no kernel version in string: %s", s)
-	}
-	// Pick the last match of the string in case there are multiple.
-	s = m[len(m)-1]
-
-	v, err := NewVersion(s)
-	if err != nil {
-		return Version{}, fmt.Errorf("parsing version string %s: %w", s, err)
-	}
-
-	return v, nil
+	return NewVersionFromCode(vc), nil
 }


Test patch:
diff --git a/internal/vdso_test.go b/internal/vdso_test.go
new file mode 100644
index 0000000..28af18e
--- /dev/null
+++ b/internal/vdso_test.go
@@ -0,0 +1,57 @@
+package internal
+
+import (
+	"errors"
+	"os"
+	"testing"
+)
+
+func TestAuxvVDSOMemoryAddress(t *testing.T) {
+	av, err := os.Open("../testdata/auxv.bin")
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() { av.Close() })
+
+	addr, err := vdsoMemoryAddress(av)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := uint64(0x7ffd377e5000)
+	if addr != expected {
+		t.Errorf("Expected vDSO memory address %x, got %x", expected, addr)
+	}
+}
+
+func TestAuxvNoVDSO(t *testing.T) {
+	// Copy of auxv.bin with the vDSO pointer removed.
+	av, err := os.Open("../testdata/auxv_no_vdso.bin")
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() { av.Close() })
+
+	_, err = vdsoMemoryAddress(av)
+	if want, got := errAuxvNoVDSO, err; !errors.Is(got, want) {
+		t.Fatalf("expected error '%v', got: %v", want, got)
+	}
+}
+
+func TestLinuxVersionCodeEmbedded(t *testing.T) {
+	vdso, err := os.Open("../testdata/vdso.bin")
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() { vdso.Close() })
+
+	vc, err := vdsoLinuxVersionCode(vdso)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := uint32(328828) // 5.4.124
+	if vc != expected {
+		t.Errorf("Expected version code %d, got %d", expected, vc)
+	}
+}
diff --git a/internal/version_test.go b/internal/version_test.go
index 95838e0..95ead52 100644
--- a/internal/version_test.go
+++ b/internal/version_test.go
@@ -1,6 +1,7 @@
 package internal
 
 import (
+	"os"
 	"testing"
 )
 
@@ -46,47 +47,39 @@ func TestKernelVersion(t *testing.T) {
 	}
 }
 
-func TestVersionDetection(t *testing.T) {
+func TestCurrentKernelVersion(t *testing.T) {
+	v, err := KernelVersion()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if evStr := os.Getenv("KERNEL_VERSION"); evStr != "" {
+		ev, err := NewVersion(evStr)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if ev[0] != v[0] || ev[1] != v[1] {
+			t.Errorf("expected kernel version %d.%d, got %d.%d", ev[0], ev[1], v[0], v[1])
+		}
+	}
+}
+
+func TestVersionFromCode(t *testing.T) {
 	var tests = []struct {
 		name string
-		s    string
+		code uint32
 		v    Version
-		err  bool
 	}{
-		{"ubuntu version_signature", "Ubuntu 4.15.0-91.92-generic 4.15.18", Version{4, 15, 18}, false},
-		{"debian uname version", "#1 SMP Debian 4.19.37-5+deb10u2 (2019-08-08)", Version{4, 19, 37}, false},
-		{"debian uname release (missing patch)", "4.19.0-5-amd64", Version{4, 19, 0}, false},
-		{"debian uname all", "Linux foo 5.6.0-0.bpo.2-amd64 #1 SMP Debian 5.6.14-2~bpo10+1 (2020-06-09) x86_64 GNU/Linux", Version{5, 6, 14}, false},
-		{"debian custom uname version", "#1577309 SMP Thu Dec 31 08:32:02 UTC 2020", Version{}, true},
-		{"debian custom uname release (missing patch)", "4.19-ovh-xxxx-std-ipv6-64", Version{4, 19, 0}, false},
-		{"arch uname version", "#1 SMP PREEMPT Thu, 11 Mar 2021 21:27:06 +0000", Version{}, true},
-		{"arch uname release", "5.5.10-arch1-1", Version{5, 5, 10}, false},
-		{"alpine uname version", "#1-Alpine SMP Thu Jan 23 10:58:18 UTC 2020", Version{}, true},
-		{"alpine uname release", "4.14.167-0-virt", Version{4, 14, 167}, false},
-		{"fedora uname version", "#1 SMP Tue May 14 18:22:28 UTC 2019", Version{}, true},
-		{"fedora uname release", "5.0.16-100.fc28.x86_64", Version{5, 0, 16}, false},
-		{"centos8 uname version", "#1 SMP Mon Mar 1 17:16:16 UTC 2021", Version{}, true},
-		{"centos8 uname release", "4.18.0-240.15.1.el8_3.x86_64", Version{4, 18, 0}, false},
-		{"devuan uname version", "#1 SMP Debian 4.19.181-1 (2021-03-19)", Version{4, 19, 181}, false},
-		{"devuan uname release", "4.19.0-16-amd64", Version{4, 19, 0}, false},
+		{"0.0.0", 0, Version{0, 0, 0}},
+		{"1.0.0", 0x10000, Version{1, 0, 0}},
+		{"4.4.255", 0x404ff, Version{4, 4, 255}},
+		{"255.255.255", 0xffffff, Version{255, 255, 255}},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			v, err := findKernelVersion(tt.s)
-			if err != nil {
-				if !tt.err {
-					t.Error("unexpected error:", err)
-				}
-				return
-			}
-
-			if tt.err {
-				t.Error("expected error, but got none")
-			}
-
+			v := NewVersionFromCode(tt.code)
 			if v != tt.v {
-				t.Errorf("unexpected version for string '%s'. got: %v, want: %v", tt.s, v, tt.v)
+				t.Errorf("unexpected version for code '%d'. got: %v, want: %v", tt.code, v, tt.v)
 			}
 		})
 	}
diff --git a/run-tests.sh b/run-tests.sh
index c5ba4f2..472bc4f 100755
--- a/run-tests.sh
+++ b/run-tests.sh
@@ -122,6 +122,8 @@ fi
 
 export GOFLAGS=-mod=readonly
 export CGO_ENABLED=0
+# LINUX_VERSION_CODE test compares this to discovered value.
+export KERNEL_VERSION="${kernel_version}"
 
 echo Testing on "${kernel_version}"
 go test -exec "$script --exec-vm $input" "${args[@]}"