populate HWCap from auxv on linux - #1
Merged
Merged
Conversation
The Go runtime fills internal/cpu.HWCap and HWCap2 from the ELF auxiliary vector before package initialization, but it does not know about this copy of the package. On linux they stayed zero, so every feature derived from them read false: all ARM64 features (AES, PMULL, SHA1/2/3/512, CRC32, ATOMICS, CPUID, DIT), ARM VFPv4/IDIVA, Loong64 LSX/LASX, MIPS64X MSA, PPC64 POWER8/9/10, DARN, SCV and S390X VX/VXE. Fill them in from a package-level variable initializer, which runs before init calls doinit, unless they are already set. The auxiliary vector comes from runtime.getAuxv on Go 1.21+, which also works for non-dumpable processes such as binaries with file capabilities, and from /proc/self/auxv on Go 1.20.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On linux, every feature that is derived from
HWCap/HWCap2readsfalse. Affected architectures:ARM64fields (AES, PMULL, SHA1/2/3/512, CRC32, ATOMICS, CPUID, DIT, IsNeoverse)ARM.HasVFPv4,ARM.HasIDIVALoong64.HasLSX,HasLASXMIPS64X.HasMSAPPC64.IsPOWER8/9/10,HasDARN,HasSCVS390X.HasVX,HasVXEExample from a UniFi Express 7 (linux/arm64, kernel 5.4,
/proc/cpuinfofeaturesfp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid), running v0.1.1:Downstream,
github.com/metacubex/tlscomputeshasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL. That value is nowfalse, so on arm64 TLS 1.3 clients offer ChaCha20-Poly1305 first. In mihomo this includes every QUIC transport, and Poly1305 then runs in generic Go.Root cause
This package is a copy of
internal/cpu. Ininternal/cpu, the runtime'sarchauxv(runtime/os_linux_*.go) fillsHWCapandHWCap2from the ELF auxiliary vector before initialization. The runtime has no knowledge ofgithub.com/metacubex/cpu.HWCap, so that variable stays 0. The single-argument//go:linkname HWCaponly lets other packages link to this symbol; it does not aliasinternal/cpu.HWCap.Fix
Only new files are added. No file copied from
internal/cpuchanges, so future syncs are unaffected.hwcap_linux.go(linux; arm, arm64, loong64, mips64x, ppc64x, s390x): setsHWCapfromAT_HWCAP, only if it is still zero. The value is assigned from a package-level variable initializer. Those run before anyinitfunction, so beforeinitcallsdoinit. Aninitfunction would not work: init functions run in file order, so one inhwcap_linux.gowould run after the one incpu.go.hwcap2_linux.go(linux; arm, ppc64x): does the same forHWCap2fromAT_HWCAP2.runtime_auxv_go121.go: on Go 1.21+, the auxiliary vector comes fromruntime.getAuxvvia linkname, as ingolang.org/x/sys/cpu. The runtime marks this symbol as linkname-able (go.dev/issue/57336, go.dev/issue/67401), so the Go 1.23+ linker accepts it. This path also works for non-dumpable processes, such as binaries with file capabilities, which cannot read/proc/self/auxv.runtime_auxv_go120.go: Go 1.20 (the module'sgodirective) has noruntime.getAuxv, so the code falls back to reading/proc/self/auxvin native word size and byte order.Why not
//go:linkname ... internal/cpu.HWCapdirectly? With Go 1.26 the linker allows that only on arm64. On arm, loong64, mips64, ppc64le and s390x it fails withinvalid reference to internal/cpu.HWCap.What this PR does not change:
Platform(AT_PLATFORM) is still not populated on linux/arm, soARM.HasV7Atomicsstaysfalseas before.Verification
gofmt -l .prints nothing.go vet ./...andgo test ./...pass on darwin/arm64 with Go 1.20.14, 1.21.13, 1.22.12, 1.23.12, 1.24.5, 1.25.4 and 1.26.8.go vet ./...andgo test -cpass for linux/{arm64, arm, amd64, 386, ppc64le, ppc64, s390x, loong64, mips64, mips64le, riscv64}, android/arm64, freebsd/arm64 and darwin/arm64.syscallwith Go 1.24.5, and it fails the same way onmain.On the UniFi Express 7 (probe built with Go 1.26.8,
golang.org/x/sysv0.30.0):HasATOMICS=falseis correct here because this CPU has no LSE atomics.ARM HasVFPv4=false HasIDIVA=false HWCap=0x0. With this PR it reportsHasVFPv4=true HasIDIVA=true HWCap=0x37b0d6, which matches x/sys/cpu.PR_SET_DUMPABLE=0before this package initialized and ran as uid 65534, so reading/proc/self/auxvfailed withpermission denied. This PR built with Go 1.26.8 still reportsHasAES=true HWCap=0x8ff. Built with Go 1.20.14, or with v0.1.1, it reportsfalse.go test -cbuilds for linux/arm64 (Go 1.26.8 and 1.20.14) and linux/arm (Go 1.26.8) passTestCpu,TestAuxvValue,TestHWCapandTestHWCapBeforeDoinit.HasDARN,IsPOWER8andIsPOWER9go from false to true. On s390x,HasVXandHasVXEgo from false to true. On loong64,HasLSXandHasLASXgo from false to true. mips64 and mips64le pass the tests; the emulated CPU reports no MSA inAT_HWCAP.New tests (linux, the affected architectures only):
TestAuxvValuechecks auxv parsing: the first entry, an entry after another tag, a value equal to a tag, entries afterAT_NULL, and a truncated vector.TestHWCapcomparesHWCapwith/proc/self/auxv. On Go 1.21+ that is a source independent of the runtime copy.TestHWCapBeforeDoinitchecks that the features derived at init match those derived fromHWCapnow.Each of these mutations was confirmed to make a test fail:
AT_NULLinit()instead of a variable initializerImpact (mihomo)
These numbers come from mihomo v1.19.31 on the same router, built with the same toolchain. The stock build was compared with a build containing an equivalent patch to this package, which read
AT_HWCAPfrom/proc/self/auxv.chacha20poly1305+poly1305.updateGenerictogcmAesDec.A TCP/uTLS carrier that already negotiated AES-GCM was measured as a control and did not change.
mihomo picks up the fix by bumping
github.com/metacubex/cpu.