cpu: darwin/arm64 feature detection

The ARM64 flags were not being correctly set for Mac M series laptops.

Fixes golang/go#43046

Change-Id: I1d884dccd3f3a0a010ad7babbab26b73731ab583
Reviewed-on: https://go-review.googlesource.com/c/sys/+/737260
Auto-Submit: Keith Randall <khr@golang.org>
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
diff --git a/cpu/asm_darwin_arm64_gc.s b/cpu/asm_darwin_arm64_gc.s
new file mode 100644
index 0000000..e07fa75
--- /dev/null
+++ b/cpu/asm_darwin_arm64_gc.s
@@ -0,0 +1,12 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build darwin && arm64 && gc
+
+#include "textflag.h"
+
+TEXT libc_sysctlbyname_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_sysctlbyname(SB)
+GLOBL	·libc_sysctlbyname_trampoline_addr(SB), RODATA, $8
+DATA	·libc_sysctlbyname_trampoline_addr(SB)/8, $libc_sysctlbyname_trampoline<>(SB)
diff --git a/cpu/cpu_darwin_arm64.go b/cpu/cpu_darwin_arm64.go
index 30ee0f2..0b47074 100644
--- a/cpu/cpu_darwin_arm64.go
+++ b/cpu/cpu_darwin_arm64.go
@@ -2,25 +2,66 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build darwin && arm64 && gc
+
 package cpu
 
-import "runtime"
-
 func doinit() {
-	ARM64.HasASIMD = true
-	ARM64.HasFP = true
+	setMinimalFeatures()
 
-	if runtime.GOOS != "ios" {
-		// M-series SoCs are at least armv8.4-a
-		ARM64.HasCRC32 = true   // armv8.1
-		ARM64.HasATOMICS = true // armv8.2
-		ARM64.HasJSCVT = true   // armv8.3, if HasFP
+	// The feature flags are explained in [Instruction Set Detection].
+	// There are some differences between MacOS versions:
+	//
+	// MacOS 11 and 12 do not have "hw.optional" sysctl values for some of the features.
+	//
+	// MacOS 13 changed some of the naming conventions to align with ARM Architecture Reference Manual.
+	// For example "hw.optional.armv8_2_sha512" became "hw.optional.arm.FEAT_SHA512".
+	// It currently checks both to stay compatible with MacOS 11 and 12.
+	// The old names also work with MacOS 13, however it's not clear whether
+	// they will continue working with future OS releases.
+	//
+	// Once MacOS 12 is no longer supported the old names can be removed.
+	//
+	// [Instruction Set Detection]: https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_instruction_set_characteristics
 
-		// Go already assumes these to be available
-		// because they were on the M1.
-		ARM64.HasAES = true
-		ARM64.HasPMULL = true
-		ARM64.HasSHA1 = true
-		ARM64.HasSHA2 = true
-	}
+	// Encryption, hashing and checksum capabilities
+
+	// For the following flags there are no MacOS 11 sysctl flags.
+	ARM64.HasAES = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_AES\x00"))
+	ARM64.HasPMULL = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_PMULL\x00"))
+	ARM64.HasSHA1 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA1\x00"))
+	ARM64.HasSHA2 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA256\x00"))
+
+	ARM64.HasSHA3 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha3\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA3\x00"))
+	ARM64.HasSHA512 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha512\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA512\x00"))
+
+	ARM64.HasCRC32 = darwinSysctlEnabled([]byte("hw.optional.armv8_crc32\x00"))
+
+	// Atomic and memory ordering
+	ARM64.HasATOMICS = darwinSysctlEnabled([]byte("hw.optional.armv8_1_atomics\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LSE\x00"))
+	ARM64.HasLRCPC = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LRCPC\x00"))
+
+	// SIMD and floating point capabilities
+	ARM64.HasFPHP = darwinSysctlEnabled([]byte("hw.optional.neon_fp16\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FP16\x00"))
+	ARM64.HasASIMDHP = darwinSysctlEnabled([]byte("hw.optional.neon_hpfp\x00")) || darwinSysctlEnabled([]byte("hw.optional.AdvSIMD_HPFPCvt\x00"))
+	ARM64.HasASIMDRDM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_RDM\x00"))
+	ARM64.HasASIMDDP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DotProd\x00"))
+	ARM64.HasASIMDFHM = darwinSysctlEnabled([]byte("hw.optional.armv8_2_fhm\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FHM\x00"))
+	ARM64.HasI8MM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_I8MM\x00"))
+
+	ARM64.HasJSCVT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_JSCVT\x00"))
+	ARM64.HasFCMA = darwinSysctlEnabled([]byte("hw.optional.armv8_3_compnum\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FCMA\x00"))
+
+	// Miscellaneous
+	ARM64.HasDCPOP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DPB\x00"))
+	ARM64.HasEVTSTRM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_ECV\x00"))
+	ARM64.HasDIT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DIT\x00"))
+
+	// Not supported, but added for completeness
+	ARM64.HasCPUID = false
+
+	ARM64.HasSM3 = false  // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM3\x00"))
+	ARM64.HasSM4 = false  // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM4\x00"))
+	ARM64.HasSVE = false  // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE\x00"))
+	ARM64.HasSVE2 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE2\x00"))
 }
