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:
James Tucker authored and James Tucker committed 2026-08-26 18:31:31 -07:00
1 parent a7769cbc33
commit f55fd4e9e4
22 files changed
+621 -149

No files matched your search

+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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+
+12
View File
@@ -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{
+1
View File
@@ -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
+1
View File
@@ -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"},
+1
View File
@@ -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
+5 -74
View File
@@ -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
}
-75
View File
@@ -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)
}
}
+20
View File
@@ -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())
}
+13
View File
@@ -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
+11
View File
@@ -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 }
+133
View File
@@ -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
}
+148
View File
@@ -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)
}
}
}
+139
View File
@@ -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
})
+15
View File
@@ -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 }
+46
View File
@@ -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")
}
}
+45
View File
@@ -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)
}
}