From f56e1d95c7af90cf1180dc12a3064064bd934604 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Fri, 9 Oct 2026 18:25:06 +0200 Subject: [PATCH] daemon/internal: select the maximum arm64 variant supported by the CPU When an image has sevaral variants for ARM64, pull the maximum one matching the current CPU. This is similar to what is done for AMD64. Instead of relying on an external lib, we implement the detection ourself using ARM64 ELF hwcaps. `golang.org/x/sys/cpu` does not expose all the flags we need. ARM v9.x is reported instead of ARM v8.(5+x) if the CPU supports SVE2 and FP16. ARM v9.4 requires an additional feature compared to ARM v8.9, which is currently hardcoded as an exception. If we want to add support for ARM v9.5 or later, we would need to add a separate requirement list as there is no ARM v8.10. There is a test checking the variants reported for the ARM64 machines supported by QEMU. The three copies of maximumSpec() are merged into platform.MaximumSpec(). See also: - https://docs.kernel.org/arch/arm64/elf_hwcaps.html - https://github.com/llvm/llvm-project/blob/main/llvm/lib/Target/AArch64/AArch64Features.td Signed-off-by: Vincent Bernat --- daemon/containerd/platform_matchers.go | 8 +- daemon/create.go | 13 +- daemon/internal/distribution/pull_v2.go | 10 -- daemon/internal/distribution/pull_v2_unix.go | 3 +- .../internal/platform/arm64variant_linux.go | 133 ++++++++++++++++++ .../platform/arm64variant_linux_test.go | 120 ++++++++++++++++ .../internal/platform/arm64variant_other.go | 7 + daemon/internal/platform/spec.go | 22 +++ 8 files changed, 288 insertions(+), 28 deletions(-) create mode 100644 daemon/internal/platform/arm64variant_linux.go create mode 100644 daemon/internal/platform/arm64variant_linux_test.go create mode 100644 daemon/internal/platform/arm64variant_other.go create mode 100644 daemon/internal/platform/spec.go diff --git a/daemon/containerd/platform_matchers.go b/daemon/containerd/platform_matchers.go index fb9cff3a5d..f8692923cd 100644 --- a/daemon/containerd/platform_matchers.go +++ b/daemon/containerd/platform_matchers.go @@ -2,8 +2,8 @@ package containerd import ( "github.com/containerd/platforms" + "github.com/moby/moby/v2/daemon/internal/platform" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - archvariant "github.com/tonistiigi/go-archvariant" ) // platformsWithPreferenceMatcher is a platform matcher that matches any of the @@ -75,9 +75,5 @@ func (i *ImageService) hostPlatformSpec() ocispec.Platform { if i.defaultPlatformOverride != nil { return *i.defaultPlatformOverride } - p := platforms.DefaultSpec() - if p.Architecture == "amd64" { - p.Variant = archvariant.AMD64Variant() - } - return p + return platform.MaximumSpec() } diff --git a/daemon/create.go b/daemon/create.go index a14a28d5f2..69ee518480 100644 --- a/daemon/create.go +++ b/daemon/create.go @@ -22,13 +22,13 @@ import ( "github.com/moby/moby/v2/daemon/internal/metrics" "github.com/moby/moby/v2/daemon/internal/multierror" "github.com/moby/moby/v2/daemon/internal/otelutil" + "github.com/moby/moby/v2/daemon/internal/platform" "github.com/moby/moby/v2/daemon/server/backend" "github.com/moby/moby/v2/daemon/server/imagebackend" "github.com/moby/moby/v2/errdefs" "github.com/moby/sys/user" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/selinux/go-selinux" - "github.com/tonistiigi/go-archvariant" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -99,7 +99,7 @@ func (daemon *Daemon) containerCreate(ctx context.Context, daemonCfg *configStor return containertypes.CreateResponse{}, err } if img != nil { - p := maximumSpec() + p := platform.MaximumSpec() imgPlat := ocispec.Platform{ OS: img.OS, Architecture: img.Architecture, @@ -396,12 +396,3 @@ func (daemon *Daemon) validateNetworkingConfig(nwConfig *networktypes.Networking return nil } - -// maximumSpec returns the distribution platform with maximum compatibility for the current node. -func maximumSpec() ocispec.Platform { - p := platforms.DefaultSpec() - if p.Architecture == "amd64" { - p.Variant = archvariant.AMD64Variant() - } - return p -} diff --git a/daemon/internal/distribution/pull_v2.go b/daemon/internal/distribution/pull_v2.go index 4156183d62..651ca73db8 100644 --- a/daemon/internal/distribution/pull_v2.go +++ b/daemon/internal/distribution/pull_v2.go @@ -30,7 +30,6 @@ import ( "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" - "github.com/tonistiigi/go-archvariant" ) var ( @@ -886,12 +885,3 @@ func toOCIPlatform(p manifestlist.PlatformSpec) *ocispec.Platform { OSVersion: p.OSVersion, } } - -// maximumSpec returns the distribution platform with maximum compatibility for the current node. -func maximumSpec() ocispec.Platform { - p := platforms.DefaultSpec() - if p.Architecture == "amd64" { - p.Variant = archvariant.AMD64Variant() - } - return p -} diff --git a/daemon/internal/distribution/pull_v2_unix.go b/daemon/internal/distribution/pull_v2_unix.go index bd74a3e68c..1d82cfb4ad 100644 --- a/daemon/internal/distribution/pull_v2_unix.go +++ b/daemon/internal/distribution/pull_v2_unix.go @@ -10,6 +10,7 @@ import ( "github.com/containerd/platforms" "github.com/docker/distribution" "github.com/docker/distribution/manifest/manifestlist" + "github.com/moby/moby/v2/daemon/internal/platform" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -53,7 +54,7 @@ func checkImageCompatibility(imageOS, imageOSVersion string) error { } func withDefault(p ocispec.Platform) ocispec.Platform { - def := maximumSpec() + def := platform.MaximumSpec() if p.OS == "" { p.OS = def.OS } diff --git a/daemon/internal/platform/arm64variant_linux.go b/daemon/internal/platform/arm64variant_linux.go new file mode 100644 index 0000000000..c1bd5eaaba --- /dev/null +++ b/daemon/internal/platform/arm64variant_linux.go @@ -0,0 +1,133 @@ +package platform + +import ( + "fmt" + "runtime" + "sync" + + "golang.org/x/sys/unix" +) + +const ( + atHWCAP = 16 + atHWCAP2 = 26 +) + +// HWCAP bits from arch/arm64/include/uapi/asm/hwcap.h. +const ( + hwcapCRC32 = 1 << 7 + hwcapATOMICS = 1 << 8 + hwcapFPHP = 1 << 9 + hwcapASIMDHP = 1 << 10 + hwcapASIMDRDM = 1 << 12 + hwcapJSCVT = 1 << 13 + hwcapFCMA = 1 << 14 + hwcapLRCPC = 1 << 15 + hwcapDCPOP = 1 << 16 + hwcapASIMDDP = 1 << 20 + hwcapDIT = 1 << 24 + hwcapUSCAT = 1 << 25 + hwcapILRCPC = 1 << 26 + hwcapFLAGM = 1 << 27 + hwcapSB = 1 << 29 + hwcapPACA = 1 << 30 + hwcapPACG = 1 << 31 + + hwcap2DCPODP = 1 << 0 + hwcap2SVE2 = 1 << 1 + hwcap2FLAGM2 = 1 << 7 + hwcap2FRINT = 1 << 8 + hwcap2I8MM = 1 << 13 + hwcap2BF16 = 1 << 14 + hwcap2BTI = 1 << 17 + hwcap2ECV = 1 << 19 + hwcap2WFXT = 1 << 31 + hwcap2CSSC = 1 << 34 + hwcap2MOPS = 1 << 43 + hwcap2SVE2P1 = 1 << 36 + hwcap2HBC = 1 << 44 +) + +// arm64Levels lists the features required by each v8 minor version, in addition +// to the ones required by the previous versions. Features implied or enabled by +// default for each architecture version in LLVM's AArch64Features.td, limited +// to the ones user space can use and the kernel reports in HWCAP. +var arm64Levels = []struct { + hwcap, hwcap2 uint64 +}{ + // v8.1: LLVM: FeatureCRC, FeatureLSE, FeatureRDM, FeaturePAN, FeatureLOR, + // FeatureVH + {hwcap: hwcapCRC32 | hwcapATOMICS | hwcapASIMDRDM}, + // v8.2: LLVM: FeaturePsUAO, FeaturePAN_RWV, FeatureRAS, FeatureCCPP + {hwcap: hwcapDCPOP}, + // v8.3: LLVM: FeatureRCPC, FeaturePAuth, FeatureJS, FeatureComplxNum + {hwcap: hwcapLRCPC | hwcapPACA | hwcapPACG | hwcapJSCVT | hwcapFCMA}, + // v8.4: LLVM: FeatureDotProd, FeatureNV, FeatureMPAM, FeatureDIT, + // FeatureTRACEV8_4, FeatureAM, FeatureSEL2, FeatureTLB_RMI, FeatureFlagM, + // FeatureRCPC_IMMO, FeatureLSE2 + {hwcap: hwcapASIMDDP | hwcapDIT | hwcapFLAGM | hwcapILRCPC | hwcapUSCAT}, + // v8.5: LLVM: FeatureAltFPCmp, FeatureFRInt3264, FeatureSpecRestrict, + // FeatureSB, FeaturePredRes, FeatureCacheDeepPersist, FeatureBranchTargetId + {hwcap: hwcapSB, hwcap2: hwcap2FLAGM2 | hwcap2FRINT | hwcap2DCPODP | hwcap2BTI}, + // v8.6: LLVM: FeatureAMVS, FeatureBF16, FeatureFineGrainedTraps, + // FeatureEnhancedCounterVirtualization, FeatureMatMulInt8 + {hwcap2: hwcap2BF16 | hwcap2ECV | hwcap2I8MM}, + // v8.7: LLVM: FeatureXS, FeatureWFxT + {hwcap2: hwcap2WFXT}, + // v8.8: LLVM: FeatureHBC, FeatureMOPS, FeatureNMI + {hwcap2: hwcap2HBC | hwcap2MOPS}, + // v8.9: LLVM: FeatureCLRBHB, FeaturePRFM_SLC, FeatureSPECRES2, FeatureCSSC, + // FeatureRASv2, FeatureCHK + {hwcap2: hwcap2CSSC}, +} + +// arm64VariantFromHWCAP returns the highest arm64 variant whose features are +// all present in hwcap and hwcap2. +func arm64VariantFromHWCAP(hwcap, hwcap2 uint64) string { + minor := 0 + for _, l := range arm64Levels { + if hwcap&l.hwcap != l.hwcap || hwcap2&l.hwcap2 != l.hwcap2 { + break + } + minor++ + } + + // Armv9.x requires SVE2, FP16 and the features of Armv8.(x+5). + const v9hwcap = hwcapFPHP | hwcapASIMDHP + const v9hwcap2 = hwcap2SVE2 + if minor >= 5 && hwcap&v9hwcap == v9hwcap && hwcap2&v9hwcap2 == v9hwcap2 { + minor -= 5 + // Armv9.4 also requires SVE2.1. + if minor >= 4 && hwcap2&hwcap2SVE2P1 == 0 { + minor = 3 + } + if minor == 0 { + return "v9" + } + return fmt.Sprintf("v9.%d", minor) + } + if minor == 0 { + return "v8" + } + return fmt.Sprintf("v8.%d", minor) +} + +var arm64Variant = sync.OnceValue(func() string { + if runtime.GOARCH != "arm64" { + return "" + } + auxv, err := unix.Auxv() + if err != nil { + return "" + } + var hwcap, hwcap2 uint64 + for _, kv := range auxv { + switch kv[0] { + case atHWCAP: + hwcap = uint64(kv[1]) + case atHWCAP2: + hwcap2 = uint64(kv[1]) + } + } + return arm64VariantFromHWCAP(hwcap, hwcap2) +}) diff --git a/daemon/internal/platform/arm64variant_linux_test.go b/daemon/internal/platform/arm64variant_linux_test.go new file mode 100644 index 0000000000..a3da3396e3 --- /dev/null +++ b/daemon/internal/platform/arm64variant_linux_test.go @@ -0,0 +1,120 @@ +package platform + +import ( + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +// upTo returns the features of the first n levels of arm64Levels. +func upTo(n int) (hwcap, hwcap2 uint64) { + for _, l := range arm64Levels[:n] { + hwcap |= l.hwcap + hwcap2 |= l.hwcap2 + } + return hwcap, hwcap2 +} + +func TestARM64VariantFromHWCAP(t *testing.T) { + const fp16 = hwcapFPHP | hwcapASIMDHP + v81, v81hwcap2 := upTo(1) + v82, v82hwcap2 := upTo(2) + v84, v84hwcap2 := upTo(4) + v85, v85hwcap2 := upTo(5) + v89, v89hwcap2 := upTo(len(arm64Levels)) + v83 := arm64Levels[2] + + tests := []struct { + name string + hwcap, hwcap2 uint64 + expected string + }{ + {name: "none", expected: "v8"}, + {name: "v8.1", hwcap: v81, hwcap2: v81hwcap2, expected: "v8.1"}, + {name: "v8.9", hwcap: v89, hwcap2: v89hwcap2, expected: "v8.9"}, + {name: "v8.3 partial", hwcap: v82 | v83.hwcap&(v83.hwcap-1), hwcap2: v82hwcap2 | v83.hwcap2, expected: "v8.2"}, + {name: "v8.3 without v8.2", hwcap: v81 | v83.hwcap, hwcap2: v81hwcap2 | v83.hwcap2, expected: "v8.1"}, + {name: "v9.0", hwcap: v85 | fp16, hwcap2: v85hwcap2 | hwcap2SVE2, expected: "v9"}, + {name: "v9.4", hwcap: v89 | fp16, hwcap2: v89hwcap2 | hwcap2SVE2 | hwcap2SVE2P1, expected: "v9.4"}, + {name: "v9.4 without SVE2.1", hwcap: v89 | fp16, hwcap2: v89hwcap2 | hwcap2SVE2, expected: "v9.3"}, + {name: "SVE2 before v8.5", hwcap: v84 | fp16, hwcap2: v84hwcap2 | hwcap2SVE2, expected: "v8.4"}, + {name: "SVE2 without FP16", hwcap: v85, hwcap2: v85hwcap2 | hwcap2SVE2, expected: "v8.5"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, arm64VariantFromHWCAP(tc.hwcap, tc.hwcap2), tc.expected) + }) + } +} + +// Features from /proc/cpuinfo, using names from the kernel. Unknown names +// are not used for the detection. +var cpuinfoHWCAP = map[string]uint64{ + "crc32": hwcapCRC32, + "atomics": hwcapATOMICS, + "fphp": hwcapFPHP, + "asimdhp": hwcapASIMDHP, + "asimdrdm": hwcapASIMDRDM, + "jscvt": hwcapJSCVT, + "fcma": hwcapFCMA, + "lrcpc": hwcapLRCPC, + "dcpop": hwcapDCPOP, + "asimddp": hwcapASIMDDP, + "dit": hwcapDIT, + "uscat": hwcapUSCAT, + "ilrcpc": hwcapILRCPC, + "flagm": hwcapFLAGM, + "sb": hwcapSB, + "paca": hwcapPACA, + "pacg": hwcapPACG, +} + +var cpuinfoHWCAP2 = map[string]uint64{ + "dcpodp": hwcap2DCPODP, + "sve2": hwcap2SVE2, + "sve2p1": hwcap2SVE2P1, + "flagm2": hwcap2FLAGM2, + "frint": hwcap2FRINT, + "i8mm": hwcap2I8MM, + "bf16": hwcap2BF16, + "bti": hwcap2BTI, + "ecv": hwcap2ECV, + "wfxt": hwcap2WFXT, + "cssc": hwcap2CSSC, + "mops": hwcap2MOPS, + "hbc": hwcap2HBC, +} + +// TestARM64VariantFromCPUInfo uses the features reported by QEMU 11.1 for +// each CPU model. +func TestARM64VariantFromCPUInfo(t *testing.T) { + tests := []struct { + cpu string + features string + expected string + }{ + {cpu: "cortex-a35", features: "fp asimd aes pmull sha1 sha2 crc32 cpuid", expected: "v8"}, + {cpu: "cortex-a53", features: "fp asimd aes pmull sha1 sha2 crc32 cpuid", expected: "v8"}, + {cpu: "cortex-a57", features: "fp asimd aes pmull sha1 sha2 crc32 cpuid", expected: "v8"}, + {cpu: "cortex-a72", features: "fp asimd aes pmull sha1 sha2 crc32 cpuid", expected: "v8"}, + {cpu: "cortex-a55", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp", expected: "v8.2"}, + {cpu: "cortex-a76", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp", expected: "v8.2"}, + {cpu: "cortex-a78ae", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp uscat ilrcpc flagm", expected: "v8.2"}, + {cpu: "neoverse-n1", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp", expected: "v8.2"}, + {cpu: "a64fx", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm fcma dcpop sve", expected: "v8.2"}, + {cpu: "neoverse-v1", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm paca pacg dcpodp svei8mm svebf16 i8mm rng", expected: "v8.4"}, + {cpu: "cortex-a710", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 bti mte", expected: "v9"}, + {cpu: "neoverse-n2", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 rng bti mte", expected: "v9"}, + } + for _, tc := range tests { + t.Run(tc.cpu, func(t *testing.T) { + var hwcap, hwcap2 uint64 + for f := range strings.FieldsSeq(tc.features) { + hwcap |= cpuinfoHWCAP[f] + hwcap2 |= cpuinfoHWCAP2[f] + } + assert.Equal(t, arm64VariantFromHWCAP(hwcap, hwcap2), tc.expected) + }) + } +} diff --git a/daemon/internal/platform/arm64variant_other.go b/daemon/internal/platform/arm64variant_other.go new file mode 100644 index 0000000000..45d1bd02bc --- /dev/null +++ b/daemon/internal/platform/arm64variant_other.go @@ -0,0 +1,7 @@ +//go:build !linux + +package platform + +func arm64Variant() string { + return "" +} diff --git a/daemon/internal/platform/spec.go b/daemon/internal/platform/spec.go new file mode 100644 index 0000000000..f56399e241 --- /dev/null +++ b/daemon/internal/platform/spec.go @@ -0,0 +1,22 @@ +package platform + +import ( + "github.com/containerd/platforms" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/tonistiigi/go-archvariant" +) + +// MaximumSpec returns the host platform with the highest CPU variant +// supported by the host. +func MaximumSpec() ocispec.Platform { + p := platforms.DefaultSpec() + switch p.Architecture { + case "amd64": + p.Variant = archvariant.AMD64Variant() + case "arm64": + if v := arm64Variant(); v != "" { + p.Variant = v + } + } + return p +}