Skip to content

Commit

Permalink
cpu: parse /proc/cpuinfo on linux/arm64 on old kernels when needed
Browse files Browse the repository at this point in the history
Updates tailscale/tailscale#5793
Fixes golang/go#57336

Change-Id: I4f8128bebcc58f265d447ecaaad2473aafa9131c
Reviewed-on: https://go-review.googlesource.com/c/sys/+/458315
Reviewed-by: Michael Pratt <mpratt@google.com>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
Auto-Submit: Brad Fitzpatrick <bradfitz@golang.org>
Reviewed-by: David Chase <drchase@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
  • Loading branch information
bradfitz authored and gopherbot committed Dec 20, 2022
1 parent 72f772c commit 2204b66
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 2 deletions.
44 changes: 42 additions & 2 deletions cpu/cpu_linux_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

package cpu

import (
"strings"
"syscall"
)

// HWCAP/HWCAP2 bits. These are exposed by Linux.
const (
hwcap_FP = 1 << 0
Expand Down Expand Up @@ -32,10 +37,45 @@ const (
hwcap_ASIMDFHM = 1 << 23
)

// linuxKernelCanEmulateCPUID reports whether we're running
// on Linux 4.11+. Ideally we'd like to ask the question about
// whether the current kernel contains
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=77c97b4ee21290f5f083173d957843b615abbff2
// but the version number will have to do.
func linuxKernelCanEmulateCPUID() bool {
var un syscall.Utsname
syscall.Uname(&un)
var sb strings.Builder
for _, b := range un.Release[:] {
if b == 0 {
break
}
sb.WriteByte(byte(b))
}
major, minor, _, ok := parseRelease(sb.String())
return ok && (major > 4 || major == 4 && minor >= 11)
}

func doinit() {
if err := readHWCAP(); err != nil {
// failed to read /proc/self/auxv, try reading registers directly
readARM64Registers()
// We failed to read /proc/self/auxv. This can happen if the binary has
// been given extra capabilities(7) with /bin/setcap.
//
// When this happens, we have two options. If the Linux kernel is new
// enough (4.11+), we can read the arm64 registers directly which'll
// trap into the kernel and then return back to userspace.
//
// But on older kernels, such as Linux 4.4.180 as used on many Synology
// devices, calling readARM64Registers (specifically getisar0) will
// cause a SIGILL and we'll die. So for older kernels, parse /proc/cpuinfo
// instead.
//
// See golang/go#57336.
if linuxKernelCanEmulateCPUID() {
readARM64Registers()
} else {
readLinuxProcCPUInfo()
}
return
}

Expand Down
43 changes: 43 additions & 0 deletions cpu/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cpu

import "strconv"

// parseRelease parses a dot-separated version number. It follows the semver
// syntax, but allows the minor and patch versions to be elided.
//
// This is a copy of the Go runtime's parseRelease from
// https://golang.org/cl/209597.
func parseRelease(rel string) (major, minor, patch int, ok bool) {
// Strip anything after a dash or plus.
for i := 0; i < len(rel); i++ {
if rel[i] == '-' || rel[i] == '+' {
rel = rel[:i]
break
}
}

next := func() (int, bool) {
for i := 0; i < len(rel); i++ {
if rel[i] == '.' {
ver, err := strconv.Atoi(rel[:i])
rel = rel[i+1:]
return ver, err == nil
}
}
ver, err := strconv.Atoi(rel)
rel = ""
return ver, err == nil
}
if major, ok = next(); !ok || rel == "" {
return
}
if minor, ok = next(); !ok || rel == "" {
return
}
patch, ok = next()
return
}
38 changes: 38 additions & 0 deletions cpu/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cpu

import "testing"

type parseReleaseTest struct {
in string
major, minor, patch int
}

var parseReleaseTests = []parseReleaseTest{
{"", -1, -1, -1},
{"x", -1, -1, -1},
{"5", 5, 0, 0},
{"5.12", 5, 12, 0},
{"5.12-x", 5, 12, 0},
{"5.12.1", 5, 12, 1},
{"5.12.1-x", 5, 12, 1},
{"5.12.1.0", 5, 12, 1},
{"5.20496382327982653440", -1, -1, -1},
}

func TestParseRelease(t *testing.T) {
for _, test := range parseReleaseTests {
major, minor, patch, ok := parseRelease(test.in)
if !ok {
major, minor, patch = -1, -1, -1
}
if test.major != major || test.minor != minor || test.patch != patch {
t.Errorf("parseRelease(%q) = (%v, %v, %v) want (%v, %v, %v)",
test.in, major, minor, patch,
test.major, test.minor, test.patch)
}
}
}
54 changes: 54 additions & 0 deletions cpu/proc_cpuinfo_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build linux && arm64
// +build linux,arm64

package cpu

import (
"errors"
"io"
"os"
"strings"
)

func readLinuxProcCPUInfo() error {
f, err := os.Open("/proc/cpuinfo")
if err != nil {
return err
}
defer f.Close()

var buf [1 << 10]byte // enough for first CPU
n, err := io.ReadFull(f, buf[:])
if err != nil && err != io.ErrUnexpectedEOF {
return err
}
in := string(buf[:n])
const features = "\nFeatures : "
i := strings.Index(in, features)
if i == -1 {
return errors.New("no CPU features found")
}
in = in[i+len(features):]
if i := strings.Index(in, "\n"); i != -1 {
in = in[:i]
}
m := map[string]*bool{}

initOptions() // need it early here; it's harmless to call twice
for _, o := range options {
m[o.Name] = o.Feature
}
// The EVTSTRM field has alias "evstrm" in Go, but Linux calls it "evtstrm".
m["evtstrm"] = &ARM64.HasEVTSTRM

for _, f := range strings.Fields(in) {
if p, ok := m[f]; ok {
*p = true
}
}
return nil
}

0 comments on commit 2204b66

Please sign in to comment.