iOS builds run AES-GCM and SHA-256 in pure Go: internal/cpu never detects ARM64 features on GOOS=ios (≈25% of the packet tunnel's CPU under load)
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 38.1k
- Forks
- 4.6k
- Avg merge
- 19d 15h
- Merged PRs (30d)
- 1
Description
Operating system
iOS
System version
iOS 26.5 / 26.6.1 on device; iOS 26.5 arm64 simulator for the local reproduction below. Applies to every GOOS=ios GOARCH=arm64 build (iPhone, iPad, Apple TV, arm64 simulator); macOS is not affected.
Installation type
Others
If you are using a graphical client, please provide the version of the client.
Not a client bug. Measured on a Go packet-tunnel extension built with Go 1.26.6 and github.com/sagernet/gomobile v0.1.13 — the same toolchain path make lib_apple / build_libbox uses — and reproduced with Go's own benchmark on the simulator. I have not profiled SFI itself; by construction every stock GOOS=ios Go build has this unless its build patches it.
Version
go version go1.26.6 darwin/arm64
github.com/sagernet/gomobile v0.1.13
Description
On GOOS=ios, Go's internal/cpu never detects ARM64 features, so crypto/aes, its GCM and crypto/sha256 take their pure-Go paths on every iPhone/iPad/Apple TV. Under a saturating download through an AES-128-GCM outbound this was about a quarter of the packet tunnel extension's CPU (aes.encryptBlockGeneric 12.5% + gcm.ghashMul 11.2% self time in Time Profiler).
Root cause, Go 1.26.6 sources:
src/internal/cpu/cpu_arm64_other.go— build constraintarm64 && !linux && !freebsd && !android && (!darwin || ios) && !openbsd, andosInit()is empty. AllARM64.Has*stay false on iOS.src/internal/cpu/cpu_arm64_darwin.go(darwin && !ios) assertsHasAES/HasPMULL/HasSHA1/HasSHA2 = truefrom the M1 baseline — macOS gets hardware, iOS does not.src/crypto/internal/fips140/aes/aes_asm.go:25(supportsAES = … || cpu.ARM64HasAES) andaes/gcm/gcm_asm.go:35(cpu.ARM64HasAES && cpu.ARM64HasPMULL) pick generic vs Armv8 assembly from those bits.crypto/tls(cipher_suites.go,hasAESGCMHardwareSupport) reads the same bits, which is why Go's TLS on iOS prefers ChaCha20-Poly1305 — the damage concentrates on fixed-AES protocols (VMessautoselects AES-128-GCM on arm64 by GOARCH; Shadowsocks aes-*-gcm; QUIC/TLS whenever AES-GCM is negotiated).
Numbers:
- Go's
crypto/cipherBenchmarkAESGCM, same test binary for the iOS arm64 simulator, stock vs. the overlay below: 64-byte 87 → 934 MB/s; 1350-byte 135 → 4,515 MB/s. - On an iPad (Time Profiler, 300 s saturating download through a VMess AES-128-GCM outbound, same node and flow): the extension's crypto share went from 32% to 5%; Activity Monitor CPU for the same flow 176.8 s → 50.9 s, interrupt wake-ups −36%. Throughput was not byte-normalized, so treat the CPU delta as a same-flow relative number; the benchmark and the profile shares are the hard evidence.
Workaround we ship, build-time only: go build -overlay replaces $GOROOT/src/internal/cpu/cpu_arm64_other.go with
//go:build arm64 && ios
package cpu
func osInit() {
ARM64.HasAES = true
ARM64.HasPMULL = true
ARM64.HasSHA1 = true
ARM64.HasSHA2 = true
}
Only the ARMv8.0 crypto extensions are asserted: the A9 that iOS 15 still runs and the A8 in the Apple TV HD have them; ARMv8.1 atomics and SHA-512/SHA-3 are left alone because those devices lack them and the runtime would fault.
Three things that cost time, in case they save yours:
sagernet/gomobilesetsGOFLAGS=-tags=…per platform, which discards a caller'sGOFLAGS=-overlay. Upstreamgolang/mobileadded-overlaytogomobile bind(40bd9ace6, 2026-01-20) and togobind(4776eadac, 2026-08-21, golang/go#80656); the fork has not merged them. Until then agoshim first on PATH that appends-overlay=<json>tobuild/install/list/test/vetworks.go buildrefuses to overlay files beneathGOMODCACHE, andGOTOOLCHAIN=autodownloads a pinned toolchain exactly there. The pinned Go has to be a real install (golang.org/dl/goX.Y.Z), or the overlay is rejected.- An overlay key naming the wrong GOROOT is ignored silently. We make the replacement define a
//go:noinlinemarker function called fromosInitandnm -arch allevery slice for it (Release builds strip locals;stringsstill finds the pclntab name).
The proper fix is in Go itself (cpu_arm64_other.go could assert the same four bits for ios, as the darwin file does for macOS). Happy to share the overlay/shim tooling if useful.
Reproduction
Local, no server, no TUN, no client — Go's own benchmark on the iOS simulator, once stock and once with the overlay:
# 1. a compiler wrapper for the arm64 iOS simulator
SDK=$(xcrun --sdk iphonesimulator --show-sdk-path)
cat > cc-sim.sh <<EOS
#!/bin/sh
exec $(xcrun --sdk iphonesimulator -f clang) -isysroot $SDK -target arm64-apple-ios15.0-simulator "\$@"
EOS
chmod +x cc-sim.sh
# 2. the overlay (GOROOT must be a real install, not a module-cache download)
GR=$(go env GOROOT)
cat > cpu_arm64_ios.go <<'EOS'
//go:build arm64 && ios
package cpu
func osInit() {
ARM64.HasAES = true
ARM64.HasPMULL = true
ARM64.HasSHA1 = true
ARM64.HasSHA2 = true
}
EOS
printf '{"Replace":{"%s/src/internal/cpu/cpu_arm64_other.go":"%s/cpu_arm64_ios.go"}}\n' "$GR" "$PWD" > overlay.json
# 3. same benchmark binary, stock and overlaid
export GOOS=ios GOARCH=arm64 CGO_ENABLED=1 CC=$PWD/cc-sim.sh GOTOOLCHAIN=local
go test -c -o cipher-stock.test crypto/cipher
go test -c -overlay=$PWD/overlay.json -o cipher-overlay.test crypto/cipher
# 4. run both on a booted arm64 simulator
xcrun simctl spawn booted ./cipher-stock.test -test.run XXX -test.bench BenchmarkAESGCM -test.benchtime 1s
xcrun simctl spawn booted ./cipher-overlay.test -test.run XXX -test.bench BenchmarkAESGCM -test.benchtime 1s
Logs
# stock (iOS arm64 simulator, Go 1.26.6)
BenchmarkAESGCM/Open-128-64-16 736.6 ns/op 86.88 MB/s
BenchmarkAESGCM/Seal-128-64-16 718.2 ns/op 89.11 MB/s
BenchmarkAESGCM/Open-128-1350-16 10003 ns/op 134.96 MB/s
BenchmarkAESGCM/Seal-128-1350-16 10021 ns/op 134.71 MB/s
# overlay
BenchmarkAESGCM/Open-128-64-16 68.55 ns/op 933.64 MB/s
BenchmarkAESGCM/Seal-128-64-16 64.76 ns/op 988.23 MB/s
BenchmarkAESGCM/Open-128-1350-16 299.0 ns/op 4514.69 MB/s
BenchmarkAESGCM/Seal-128-1350-16 306.1 ns/op 4409.87 MB/s
Supporter
- I am a sponsor
Integrity requirements
- I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values.
- I confirm that I have provided the server and client configuration files and process that can be reproduced locally, instead of a complicated client configuration file that has been simplified. (No sing-box configuration is involved: the defect is in the Go toolchain and the reproduction is Go's own benchmark.)
- I confirm that I have provided the simplest configuration that can be used to reproduce the error I reported, instead of depending on remote servers, TUN, graphical interface clients, or other closed-source software.
- I confirm that I have provided the complete configuration files and logs, rather than just providing parts I think are useful out of confidence in my own intelligence.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with Go's src/internal/cpu/cpu_arm64_other.go and cpu_arm64_darwin.go, then inspect the crypto paths named in crypto/internal/fips140/aes/aes_asm.go, aes/gcm/gcm_asm.go, and crypto/tls/cipher_suites.go. Run the provided iOS simulator BenchmarkAESGCM reproduction with and without the overlay. Done requires determining whether the fix belongs upstream in Go rather than in sing-box and validating the reported hardware-feature detection.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, ios
- Domain
- mobile-dev, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 38/100