
CVE-2026-53360のPoC: KVM SEV-SNPのページ状態変更(PSC)処理におけるゲストトリガーのヒープ範囲外読み取り/書き込み
KVMのSEV-SNP Page State Change(PSC)処理におけるヒープ領域外読み取りおよび書き込みの概念実証です。悪意のあるSEV-SNPゲストがホストカーネルにPSCエントリ配列をスラブ割り当ての末尾まで走査させます。これにより、隣接する kmalloc-cg-32 オブジェクトのレイアウトが漏洩し、制御された小さな値がそれらに書き込まれます。ゲストはこれを好きなだけ繰り返すことができます。
Full writeup: 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 (mainline, 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ゲストは、4KBの共有ページであるGHCBを介してホストと通信します。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バイトの割り当てが得られます。これにはヘッダと2つのエントリが収まります。entries[1] 以降はすべて別のオブジェクトのメモリです。
そして snp_begin_psc() は、エントリ数を実際に割り当てたバッファではなく、プロトコル定数に対してチェックします:
idx_end = hdr->end_entry;
if (idx_end >= VMGEXIT_PSC_MAX_COUNT) { /* checks 253, NOT the buffer size */
snp_complete_psc(svm, ...);
return 1;
}
for (idx = idx_start; idx <= idx_end; idx++) {
entry_start = entries[idx]; /* OOB once idx >= 2 */
...
}
24バイトのバッファでは2つのエントリしか存在しませんが、チェックでは end_entry を252まで許可します。これを252に設定すると、ループは割り当ての約2KB先、隣接するスラブオブジェクトをまたいで歩きます。
末尾を超える各ステップでは、スラブメモリの次の8バイトを psc_entry として解釈し、PSCコードを通して処理します。これにより3つのことが得られます:
entry.gfn と entry.operation を引き出します。これがKASANが捕捉するスラブ領域外読み取りです。KVM_HC_MAP_GPA_RANGE としてディスパッチされた場合、完了コードが同じOOBスロットに書き戻します: entries[idx].cur_page = entry.pagesize ? 512 : 1。ゲストが選択したワードの下位12ビットに2つの小さな値のうちの1つを書き込み、繰り返し可能です。SW_EXITINFO2 の応答は停止したインデックスを報告します。end_entry を1つずつ増やすことで、スロットごとに隣接メモリがno-opとしてデコードされたか、失敗したものとしてデコードされたかが漏洩し、オブジェクトの境界を見つけ、ゼロと非ゼロを区別するのに十分です。各VMGEXITはスクラッチバッファを再割り当てするため、繰り返しリクエストは異なるフリーリストスロットに配置され、ゲストは1つに固定されずに隣接オブジェクトをスイープできます。これらを組み合わせると、ヒープレイアウトの開示、上記の制約付き書き込み、およびリクエスト間でのuse-after-freeが得られます。
trigger.c はゲストカーネルモジュールです。SEV-SNPゲスト内でロードすると、単一の insmod からホストに対して4つのステージを実行します:
cur_page を書き込み、後のリクエストがそれをスキップすることを確認することで、OOB書き込みがVMGEXIT間で持続することを証明します。end_entry=200 で1つのリクエストを発行し、非ゼロデータに到達するまでOOB読み取りがどの程度まで到達するかを測定します。entries[3..10] を領域外にして200のリクエストを発行し、それぞれがホスト上で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をチェックし、それ以外の場所では実行を拒否します。4つのステージを実行し、自身をアンロードします(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件のスラブ領域外アクセス、7件のスラブ解放後使用、4件の解放後使用)。すべて kmalloc-cg-32 に対するものです。テストホスト: AMD EPYC 7443P, Ubuntu 24.04.4, カーネル6.11.11 (KASAN有効), ゲストはAMDESE QEMU (snp-latest)。
上流の修正では、setup_vmgexit_scratch() においてGHCB v2以降でGHCB外のスクラッチ領域を拒否し、バッファを固定の既知のサイズに固定するため、ループが末尾を超えて実行されることがなくなります:
} 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
それら4行が db3f219 です。これらは、エントリ数を実際のバッファサイズに対して制限し、記述子を READ_ONCE() で一度だけ読み直すという、より大きなシリーズの一部として導入されました。これにより、同じハンドラ内でのバッファ内オフセットの亜種と、チェック時刻/使用時刻の競合が修正されます。
| ファイル | 説明 |
|---|---|
trigger.c | 4つのPoCステージを実行するゲストカーネルモジュール |
Makefile | 実行中のゲストカーネルに対して trigger.ko をビルド |
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をトリガーし、ホストをクラッシュさせる可能性があります。自分が制御する使い捨てのテストホストで、自分が所有するVM内でのみ実行してください。共有または本番インフラストラクチャに対して実行しないでください。
db3f219 を追跡)db3f219, 作者 Mike Roth, レビュー Tom Lendacky, コミット Paolo Bonzini9b54e248d264; 修正タグ Fixes: 4af663ctrigger.c はGPL-2.0であり、その MODULE_LICENSE に一致します。LICENSE を参照してください。
GPL-2.0, モジュールの MODULE_LICENSE に一致 |