
PoC per CVE-2026-53360: lettura/scrittura heap out-of-bounds innescata dall'ospite nella gestione del cambio di stato della pagina (PSC) di KVM SEV-SNP.
Proof of concept per una lettura e scrittura heap out-of-bounds nella gestione del Page State Change (PSC) di KVM SEV-SNP. Un guest SEV-SNP malevolo fa sì che il kernel host percorra un array di voci PSC oltre la fine della sua allocazione slab. Questo rivela il layout degli oggetti kmalloc-cg-32 vicini e vi scrive un piccolo valore controllato, e il guest può ripetere l'operazione tutte le volte che vuole.
Articolo completo: https://cyberstan.co.uk/sev-snp-oob/
| CVE | CVE-2026-53360 |
| Componente | Supporto host KVM SNP, arch/x86/kvm/svm/sev.c |
| Introdotto | 9b54e248d264 (prima gestione PSC KVM SNP, maggio 2024, ~v6.10) |
| Corretto | db3f219 (mainline, maggio 2026, Cc: stable), taggato Fixes: 4af663c |
| Segnalato | [email protected], 8 aprile 2026 |
| Interessati | Solo percorso host SEV-SNP. KVM non abilita PSC per guest SEV-ES semplici. |
Qualsiasi guest SEV-SNP può corrompere l'heap del kernel host e leggere informazioni sul suo layout inviando una richiesta PSC malformata. Questa è la direzione guest verso host: SEV-SNP è progettato per proteggere il guest da un host non fidato, ma l'host deve comunque difendersi da un guest malevolo, e questo gestore non lo fa.
È necessario hardware SEV-SNP reale. Non può essere riprodotto su Intel, e la virtualizzazione annidata non fornirà un guest SNP.
Hardware:
*.metal, Hetzner AX e simili).Kernel host:
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
Userspace host:
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 e linux-headers-$(uname -r) installati per poter compilare il modulo al suo interno.I guest SEV-SNP comunicano con l'host attraverso il GHCB, una pagina condivisa di 4 KB. Una richiesta PSC imposta SW_EXITCODE a SVM_VMGEXIT_PSC (0x80000010), punta SW_SCRATCH a un descrittore e inserisce la lunghezza del descrittore in SW_EXITINFO2.
Il descrittore è un struct psc_buffer: un'intestazione di 8 byte seguita da un array di voci di 8 byte. Non esiste un campo contatore esplicito. L'host elabora le voci da hdr->cur_entry a hdr->end_entry, entrambi controllati dal guest.
struct psc_hdr {
u16 cur_entry;
u16 end_entry;
u32 reserved;
} __packed; /* 8 byte */
struct psc_entry {
u64 cur_page : 12;
u64 gfn : 40;
u64 operation : 4;
u64 pagesize : 1;
u64 reserved : 7;
} __packed; /* 8 byte */
Un guest GHCB v2+ dovrebbe mantenere la sua area scratch all'interno del Shared Buffer di 2032 byte del GHCB, in modo che l'host possa riutilizzare il suo mapping esistente. (2032 - 8) / 8 = 253 voci ci stanno, da cui deriva il massimo del protocollo VMGEXIT_PSC_MAX_COUNT (253). Quel numero ha senso solo quando il buffer è effettivamente il Shared Buffer.
Se il guest punta l'area scratch al di fuori del GHCB, l'host non può usare il suo mapping, quindi setup_vmgexit_scratch() alloca un buffer separato della dimensione richiesta dal guest. SNP non dovrebbe mai seguire questo percorso, ma nulla lo impedisce:
scratch_va = kvzalloc(len, GFP_KERNEL_ACCOUNT); /* len == exit_info_2, controllato dal guest */
len proviene direttamente dal guest, e GFP_KERNEL_ACCOUNT pone l'allocazione nelle cache kmalloc-cg-N contabilizzate per cgroup. Richiedi exit_info_2 = 24 e ottieni un'allocazione di 24 byte nello slot da 32 byte di kmalloc-cg-32: spazio per l'intestazione più due voci. Tutto ciò che va oltre entries[1] è memoria di un altro oggetto.
Poi snp_begin_psc() controlla il numero di voci rispetto alla costante del protocollo, non rispetto al buffer effettivamente allocato:
idx_end = hdr->end_entry;
if (idx_end >= VMGEXIT_PSC_MAX_COUNT) { /* controlla 253, NON la dimensione del buffer */
snp_complete_psc(svm, ...);
return 1;
}
for (idx = idx_start; idx <= idx_end; idx++) {
entry_start = entries[idx]; /* OOB appena idx >= 2 */
...
}
Con un buffer di 24 byte esistono solo due voci, ma il controllo permette end_entry fino a 252. Impostalo a 252 e il ciclo percorre circa 2 KB oltre l'allocazione, attraverso oggetti slab adiacenti.
Ogni passo oltre la fine reinterpreta i successivi 8 byte di memoria slab come un psc_entry e li elabora con il codice PSC. Questo dà tre cose:
entry.gfn e entry.operation da memoria che il buffer non possedeva mai. Questa è la lettura slab-out-of-bounds che KASAN cattura.KVM_HC_MAP_GPA_RANGE, il codice di completamento riscrive nello stesso slot OOB: entries[idx].cur_page = entry.pagesize ? 512 : 1. Uno di due piccoli valori nei 12 bit bassi di una parola scelta dal guest, ripetibile.SW_EXITINFO2 riporta l'indice a cui si è fermata. Aumentando end_entry di uno alla volta si rivela, slot per slot, se la memoria adiacente è stata decodificata come un no-op o come qualcosa che è fallito, il che è sufficiente per trovare i confini degli oggetti e distinguere zero da non zero.Ogni VMGEXIT rialloca il buffer scratch, quindi richieste ripetute finiscono in slot freelist diversi e permettono al guest di spazzare attraverso i vicini invece di rimanere bloccato su uno solo. Insieme, questo produce divulgazione del layout dell'heap, la scrittura vincolata sopra descritta e use-after-free tra richieste.
trigger.c è un modulo kernel guest. Caricalo all'interno di un guest SEV-SNP e guida quattro fasi contro l'host da un singolo insmod:
cur_page in un vicino zero e confermando che una richiesta successiva lo salta.end_entry=200 e misura fino a quanto arriva la lettura OOB prima di incontrare dati non zero.entries[3..10] out of bounds, ciascuna delle quali fa scattare un report KASAN sull'host.Il modulo alloca una pagina, la segna come decifrata con set_memory_decrypted(), la usa come area scratch e costruisce manualmente la richiesta PSC GHCB. Esce con -EAGAIN in modo da non rimanere caricato.
Adatta i percorsi di OVMF e disco alla tua configurazione:
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
Copia trigger.c e Makefile nel guest, quindi:
make
insmod trigger.ko
Il modulo controlla prima la presenza di SEV-SNP tramite CPUID e si rifiuta di funzionare altrove. Esegue le sue quattro fasi e si scarica (init restituisce -EAGAIN, quindi non rimane mai residente).
Sull'host:
dmesg | grep -E "KASAN|BUG|snp_begin_psc"
Output atteso:
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
Un singolo insmod ha prodotto 73 report KASAN sull'host di test (62 slab-out-of-bounds, 7 slab-use-after-free, 4 use-after-free), tutti contro kmalloc-cg-32. Host di test: AMD EPYC 7443P, Ubuntu 24.04.4, kernel 6.11.11 con KASAN, guest sotto AMDESE QEMU (snp-latest).
La correzione upstream respinge qualsiasi area scratch fuori dal GHCB per GHCB v2 e successivi in setup_vmgexit_scratch(), il che fissa il buffer a una dimensione nota e fissa, in modo che il ciclo non possa mai superare la fine:
} 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
Quelle quattro righe sono db3f219. Fanno parte di una serie più ampia che limita anche il conteggio delle voci rispetto alla dimensione reale del buffer e rilegge il descrittore una volta con READ_ONCE(), chiudendo una variante con offset nel buffer e una race condition time-of-check/time-of-use nello stesso gestore.
| File | Descrizione |
|---|---|
trigger.c | Modulo kernel guest che guida le quattro fasi del PoC |
Makefile | Compila trigger.ko rispetto al kernel guest in esecuzione |
not an SEV-SNP guest: QEMU non è stato avviato con sev-snp-guest, oppure SNP host è disattivato.SEV-SNP not supported: controlla /sys/module/kvm_amd/parameters/sev_snp, le impostazioni del BIOS e i parametri di avvio.LAUNCH_START failed: il PSP non è inizializzato. Controlla dmesg | grep psp e CONFIG_CRYPTO_DEV_SP_PSP=y.CONFIG_KASAN=y e kasan_multi_shot sulla riga di comando dell'host.Questo corrompe la memoria heap del kernel host, fa scattare KASAN e può mandare in crash l'host. Eseguilo solo su un host di test usa e getta che controlli, all'interno di una VM di tua proprietà. Non eseguirlo su infrastrutture condivise o di produzione.
db3f219 su linux-cve-announce)db3f219, autore Mike Roth, revisionato da Tom Lendacky, commit di Paolo Bonzini9b54e248d264; correzione taggata Fixes: 4af663ctrigger.c è GPL-2.0, corrispondente alla sua MODULE_LICENSE. Vedi LICENSE.
LICENSE | GPL-2.0, corrispondente a MODULE_LICENSE del modulo |