
PoC pour CVE-2026-53360 : lecture/écriture hors limites du tas déclenchée par l'invité dans la gestion des changements d'état de page KVM SEV-SNP (PSC).
Preuve de concept pour une lecture et une écriture hors limites dans le tas (heap out‑of‑bounds) dans la gestion du changement d’état de page (PSC) SEV‑SNP de KVM. Un invité SEV‑SNP malveillant force le noyau hôte à parcourir un tableau d’entrées PSC au‑delà de la fin de son allocation de slab. Cela divulgue la disposition des objets kmalloc-cg-32 voisins et écrit une petite valeur contrôlée dans ceux-ci, et l’invité peut le répéter aussi souvent qu’il le souhaite.
Article complet : https://cyberstan.co.uk/sev-snp-oob/
| CVE | CVE-2026-53360 |
| Composant | Support hôte KVM SNP, arch/x86/kvm/svm/sev.c |
| Introduit | 9b54e248d264 (première gestion PSC KVM SNP, mai 2024, ~v6.10) |
| Corrigé | db3f219 (mainline, mai 2026, Cc: stable), étiqueté Fixes: 4af663c |
| Signalé | [email protected], 8 avril 2026 |
| Affecté | Chemin hôte SEV‑SNP uniquement. KVM n’active pas PSC pour les invités SEV‑ES simples. |
Tout invité SEV‑SNP peut corrompre le tas du noyau hôte et lire des informations sur sa disposition en envoyant une requête PSC malformée. Il s’agit de la direction invité → hôte : SEV‑SNP est conçu pour protéger l’invité d’un hôte non fiable, mais l’hôte doit quand même se défendre contre un invité malveillant, et ce gestionnaire ne le fait pas.
Cela nécessite du matériel SEV‑SNP réel. Cela ne peut pas être reproduit sur Intel, et la virtualisation imbriquée ne vous donnera pas d’invité SNP.
Matériel :
*.metal, Hetzner AX et similaires).Noyau hôte :
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
Espace utilisateur hôte :
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)
Invité :
build-essential et linux-headers-$(uname -r) installés pour pouvoir y compiler le module.Les invités SEV‑SNP communiquent avec l’hôte via le GHCB, une page partagée de 4 Ko. Une requête PSC définit SW_EXITCODE à SVM_VMGEXIT_PSC (0x80000010), pointe SW_SCRATCH vers un descripteur et place la longueur du descripteur dans SW_EXITINFO2.
Le descripteur est un struct psc_buffer : un en‑tête de 8 octets suivi d’un tableau d’entrées de 8 octets. Il n’y a pas de champ de comptage explicite. L’hôte traite les entrées de hdr->cur_entry à hdr->end_entry, toutes deux contrôlées par l’invité.
struct psc_hdr {
u16 cur_entry;
u16 end_entry;
u32 reserved;
} __packed; /* 8 octets */
struct psc_entry {
u64 cur_page : 12;
u64 gfn : 40;
u64 operation : 4;
u64 pagesize : 1;
u64 reserved : 7;
} __packed; /* 8 octets */
Un invité GHCB v2+ est censé garder sa zone scratch à l’intérieur du tampon partagé de 2032 octets du GHCB, afin que l’hôte puisse réutiliser son mappage existant. (2032 - 8) / 8 = 253 entrées tiennent, ce qui correspond au maximum du protocole VMGEXIT_PSC_MAX_COUNT (253). Ce nombre n’a de sens que lorsque le tampon est réellement le tampon partagé.
Si l’invité pointe la zone scratch en dehors du GHCB, l’hôte ne peut pas utiliser son mappage, donc setup_vmgexit_scratch() alloue un tampon séparé de la taille demandée par l’invité. SNP ne devrait jamais emprunter ce chemin, mais rien ne l’empêche :
scratch_va = kvzalloc(len, GFP_KERNEL_ACCOUNT); /* len == exit_info_2, contrôlé par l'invité */
len vient directement de l’invité, et GFP_KERNEL_ACCOUNT place l’allocation dans les caches kmalloc-cg-N comptabilisés par cgroup. Demandez exit_info_2 = 24 et vous obtenez une allocation de 24 octets dans l’emplacement kmalloc-cg-32 de 32 octets : de la place pour l’en‑tête plus deux entrées. Tout ce qui dépasse entries[1] est la mémoire d’un autre objet.
Ensuite, snp_begin_psc() vérifie le nombre d’entrées par rapport à la constante du protocole, et non par rapport au tampon réellement alloué :
idx_end = hdr->end_entry;
if (idx_end >= VMGEXIT_PSC_MAX_COUNT) { /* vérifie 253, PAS la taille du tampon */
snp_complete_psc(svm, ...);
return 1;
}
for (idx = idx_start; idx <= idx_end; idx++) {
entry_start = entries[idx]; /* OOB dès que idx >= 2 */
...
}
Avec un tampon de 24 octets, il n’existe que deux entrées, mais la vérification autorise end_entry jusqu’à 252. Mettez‑le à 252 et la boucle parcourt environ 2 Ko au‑delà de l’allocation, à travers les objets slab voisins.
Chaque pas au‑delà de la fin réinterprète les 8 octets suivants de la mémoire slab comme un psc_entry et les exécute dans le code PSC. Cela donne trois choses :
entry.gfn et entry.operation de la mémoire que le tampon ne possédait pas. C’est la lecture hors limites (slab-out-of-bounds) que KASAN détecte.KVM_HC_MAP_GPA_RANGE, le code de terminaison écrit dans le même emplacement OOB : entries[idx].cur_page = entry.pagesize ? 512 : 1. Une ou deux petites valeurs dans les 12 bits de poids faible d’un mot que l’invité choisit, répétable.SW_EXITINFO2 signale l’index auquel elle s’est arrêtée. En augmentant end_entry une par une, on divulgue, slot par slot, si la mémoire adjacente a été décodée comme une opération nulle ou comme quelque chose qui a échoué, ce qui suffit pour trouver les limites des objets et distinguer zéro de non‑zéro.Chaque VMGEXIT réalloue le tampon scratch, donc des requêtes répétées atterrissent dans différents slots de la free list et permettent à l’invité de balayer les voisins plutôt que de rester bloqué sur un seul. Mis ensemble, cela donne une divulgation de la disposition du tas, l’écriture contrainte ci‑dessus, et une use‑after‑free entre les requêtes.
trigger.c est un module noyau invité. Chargez‑le dans un invité SEV‑SNP et il exécute quatre étapes contre l’hôte depuis un simple insmod :
cur_page dans un voisin zéro et en confirmant qu’une requête ultérieure le saute.end_entry=200 et mesure jusqu’où la lecture OOB atteint avant de rencontrer des données non‑nulles.entries[3..10] hors limites, chacune déclenchant un rapport KASAN sur l’hôte.Le module alloue une page, la marque déchiffrée avec set_memory_decrypted(), l’utilise comme zone scratch et construit manuellement la requête PSC GHCB. Il se termine avec -EAGAIN pour ne pas rester chargé.
Ajustez les chemins OVMF et du disque à votre configuration :
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
Copiez trigger.c et Makefile dans l’invité, puis :
make
insmod trigger.ko
Le module vérifie d’abord la présence de SEV‑SNP via CPUID et refuse de s’exécuter ailleurs. Il exécute ses quatre étapes et se décharge (init retourne -EAGAIN, donc il ne reste jamais résident).
Sur l’hôte :
dmesg | grep -E "KASAN|BUG|snp_begin_psc"
Sortie attendue :
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 seul insmod a produit 73 rapports KASAN sur l’hôte de test (62 slab‑out‑of‑bounds, 7 slab‑use‑after‑free, 4 use‑after‑free), tous contre kmalloc-cg-32. Hôte de test : AMD EPYC 7443P, Ubuntu 24.04.4, noyau 6.11.11 avec KASAN, invité sous QEMU AMDESE (snp-latest).
Le correctif upstream rejette toute zone scratch hors GHCB pour GHCB v2 et ultérieur dans setup_vmgexit_scratch(), ce qui épingle le tampon à une taille fixe connue, empêchant la boucle de dépasser la fin :
} else {
+ /* GHCB v2 exige que la zone scratch soit dans le GHCB. */
+ if (to_kvm_sev_info(svm->vcpu.kvm)->ghcb_version >= 2)
+ goto e_scratch;
+
/*
* La mémoire de l'invité doit être lue dans un tampon noyau, donc
* limitez la taille
Ces quatre lignes sont db3f219. Elles font partie d’une série plus large qui limite également le nombre d’entrées par rapport à la taille réelle du tampon et relit le descripteur avec READ_ONCE(), ce qui ferme une variante de décalage dans le tampon et une race time‑of‑check/time‑of‑use dans le même gestionnaire.
| Fichier | Description |
|---|---|
trigger.c | Module noyau invité qui exécute les quatre étapes du PoC |
Makefile | Compile trigger.ko avec le noyau invité en cours d’exécution |
not an SEV-SNP guest : QEMU n’a pas été lancé avec sev-snp-guest, ou le SNP hôte est désactivé.SEV-SNP not supported : vérifiez /sys/module/kvm_amd/parameters/sev_snp, les paramètres BIOS et les options de démarrage.LAUNCH_START failed : le PSP n’est pas initialisé. Vérifiez dmesg | grep psp et CONFIG_CRYPTO_DEV_SP_PSP=y.CONFIG_KASAN=y et kasan_multi_shot sur la ligne de commande de l’hôte.Cela corrompt la mémoire du tas du noyau hôte, déclenche KASAN et peut planter l’hôte. Exécutez‑le uniquement sur un hôte de test jetable que vous contrôlez, dans une VM que vous possédez. Ne l’exécutez pas sur une infrastructure partagée ou de production.
db3f219 sur linux-cve-announce)db3f219, auteur Mike Roth, revu par Tom Lendacky, commit par Paolo Bonzini9b54e248d264 ; correctif étiqueté Fixes: 4af663ctrigger.c est sous licence GPL‑2.0, correspondant à son MODULE_LICENSE. Voir LICENSE.
LICENSE | GPL‑2.0, correspondant à MODULE_LICENSE du module |