cpu: parse /proc/cpuinfo on linux/arm64 on old kernels when needed

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>
diff --git a/cpu/cpu_linux_arm64.go b/cpu/cpu_linux_arm64.go
index 79a38a0..a968b80 100644
--- a/cpu/cpu_linux_arm64.go
+++ b/cpu/cpu_linux_arm64.go
@@ -4,6 +4,11 @@
 
 package cpu
 
+import (
+	"strings"
+	"syscall"
+)
+
 // HWCAP/HWCAP2 bits. These are exposed by Linux.
 const (
 	hwcap_FP       = 1 << 0
@@ -32,10 +37,45 @@
 	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
 	}
 
diff --git a/cpu/parse.go b/cpu/parse.go
new file mode 100644
index 0000000..762b63d
--- /dev/null
+++ b/cpu/parse.go
@@ -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
+}
diff --git a/cpu/parse_test.go b/cpu/parse_test.go
new file mode 100644
index 0000000..5a35881
--- /dev/null
+++ b/cpu/parse_test.go
@@ -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)
+		}
+	}
+}
diff --git a/cpu/proc_cpuinfo_linux.go b/cpu/proc_cpuinfo_linux.go
new file mode 100644
index 0000000..d87bd6b
--- /dev/null
+++ b/cpu/proc_cpuinfo_linux.go
@@ -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
+}