
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 и диску в соответствии с вашей конфигурацией: