SagerNet / SagerNet/sing-box

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)

Open
#4,486 2 comments 0 reactions 0 assignees View on GitHub

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 constraint arm64 && !linux && !freebsd && !android && (!darwin || ios) && !openbsd, and osInit() is empty. All ARM64.Has* stay false on iOS.
  • src/internal/cpu/cpu_arm64_darwin.go (darwin && !ios) asserts HasAES/HasPMULL/HasSHA1/HasSHA2 = true from the M1 baseline — macOS gets hardware, iOS does not.
  • src/crypto/internal/fips140/aes/aes_asm.go:25 (supportsAES = … || cpu.ARM64HasAES) and aes/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 (VMess auto selects AES-128-GCM on arm64 by GOARCH; Shadowsocks aes-*-gcm; QUIC/TLS whenever AES-GCM is negotiated).

Numbers:

  • Go's crypto/cipher BenchmarkAESGCM, 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:

  1. sagernet/gomobile sets GOFLAGS=-tags=… per platform, which discards a caller's GOFLAGS=-overlay. Upstream golang/mobile added -overlay to gomobile bind (40bd9ace6, 2026-01-20) and to gobind (4776eadac, 2026-08-21, golang/go#80656); the fork has not merged them. Until then a go shim first on PATH that appends -overlay=<json> to build/install/list/test/vet works.
  2. go build refuses to overlay files beneath GOMODCACHE, and GOTOOLCHAIN=auto downloads 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.
  3. An overlay key naming the wrong GOROOT is ignored silently. We make the replacement define a //go:noinline marker function called from osInit and nm -arch all every slice for it (Release builds strip locals; strings still 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
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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.