[PATCH 1/2] x86/cacheinfo: Bounds-check sibling leaf indexing

From: Yunseong Kim

Date: Thu Aug 27 2026 - 20:04:17 EST


__cache_cpumap_setup() and __cache_amd_cpumap_setup() reuse the current
CPU's leaf index to address a sibling CPU's cacheinfo array:

sibling_ci = sib_cpu_ci->info_list + index;
cpumask_set_cpu(cpu, &sibling_ci->shared_cpu_map);

The only guard is that the sibling has an info_list at all, so this
assumes every CPU selected by the APIC-ID tests enumerated the same
number of cache leaves. That assumption does not hold when CPUs have
different cache hierarchies, and the result is a slab out-of-bounds
write into whatever follows the sibling's smaller array.

The assumption was true until commit 9677be09e5e4 ("x86/cacheinfo:
Delete global num_cache_leaves"). Before it, init_cache_level() assigned
every CPU the same global num_cache_leaves, so all the per-CPU arrays
had identical length and indexing a sibling with this CPU's index could
not run off the end. That commit made the leaf count per-CPU precisely
because hybrid parts enumerate different counts per CPU, which is what
makes the unbounded indexing reachable.

All three sibling-indexing sites are affected. On AMD and Hygon,
__cache_amd_cpumap_setup() handles index 3 via cpu_llc_shared_mask() and,
when X86_FEATURE_TOPOEXT is set, every other index via an APIC-ID window;
both do the same info_list + index with only a NULL check, and
__cache_cpumap_setup() returns early whenever it handled the leaf, so a
check placed only there would never be reached on those vendors. Their
leaf counts are per-CPU too: init_amd_cacheinfo() and
init_hygon_cacheinfo() both derive num_leaves from find_num_cache_leaves().

It is reachable from userspace. populate_cache_leaves() is called from
cacheinfo_cpu_online(), the CPUHP_AP_BASE_CACHEINFO_ONLINE callback, so
it runs whenever a CPU comes online - during boot, and equally when
userspace writes to /sys/devices/system/cpu/cpuN/online. It only runs on
a CPU's first online, because free_cache_attributes() never frees
info_list, so last_level_cache_is_valid() stays true afterwards and
detect_cache_attributes() skips to the generic
cache_shared_cpu_map_setup(). Holding a CPU back from boot with
maxcpus= therefore leaves its first
online for userspace to perform, in the order userspace chooses, and the
faulting order is the one where the CPU with more leaves comes up second:

crosvm run --cpus num-cores=2 --cpu-affinity 0=4:1=0 \
--params "root=/dev/vda1 rw console=ttyS0 init=/bin/bash maxcpus=1" \
bzImage

# in the guest, on an otherwise clean boot log:
echo 1 > /sys/devices/system/cpu/cpu1/online <- KASAN fires here

Observed on a Debian 7.2~rc7 KASAN kernel under crosvm on a hybrid Intel
host (Dell Pro 14 Premium PA 14250, Core Ultra 7 268V: P-cores enumerate
four cache leaves, E-cores three). crosvm evaluates CPUID leaf 4 per vCPU
by executing CPUID inline on whichever host CPU the vCPU thread is pinned
to, and applies --cpu-affinity before configuring that vCPU's CPUID, so
the two vCPUs can be given different leaf counts on purpose while leaf
0xB/0x1F still presents them as SMT siblings of one core:

[ 34.736208] BUG: KASAN: slab-out-of-bounds in populate_cache_leaves+0x9d0/0x16d0
[ 34.736477] Write of size 8 at addr ffff888003752ce0 by task cpuhp/1/112
[ 34.736477] Call Trace:
[ 34.736477] <TASK>
[ 34.736477] kasan_check_range+0x134/0x220
[ 34.736477] populate_cache_leaves+0x9d0/0x16d0
[ 34.736477] detect_cache_attributes+0x323/0x11a0
[ 34.736477] cacheinfo_cpu_online+0x29/0xb30
[ 34.736477] cpuhp_invoke_callback+0x3f6/0x1530
[ 34.736477] cpuhp_thread_fun+0x3e6/0x800
[ 34.736477] smpboot_thread_fn+0x42a/0x9e0
[ 34.736477] kthread+0x3e1/0x4e0
[ 34.736477] ret_from_fork+0x8f1/0xcb0
[ 34.736477] ret_from_fork_asm+0x1a/0x30
[ 34.736477] </TASK>
[ 34.745437] The buggy address is located 32 bytes to the right of
[ 34.745437] allocated 3264-byte region [ffff888003752000, ffff888003752cc0)

3264 is 3 * sizeof(struct cacheinfo) with CONFIG_NR_CPUS=8192, and 32 is
the offset of shared_cpu_map, i.e. the write lands exactly on
info_list[3].shared_cpu_map of a CPU that allocated only three leaves.
Six out of six runs faulted; with this patch, five out of five are clean
with the leaf-count mismatch confirmed present in each run.

QEMU does not reproduce it: it computes one CPUID set and applies it to
every vCPU, so under the same pinning both vCPUs report four leaves and
the mismatch never arises.

On bare metal that part does not fault, and only APIC-ID numbering
prevents it: the P-cores' L3 leaf reports num_threads_sharing=64, so the
sibling window is apicid >> 6, and the E-cores' APIC IDs (64, 66, 68, 70)
fall outside the P-cores' window (0, 8, 16, 24). A hybrid part whose
cores land in the same window reaches this with no VMM involved.

Skip siblings that do not have this leaf rather than writing past the end
of their array. This is the minimal containment; the next patch removes
the index-alignment assumption itself.

Fixes: 9677be09e5e4 ("x86/cacheinfo: Delete global num_cache_leaves")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: Claude:claude-opus-5
Signed-off-by: Yunseong Kim <yunseong.kim@xxxxxxxx>
---
arch/x86/kernel/cpu/cacheinfo.c | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)

