
PoC para CVE-2026-53360: lectura/escritura fuera de los límites del montón desencadenada por el invitado en el manejo de Page State Change (PSC) de KVM SEV-SNP.
Prueba de concepto de una lectura y escritura fuera de los límites del montón en el manejo del Cambio de Estado de Página (PSC) de SEV-SNP de KVM. Un invitado SEV-SNP malicioso hace que el kernel anfitrión recorra un array de entradas PSC más allá del final de su asignación de slab. Esto filtra el diseño de los objetos kmalloc-cg-32 vecinos y escribe un valor pequeño controlado en ellos, y el invitado puede repetirlo tantas veces como desee.
Artículo completo: https://cyberstan.co.uk/sev-snp-oob/
| CVE | CVE-2026-53360 |
| Component | Soporte anfitrión SNP de KVM, arch/x86/kvm/svm/sev.c |
| Introducido | 9b54e248d264 (primer manejo de PSC SNP de KVM, mayo 2024, ~v6.10) |
| Corregido | db3f219 (mainline, mayo 2026, Cc: stable), etiquetado Fixes: 4af663c |
| Reportado | [email protected], 8 de abril de 2026 |
| Afectado | Solo ruta anfitrión SEV-SNP. KVM no habilita PSC para invitados SEV-ES simples. |
Cualquier invitado SEV-SNP puede corromper el montón del kernel anfitrión y leer información sobre su diseño enviando una solicitud PSC malformada. Esta es la dirección invitado a anfitrión: SEV-SNP está diseñado para proteger al invitado de un anfitrión no confiable, pero el anfitrión aún tiene que defenderse contra un invitado malicioso, y este manejador no lo hace.
Esto requiere hardware SEV-SNP real. No se puede reproducir en Intel, y la virtualización anidada no le dará un invitado SNP.
Hardware:
*.metal, Hetzner AX y similares).Kernel anfitrión:
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
Espacio de usuario anfitrión:
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)
Invitado:
build-essential y linux-headers-$(uname -r) instalados para poder construir el módulo en él.Los invitados SEV-SNP se comunican con el anfitrión a través del GHCB, una página compartida de 4 KB. Una solicitud PSC establece SW_EXITCODE en SVM_VMGEXIT_PSC (0x80000010), apunta SW_SCRATCH a un descriptor y coloca la longitud del descriptor en SW_EXITINFO2.
El descriptor es un struct psc_buffer: un encabezado de 8 bytes seguido de un array de entradas de 8 bytes. No hay un campo de conteo explícito. El anfitrión procesa las entradas desde hdr->cur_entry hasta hdr->end_entry, ambos controlados por el invitado.
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 */
Se supone que un invitado GHCB v2+ mantiene su área de scratch dentro del Búfer Compartido de 2032 bytes del GHCB, para que el anfitrión pueda reutilizar su mapeo existente. Allí caben (2032 - 8) / 8 = 253 entradas, de donde proviene el máximo de protocolo VMGEXIT_PSC_MAX_COUNT (253). Ese número solo tiene sentido cuando el búfer es realmente el Búfer Compartido.
Si el invitado apunta el área de scratch fuera del GHCB, el anfitrión no puede usar su mapeo, por lo que setup_vmgexit_scratch() asigna un búfer separado del tamaño que el invitado solicitó. SNP nunca debería tomar este camino, pero nada lo impide:
scratch_va = kvzalloc(len, GFP_KERNEL_ACCOUNT); /* len == exit_info_2, controlado por el invitado */
len proviene directamente del invitado, y GFP_KERNEL_ACCOUNT coloca la asignación en los cachés kmalloc-cg-N contabilizados por cgroup. Solicite exit_info_2 = 24 y obtendrá una asignación de 24 bytes en el slot de 32 bytes de kmalloc-cg-32: espacio para el encabezado más dos entradas. Todo más allá de entries[1] es memoria de otro objeto.
Luego snp_begin_psc() verifica el conteo de entradas contra la constante del protocolo, no contra el búfer que realmente asignó:
idx_end = hdr->end_entry;
if (idx_end >= VMGEXIT_PSC_MAX_COUNT) { /* verifica 253, NO el tamaño del búfer */
snp_complete_psc(svm, ...);
return 1;
}
for (idx = idx_start; idx <= idx_end; idx++) {
entry_start = entries[idx]; /* OOB una vez idx >= 2 */
...
}
Con un búfer de 24 bytes solo existen dos entradas, pero la verificación permite end_entry hasta 252. Establézcalo en 252 y el bucle recorre unos 2 KB más allá de la asignación, a través de objetos slab vecinos.
Cada paso más allá del final reinterpreta los siguientes 8 bytes de memoria slab como un psc_entry y los procesa a través del código PSC. Esto proporciona tres cosas:
entry.gfn y entry.operation de memoria que el búfer nunca poseyó. Esta es la lectura fuera de los límites del slab que KASAN detecta.KVM_HC_MAP_GPA_RANGE, el código de finalización escribe de vuelta en el mismo slot OOB: entries[idx].cur_page = entry.pagesize ? 512 : 1. Uno de dos valores pequeños en los 12 bits bajos de una palabra que el invitado elige, repetible.SW_EXITINFO2 informa el índice en el que se detuvo. Incrementar end_entry de a uno filtra, slot por slot, si la memoria adyacente se decodificó como un no-op o como algo que falló, lo cual es suficiente para encontrar límites de objetos y distinguir cero de no cero.Cada VMGEXIT reasigna el búfer de scratch, por lo que las solicitudes repetidas caen en diferentes slots de la lista libre y permiten al invitado barrer los vecinos en lugar de quedarse atascado con uno. En conjunto, esto produce divulgación del diseño del montón, la escritura restringida anterior y use-after-free entre solicitudes.
trigger.c es un módulo del kernel del invitado. Cárguelo dentro de un invitado SEV-SNP y ejecuta cuatro etapas contra el anfitrión desde un solo insmod:
cur_page en un vecino cero y confirmando que una solicitud posterior lo omite.end_entry=200 y mide hasta dónde llega la lectura OOB antes de encontrar datos no cero.entries[3..10] fuera de los límites, cada una de las cuales genera un informe KASAN en el anfitrión.El módulo asigna una página, la marca como descifrada con set_memory_decrypted(), la usa como área de scratch y construye manualmente la solicitud GHCB PSC. Sale con -EAGAIN para no permanecer cargado.
Ajuste las rutas de OVMF y disco a su configuración:
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
Copie trigger.c y Makefile dentro del invitado, luego:
make
insmod trigger.ko
El módulo verifica SEV-SNP mediante CPUID primero y se niega a ejecutarse en cualquier otro lugar. Ejecuta sus cuatro etapas y se descarga a sí mismo (init devuelve -EAGAIN, por lo que nunca permanece residente).
En el anfitrión:
dmesg | grep -E "KASAN|BUG|snp_begin_psc"
Salida esperada:
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 solo insmod produjo 73 informes KASAN en el anfitrión de prueba (62 slab-out-of-bounds, 7 slab-use-after-free, 4 use-after-free), todos contra kmalloc-cg-32. Anfitrión de prueba: AMD EPYC 7443P, Ubuntu 24.04.4, kernel 6.11.11 con KASAN, invitado bajo AMDESE QEMU (snp-latest).
La corrección en el upstream rechaza cualquier área de scratch fuera del GHCB para GHCB v2 y posteriores en setup_vmgexit_scratch(), lo que fija el búfer a un tamaño fijo conocido para que el bucle nunca pueda ejecutarse más allá del final:
} 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
Esas cuatro líneas son db3f219. Llegaron como parte de una serie más grande que también limita el conteo de entradas contra el tamaño real del búfer y relee el descriptor una vez mediante READ_ONCE(), lo que cierra una variante de desplazamiento dentro del búfer y una condición de carrera de tiempo de verificación/tiempo de uso en el mismo manejador.
| Archivo | Descripción |
|---|---|
trigger.c | Módulo del kernel del invitado que ejecuta las cuatro etapas de la PoC |
Makefile | Construye contra el kernel del invitado en ejecución |
not an SEV-SNP guest: QEMU no se inició con sev-snp-guest, o el SNP del anfitrión está desactivado.SEV-SNP not supported: verifique /sys/module/kvm_amd/parameters/sev_snp, la configuración de la BIOS y los parámetros de arranque.LAUNCH_START failed: el PSP no está inicializado. Verifique dmesg | grep psp y CONFIG_CRYPTO_DEV_SP_PSP=y.CONFIG_KASAN=y y kasan_multi_shot en la línea de comandos del anfitrión.Esto corrompe la memoria del montón del kernel del anfitrión, dispara KASAN y puede bloquear el anfitrión. Ejecútelo solo contra un anfitrión de prueba desechable que controle, dentro de una VM propia. No lo ejecute contra infraestructura compartida o de producción.
db3f219 en linux-cve-announce)db3f219, autor Mike Roth, revisado por Tom Lendacky, enviado por Paolo Bonzini9b54e248d264; corrección etiquetada Fixes: 4af663ctrigger.c es GPL-2.0, coincidiendo con su MODULE_LICENSE. Ver LICENSE.
trigger.koLICENSE | GPL-2.0, coincidiendo con el MODULE_LICENSE del módulo |