mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-14 23:11:39 -04:00
util/cpucaps, tsweb/varz: extract and extend GOAMD64 and CPU capability detection
Move the goamd64_capable/goamd64_compiled logic out of tsweb/varz into a new util/cpucaps package so that a future hostinfo extension can share it, and extend it to all supported platforms. The host GOAMD64 level is now computed from CPUID feature bits via x/sys/cpu rather than by parsing /proc/cpuinfo, which removes the linux/amd64 restriction and all file I/O. CPUID is also what the Go runtime checks at startup, so it directly answers the question the metric asks: could this machine run a binary built at a higher level. Four flags required by the runtime checks have no x/sys/cpu field: LAHF/SAHF (v2) and MOVBE, LZCNT and F16C (v3). We report a level when all of its other flags are present. That is exact on real hardware: LAHF/SAHF exists on every 64-bit CPU that also has SSE4.2, and MOVBE/LZCNT/F16C exist on every CPU that also has AVX2, BMI2 and FMA (Haswell and Zen onwards). A linux/amd64 test cross-checks the CPUID result against the exact /proc/cpuinfo flag lists, so a test run on any machine where the assumption fails, or where the kernel's view diverges from CPUID (e.g. clearcpuid=), reports the difference. Add a Caps bitmask of the CPU capabilities that Go's runtime and standard library dispatch on, normalized across amd64 and arm64: AES, CLMUL/PMULL, SHA2, SHA512, CRC32, ADX and LSE, plus the vector capabilities not implied by the GOAMD64 level: AVX, AVX2, AVX512 (the v4 F+BW+CD+DQ+VL subset), VAES, VPCLMULQDQ and GFNI. Detection is lazy and reads only x/sys/cpu globals and build info; there is no init-time work and no new dependency. Coverage includes all key client platforms: linux, windows, darwin, freebsd, android (via the linux auxv/MRS path in x/sys/cpu) and ios (via the darwin sysctl path). x/sys/cpu has no arm64 detection on FreeBSD, so read ID_AA64ISAR0_EL1 directly; the FreeBSD kernel emulates EL0 reads of the ID registers, and Go's runtime already depends on that emulation on this platform. The whole feature is behind a new cpucaps feature tag. Building with ts_omit_cpucaps removes the varz metrics and all detection code following the tka stub pattern: the detection functions compile to stubs returning zero inside the package itself, so future importers cannot reintroduce the code by forgetting a build tag, and the Caps type stays available for decoding recorded values. A tailscaled dependency test enforces the omission, and the generated buildfeatures.HasCPUCaps const is available for the later hostinfo extension. tsweb/varz metric names are unchanged. gauge_goamd64_capable values only change on machines where /proc/cpuinfo disagrees with CPUID. Updates #21002 Signed-off-by: James Tucker <james@tailscale.com>
This commit is contained in:
1 parent
a7769cbc33
commit
f55fd4e9e4
22 files changed
+621
-149
No files matched your search
@@ -155,6 +155,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/util/cibuild from tailscale.com/health+
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netmon+
|
||||
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/cpucaps from tailscale.com/tsweb/varz
|
||||
tailscale.com/util/ctxkey from tailscale.com/tsweb+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
|
||||
@@ -887,6 +887,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
|
||||
tailscale.com/util/cloudinfo from tailscale.com/wgengine/magicsock
|
||||
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
L tailscale.com/util/cpucaps from tailscale.com/tsweb/varz
|
||||
tailscale.com/util/ctxkey from tailscale.com/client/tailscale/apitype+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting
|
||||
tailscale.com/util/def from tailscale.com/ipn/localapi
|
||||
|
||||
@@ -80,6 +80,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/types/tkatype from tailscale.com/tailcfg+
|
||||
tailscale.com/types/views from tailscale.com/net/tsaddr+
|
||||
tailscale.com/util/bufiox from tailscale.com/types/key
|
||||
L tailscale.com/util/cpucaps from tailscale.com/tsweb/varz
|
||||
tailscale.com/util/ctxkey from tailscale.com/tsweb+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/tailcfg
|
||||
|
||||
@@ -294,6 +294,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy+
|
||||
L tailscale.com/util/cpucaps from tailscale.com/tsweb/varz
|
||||
tailscale.com/util/ctxkey from tailscale.com/types/logger+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
|
||||
@@ -467,6 +467,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/util/cloudinfo from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
L tailscale.com/util/cpucaps from tailscale.com/tsweb/varz
|
||||
tailscale.com/util/ctxkey from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting
|
||||
tailscale.com/util/def from tailscale.com/feature/debugportmapper+
|
||||
|
||||
@@ -37,6 +37,18 @@ func TestOmitFavorites(t *testing.T) {
|
||||
}.Check(t)
|
||||
}
|
||||
|
||||
func TestOmitCPUCaps(t *testing.T) {
|
||||
const msg = "unexpected with ts_omit_cpucaps"
|
||||
deptest.DepChecker{
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
Tags: "ts_omit_cpucaps,ts_include_cli",
|
||||
BadDeps: map[string]string{
|
||||
"tailscale.com/util/cpucaps": msg,
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
|
||||
func TestOmitSSH(t *testing.T) {
|
||||
const msg = "unexpected with ts_omit_ssh"
|
||||
deptest.DepChecker{
|
||||
|
||||
@@ -275,6 +275,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
|
||||
tailscale.com/util/cloudinfo from tailscale.com/wgengine/magicsock
|
||||
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
L tailscale.com/util/cpucaps from tailscale.com/tsweb/varz
|
||||
tailscale.com/util/ctxkey from tailscale.com/client/tailscale/apitype+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting
|
||||
tailscale.com/util/def from tailscale.com/ipn/localapi
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_cpucaps
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasCPUCaps is whether the binary was built with support for modular feature "CPU capability and GOAMD64 microarchitecture level detection and reporting".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_cpucaps" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasCPUCaps = false
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_cpucaps
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasCPUCaps is whether the binary was built with support for modular feature "CPU capability and GOAMD64 microarchitecture level detection and reporting".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_cpucaps" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasCPUCaps = true
|
||||
@@ -140,6 +140,7 @@ type FeatureMeta struct {
|
||||
},
|
||||
"completion": {Sym: "Completion", Desc: "CLI shell completion"},
|
||||
"conn25": {Sym: "Conn25", Desc: "Route traffic for configured domains through connector devices"},
|
||||
"cpucaps": {Sym: "CPUCaps", Desc: "CPU capability and GOAMD64 microarchitecture level detection and reporting"},
|
||||
"completion_scripts": {
|
||||
Sym: "CompletionScripts", Desc: "embed CLI shell completion scripts",
|
||||
Deps: []FeatureTag{"completion"},
|
||||
|
||||
@@ -270,6 +270,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
|
||||
tailscale.com/util/cloudinfo from tailscale.com/wgengine/magicsock
|
||||
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
L tailscale.com/util/cpucaps from tailscale.com/tsweb/varz
|
||||
tailscale.com/util/ctxkey from tailscale.com/client/tailscale/apitype+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting
|
||||
tailscale.com/util/def from tailscale.com/ipn/localapi
|
||||
|
||||
@@ -1,88 +1,19 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !android
|
||||
//go:build !android && !ts_omit_cpucaps
|
||||
|
||||
package varz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"expvar"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/util/cpucaps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
var capable any = hostGOAMD64Level()
|
||||
var capable any = cpucaps.HostGOAMD64Level()
|
||||
expvar.Publish("gauge_goamd64_capable", expvar.Func(func() any { return capable }))
|
||||
var compiled any = compiledGOAMD64Level()
|
||||
var compiled any = cpucaps.CompiledGOAMD64Level()
|
||||
expvar.Publish("gauge_goamd64_compiled", expvar.Func(func() any { return compiled }))
|
||||
}
|
||||
|
||||
// goamd64LevelFlags maps each x86-64 microarchitecture level to the
|
||||
// /proc/cpuinfo flag names it requires beyond the previous level.
|
||||
// It matches the per-level feature checks in Go's runtime/asm_amd64.s.
|
||||
// Note that /proc/cpuinfo spells SSE3 as "pni" and LZCNT as "abm",
|
||||
// and the kernel clears the AVX and AVX-512 flags when the OS lacks
|
||||
// XSAVE support, so OSXSAVE needs no separate check.
|
||||
var goamd64LevelFlags = [...][]string{
|
||||
2: {"cx16", "lahf_lm", "popcnt", "pni", "sse4_1", "sse4_2", "ssse3"},
|
||||
3: {"abm", "avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "movbe"},
|
||||
4: {"avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"},
|
||||
}
|
||||
|
||||
// hostGOAMD64Level returns the maximum GOAMD64 microarchitecture
|
||||
// level (1-4) that the host CPU supports, regardless of what level
|
||||
// this binary was compiled for. It returns 0 on error.
|
||||
func hostGOAMD64Level() int {
|
||||
cpuinfo, err := os.ReadFile("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
for line := range bytes.Lines(cpuinfo) {
|
||||
name, rest, ok := bytes.Cut(line, []byte(":"))
|
||||
if ok && string(bytes.TrimSpace(name)) == "flags" {
|
||||
return goamd64Level(strings.Fields(string(rest)))
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// goamd64Level returns the maximum GOAMD64 level (1-4) supported by
|
||||
// a CPU with the given /proc/cpuinfo flags.
|
||||
func goamd64Level(flags []string) int {
|
||||
has := make(map[string]bool, len(flags))
|
||||
for _, f := range flags {
|
||||
has[f] = true
|
||||
}
|
||||
level := 1
|
||||
for next := 2; next < len(goamd64LevelFlags); next++ {
|
||||
for _, f := range goamd64LevelFlags[next] {
|
||||
if !has[f] {
|
||||
return level
|
||||
}
|
||||
}
|
||||
level = next
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
// compiledGOAMD64Level returns the GOAMD64 microarchitecture level
|
||||
// (1-4) that this binary was compiled for, as recorded in its build
|
||||
// info. It returns 0 if unknown.
|
||||
func compiledGOAMD64Level() int {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
for _, s := range bi.Settings {
|
||||
if s.Key == "GOAMD64" {
|
||||
if v, ok := strings.CutPrefix(s.Value, "v"); ok && len(v) == 1 && v[0] >= '1' && v[0] <= '4' {
|
||||
return int(v[0] - '0')
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !android
|
||||
|
||||
package varz
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const v4Flags = "fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov " +
|
||||
"pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm " +
|
||||
"constant_tsc rep_good nopl xtopology nonstop_tsc cpuid aperfmperf " +
|
||||
"tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic " +
|
||||
"movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor " +
|
||||
"lahf_lm abm 3dnowprefetch invpcid_single pti fsgsbase tsc_adjust bmi1 " +
|
||||
"avx2 smep bmi2 erms invpcid avx512f avx512dq rdseed adx smap clflushopt " +
|
||||
"clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves"
|
||||
|
||||
func withoutFlags(flags string, remove ...string) []string {
|
||||
var out []string
|
||||
fs := strings.Fields(flags)
|
||||
Outer:
|
||||
for _, f := range fs {
|
||||
for _, r := range remove {
|
||||
if f == r {
|
||||
continue Outer
|
||||
}
|
||||
}
|
||||
out = append(out, f)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestGoAMD64Level(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string
|
||||
want int
|
||||
}{
|
||||
{"v4", withoutFlags(v4Flags), 4},
|
||||
{"v3_no_avx512vl", withoutFlags(v4Flags, "avx512vl"), 3},
|
||||
{"v3_no_avx512", withoutFlags(v4Flags, "avx512f", "avx512dq", "avx512cd", "avx512bw", "avx512vl"), 3},
|
||||
{"v2_no_avx2", withoutFlags(v4Flags, "avx512f", "avx2"), 2},
|
||||
{"v2_no_abm", withoutFlags(v4Flags, "avx512bw", "abm"), 2},
|
||||
{"v1_no_popcnt", withoutFlags(v4Flags, "avx2", "popcnt"), 1},
|
||||
{"v1_no_sse42", withoutFlags(v4Flags, "sse4_2"), 1},
|
||||
{"empty", nil, 1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := goamd64Level(tt.flags); got != tt.want {
|
||||
t.Errorf("goamd64Level = %d; want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostGOAMD64Level(t *testing.T) {
|
||||
got := hostGOAMD64Level()
|
||||
t.Logf("hostGOAMD64Level = %d", got)
|
||||
if got < 0 || got > 4 {
|
||||
t.Errorf("out of range [0,4]")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompiledGOAMD64Level(t *testing.T) {
|
||||
got := compiledGOAMD64Level()
|
||||
t.Logf("compiledGOAMD64Level = %d", got)
|
||||
if got < 1 || got > 4 {
|
||||
t.Errorf("got %d; want in range [1,4] since test binaries have build info", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && arm64 && !ts_omit_cpucaps
|
||||
|
||||
package cpucaps
|
||||
|
||||
// getisar0 returns the ID_AA64ISAR0_EL1 instruction set attribute
|
||||
// register. The register is EL1-privileged, but the FreeBSD kernel
|
||||
// emulates EL0 MRS reads of the ID_AA64* registers; Go's
|
||||
// runtime/internal/cpu relies on the same emulation on this
|
||||
// platform, so any running Go binary proves it is available.
|
||||
func getisar0() uint64
|
||||
|
||||
// hostCapsArch supplements x/sys/cpu, which has no arm64 feature
|
||||
// detection on FreeBSD (it reports only the baseline FP/ASIMD
|
||||
// features there).
|
||||
func hostCapsArch() Caps {
|
||||
return capsFromISAR0(getisar0())
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && arm64 && gc && !ts_omit_cpucaps
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func getisar0() uint64
|
||||
TEXT ·getisar0(SB), NOSPLIT, $0-8
|
||||
// get Instruction Set Attributes 0 into R0
|
||||
MRS ID_AA64ISAR0_EL1, R0
|
||||
MOVD R0, ret+0(FP)
|
||||
RET
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !(freebsd && arm64) && !ts_omit_cpucaps
|
||||
|
||||
package cpucaps
|
||||
|
||||
// hostCapsArch returns capabilities not detectable via x/sys/cpu.
|
||||
// On most platforms x/sys/cpu is complete and this returns 0; see
|
||||
// caps_freebsd_arm64.go for the exception.
|
||||
func hostCapsArch() Caps { return 0 }
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package cpucaps reports capabilities of the host CPU that Go
|
||||
// generated code and assembly can make use of, along with the
|
||||
// x86-64 microarchitecture level the running binary was compiled
|
||||
// for.
|
||||
//
|
||||
// Detection is lazy (no init-time work) and sources its data from
|
||||
// golang.org/x/sys/cpu, which uses raw CPUID on x86 and in-process
|
||||
// auxv/HWCAP (with trapped MRS register reads as fallback) on
|
||||
// linux/arm64, so normally no files are read or parsed (x/sys/cpu
|
||||
// retains /proc fallbacks on linux/arm64 that are unreachable when
|
||||
// built with Go 1.21+). The host GOAMD64 level is derived from CPUID
|
||||
// feature bits, mirroring the per-level checks in Go's
|
||||
// runtime/asm_amd64.s: raw CPUID is what the Go runtime itself
|
||||
// dispatches on, so it is the ground truth for what level a binary
|
||||
// could run at on this machine. A linux/amd64 test cross-checks the
|
||||
// derivation against the exact /proc/cpuinfo flag lists.
|
||||
//
|
||||
// All key target platforms are supported without external OS
|
||||
// dependencies (no files parsed, no processes executed):
|
||||
//
|
||||
// - amd64 (linux, windows, darwin, freebsd, android): raw CPUID
|
||||
// - linux+android/arm64: in-process auxv HWCAP, falling back to
|
||||
// kernel-emulated MRS reads of the ID_AA64* registers
|
||||
// - darwin+ios/arm64: sysctl syscalls (the ios GOOS satisfies the
|
||||
// darwin build constraint in x/sys/cpu)
|
||||
// - windows/arm64: IsProcessorFeaturePresent
|
||||
// - freebsd/arm64: kernel-emulated MRS read of ID_AA64ISAR0_EL1,
|
||||
// as Go's runtime/internal/cpu does on this platform (x/sys/cpu
|
||||
// does not yet support it)
|
||||
//
|
||||
// Building with the ts_omit_cpucaps tag removes all detection code:
|
||||
// the detection functions then return zero values. The Caps type and
|
||||
// its names remain available for decoding recorded values.
|
||||
package cpucaps
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Caps is a bitmask of host CPU capabilities that Go's runtime and
|
||||
// standard library dispatch on, normalized across architectures
|
||||
// (e.g. CapAES means x86 AES-NI or arm64 FEAT_AES).
|
||||
//
|
||||
// Bits are append-only: never reuse or renumber a bit, even for
|
||||
// features that become obsolete, so recorded values keep their
|
||||
// meaning. Keep bit positions below 53 so values survive JSON
|
||||
// round-trips through IEEE-754 consumers.
|
||||
type Caps uint64
|
||||
|
||||
const (
|
||||
CapAES Caps = 1 << iota // x86 AES-NI / arm64 FEAT_AES
|
||||
CapCLMUL // carry-less multiply for GHASH/GCM: x86 PCLMULQDQ / arm64 FEAT_PMULL
|
||||
CapSHA2 // SHA-256 instructions: arm64 FEAT_SHA256 (x86 SHA-NI not yet exposed by x/sys/cpu)
|
||||
CapSHA512 // SHA-512 instructions: arm64 FEAT_SHA512
|
||||
CapCRC32 // arm64 FEAT_CRC32 (on x86, CRC32 is implied by GOAMD64 level >= 2 via SSE4.2)
|
||||
CapADX // x86 multi-precision add-carry (math/big)
|
||||
CapLSE // arm64 FEAT_LSE atomics
|
||||
CapAVX // x86 AVX, incl. OS XSAVE support
|
||||
CapAVX2 // x86 AVX2, incl. OS XSAVE support (GOAMD64 v3 vector baseline)
|
||||
CapAVX512 // x86 AVX-512 F+BW+CD+DQ+VL, the GOAMD64 v4 usable subset, incl. OS ZMM state support
|
||||
CapVAES // x86 VAES: vectorized AES over 256/512-bit lanes
|
||||
CapVPCLMUL // x86 VPCLMULQDQ: vectorized carry-less multiply
|
||||
CapGFNI // x86 Galois field new instructions
|
||||
|
||||
capMax
|
||||
)
|
||||
|
||||
var capNames = [...]string{
|
||||
"aes", "clmul", "sha2", "sha512", "crc32", "adx", "lse",
|
||||
"avx", "avx2", "avx512", "vaes", "vpclmul", "gfni",
|
||||
}
|
||||
|
||||
// String returns a pipe-separated lower-case list of the set
|
||||
// capability names, e.g. "aes|clmul|lse", or "" if none are set.
|
||||
// Set bits without a known name (e.g. in a value recorded by a
|
||||
// newer binary) are reported as a single hex remainder, e.g.
|
||||
// "aes|unknown(0x4000)".
|
||||
func (c Caps) String() string {
|
||||
var sb strings.Builder
|
||||
for i, name := range capNames {
|
||||
if c&(1<<i) != 0 {
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteByte('|')
|
||||
}
|
||||
sb.WriteString(name)
|
||||
}
|
||||
}
|
||||
if unknown := c &^ (capMax - 1); unknown != 0 {
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteByte('|')
|
||||
}
|
||||
sb.WriteString("unknown(0x")
|
||||
sb.WriteString(strconv.FormatUint(uint64(unknown), 16))
|
||||
sb.WriteString(")")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// capsFromISAR0 returns the capabilities encoded in the arm64
|
||||
// ID_AA64ISAR0_EL1 instruction set attribute register. Field values
|
||||
// follow the ARM Architecture Reference Manual and match the
|
||||
// decoding in x/sys/cpu's parseARM64SystemRegisters and Go's
|
||||
// runtime/internal/cpu. It is used on platforms where x/sys/cpu
|
||||
// lacks arm64 feature detection (currently FreeBSD).
|
||||
func capsFromISAR0(isar0 uint64) (c Caps) {
|
||||
extract := func(lo uint) uint64 { return (isar0 >> lo) & 0xf }
|
||||
|
||||
switch extract(4) { // AES field
|
||||
case 2:
|
||||
c |= CapCLMUL
|
||||
fallthrough
|
||||
case 1:
|
||||
c |= CapAES
|
||||
}
|
||||
switch extract(12) { // SHA2 field
|
||||
case 2:
|
||||
c |= CapSHA512
|
||||
fallthrough
|
||||
case 1:
|
||||
c |= CapSHA2
|
||||
}
|
||||
if extract(16) >= 1 { // CRC32 field
|
||||
c |= CapCRC32
|
||||
}
|
||||
if extract(20) >= 2 { // Atomic field
|
||||
c |= CapLSE
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cpucaps
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// goamd64LevelFlags maps each x86-64 microarchitecture level to the
|
||||
// /proc/cpuinfo flag names it requires beyond the previous level.
|
||||
// It matches the per-level feature checks in Go's runtime/asm_amd64.s
|
||||
// and serves as the exact reference that the x/sys/cpu-derived
|
||||
// implementation is checked against. Note that /proc/cpuinfo spells
|
||||
// SSE3 as "pni" and LZCNT as "abm", and the kernel clears the AVX
|
||||
// and AVX-512 flags when the OS lacks XSAVE support, so OSXSAVE
|
||||
// needs no separate check.
|
||||
var goamd64LevelFlags = [...][]string{
|
||||
2: {"cx16", "lahf_lm", "popcnt", "pni", "sse4_1", "sse4_2", "ssse3"},
|
||||
3: {"abm", "avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "movbe"},
|
||||
4: {"avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"},
|
||||
}
|
||||
|
||||
// goamd64LevelFromFlags returns the maximum GOAMD64 level (1-4)
|
||||
// supported by a CPU with the given /proc/cpuinfo flags.
|
||||
func goamd64LevelFromFlags(flags []string) int {
|
||||
has := make(map[string]bool, len(flags))
|
||||
for _, f := range flags {
|
||||
has[f] = true
|
||||
}
|
||||
level := 1
|
||||
for next := 2; next < len(goamd64LevelFlags); next++ {
|
||||
for _, f := range goamd64LevelFlags[next] {
|
||||
if !has[f] {
|
||||
return level
|
||||
}
|
||||
}
|
||||
level = next
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
const v4Flags = "fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov " +
|
||||
"pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm " +
|
||||
"constant_tsc rep_good nopl xtopology nonstop_tsc cpuid aperfmperf " +
|
||||
"tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic " +
|
||||
"movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor " +
|
||||
"lahf_lm abm 3dnowprefetch invpcid_single pti fsgsbase tsc_adjust bmi1 " +
|
||||
"avx2 smep bmi2 erms invpcid avx512f avx512dq rdseed adx smap clflushopt " +
|
||||
"clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves"
|
||||
|
||||
func withoutFlags(flags string, remove ...string) []string {
|
||||
var out []string
|
||||
fs := strings.Fields(flags)
|
||||
Outer:
|
||||
for _, f := range fs {
|
||||
for _, r := range remove {
|
||||
if f == r {
|
||||
continue Outer
|
||||
}
|
||||
}
|
||||
out = append(out, f)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestGoAMD64LevelFromFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string
|
||||
want int
|
||||
}{
|
||||
{"v4", withoutFlags(v4Flags), 4},
|
||||
{"v3_no_avx512vl", withoutFlags(v4Flags, "avx512vl"), 3},
|
||||
{"v3_no_avx512", withoutFlags(v4Flags, "avx512f", "avx512dq", "avx512cd", "avx512bw", "avx512vl"), 3},
|
||||
{"v2_no_avx2", withoutFlags(v4Flags, "avx512f", "avx2"), 2},
|
||||
{"v2_no_abm", withoutFlags(v4Flags, "avx512bw", "abm"), 2},
|
||||
{"v1_no_popcnt", withoutFlags(v4Flags, "avx2", "popcnt"), 1},
|
||||
{"v1_no_sse42", withoutFlags(v4Flags, "sse4_2"), 1},
|
||||
{"empty", nil, 1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := goamd64LevelFromFlags(tt.flags); got != tt.want {
|
||||
t.Errorf("goamd64LevelFromFlags = %d; want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapNamesInSync(t *testing.T) {
|
||||
if want := Caps(1) << len(capNames); capMax != want {
|
||||
t.Errorf("capMax = %#x, want %#x; capNames and the Cap constants are out of sync", uint64(capMax), uint64(want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapsFromISAR0(t *testing.T) {
|
||||
// Field values per the ARM ARM for ID_AA64ISAR0_EL1:
|
||||
// AES [7:4], SHA2 [15:12], CRC32 [19:16], Atomic [23:20].
|
||||
mk := func(aes, sha2, crc32, atomic uint64) uint64 {
|
||||
return aes<<4 | sha2<<12 | crc32<<16 | atomic<<20
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
isar0 uint64
|
||||
want Caps
|
||||
}{
|
||||
{"none", 0, 0},
|
||||
{"aes_only", mk(1, 0, 0, 0), CapAES},
|
||||
{"aes_pmull", mk(2, 0, 0, 0), CapAES | CapCLMUL},
|
||||
{"sha256", mk(0, 1, 0, 0), CapSHA2},
|
||||
{"sha512", mk(0, 2, 0, 0), CapSHA2 | CapSHA512},
|
||||
{"crc32", mk(0, 0, 1, 0), CapCRC32},
|
||||
{"atomic_v81", mk(0, 0, 0, 2), CapLSE},
|
||||
{"atomic_lse128", mk(0, 0, 0, 3), CapLSE},
|
||||
{"atomic_reserved1", mk(0, 0, 0, 1), 0},
|
||||
{"neoverse_n1", mk(2, 2, 1, 2), CapAES | CapCLMUL | CapSHA2 | CapSHA512 | CapCRC32 | CapLSE},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := capsFromISAR0(tt.isar0); got != tt.want {
|
||||
t.Errorf("capsFromISAR0(%#x) = %v; want %v", tt.isar0, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapsString(t *testing.T) {
|
||||
tests := []struct {
|
||||
c Caps
|
||||
want string
|
||||
}{
|
||||
{0, ""},
|
||||
{CapAES, "aes"},
|
||||
{CapAES | CapCLMUL | CapLSE, "aes|clmul|lse"},
|
||||
{CapSHA512 | CapADX, "sha512|adx"},
|
||||
{CapAVX | CapAVX2 | CapAVX512, "avx|avx2|avx512"},
|
||||
{CapVAES | CapVPCLMUL | CapGFNI, "vaes|vpclmul|gfni"},
|
||||
{capMax, "unknown(0x2000)"},
|
||||
{CapAES | capMax | capMax<<1, "aes|unknown(0x6000)"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := tt.c.String(); got != tt.want {
|
||||
t.Errorf("Caps(%#x).String() = %q; want %q", uint64(tt.c), got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_cpucaps
|
||||
|
||||
package cpucaps
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/cpu"
|
||||
)
|
||||
|
||||
// Host returns the capabilities of the host CPU.
|
||||
//
|
||||
// The result is computed once on first call from x/sys/cpu feature
|
||||
// bits that were already initialized at process start; subsequent
|
||||
// calls are free.
|
||||
func Host() Caps { return hostCaps() }
|
||||
|
||||
var hostCaps = sync.OnceValue(func() (c Caps) {
|
||||
if runtime.GOARCH == "amd64" || runtime.GOARCH == "386" {
|
||||
x := cpu.X86
|
||||
if x.HasAES {
|
||||
c |= CapAES
|
||||
}
|
||||
if x.HasPCLMULQDQ {
|
||||
c |= CapCLMUL
|
||||
}
|
||||
if x.HasADX {
|
||||
c |= CapADX
|
||||
}
|
||||
if x.HasAVX {
|
||||
c |= CapAVX
|
||||
}
|
||||
if x.HasAVX2 {
|
||||
c |= CapAVX2
|
||||
}
|
||||
if x.HasAVX512F && x.HasAVX512BW && x.HasAVX512CD && x.HasAVX512DQ && x.HasAVX512VL {
|
||||
c |= CapAVX512
|
||||
}
|
||||
// Note: x/sys/cpu detects VAES/VPCLMULQDQ/GFNI only when
|
||||
// the OS supports AVX-512 state, so hybrid parts that pair
|
||||
// these with only 256-bit vectors (e.g. Alder Lake E-core
|
||||
// configs) are not reported.
|
||||
if x.HasAVX512VAES {
|
||||
c |= CapVAES
|
||||
}
|
||||
if x.HasAVX512VPCLMULQDQ {
|
||||
c |= CapVPCLMUL
|
||||
}
|
||||
if x.HasAVX512GFNI {
|
||||
c |= CapGFNI
|
||||
}
|
||||
// x/sys/cpu does not expose x86 SHA-NI; CapSHA2 is
|
||||
// currently reported on arm64 only.
|
||||
}
|
||||
if runtime.GOARCH == "arm64" {
|
||||
a := cpu.ARM64
|
||||
if a.HasAES {
|
||||
c |= CapAES
|
||||
}
|
||||
if a.HasPMULL {
|
||||
c |= CapCLMUL
|
||||
}
|
||||
if a.HasSHA2 {
|
||||
c |= CapSHA2
|
||||
}
|
||||
if a.HasSHA512 {
|
||||
c |= CapSHA512
|
||||
}
|
||||
if a.HasCRC32 {
|
||||
c |= CapCRC32
|
||||
}
|
||||
if a.HasATOMICS {
|
||||
c |= CapLSE
|
||||
}
|
||||
}
|
||||
c |= hostCapsArch()
|
||||
return c
|
||||
})
|
||||
|
||||
// HostGOAMD64Level returns the maximum GOAMD64 microarchitecture
|
||||
// level (1-4) that the host CPU supports per its CPUID feature bits,
|
||||
// regardless of what level this binary was compiled for. It returns
|
||||
// 0 if the host is not x86-64.
|
||||
func HostGOAMD64Level() int { return hostGOAMD64Level() }
|
||||
|
||||
var hostGOAMD64Level = sync.OnceValue(goamd64LevelFromXSysCPU)
|
||||
|
||||
// goamd64LevelFromXSysCPU returns the maximum GOAMD64 level (1-4)
|
||||
// derived from x/sys/cpu feature bits, or 0 if not running on amd64.
|
||||
// It mirrors the per-level feature checks in Go's runtime/asm_amd64.s,
|
||||
// with one gap: x/sys/cpu does not expose LAHF/SAHF (level 2) or
|
||||
// MOVBE, LZCNT and F16C (level 3), so those are assumed present when
|
||||
// the remaining flags of their level are; no known x86-64 CPU has
|
||||
// the checked subset without them. TestCPUInfoMatchesXSysCPU
|
||||
// cross-checks this assumption against the exact /proc/cpuinfo flag
|
||||
// lists on linux/amd64.
|
||||
func goamd64LevelFromXSysCPU() int {
|
||||
if runtime.GOARCH != "amd64" {
|
||||
return 0
|
||||
}
|
||||
x := cpu.X86
|
||||
if !(x.HasCX16 && x.HasPOPCNT && x.HasSSE3 && x.HasSSSE3 && x.HasSSE41 && x.HasSSE42) {
|
||||
return 1
|
||||
}
|
||||
if !(x.HasAVX && x.HasAVX2 && x.HasBMI1 && x.HasBMI2 && x.HasFMA && x.HasOSXSAVE) {
|
||||
return 2
|
||||
}
|
||||
if !(x.HasAVX512F && x.HasAVX512BW && x.HasAVX512CD && x.HasAVX512DQ && x.HasAVX512VL) {
|
||||
return 3
|
||||
}
|
||||
return 4
|
||||
}
|
||||
|
||||
// CompiledGOAMD64Level returns the GOAMD64 microarchitecture level
|
||||
// (1-4) that this binary was compiled for, as recorded in its build
|
||||
// info. It returns 0 if unknown or if the binary is not amd64.
|
||||
func CompiledGOAMD64Level() int { return compiledGOAMD64Level() }
|
||||
|
||||
var compiledGOAMD64Level = sync.OnceValue(func() int {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
for _, s := range bi.Settings {
|
||||
if s.Key == "GOAMD64" {
|
||||
if v, ok := strings.CutPrefix(s.Value, "v"); ok && len(v) == 1 && v[0] >= '1' && v[0] <= '4' {
|
||||
return int(v[0] - '0')
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return 0
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_cpucaps
|
||||
|
||||
package cpucaps
|
||||
|
||||
// Host returns 0 in binaries built with ts_omit_cpucaps.
|
||||
func Host() Caps { return 0 }
|
||||
|
||||
// HostGOAMD64Level returns 0 in binaries built with ts_omit_cpucaps.
|
||||
func HostGOAMD64Level() int { return 0 }
|
||||
|
||||
// CompiledGOAMD64Level returns 0 in binaries built with ts_omit_cpucaps.
|
||||
func CompiledGOAMD64Level() int { return 0 }
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_cpucaps
|
||||
|
||||
package cpucaps
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHostGOAMD64Level(t *testing.T) {
|
||||
got := HostGOAMD64Level()
|
||||
t.Logf("HostGOAMD64Level = %d", got)
|
||||
if runtime.GOARCH == "amd64" {
|
||||
if got < 1 || got > 4 {
|
||||
t.Errorf("got %d; want in range [1,4] on amd64", got)
|
||||
}
|
||||
} else if got != 0 {
|
||||
t.Errorf("got %d; want 0 on non-amd64", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompiledGOAMD64Level(t *testing.T) {
|
||||
got := CompiledGOAMD64Level()
|
||||
t.Logf("CompiledGOAMD64Level = %d", got)
|
||||
if runtime.GOARCH == "amd64" {
|
||||
if got < 1 || got > 4 {
|
||||
t.Errorf("got %d; want in range [1,4] since test binaries have build info", got)
|
||||
}
|
||||
} else if got != 0 {
|
||||
t.Errorf("got %d; want 0 on non-amd64", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostCaps(t *testing.T) {
|
||||
c := Host()
|
||||
t.Logf("Host caps = %#x (%v)", uint64(c), c)
|
||||
if c >= capMax {
|
||||
t.Errorf("unexpected bits set beyond capMax: %#x", uint64(c))
|
||||
}
|
||||
if Host() != c {
|
||||
t.Errorf("Host not stable across calls")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64 && !android && !ts_omit_cpucaps
|
||||
|
||||
package cpucaps
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCPUInfoMatchesXSysCPU cross-checks the x/sys/cpu CPUID
|
||||
// derivation of the host GOAMD64 level against the exact
|
||||
// /proc/cpuinfo flag lists. The x/sys/cpu path assumes LAHF/SAHF,
|
||||
// MOVBE, LZCNT and F16C accompany the rest of their level's flags; a
|
||||
// mismatch here either means we found real hardware where that
|
||||
// assumption is wrong and HostGOAMD64Level would over-report, or
|
||||
// that the kernel's view diverges from raw CPUID (e.g. a clearcpuid=
|
||||
// boot parameter or a hypervisor filtering /proc/cpuinfo but not
|
||||
// guest CPUID).
|
||||
func TestCPUInfoMatchesXSysCPU(t *testing.T) {
|
||||
cpuinfo, err := os.ReadFile("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
t.Skipf("skipping: %v", err)
|
||||
}
|
||||
exact := -1
|
||||
for line := range bytes.Lines(cpuinfo) {
|
||||
name, rest, ok := bytes.Cut(line, []byte(":"))
|
||||
if ok && string(bytes.TrimSpace(name)) == "flags" {
|
||||
exact = goamd64LevelFromFlags(strings.Fields(string(rest)))
|
||||
break
|
||||
}
|
||||
}
|
||||
if exact == -1 {
|
||||
t.Skip("skipping: no flags line in /proc/cpuinfo")
|
||||
}
|
||||
approx := HostGOAMD64Level()
|
||||
t.Logf("cpuinfo=%d x/sys/cpu=%d", exact, approx)
|
||||
if exact != approx {
|
||||
t.Errorf("/proc/cpuinfo level %d != x/sys/cpu level %d", exact, approx)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user