Skip to content

Commit

Permalink
Read LINUX_VERSION_CODE from vDSO ELF header instead of uname
Browse files Browse the repository at this point in the history
  • Loading branch information
brycekahle authored and ti-mo committed Nov 24, 2021
1 parent 89c6acb commit 2a3b945
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 102 deletions.
3 changes: 1 addition & 2 deletions features/prog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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
}

Expand Down
145 changes: 145 additions & 0 deletions internal/vdso.go
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
57 changes: 57 additions & 0 deletions internal/vdso_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
80 changes: 12 additions & 68 deletions internal/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ package internal

import (
"fmt"
"os"
"regexp"
"sync"

"github.com/cilium/ebpf/internal/unix"
)

const (
Expand All @@ -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
Expand All @@ -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])
Expand Down Expand Up @@ -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
}
Loading

0 comments on commit 2a3b945

Please sign in to comment.