Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read LINUX_VERSION_CODE from vDSO ELF header instead of uname #500

Merged
merged 1 commit into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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