* [PATCH 1/2] x86/cacheinfo: Bounds-check sibling leaf indexing
2026-08-28 0:02 [PATCH 0/2] x86/cacheinfo: Fix slab-out-of-bounds write on hybrid/VM topologies Yunseong Kim
@ 2026-08-28 0:02 ` Yunseong Kim
2026-08-28 0:02 ` [PATCH 2/2] x86/cacheinfo: Match sibling leaves by level and type, not by index Yunseong Kim
1 sibling, 0 replies; 3+ messages in thread
From: Yunseong Kim @ 2026-08-28 0:02 UTC (permalink / raw)
To: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen,
H. Peter Anvin, Ricardo Neri, x86, linux-kernel, stable,
crosvm-dev, syzkaller, Yunseong Kim, David Nyström
__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@vger.kernel.org
Assisted-by: Claude:claude-opus-5
Signed-off-by: Yunseong Kim <yunseong.kim@est.tech>
---
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
^ permalink raw reply [flat|nested] 3+ messages in thread* [PATCH 2/2] x86/cacheinfo: Match sibling leaves by level and type, not by index
2026-08-28 0:02 [PATCH 0/2] x86/cacheinfo: Fix slab-out-of-bounds write on hybrid/VM topologies Yunseong Kim
2026-08-28 0:02 ` [PATCH 1/2] x86/cacheinfo: Bounds-check sibling leaf indexing Yunseong Kim
@ 2026-08-28 0:02 ` Yunseong Kim
1 sibling, 0 replies; 3+ messages in thread
From: Yunseong Kim @ 2026-08-28 0:02 UTC (permalink / raw)
To: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen,
H. Peter Anvin, Ricardo Neri, x86, linux-kernel, stable,
crosvm-dev, syzkaller, Yunseong Kim, David Nyström
The previous patch stops the out-of-bounds write, but it leaves the
assumption that caused it in place: that leaf index N describes the same
cache on every CPU the APIC-ID tests select as a sibling. Bounding the
index only handles the case where the sibling's array is too short. When
two CPUs enumerate different leaves but the index happens to be in range,
the code still cross-links whichever leaf sits at that index, so a CPU can
be recorded as sharing a cache of a level and type it does not have there.
The generic implementation stopped doing this in
commit 198102c9103f ("cacheinfo: Fix shared_cpu_map to handle shared
caches at different levels"): cache_shared_cpu_map_setup() walks the
sibling's own leaves and matches on level and type. x86 keeps a parallel
implementation that was not updated.
Do the same here. sibling_cache_leaf() looks the sibling's leaf up by
level and type over that CPU's own num_leaves, which cannot leave the
array, and returns NULL when the sibling has no such cache - so the
explicit bounds checks are no longer needed and are folded into it. All
three sibling-indexing sites use it, including both branches of
__cache_amd_cpumap_setup().
It also removes a smaller hazard: allocate_cache_info() kzalloc()s
info_list before populate_cache_leaves() fills it, so a sibling can have a
zeroed array. Indexing it by number wrote into a leaf describing no cache;
matching by level and type skips it, since a zeroed leaf has level 0 and
type CACHE_TYPE_NOCACHE and can never match a real one.
No functional change on machines whose CPUs enumerate identical leaves:
there the level-and-type match resolves to the same leaf the index did.
What this cannot fix is CPUID data that is inconsistent in the first
place. Two CPUs presented as SMT siblings while reporting different cache
geometry still derive the same cache id (apicid >> index_msb), so they are
still linked - by this code and by the generic matcher alike. That is
wrong topology from wrong input; the point of this patch is that it is no
longer memory-unsafe, and that a sibling's array is never addressed with
another CPU's index.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Yunseong Kim <yunseong.kim@est.tech>
---
arch/x86/kernel/cpu/cacheinfo.c | 82 +++++++++++++++++++++++------------------
1 file changed, 47 insertions(+), 35 deletions(-)
diff --git a/arch/x86/kernel/cpu/cacheinfo.c b/arch/x86/kernel/cpu/cacheinfo.c
index 3a1c10699646..1024a6697d0e 100644
--- a/arch/x86/kernel/cpu/cacheinfo.c
+++ b/arch/x86/kernel/cpu/cacheinfo.c
@@ -482,13 +482,46 @@ void init_intel_cacheinfo(struct cpuinfo_x86 *c)
intel_cacheinfo_0x2(c);
}
+/*
+ * Find the leaf of @cpu that describes the same cache level and type as
+ * @this_leaf, or NULL if it has none.
+ *
+ * The number of cache leaves is per-CPU since commit 9677be09e5e4
+ * ("x86/cacheinfo: Delete global num_cache_leaves"), so CPUs that the APIC-ID
+ * tests below treat as cache siblings may enumerate different leaves - on
+ * hybrid parts, or under a VMM that does not normalise CPUID leaf 4 across
+ * vCPUs. A leaf index is therefore only meaningful on the CPU it came from:
+ * index N need not describe the same cache on a sibling, and need not exist
+ * there at all. Match on level and type instead, the way
+ * cache_shared_cpu_map_setup() does in drivers/base/cacheinfo.c.
+ */
+static struct cacheinfo *sibling_cache_leaf(unsigned int cpu,
+ const struct cacheinfo *this_leaf)
+{
+ struct cpu_cacheinfo *sib_cpu_ci = get_cpu_cacheinfo(cpu);
+ unsigned int i;
+
+ if (!sib_cpu_ci->info_list)
+ return NULL;
+
+ for (i = 0; i < sib_cpu_ci->num_leaves; i++) {
+ struct cacheinfo *sibling_ci = sib_cpu_ci->info_list + i;
+
+ if (sibling_ci->level == this_leaf->level &&
+ sibling_ci->type == this_leaf->type)
+ return sibling_ci;
+ }
+
+ return NULL;
+}
+
/*
* <linux/cacheinfo.h> shared_cpu_map setup, AMD/Hygon
*/
static int __cache_amd_cpumap_setup(unsigned int cpu, int index,
- const struct _cpuid4_info *id4)
+ const struct _cpuid4_info *id4,
+ const struct cacheinfo *this_leaf)
{
- struct cpu_cacheinfo *this_cpu_ci;
struct cacheinfo *ci;
int i, sibling;
@@ -498,19 +531,10 @@ static int __cache_amd_cpumap_setup(unsigned int cpu, int index,
*/
if (index == 3) {
for_each_cpu(i, cpu_llc_shared_mask(cpu)) {
- this_cpu_ci = get_cpu_cacheinfo(i);
- 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)
+ ci = sibling_cache_leaf(i, this_leaf);
+ if (!ci)
continue;
- ci = this_cpu_ci->info_list + index;
for_each_cpu(sibling, cpu_llc_shared_mask(cpu)) {
if (!cpu_online(sibling))
continue;
@@ -526,20 +550,14 @@ static int __cache_amd_cpumap_setup(unsigned int cpu, int index,
last = first + nshared - 1;
for_each_online_cpu(i) {
- this_cpu_ci = get_cpu_cacheinfo(i);
- if (!this_cpu_ci->info_list)
- continue;
-
apicid = cpu_data(i).topo.apicid;
if ((apicid < first) || (apicid > last))
continue;
- /* Same per-CPU leaf count caveat as above. */
- if (index >= this_cpu_ci->num_leaves)
+ ci = sibling_cache_leaf(i, this_leaf);
+ if (!ci)
continue;
- ci = this_cpu_ci->info_list + index;
-
for_each_online_cpu(sibling) {
apicid = cpu_data(sibling).topo.apicid;
if ((apicid < first) || (apicid > last))
@@ -561,16 +579,16 @@ static void __cache_cpumap_setup(unsigned int cpu, int index,
{
struct cpu_cacheinfo *this_cpu_ci = get_cpu_cacheinfo(cpu);
struct cpuinfo_x86 *c = &cpu_data(cpu);
- struct cacheinfo *ci, *sibling_ci;
+ struct cacheinfo *ci = this_cpu_ci->info_list + index;
+ struct cacheinfo *sibling_ci;
unsigned long num_threads_sharing;
int index_msb, i;
if (c->x86_vendor == X86_VENDOR_AMD || c->x86_vendor == X86_VENDOR_HYGON) {
- if (__cache_amd_cpumap_setup(cpu, index, id4))
+ if (__cache_amd_cpumap_setup(cpu, index, id4, ci))
return;
}
- ci = this_cpu_ci->info_list + index;
num_threads_sharing = 1 + id4->eax.split.num_threads_sharing;
cpumask_set_cpu(cpu, &ci->shared_cpu_map);
@@ -581,23 +599,17 @@ static void __cache_cpumap_setup(unsigned int cpu, int index,
for_each_online_cpu(i)
if (cpu_data(i).topo.apicid >> index_msb == c->topo.apicid >> index_msb) {
- struct cpu_cacheinfo *sib_cpu_ci = get_cpu_cacheinfo(i);
-
- /* Skip if itself or no cacheinfo */
- if (i == cpu || !sib_cpu_ci->info_list)
+ if (i == cpu)
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.
+ * Skip siblings without cacheinfo yet, and those that
+ * have no cache of this level and type.
*/
- if (index >= sib_cpu_ci->num_leaves)
+ sibling_ci = sibling_cache_leaf(i, ci);
+ if (!sibling_ci)
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
^ permalink raw reply [flat|nested] 3+ messages in thread