diff --git a/cpu/cpu_darwin_arm64_other.go b/cpu/cpu_darwin_arm64_other.go
new file mode 100644
index 0000000..4ee68e3
--- /dev/null
+++ b/cpu/cpu_darwin_arm64_other.go
@@ -0,0 +1,29 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build darwin && arm64 && !gc
+
+package cpu
+
+func doinit() {
+	setMinimalFeatures()
+
+	ARM64.HasASIMD = true
+	ARM64.HasFP = true
+
+	// Go already assumes these to be available because they were on the M1
+	// and these are supported on all Apple arm64 chips.
+	ARM64.HasAES = true
+	ARM64.HasPMULL = true
+	ARM64.HasSHA1 = true
+	ARM64.HasSHA2 = true
+
+	if runtime.GOOS != "ios" {
+		// Apple A7 processors do not support these, however
+		// M-series SoCs are at least armv8.4-a
+		ARM64.HasCRC32 = true   // armv8.1
+		ARM64.HasATOMICS = true // armv8.2
+		ARM64.HasJSCVT = true   // armv8.3, if HasFP
+	}
+}
diff --git a/cpu/cpu_other_arm64.go b/cpu/cpu_other_arm64.go
index c604824..6c7c5bf 100644
--- a/cpu/cpu_other_arm64.go
+++ b/cpu/cpu_other_arm64.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !linux && !netbsd && !openbsd && !windows && !darwin && arm64
+//go:build !darwin && !linux && !netbsd && !openbsd && !windows && arm64
 
 package cpu
 
diff --git a/cpu/syscall_darwin_arm64_gc.go b/cpu/syscall_darwin_arm64_gc.go
new file mode 100644
index 0000000..7b4e67f
--- /dev/null
+++ b/cpu/syscall_darwin_arm64_gc.go
@@ -0,0 +1,54 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Minimal copy from internal/cpu and runtime to make sysctl calls.
+
+//go:build darwin && arm64 && gc
+
+package cpu
+
+import (
+	"syscall"
+	"unsafe"
+)
+
+type Errno = syscall.Errno
+
+// adapted from internal/cpu/cpu_arm64_darwin.go
+func darwinSysctlEnabled(name []byte) bool {
+	out := int32(0)
+	nout := unsafe.Sizeof(out)
+	if ret := sysctlbyname(&name[0], (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); ret != nil {
+		return false
+	}
+	return out > 0
+}
+
+//go:cgo_import_dynamic libc_sysctl sysctl "/usr/lib/libSystem.B.dylib"
+
+var libc_sysctlbyname_trampoline_addr uintptr
+
+// adapted from runtime/sys_darwin.go in the pattern of sysctl() above, as defined in x/sys/unix
+func sysctlbyname(name *byte, old *byte, oldlen *uintptr, new *byte, newlen uintptr) error {
+	if _, _, err := syscall_syscall6(
+		libc_sysctlbyname_trampoline_addr,
+		uintptr(unsafe.Pointer(name)),
+		uintptr(unsafe.Pointer(old)),
+		uintptr(unsafe.Pointer(oldlen)),
+		uintptr(unsafe.Pointer(new)),
+		uintptr(newlen),
+		0,
+	); err != 0 {
+		return err
+	}
+
+	return nil
+}
+
+//go:cgo_import_dynamic libc_sysctlbyname sysctlbyname "/usr/lib/libSystem.B.dylib"
+
+// Implemented in the runtime package (runtime/sys_darwin.go)
+func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
+
+//go:linkname syscall_syscall6 syscall.syscall6