
PoC für CVE-2026-53360: gastausgelöster Heap-Out-of-Bounds-Lese-/Schreibzugriff in der KVM SEV-SNP Page State Change (PSC)-Verarbeitung.
Proof of Concept für einen Heap-Out-of-Bounds-Read und -Write in der SEV-SNP Page State Change (PSC)-Behandlung von KVM. Ein bösartiger SEV-SNP-Gast bringt den Host-Kernel dazu, ein PSC-Eintragsarray über das Ende seiner Slab-Allokation hinaus zu durchlaufen. Dadurch wird das Layout benachbarter kmalloc-cg-32-Objekte preisgegeben, und ein kontrollierter kleiner Wert wird in diese geschrieben. Der Gast kann dies beliebig oft wiederholen.
Ausführlicher Artikel: https://cyberstan.co.uk/sev-snp-oob/
| CVE | CVE-2026-53360 |
| Komponente | KVM SNP Host-Unterstützung, arch/x86/kvm/svm/sev.c |
| Eingeführt | 9b54e248d264 (erste KVM SNP PSC-Behandlung, Mai 2024, ~v6.10) |
| Behebung | db3f219 (Mainline, Mai 2026, Cc: stable), getaggt Fixes: 4af663c |
| Gemeldet | [email protected], 8. April 2026 |
| Betroffen | Nur SEV-SNP-Host-Pfad. KVM aktiviert PSC nicht für normale SEV-ES-Gäste. |
Jeder SEV-SNP-Gast kann den Heap des Host-Kernels korrumpieren und Informationen über dessen Layout auslesen, indem er eine fehlerhafte PSC-Anfrage sendet. Dies ist die Richtung Gast → Host: SEV-SNP soll den Gast vor einem nicht vertrauenswürdigen Host schützen, aber der Host muss sich auch gegen einen bösartigen Gast verteidigen – und dieser Handler tut das nicht.
Hierfür wird echte SEV-SNP-Hardware benötigt. Es kann nicht auf Intel reproduziert werden, und durch verschachtelte Virtualisierung erhält man keinen SNP-Gast.
Hardware:
*.metal, Hetzner AX und ähnliche).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-Benutzerbereich:
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)
Gast:
build-essential und linux-headers-$(uname -r), um das Modul darin bauen zu können.SEV-SNP-Gäste kommunizieren mit dem Host über die GHCB, eine 4-KB-seitige Shared Page. Eine PSC-Anfrage setzt SW_EXITCODE auf SVM_VMGEXIT_PSC (0x80000010), zeigt mit SW_SCRATCH auf einen Deskriptor und legt die Deskriptorlänge in SW_EXITINFO2 ab.
Der Deskriptor ist ein struct psc_buffer: ein 8-Byte-Header, gefolgt von einem Array aus 8-Byte-Einträgen. Es gibt kein explizites Zählfeld. Der Host verarbeitet Einträge von hdr->cur_entry bis hdr->end_entry, beide vom Gast gesteuert.
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 */
Ein GHCB-v2+-Gast soll seinen Scratch-Bereich innerhalb des 2032-Byte Shared Buffers der GHCB halten, damit der Host sein bestehendes Mapping wiederverwenden kann. (2032 - 8) / 8 = 253 Einträge passen hinein – daher kommt das Protokoll-Maximum VMGEXIT_PSC_MAX_COUNT (253). Diese Zahl ergibt nur Sinn, wenn der Puffer wirklich der Shared Buffer ist.
Wenn der Gast den Scratch-Bereich außerhalb der GHCB platziert, kann der Host sein Mapping nicht verwenden, daher allokiert setup_vmgexit_scratch() einen separaten Puffer in der vom Gast angeforderten Größe. SNP sollte diesen Pfad nie nehmen, aber nichts verhindert es:
scratch_va = kvzalloc(len, GFP_KERNEL_ACCOUNT); /* len == exit_info_2, vom Gast gesteuert */
len kommt direkt vom Gast, und GFP_KERNEL_ACCOUNT platziert die Allokation in den Cgroup-kontierten kmalloc-cg-N-Caches. Fordert man exit_info_2 = 24 an, erhält man eine 24-Byte-Allokation im 32-Byte-Slot kmalloc-cg-32: Platz für den Header plus zwei Einträge. Alles jenseits von entries[1] ist der Speicher eines anderen Objekts.
Dann prüft snp_begin_psc() die Anzahl der Einträge gegen die Protokollkonstante, nicht gegen den tatsächlich allokierten Puffer:
idx_end = hdr->end_entry;
if (idx_end >= VMGEXIT_PSC_MAX_COUNT) { /* prüft auf 253, NICHT auf die Puffergröße */
snp_complete_psc(svm, ...);
return 1;
}
for (idx = idx_start; idx <= idx_end; idx++) {
entry_start = entries[idx]; /* OOB sobald idx >= 2 */
...
}
Bei einem 24-Byte-Puffer existieren nur zwei Einträge, aber die Prüfung erlaubt end_entry bis zu 252. Setzt man es auf 252, durchläuft die Schleife etwa 2 KB über die Allokation hinaus, über angrenzende Slab-Objekte hinweg.
Jeder Schritt über das Ende hinaus interpretiert die nächsten 8 Bytes des Slab-Speichers als psc_entry und führt sie durch den PSC-Code. Das ergibt drei Dinge:
entry.gfn und entry.operation aus Speicher, der dem Puffer nie gehörte. Dies ist der Slab-Out-of-Bounds-Read, den KASAN abfängt.KVM_HC_MAP_GPA_RANGE abgesetzt wird, schreibt der Abschlusscode zurück in denselben OOB-Slot: entries[idx].cur_page = entry.pagesize ? 512 : 1. Einer von zwei kleinen Werten in die unteren 12 Bits eines Wortes, das der Gast auswählt – wiederholbar.SW_EXITINFO2 den Index, bei dem abgebrochen wurde. Erhöht man end_entry schrittweise, wird Slot für Slot preisgegeben, ob der benachbarte Speicher als No-Op oder als etwas Fehlschlagendes dekodiert wurde. Das reicht aus, um Objektgrenzen zu finden und Null von Nicht-Null zu unterscheiden.Jeder VMGEXIT allokiert den Scratch-Puffer neu, daher landen wiederholte Anfragen in verschiedenen Freelist-Slots und lassen den Gast über Nachbarn streifen, anstatt bei einem hängenzubleiben. Zusammen ergibt dies Heap-Layout-Offenlegung, den eingeschränkten Write von oben und Use-After-Free über Anfragen hinweg.
trigger.c ist ein Gast-Kernel-Modul. Lädt man es innerhalb eines SEV-SNP-Gasts, durchläuft es ab einem einzigen insmod vier Stufen gegen den Host:
cur_page in einen Null-Nachbarn geschrieben wird und eine spätere Anfrage bestätigt, dass dieser übersprungen wird.end_entry=200 und misst, wie weit der OOB-Read reicht, bevor er auf Nicht-Null-Daten trifft.entries[3..10] außerhalb der Grenzen, von denen jede einen KASAN-Bericht auf dem Host auslöst.Das Modul allokiert eine Seite, markiert sie mit set_memory_decrypted() als entschlüsselt, verwendet sie als Scratch-Bereich und baut die GHCB-PSC-Anfrage manuell zusammen. Es beendet sich mit -EAGAIN, bleibt also nicht geladen.
Passen Sie OVMF- und Disk-Pfade an Ihre Umgebung an:
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
Kopieren Sie trigger.c und Makefile in den Gast, dann:
make
insmod trigger.ko
Das Modul prüft zuerst per CPUID auf SEV-SNP und weigert sich, woanders zu laufen. Es durchläuft seine vier Stufen und entlädt sich selbst (init gibt -EAGAIN zurück, sodass es nie resident bleibt).
Auf dem Host:
dmesg | grep -E "KASAN|BUG|snp_begin_psc"
Erwartete Ausgabe:
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
Ein einziges insmod erzeugte 73 KASAN-Berichte auf dem Testhost (62 slab-out-of-bounds, 7 slab-use-after-free, 4 use-after-free), alle gegen kmalloc-cg-32. Testhost: AMD EPYC 7443P, Ubuntu 24.04.4, Kernel 6.11.11 mit KASAN, Gast unter AMDESE QEMU (snp-latest).
Der Upstream-Fix lehnt jeden Scratch-Bereich außerhalb der GHCB für GHCB v2 und höher in setup_vmgexit_scratch() ab, wodurch der Puffer auf eine feste, bekannte Größe festgelegt wird, sodass die Schleife niemals über das Ende hinauslaufen kann:
} else {
+ /* GHCB v2 erfordert, dass der Scratch-Bereich innerhalb der GHCB liegt. */
+ if (to_kvm_sev_info(svm->vcpu.kvm)->ghcb_version >= 2)
+ goto e_scratch;
+
/*
* Der Gastspeicher muss in einen Kernel-Puffer eingelesen werden, daher
* die Größe begrenzen.
Diese vier Zeilen sind db3f219. Sie waren Teil einer größeren Serie, die auch die Eintragsanzahl gegen die tatsächliche Puffergröße begrenzt und den Deskriptor einmalig über READ_ONCE() erneut einliest, wodurch eine Offset-in-den-Puffer-Variante und eine Time-of-Check/Time-of-Use-Race im selben Handler geschlossen werden.
| Datei | Beschreibung |
|---|---|
trigger.c | Gast-Kernel-Modul, das die vier PoC-Stufen durchführt |
Makefile | Baut trigger.ko gegen den laufenden Gast-Kernel |
not an SEV-SNP guest: QEMU wurde nicht mit sev-snp-guest gestartet, oder Host-SNP ist deaktiviert.SEV-SNP not supported: Überprüfen Sie /sys/module/kvm_amd/parameters/sev_snp, die BIOS-Einstellungen und die Boot-Parameter.LAUNCH_START failed: Das PSP ist nicht initialisiert. Überprüfen Sie dmesg | grep psp und CONFIG_CRYPTO_DEV_SP_PSP=y.CONFIG_KASAN=y und kasan_multi_shot in der Host-Befehlszeile gesetzt sind.Dies korrumpiert den Host-Kernel-Heap-Speicher, löst KASAN aus und kann den Host zum Absturz bringen. Führen Sie es nur auf einem von Ihnen kontrollierten Einweg-Testhost innerhalb einer eigenen VM aus. Führen Sie es nicht gegen geteilte oder Produktionsinfrastruktur aus.
db3f219 auf linux-cve-announce verfolgen)db3f219, Autor Mike Roth, Reviewed by Tom Lendacky, committed by Paolo Bonzini9b54e248d264; Fix getaggt Fixes: 4af663ctrigger.c ist GPL-2.0, entsprechend seiner MODULE_LICENSE. Siehe LICENSE.
LICENSE | GPL-2.0, entsprechend der MODULE_LICENSE des Moduls |