
PoC для CVE-2026-53360: вызванный гостем выход за границы кучи при чтении/записи в обработке KVM SEV-SNP Page State Change (PSC).
Доказательство концепции для чтения и записи за пределами кучи в обработчике Page State Change (PSC) SEV-SNP KVM. Злоумышленный гость SEV-SNP заставляет ядро хоста обходить массив записей PSC за пределами его slab-выделения. Это раскрывает расположение соседних объектов kmalloc-cg-32 и записывает в них контролируемое небольшое значение, и гость может повторять это сколько угодно.
Полное описание: https://cyberstan.co.uk/sev-snp-oob/
| CVE | CVE-2026-53360 |
| Компонент | Поддержка SNP хоста KVM, arch/x86/kvm/svm/sev.c |
| Введён | 9b54e248d264 (первая обработка PSC KVM SNP, май 2024, ~v6.10) |
| Исправлен | db3f219 (основная ветка, май 2026, Cc: stable), помечено Fixes: 4af663c |
| Сообщён | [email protected], 8 апреля 2026 |
| Затронуты | Только путь хоста SEV-SNP. KVM не включает PSC для обычных гостей SEV-ES. |
Любой гость SEV-SNP может повредить кучу ядра хоста и прочитать информацию о её расположении, отправив некорректный запрос PSC. Это направление от гостя к хосту: SEV-SNP создан для защиты гостя от недоверенного хоста, но хост всё ещё должен защищаться от злонамеренного гостя, и этот обработчик этого не делает.
Для этого требуется реальное оборудование SEV-SNP. Его нельзя воспроизвести на Intel, и вложенная виртуализация не даст вам гостя SNP.
Оборудование:
*.metal, Hetzner AX и аналогичные).Ядро хоста:
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
Пользовательское пространство хоста:
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)
Гость:
build-essential и linux-headers-$(uname -r), чтобы можно было собрать модуль в нём.Гости SEV-SNP общаются с хостом через GHCB — общую страницу размером 4 КБ. Запрос PSC устанавливает SW_EXITCODE в SVM_VMGEXIT_PSC (0x80000010), указывает SW_SCRATCH на дескриптор и помещает длину дескриптора в SW_EXITINFO2.
Дескриптор — это struct psc_buffer: заголовок из 8 байт, за которым следует массив записей по 8 байт. Явного поля счётчика нет. Хост обрабатывает записи от hdr->cur_entry до hdr->end_entry, оба значения контролируются гостем.
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 */
Гость GHCB v2+ должен хранить свою scratch-область внутри Shared Buffer GHCB размером 2032 байта, чтобы хост мог использовать существующее отображение. (2032 - 8) / 8 = 253 записи туда помещается, откуда и берётся максимум протокола VMGEXIT_PSC_MAX_COUNT (253). Это число имеет смысл только когда буфер действительно является Shared Buffer.
Если гость указывает scratch-область вне GHCB, хост не может использовать своё отображение, поэтому setup_vmgexit_scratch() выделяет отдельный буфер запрошенного гостем размера. SNP никогда не должен идти по этому пути, но ничто этого не предотвращает:
scratch_va = kvzalloc(len, GFP_KERNEL_ACCOUNT); /* len == exit_info_2, guest-controlled */
len поступает напрямую от гостя, а GFP_KERNEL_ACCOUNT помещает выделение в кэши kmalloc-cg-N с учётом cgroup. Запросите exit_info_2 = 24 и получите выделение в 24 байта в слоте kmalloc-cg-32 размером 32 байта: место для заголовка и двух записей. Всё, что после entries[1], — это память другого объекта.
Затем snp_begin_psc() проверяет количество записей по константе протокола, а не по реально выделенному буферу:
idx_end = hdr->end_entry;
if (idx_end >= VMGEXIT_PSC_MAX_COUNT) { /* проверяет 253, а НЕ размер буфера */
snp_complete_psc(svm, ...);
return 1;
}
for (idx = idx_start; idx <= idx_end; idx++) {
entry_start = entries[idx]; /* OOB после idx >= 2 */
...
}
При буфере в 24 байта существует только две записи, но проверка допускает end_entry до 252. Установите его в 252, и цикл пройдёт примерно на 2 КБ за пределы выделения, через соседние slab-объекты.
Каждый шаг за границу интерпретирует следующие 8 байт slab-памяти как psc_entry и пропускает их через код PSC. Это даёт три вещи:
entry.gfn и entry.operation из памяти, которой буфер никогда не владел. Это выход за границы slab при чтении, который регистрирует KASAN.KVM_HC_MAP_GPA_RANGE, код завершения записывает обратно в тот же OOB-слот: entries[idx].cur_page = entry.pagesize ? 512 : 1. Одно из двух маленьких значений в младшие 12 бит слова, выбранного гостем, повторяемо.SW_EXITINFO2 сообщает индекс, на котором остановилась обработка. Увеличивая end_entry по одному, можно выяснить, слот за слотом, была ли соседняя память декодирована как недействующая или как нечто, вызвавшее ошибку. Этого достаточно, чтобы найти границы объектов и отличить ноль от ненулевого значения.Каждый VMGEXIT перераспределяет scratch-буфер, поэтому повторные запросы попадают в разные слоты freelist и позволяют гостю сканировать соседей, а не зацикливаться на одном. Вместе это даёт раскрытие структуры кучи, описанную ограниченную запись и использование после освобождения между запросами.
trigger.c — это гостевой модуль ядра. Загрузите его внутри гостя SEV-SNP, и он выполнит четыре этапа против хоста из одного insmod:
cur_page в нулевого соседа и подтверждая, что последующий запрос его пропускает.end_entry=200 и измеряет, насколько далеко достигает OOB-чтение до попадания в ненулевые данные.entries[3..10] за границами, каждый из которых вызывает сообщение KASAN на хосте.Модуль выделяет страницу, помечает её как расшифрованную с помощью set_memory_decrypted(), использует её как scratch-область и вручную формирует запрос PSC через GHCB. Он завершается с кодом -EAGAIN, чтобы не оставаться загруженным.
Настройте пути к OVMF и диску в соответствии с вашей конфигурацией:
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
Скопируйте trigger.c и Makefile в гостя, затем:
make
insmod trigger.ko
Модуль сначала проверяет SEV-SNP через CPUID и отказывается работать где-либо ещё. Он выполняет свои четыре этапа и выгружает себя (init возвращает -EAGAIN, поэтому он никогда не остаётся резидентным).
На хосте:
dmesg | grep -E "KASAN|BUG|snp_begin_psc"
Ожидаемый вывод:
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
Один insmod породил 73 сообщения KASAN на тестовом хосте (62 выхода за границы slab, 7 slab-use-after-free, 4 use-after-free), все против kmalloc-cg-32. Тестовый хост: AMD EPYC 7443P, Ubuntu 24.04.4, ядро 6.11.11 с KASAN, гость под AMDESE QEMU (snp-latest).
Исправление в основной ветке отвергает любую scratch-область вне GHCB для GHCB v2 и новее в setup_vmgexit_scratch(), что фиксирует буфер фиксированного известного размера, так что цикл никогда не сможет выйти за конец:
} 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
Эти четыре строки — db3f219. Они были включены в более крупную серию, которая также ограничивает количество записей реальным размером буфера и перечитывает дескриптор через READ_ONCE(), что закрывает вариант со смещением внутрь буфера и состояние гонки "проверка-использование" в том же обработчике.
| Файл | Описание |
|---|---|
trigger.c | Гостевой модуль ядра, выполняющий четыре этапа PoC |
Makefile | Собирает trigger.ko под текущее гостевой ядро |
not an SEV-SNP guest: QEMU не был запущен с sev-snp-guest, или SNP на хосте выключен.SEV-SNP not supported: проверьте /sys/module/kvm_amd/parameters/sev_snp, настройки BIOS и параметры загрузки.LAUNCH_START failed: PSP не инициализирован. Проверьте dmesg | grep psp и CONFIG_CRYPTO_DEV_SP_PSP=y.CONFIG_KASAN=y и kasan_multi_shot указаны в командной строке хоста.Этот код повреждает память кучи ядра хоста, вызывает сообщения KASAN и может привести к краху хоста. Запускайте его только на одноразовом тестовом хосте, который вы контролируете, внутри виртуальной машины, принадлежащей вам. Не запускайте его на общей или производственной инфраструктуре.
db3f219 в linux-cve-announce)db3f219, автор Mike Roth, рецензент Tom Lendacky, закоммитил Paolo Bonzini9b54e248d264; исправление помечено Fixes: 4af663ctrigger.c распространяется под лицензией GPL-2.0, что соответствует его MODULE_LICENSE. См. LICENSE.
LICENSE | GPL-2.0, соответствует MODULE_LICENSE модуля |