diff --git a/arch/x86/kernel/cpu/cacheinfo.c b/arch/x86/kernel/cpu/cacheinfo.c
index 13ed16527905..3a1c10699646 100644
--- a/arch/x86/kernel/cpu/cacheinfo.c
+++ b/arch/x86/kernel/cpu/cacheinfo.c
@@ -502,6 +502,14 @@ static int __cache_amd_cpumap_setup(unsigned int cpu, int index,
if (!this_cpu_ci->info_list)
continue;

+ /*
+ * The leaf count is per-CPU, so a CPU sharing the LLC
+ * may have enumerated fewer leaves than this one.
+ * Never index past the end of its array.
+ */
+ if (index >= this_cpu_ci->num_leaves)
+ continue;
+
ci = this_cpu_ci->info_list + index;
for_each_cpu(sibling, cpu_llc_shared_mask(cpu)) {
if (!cpu_online(sibling))
@@ -526,6 +534,10 @@ static int __cache_amd_cpumap_setup(unsigned int cpu, int index,
if ((apicid < first) || (apicid > last))
continue;

+ /* Same per-CPU leaf count caveat as above. */
+ if (index >= this_cpu_ci->num_leaves)
+ continue;
+
ci = this_cpu_ci->info_list + index;

for_each_online_cpu(sibling) {
@@ -575,6 +587,16 @@ static void __cache_cpumap_setup(unsigned int cpu, int index,
if (i == cpu || !sib_cpu_ci->info_list)
continue;

+ /*
+ * CPUs that the APIC-ID test treats as cache siblings
+ * may still enumerate a different number of leaves,
+ * e.g. on hybrid parts or under a VMM that does not
+ * normalise CPUID leaf 4 across vCPUs. Never index
+ * past the end of the sibling's array.
+ */
+ if (index >= sib_cpu_ci->num_leaves)
+ continue;
+
sibling_ci = sib_cpu_ci->info_list + index;
cpumask_set_cpu(i, &ci->shared_cpu_map);
cpumask_set_cpu(cpu, &sibling_ci->shared_cpu_map);

--
2.47.3