{"uuid": "72804d5f-628a-44bb-839b-eec382e98105", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2023-53254", "type": "seen", "source": "https://gist.github.com/yskzalloc/5acfdef88e6354dc047604c9883faa8b", "content": "# Userspace reproducer: slab-out-of-bounds write in `populate_cache_leaves()`\n\nA `write(2)` to `/sys/devices/system/cpu/cpuN/online` makes the kernel perform\nan 8-byte out-of-bounds write in\n`arch/x86/kernel/cpu/cacheinfo.c:__cache_cpumap_setup()`.\n\nThis directory contains two programs:\n\n| program | needs root | what it does |\n|---|---|---|\n| `cacheinfo-oob-predict` | no | Reads CPUID on every online CPU, replays the kernel's own sibling test, and says whether this machine can hit the out-of-bounds write. Writes nothing. |\n| `cacheinfo-oob-trigger` | yes | Brings a CPU online, so the buggy path runs, and captures the KASAN report from `/dev/kmsg`. Prints the predicted offsets *before* triggering, then compares them with what KASAN reports. |\n\n```\nmake            # or: make static, for dropping into a guest image\n./cacheinfo-oob-predict\nsudo ./cacheinfo-oob-trigger --all\n```\n\nExit codes: `cacheinfo-oob-predict` returns 0 affected, 1 not affected,\n2 indeterminate (some CPU is offline, so its leaf count cannot be read yet).\n`cacheinfo-oob-trigger` returns 0 reproduced, 1 not reproduced, 2 nothing left\nto try, 3 usage error.\n\n## The bug\n\n`__cache_cpumap_setup()` addresses a *sibling's* cacheinfo array with *this*\nCPU's leaf index:\n\n```c\n\tfor_each_online_cpu(i)\n\t\tif (cpu_data(i).topo.apicid &gt;&gt; index_msb == c-&gt;topo.apicid &gt;&gt; index_msb) {\n\t\t\tstruct cpu_cacheinfo *sib_cpu_ci = get_cpu_cacheinfo(i);\n\n\t\t\t/* Skip if itself or no cacheinfo */\n\t\t\tif (i == cpu || !sib_cpu_ci-&gt;info_list)\n\t\t\t\tcontinue;\n\n\t\t\tsibling_ci = sib_cpu_ci-&gt;info_list + index;\t\t/* &lt;-- */\n\t\t\tcpumask_set_cpu(i, &amp;ci-&gt;shared_cpu_map);\n\t\t\tcpumask_set_cpu(cpu, &amp;sibling_ci-&gt;shared_cpu_map);\t/* &lt;-- OOB */\n\t\t}\n```\n\nThe only guard is \"does the sibling have an array at all\". Nothing checks\n`index &lt; sib_cpu_ci-&gt;num_leaves`. Since commit 9677be09e5e4 (\"x86/cacheinfo:\nDelete global num_cache_leaves\") the leaf count is per-CPU, so a CPU selected\nby the APIC-ID test can have a shorter array than the CPU indexing it, and the\n`cpumask_set_cpu()` writes past its end.\n\nThe generic implementation was fixed for exactly this in\n`drivers/base/cacheinfo.c:cache_shared_cpu_map_setup()` (CVE-2023-53254): it\nwalks the *sibling's* own leaf count and matches on level and type. The x86\nimplementation was not updated.\n\n## Why a userspace program can reach it\n\n`populate_cache_leaves()` is only called from `cacheinfo_cpu_online()`, the\n`CPUHP_AP_BASE_CACHEINFO_ONLINE` callback. That callback runs on every CPU\nthat comes online during boot, and equally when userspace writes `1` to\n`/sys/devices/system/cpu/cpuN/online`. So the path is a plain syscall away.\n\nTwo properties of the path shape the reproducer:\n\n1. **It is a first-online path.** Offlining and re-onlining a CPU does not\n   re-enter it: `free_cache_attributes()` only clears the shared maps and never\n   frees `info_list`, so `last_level_cache_is_valid()` stays true and\n   `detect_cache_attributes()` skips `populate_cache_leaves()` and goes\n   straight to the generic (fixed) `cache_shared_cpu_map_setup()`. The buggy\n   x86 code therefore runs once per CPU per boot.\n\n2. **Direction matters.** The fault needs the CPU with *more* leaves to come up\n   while a CPU with *fewer* leaves is already online, so that its loop reaches\n   an index the shorter array does not have. The reverse order is harmless.\n\nHold a CPU back from boot with `maxcpus=`, and userspace controls that order.\nThat turns a boot-time race into a deterministic, on-demand reproducer:\n\n```\nboot with maxcpus=1\necho 1 &gt; /sys/devices/system/cpu/cpu1/online     &lt;- the out-of-bounds write\n```\n\n## Reproducing it\n\nThe precondition is two CPUs that enumerate different numbers of CPUID leaf 4\nsubleaves while the APIC-ID test still treats them as cache siblings. Run\n`cacheinfo-oob-predict` to find out whether a given machine has it; it prints\nthe leaf counts, APIC IDs, `num_threads_sharing`, `index_msb` and the resulting\nsibling relation.\n\n### Under crosvm on a hybrid host\n\ncrosvm builds CPUID per vCPU and its leaf-4 arm executes the host `CPUID`\ninstruction inline, and in each vCPU thread `set_vcpu_thread_scheduling()`\n(which applies `--cpu-affinity`) runs *before* `configure_vcpu()` \u2192\n`setup_cpuid()`. So each vCPU samples leaf 4 from the host CPU it is pinned to,\nand `--cpu-affinity` selects the orientation directly: no waiting for a\nfavourable boot.\n\n`run-crosvm-repro.sh` does this. On the host used here (Intel Core Ultra 7 268V:\nP-cores `cpu0-3` enumerate 4 leaves, E-cores `cpu4-7` enumerate 3):\n\n```sh\n./run-crosvm-repro.sh                  # unpatched kernel: KASAN report\n./run-crosvm-repro.sh -k      # patched kernel: same setup, no report\n./run-qemu-control.sh                  # QEMU: no leaf mismatch arises at all\n```\n\n`-e 4` pins vcpu0 to an E-core (3 leaves) and `-p 0` pins vcpu1 to a P-core\n(4 leaves), so `cpu0` is populated during boot with the shorter array and\n`cpu1` onlined later, by the program is the one that overruns it.\n\n### On bare metal\n\nThe same code is one APIC-ID assignment away from faulting without any VMM. On\nthe Lunar Lake host used here it does not fault, and `cacheinfo-oob-predict`\nshows why:\n\n```\n  cpu0   apicid=0   num_leaves=4\n         index3 L3 Unified  threads_sharing=64  index_msb=6  cache_id=0  shared_cpu_list=0-3\n  cpu4   apicid=64  num_leaves=3\n         index2 L2 Unified  threads_sharing=8   index_msb=3  cache_id=8   shared_cpu_list=4-7\n```\n\nThe P-cores' L3 leaf reports `num_threads_sharing=64`, giving `index_msb=6`, so\nthe sibling window is `apicid &gt;&gt; 6`. P-core APIC IDs are 0, 8, 16, 24 (window\n0) and E-core APIC IDs are 64, 66, 68, 70 (window 1), so the 3-leaf E-cores\nfall outside the 4-leaf P-cores' window and are never indexed. Nothing but that\nnumbering prevents the write: a part whose hybrid cores land in the same\n`apicid &gt;&gt; index_msb` window, or firmware that numbers them more densely, hits\nit on bare metal, in which case the reproducer above works unchanged with\n`maxcpus=1` and `--order` chosen so a 4-leaf CPU comes up after a 3-leaf one.\n\nRun `cacheinfo-oob-predict` on any hybrid part to check that machine.\n\n## Results on this host\n\nHost: Dell Pro 14 Premium PA14250, Intel Core Ultra 7 268V (Lunar Lake).\nGuest kernel: Debian `7.2~rc7-1~exp1` amd64 with `CONFIG_KASAN=y`,\n`CONFIG_NR_CPUS=8192`. Guest booted with `maxcpus=1`, two vCPUs,\n`--cpu-affinity 0=4:1=0`.\n\n| VMM | kernel | cpu0 / cpu1 leaves | boot | after `echo 1 &gt; cpu1/online` | trigger exit |\n|---|---|---|---|---|---|\n| crosvm | unpatched | 3 / 4 | clean | **KASAN slab-out-of-bounds write** (6/6 runs) | 0 |\n| crosvm | patched | 3 / 4 | clean | clean (5/5 runs) | 1 |\n| QEMU | unpatched | 4 / 4 | clean | clean, no mismatch to begin with | 1 |\n\nThe boot itself is clean in every case, so the KASAN report is attributable to\nthe `write(2)` and nothing else. The patched run is the meaningful control: the\nleaf-count mismatch is present and the reproducer confirms it (`cpu0` 3 leaves,\n`cpu1` 4 leaves, out-of-bounds write predicted), and the report does not\nhappen.\n\nThe prediction is computed from CPUID before the trigger runs, and matches the\nreport exactly:\n\n```\n  cpu1 (leaf index 3, 4 leaves) -&gt; cpu0 (3 leaves)\n    sibling test: num_threads_sharing=64 index_msb=6, apicid 1&gt;&gt;6 == 0&gt;&gt;6 == 0\n    write:        8 bytes at info_list(cpu0) + 3*1088 + 32 = +3296,\n                  32 bytes past the end of the 3264-byte allocation\n    KASAN should report: \"Write of size 8\" ... \"located 32 bytes to the right of\n                         allocated 3264-byte region\"\n...\n  prediction vs report:\n    allocated region : predicted 3264, reported 3264  MATCH\n    bytes to the right: predicted 32, reported 32  MATCH\n```\n\nwith the kernel reporting:\n\n```\nBUG: KASAN: slab-out-of-bounds in populate_cache_leaves+0x9d0/0x16d0\nWrite of size 8 at addr ffff8880038f2ce0 by task cpuhp/1/112\n...\n populate_cache_leaves+0x9d0/0x16d0\n detect_cache_attributes+0x323/0x11a0\n cacheinfo_cpu_online+0x29/0xb30\n cpuhp_invoke_callback+0x3f6/0x1530\n...\nThe buggy address is located 32 bytes to the right of\n allocated 3264-byte region [ffff8880038f2000, ffff8880038f2cc0)\n```\n\n3264 = 3 \u00d7 `sizeof(struct cacheinfo)` (1088 at `CONFIG_NR_CPUS=8192`) and 32 =\n`offsetof(struct cacheinfo, shared_cpu_map)`, i.e. the write lands exactly on\n`info_list[3].shared_cpu_map` of a CPU that allocated only three leaves. This\nis the same signature as the original crash found by boot churn under\nsyzkaller.\n\n## Notes\n\n* Without `CONFIG_KASAN=y` the write still happens; at these sizes it lands in\n  the slack of the 4096-byte kmalloc object, so nothing reports it. The tools\n  print, per prediction, whether the write stays in slack or leaves the object\n  (which depends on `CONFIG_NR_CPUS` and the leaf count) when it leaves, it\n  corrupts the next object. `slub_debug=Z` also catches the in-slack case.\n* A KASAN-free footprint is checked too: if sysfs lists a CPU in a cache's\n  `shared_cpu_list` while that CPU has no cache of the same level and type,\n  that cross-link can only come from this write. Both tools report it.\n* Only the Intel/fallback `__cache_cpumap_setup()` path is modelled.\n  `__cache_amd_cpumap_setup()` has the same unbounded `info_list + index`\n  pattern for `index == 3` and for `X86_FEATURE_TOPOEXT`, also with only a NULL\n  check, and is not covered by these tools.\n\nAuthor: Yunseong Kim ", "creation_timestamp": "2026-08-28T00:01:49.203051Z"}