Skip to contentSkip to conversation

KVM: SEV: sev_dbg_crypt() fix and overhaul

PATCH Applied Sean Christopherson 4 participants 19 messages May 1 – Jul 23 3 messages not in the archive

Subsystems (7)
x86 KVM Kselftest Crypto API +3 more subsystems
lore
Share & tools
lore

Sean Christopherson

author maintainer
#0 lore raw

v2 cover KVM: SEV: sev_dbg_crypt() fix and overhaul

Disclaimer: I could have sworn I sent this last week, but obviously did not, and I can't remember _why_ I didn't send it. So don't be too surprised if there's some glaring flaw or boneheaded mistake.

Ashutosh's fix for a heap OOB/UAF bug in the debug {de,en}crypt code, now with a selftest to detect the bug (and confirm the fix), and to validate the functionality.

The rest of the patches completely rewrite the code. When creating the selftest, I did the silly thing of testing arbitrary offsets+sizes, and couldn't trigger the true badness because the test failed long before it got to the larger sizes.

Specifically (or, at least) the current code fails to handle cases where an address and the size aren't naturally aligned. E.g. when encrypting 9 bytes at offset 8, KVM needs to _decrypt_ destination[31:0] into a temporary buffer, buffer[31:0], then copy 9 bytes from source[8:0] to buffer[16:8], then encrypt buffer[31:0] back into destination[31:0]. The current code only ever copies 16 bytes, and bizarrely uses a temporary buffer for the source as well.

A wholesale rewrite in a single patch isn't my first choice, but the existing code obviously hasn't been tested, and it's so bizarre and unnecessarily complex that I've zero confidence that an iterative cleanup would be a net positive, especially given how many hours it would take.

The initial fix is 7.1 material, the rest (including the selftest, because it won't pass), can wait for 7.2.

v1: https://lore.kernel.org/all/[email protected]

Ashutosh Desai (1):
KVM: SVM: Fix page overflow in sev_dbg_crypt() for ENCRYPT path

Sean Christopherson (5):
KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls
KVM: SEV: Explicitly validate the dst buffer for debug operations
KVM: SEV: Add helper function to pin/unpin a single page
KVM: SEV: Rewrite logic to {de,en}crypt memory for debug
KVM: SEV: Allocate only as many bytes as needed for temp crypt buffers

 arch/x86/kvm/svm/sev.c                        | 423 +++++++++---------
 tools/testing/selftests/kvm/Makefile.kvm      |   1 +
 tools/testing/selftests/kvm/include/x86/sev.h |  24 +
 .../testing/selftests/kvm/x86/sev_dbg_test.c  | 118 +++++
 4 files changed, 347 insertions(+), 219 deletions(-)
 create mode 100644 tools/testing/selftests/kvm/x86/sev_dbg_test.c

base-commit: 39f1c201b93f4ff71631bac72cff6eb155f976a4

Signature · 1 line
2.54.0.545.g6539524ca2-goog
author maintainer
#1 lore raw

PATCH v2 1/6 KVM: SVM: Fix page overflow in sev_dbg_crypt() for ENCRYPT path

From: Ashutosh Desai <[email protected]>

In sev_dbg_crypt(), the per-iteration transfer length is bounded by the source page offset (PAGE_SIZE - s_off) but not by the destination page offset (PAGE_SIZE - d_off). When d_off > s_off, the encrypt path (__sev_dbg_encrypt_user) performs a read-modify-write using a single-page intermediate buffer (dst_tpage):

1. __sev_dbg_decrypt() expands the size to round_up(len + (d_off & 15), 16) before issuing the PSP command. If len + (d_off & 15) > PAGE_SIZE, the PSP writes beyond the end of the 4096-byte dst_tpage allocation.

2. The subsequent memcpy()/copy_from_user() into page_address(dst_tpage) + (d_off & 15) of 'len' bytes overflows by up to 15 bytes under the same condition.

Trigger example: s_off = 0, d_off = 1, debug.len = PAGE_SIZE - the PSP is instructed to write round_up(4097, 16) = 4112 bytes to a 4096-byte buffer.

Fix by also bounding len by (PAGE_SIZE - d_off), the same check that sev_send_update_data() already performs for its single-page guest region.

==================================================================

 BUG: KASAN: slab-use-after-free in sev_dbg_crypt+0x993/0xd10 [kvm_amd]
 Write of size 4095 at addr ff110062293bb009 by task sev_dbg_test/228214

 CPU: 96 UID: 0 PID: 228214 Comm: sev_dbg_test Tainted: G     U  W           7.0.0-smp--5ce9b0c48211-dbg #156 PREEMPTLAZY
 Tainted: [U]=USER, [W]=WARN
 Hardware name: Google Astoria/astoria, BIOS 0.20250817.1-0 08/25/2025
 Call Trace:
  <TASK>
  dump_stack_lvl+0x54/0x70
  print_report+0xbc/0x260
  kasan_report+0xa2/0xd0
  kasan_check_range+0x25f/0x2c0
  __asan_memcpy+0x40/0x70
  sev_dbg_crypt+0x993/0xd10 [kvm_amd]
  sev_mem_enc_ioctl+0x33c/0x450 [kvm_amd]
  kvm_vm_ioctl+0x65d/0x6d0 [kvm]
  __se_sys_ioctl+0xb2/0x100
  do_syscall_64+0xe8/0x870
  entry_SYSCALL_64_after_hwframe+0x4b/0x53
  </TASK>

 The buggy address belongs to the physical page:
 page: refcount:1 mapcount:0 mapping:0000000000000000 index:0x7fe72b6a0 pfn:0x62293bb
 memcg:ff11000112827d82
 flags: 0x1400000000000000(node=1|zone=1)
 raw: 1400000000000000 0000000000000000 dead000000000122 0000000000000000
 raw: 00000007fe72b6a0 0000000000000000 00000001ffffffff ff11000112827d82
 page dumped because: kasan: bad access detected

 Memory state around the buggy address:
  ff110062293bbf00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  ff110062293bbf80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 >ff110062293bc000: fa fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
                    ^
  ff110062293bc080: fa fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
  ff110062293bc100: fa fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
 ==================================================================
 Disabling lock debugging due to kernel taint
Fixes 24f41fb23a39 ("KVM: SVM: Add support for SEV DEBUG_DECRYPT command")
Fixes 7d1594f5d94b ("KVM: SVM: Add support for SEV DEBUG_ENCRYPT command")
[sean: add sample KASAN splat, Fixes, and stable@]
---
 arch/x86/kvm/svm/sev.c | 1 +
 1 file changed, 1 insertion(+)
arch/x86/kvm/svm/sev.c +1 -0
index c2126b3c3072..b9d7bd868e0b 100644
@@ -1396,6 +1396,7 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp, bool dec)
 		s_off = vaddr & ~PAGE_MASK;
 		d_off = dst_vaddr & ~PAGE_MASK;
 		len = min_t(size_t, (PAGE_SIZE - s_off), size);
+		len = min_t(size_t, len, PAGE_SIZE - d_off);
 
 		if (dec)
 			ret = __sev_dbg_decrypt_user(kvm,
Signature · 1 line
2.54.0.545.g6539524ca2-goog
author maintainer
#2 lore raw

PATCH v2 2/6 KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls

In reply to Sean Christopherson

Add a selftest to verify KVM's handling of {de,en}crypt debug ioctls, specifically focusing on edge cases around the chunk (16 bytes) and page (4096) sizes, where KVM had multiple bugs. E.g. KVM would fail to handle small sizes that aren't naturally aligned and sized, would buffer overflow if the destination was unaligned but the source was not, etc.

Attempt to strike a balance between an exhaustive test and a reasonable runtime. On a system with both SEV and SEV-ES support, the current runtime is under 45 seconds. Which isn't great, but it's tolerable, and it's not obvious which of the combinations are "better" than the others.

---
 tools/testing/selftests/kvm/Makefile.kvm      |   1 +
 tools/testing/selftests/kvm/include/x86/sev.h |  24 ++++
 .../testing/selftests/kvm/x86/sev_dbg_test.c  | 118 ++++++++++++++++++
 3 files changed, 143 insertions(+)
 create mode 100644 tools/testing/selftests/kvm/x86/sev_dbg_test.c
tools/testing/selftests/kvm/Makefile.kvm +1 -0
index 9118a5a51b89..82fa943b9503 100644
@@ -140,6 +140,7 @@ TEST_GEN_PROGS_x86 += x86/tsc_msrs_test
 TEST_GEN_PROGS_x86 += x86/vmx_pmu_caps_test
 TEST_GEN_PROGS_x86 += x86/xen_shinfo_test
 TEST_GEN_PROGS_x86 += x86/xen_vmcall_test
+TEST_GEN_PROGS_x86 += x86/sev_dbg_test
 TEST_GEN_PROGS_x86 += x86/sev_init2_tests
 TEST_GEN_PROGS_x86 += x86/sev_migrate_tests
 TEST_GEN_PROGS_x86 += x86/sev_smoke_test
tools/testing/selftests/kvm/include/x86/sev.h +24 -0
index 1af44c151d60..dec383e59a47 100644
@@ -144,4 +144,28 @@ static inline void snp_launch_update_data(struct kvm_vm *vm, gpa_t gpa,
 	vm_sev_ioctl(vm, KVM_SEV_SNP_LAUNCH_UPDATE, &update_data);
 }
 
+static inline void sev_dbg_crypt_memory(struct kvm_vm *vm, unsigned int cmd,
+					void *dst, void *src, unsigned int len)
+{
+	struct kvm_sev_dbg dbg = {
+		.src_uaddr = (unsigned long)src,
+		.dst_uaddr = (unsigned long)dst,
+		.len = len,
+	};
+
+	vm_sev_ioctl(vm, cmd, &dbg);
+}
+
+static inline void sev_decrypt_memory(struct kvm_vm *vm, void *dst, void *src,
+				      unsigned int len)
+{
+	sev_dbg_crypt_memory(vm, KVM_SEV_DBG_DECRYPT, dst, src, len);
+}
+
+static inline void sev_encrypt_memory(struct kvm_vm *vm, void *dst, void *src,
+				      unsigned int len)
+{
+	sev_dbg_crypt_memory(vm, KVM_SEV_DBG_ENCRYPT, dst, src, len);
+}
+
 #endif /* SELFTEST_KVM_SEV_H */
tools/testing/selftests/kvm/x86/sev_dbg_test.c new file +118 -0

118 lines not on this page · raw message

Signature · 1 line
2.54.0.545.g6539524ca2-goog
#3 lore raw

Re: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt

kernel test robot noticed "kernel-selftests.kvm.make.fail" on:

Hello,

kernel test robot noticed "kernel-selftests.kvm.make.fail" on:

commit: f6ed3165a8ced57973a0919d0ead11eaff393477 ("[PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt")
url: https://github.com/intel-lab-lkp/linux/commits/Sean-Christopherson/KVM-SVM-Fix-page-overflow-in-sev_dbg_crypt-for-ENCRYPT-path/20260502-190513
patch link: https://lore.kernel.org/all/[email protected]/
patch subject: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt

in testcase: kernel-selftests
version: kernel-selftests-x86_64-917719c412c4-1_20260508 with following parameters:

group: kvm

config: x86_64-rhel-9.4-kselftests
compiler: gcc-14
test machine: 16 threads Intel(R) Core(TM) i7-13620H (Raptor Lake) with 32G memory

(please refer to attached dmesg/kmsg for entire log/backtrace)

If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <[email protected]>
| Closes: https://lore.kernel.org/oe-lkp/[email protected]

gcc -D_GNU_SOURCE= -I/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/../../../tools/testing/selftests -I/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/cgroup/lib/include -Wall -Wstrict-prototypes -Wuninitialized -O2 -g -std=gnu99 -Wno-gnu-variable-sized-type-not-at-end -MD -MP -DCONFIG_64BIT -U_FORTIFY_SOURCE -fno-builtin-memcmp -fno-builtin-memcpy -fno-builtin-memset -fno-builtin-strnlen -fno-stack-protector -fno-PIE -fno-strict-aliasing -I/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/../../../tools/include -I/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/../../../tools/arch/x86/include -I/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/../../../usr/include/ -Iinclude -I. -Iinclude/x86 -I ../rseq -I.. -isystem /usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/usr/include -march=x86-64-v2 -c dirty_log_perf_test.c -o /usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/kvm/dirty_log_perf_test.o
x86/sev_dbg_test.c: In function ‘test_sev_dbg’:
x86/sev_dbg_test.c:85:33: error: implicit declaration of function ‘vm_vaddr_alloc’ [-Wimplicit-function-declaration]

   85 |         data = addr_gva2hva(vm, vm_vaddr_alloc(vm, BUFFER_SIZE, KVM_UTIL_MIN_VADDR));
      |                                 ^~~~~~~~~~~~~~

At top level:
cc1: note: unrecognized command-line option ‘-Wno-gnu-variable-sized-type-not-at-end’ may have been intended to silence earlier diagnostics
make[1]: *** [Makefile.kvm:329: /usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/kvm/x86/sev_dbg_test.o] Error 1 make[1]: *** Waiting for unfinished jobs....
make[1]: Leaving directory '/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/kvm'
make: *** [Makefile:217: all] Error 2

The kernel config and materials to reproduce are available at:
https://download.01.org/0day-ci/archive/20260521/[email protected]

Signature · 2 lines
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
author maintainer
#4 lore raw

Re: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt

Quoted 7 lines · kernel test robot

Hello,

kernel test robot noticed "kernel-selftests.kvm.make.fail" on:

commit: f6ed3165a8ced57973a0919d0ead11eaff393477 ("[PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt")

Ah, this is a local commit, from applying the patch. I fixed up the semantic conflict when applying, i.e. I did NOT botch 6edd35e77a42 ("KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls"). :-)

Quoted 49 lines

url: https://github.com/intel-lab-lkp/linux/commits/Sean-Christopherson/KVM-SVM-Fix-page-overflow-in-sev_dbg_crypt-for-ENCRYPT-path/20260502-190513
patch link: https://lore.kernel.org/all/[email protected]/
patch subject: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt

in testcase: kernel-selftests
version: kernel-selftests-x86_64-917719c412c4-1_20260508 with following parameters:

group: kvm

config: x86_64-rhel-9.4-kselftests
compiler: gcc-14
test machine: 16 threads Intel(R) Core(TM) i7-13620H (Raptor Lake) with 32G memory

(please refer to attached dmesg/kmsg for entire log/backtrace)

If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <[email protected]>
| Closes: https://lore.kernel.org/oe-lkp/[email protected]

gcc -D_GNU_SOURCE= -I/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/../../../tools/testing/selftests -I/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/cgroup/lib/include -Wall -Wstrict-prototypes -Wuninitialized -O2 -g -std=gnu99 -Wno-gnu-variable-sized-type-not-at-end -MD -MP -DCONFIG_64BIT -U_FORTIFY_SOURCE -fno-builtin-memcmp -fno-builtin-memcpy -fno-builtin-memset -fno-builtin-strnlen -fno-stack-protector -fno-PIE -fno-strict-aliasing -I/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/../../../tools/include -I/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/../../../tools/arch/x86/include -I/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/../../../usr/include/ -Iinclude -I. -Iinclude/x86 -I ../rseq -I.. -isystem /usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/usr/include -march=x86-64-v2 -c dirty_log_perf_test.c -o /usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/kvm/dirty_log_perf_test.o
x86/sev_dbg_test.c: In function ‘test_sev_dbg’:
x86/sev_dbg_test.c:85:33: error: implicit declaration of function ‘vm_vaddr_alloc’ [-Wimplicit-function-declaration]

   85 |         data = addr_gva2hva(vm, vm_vaddr_alloc(vm, BUFFER_SIZE, KVM_UTIL_MIN_VADDR));
      |                                 ^~~~~~~~~~~~~~

At top level:
cc1: note: unrecognized command-line option ‘-Wno-gnu-variable-sized-type-not-at-end’ may have been intended to silence earlier diagnostics
make[1]: *** [Makefile.kvm:329: /usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/kvm/x86/sev_dbg_test.o] Error 1 make[1]: *** Waiting for unfinished jobs....
make[1]: Leaving directory '/usr/src/perf_selftests-x86_64-rhel-9.4-kselftests-f6ed3165a8ced57973a0919d0ead11eaff393477/tools/testing/selftests/kvm'
make: *** [Makefile:217: all] Error 2

The kernel config and materials to reproduce are available at:
https://download.01.org/0day-ci/archive/20260521/[email protected]

Signature · 2 lines
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

A message that is not in the archive (sent off-list or not yet received): [email protected]

A message that is not in the archive (sent off-list or not yet received): tx3hcnrhfbcz2p7e7umc5o2zrw74pluzuzgtlqt4dz5cc5nhxx@6tzzorc25nqw

A message that is not in the archive (sent off-list or not yet received): s7jfbw3tnfgmp6blfqlfoi7yhq6f5n6xl62zzty5up45cqtsty@ljpjlvqw2gyy

author maintainer
#8 lore raw

Re: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls

+lists

Quoted 28 lines · Michael Roth
Quoted 27 lines · Michael Roth
Quoted 12 lines · Sean Christopherson

off-list in case this is double-ungood.

I ran this on an SNP host today (I think this is the first time I've run the full test on an SNP host), and it explodes with RMP violation #PFs in weird ways. I don't _think_ it's a KVM bug? Because I get the RMP violations even without ever running an SNP guest. I'm using kvm/next, at commit ef057cbf825e ("KVM: x86/mmu:
Ensure hugepage is in by slot before checking max mapping level")

It might be related to edge cases around pages, or maybe large sizes, as the test passes if I comment out the testcases that focus on the larger sizes. I haven't narrowed it down further than this (reboots aren't exactly fast).

Quoted 8 lines

What's especially odd is that on most of the crashes, there's no apparent reason for the RMP violation, as the RMP says the page is unassigned. I did get one crash (#2 below) where the RMP entry was non-zero. That one was after booting an SNP VM, but it was quite some time after shutting down that VM. The RMP dump is equally confusing to me, because AFAICT the RMP entry is corrupted.

Mike/Tom, can you try and repro and debug? I don't have the bandwidth to dig deep on this, and even if I did, I suspect it's beyond my abilities to debug.

Hi Sean,

I was able to reproduce this after running the test in a loop for a few minutes after a clean boot (trace below).

Quoted 7 lines
Quoted 2 lines

Not making much sense to be ATM either, but might be related to RMP table memory itself. Will keep investigating.

It looks like this was a recent regression caused by changes to how the temp buffers are allocated. I tested the below revert patch ~14 hours and the issue seems to be fixed (I generally was reproducing it within an couple hours or so max so it seems good).

Thanks for root causing!

Quoted 1 line

Planning to submit upstream today if there's no objections/concerns.

I object :-)

Quoted 8 lines

From: Michael Roth <[email protected]>
Date: Mon, 22 Jun 2026 22:47:03 -0500
Subject: [PATCH] Revert "KVM: SEV: Allocate only as many bytes as needed for
temp crypt buffers"

When SNP is enabled, the data passed to firmware must be contained within pages that have been transferred to firmware ownership via the corresponding RMP table updates.

IIUC, and I'm pretty sure I do at this point, this is wrong, or at least *very* misleading. Nothing in KVM performs RMP table updates, and so saying "the data passed to firmware must be contained within pages that have been transferred to firmware ownership" is completely inaccurate.

Piecing together the changelog and diff, and my own observations, my understanding is that **firmware** modifies the RMP to *temporarily* take ownership of the page while performing the {DE,EN}CRYPT operation. So there's no requirement that the pages be in any specific state, the only "requirement" is that software needs to either prevent concurrent accesses or be prepared to handle spurious RMP #PFs due to the temporarily RMP modifications.

SNP_DBG_DECRYPT does require software to convert the destination to be a firmware
page: "The firmware also checks that the destination page is a Firmware page." But this is about SEV_DBG_{DE,EN}CRYPT, not SNP_DBG_ENCRYPT.

Quoted 1 line

This is not compatible with uses kmalloc() allocations since kernel accesses

As is the statement about kmalloc(). kmalloc() of a PAGE_SIZE is a-ok, because the kernel will always hand out a full page.

Quoted 1 line

to other allocations within that page will trigger an RMP fault and crash the

And it's specificaly about other *accesses*, not simply other allocations.

Quoted 1 line

host. Fix this by moving back to page-based allocations.o

Always forcing full page allocations is overkill for SEV and SEV-ES, and robs us of the opportunity to document that SNP+ is special, which is especially important because AFAICT, none of the specs are so kind as to document this "minor" behavior.

Rather than fully revert, just force PAGE_SIZE allocations if the host supports SNP. I haven't run anywhere near 14 hours, but I was also getting failures on every single run of the test, and I ran the test 100 times without problem.

However, this only addresses the case where KVM is using temporary buffers. For small, nicely aligned operations, a misbehaving userspace could induce a crash by coercing the kernel into accessing the to-be-{de,en}crypted page via a kernel mapping while the crypo op is in-progess. I don't see a less awful option than forcing KVM to use the "slow" path, e.g. if the problem is limited to the dest for a decrypt operation:

x86/kvm/svm/sev.c +2 -1
index 87025d0d2f91..7e3334c90c57 100644
@@ -1406,7 +1406,8 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp,
                sev_clflush_pages(&src_p, 1);
                sev_clflush_pages(&dst_p, 1);
 
-               if (IS_ALIGNED(src, 16) && IS_ALIGNED(dst, 16) && IS_ALIGNED(len, 16))
+               if (IS_ALIGNED(src, 16) && IS_ALIGNED(dst, 16) && IS_ALIGNED(len, 16) &&
+                   (!cc_platform_has(CC_ATTR_HOST_SEV_SNP) || cmd != KVM_SEV_DBG_DECRYPT))
                        ret = sev_issue_dbg_cmd(kvm,
                                                __sme_page_pa(src_p) + s_off,
                                                __sme_page_pa(dst_p) + d_off,

But if firmware temporarily converts *both* pages, then we're "stuck" because at some point KVM needs to actually target the correct guest-owned page. The only option I can think of is to require capable(CAP_SYS_BOOT), *if* the above doesn't suffice.

So what exactly is the behavior here? Because I can't find anything in the specs, I can only make educated guesses based on the SNP_DBG_DECRYPT documentation.

---
From: Sean Christopherson <[email protected]>
Date: Tue, 23 Jun 2026 12:21:56 -0700
Subject: [PATCH] KVM: SEV: Allocate full pages for {DE,EN}CRYPT ops on
SNP-enabled hosts

When {de,en}crypting memory of an SEV or SEV-ES guest on an SNP-enabled host via a temporary buffer, allocate a full 4KiB page for the buffer to ensure the kernel won't concurrently access the page that's being {de,en}crypted. On SNP-enabled platforms, firmware modifies the RMP to temporarily take ownership of the to-be-{de,en}crypted page, and so using a sub-allocation results in unexpected (and seemingly spurious) RMP #PF violations due to software attempting to access a firmware-owned page.

Note, it is unclear whether the RMP updates are considered architectural, or an implementation quirk, as none of the documentation for the non-SNP DBG_{DE,EN}CRYPT commands say anything about RMP updates, nor does the SNP specific firmware ABI spec.

Fixes 4c735bf1bc22 ("KVM: SEV: Allocate only as many bytes as needed for temp crypt buffers")
---
 arch/x86/kvm/svm/sev.c | 10 ++++++++++
 1 file changed, 10 insertions(+)
arch/x86/kvm/svm/sev.c +10 -0
index 74fb15551e83..87025d0d2f91 100644
@@ -1280,6 +1280,16 @@ static void *sev_dbg_crypt_slow_alloc(struct page *page, unsigned long __va,
 	if (WARN_ON_ONCE((*pa & PAGE_MASK) != ((*pa + *nr_bytes - 1) & PAGE_MASK)))
 		return NULL;
 
+	/*
+	 * If SNP is enabled, i.e. the RMP is active, allocate a full page to
+	 * prevent concurrent accesses to the page.  Firmware modifies the RMP
+	 * to temporarily take ownership of the page while the {DE,EN}CRYPT
+	 * operation is in-progress, and so concurrent software accesses will
+	 * encounter seemingly spurious RMP #PF violations
+	 */
+	if (cc_platform_has(CC_ATTR_HOST_SEV_SNP))
+		return kmalloc(PAGE_SIZE, GFP_KERNEL);
+
 	return kmalloc(*nr_bytes, GFP_KERNEL);
 }
 

base-commit: 9d4853b044beefa21c4ee3e18c40653601a64ced

#9 lore raw

Re: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls

Quoted 39 lines · Sean Christopherson

+lists

Quoted 28 lines · Michael Roth
Quoted 27 lines · Michael Roth
Quoted 12 lines · Sean Christopherson

off-list in case this is double-ungood.

I ran this on an SNP host today (I think this is the first time I've run the full test on an SNP host), and it explodes with RMP violation #PFs in weird ways. I don't _think_ it's a KVM bug? Because I get the RMP violations even without ever running an SNP guest. I'm using kvm/next, at commit ef057cbf825e ("KVM: x86/mmu:
Ensure hugepage is in by slot before checking max mapping level")

It might be related to edge cases around pages, or maybe large sizes, as the test passes if I comment out the testcases that focus on the larger sizes. I haven't narrowed it down further than this (reboots aren't exactly fast).

Quoted 8 lines

What's especially odd is that on most of the crashes, there's no apparent reason for the RMP violation, as the RMP says the page is unassigned. I did get one crash (#2 below) where the RMP entry was non-zero. That one was after booting an SNP VM, but it was quite some time after shutting down that VM. The RMP dump is equally confusing to me, because AFAICT the RMP entry is corrupted.

Mike/Tom, can you try and repro and debug? I don't have the bandwidth to dig deep on this, and even if I did, I suspect it's beyond my abilities to debug.

Hi Sean,

I was able to reproduce this after running the test in a loop for a few minutes after a clean boot (trace below).

Quoted 7 lines
Quoted 2 lines

Not making much sense to be ATM either, but might be related to RMP table memory itself. Will keep investigating.

It looks like this was a recent regression caused by changes to how the temp buffers are allocated. I tested the below revert patch ~14 hours and the issue seems to be fixed (I generally was reproducing it within an couple hours or so max so it seems good).

Quoted 5 lines

Thanks for root causing!

Quoted 1 line

Planning to submit upstream today if there's no objections/concerns.

I object :-)

Drat :)

Quoted 6 lines
Quoted 6 lines

From: Michael Roth <[email protected]>
Date: Mon, 22 Jun 2026 22:47:03 -0500
Subject: [PATCH] Revert "KVM: SEV: Allocate only as many bytes as needed for
temp crypt buffers"

Quoted 8 lines
Quoted 3 lines

When SNP is enabled, the data passed to firmware must be contained within pages that have been transferred to firmware ownership via the corresponding RMP table updates.

IIUC, and I'm pretty sure I do at this point, this is wrong, or at least *very* misleading. Nothing in KVM performs RMP table updates, and so saying "the data passed to firmware must be contained within pages that have been transferred to firmware ownership" is completely inaccurate.

The driver will place it into the 'firmware' state ("5.3 Page States" in SEV-SNP Firmware ABI) prior to issuing the command to firmware, so from a kernel perspective I don't think it is inaccurate. It's true that this is handled transparently to KVM, but I don't think the wording is really in conflict with that.

Just trying to clarify and understand if I'm missing something more subtle here, since your proposed fix seems nicer and more complete anyway.

Quoted 3 lines

Piecing together the changelog and diff, and my own observations, my understanding is that **firmware** modifies the RMP to *temporarily* take ownership of the page

rmp_mark_pages_firmware() in drivers/crypto/ccp/sev-dev.c does the RMP table update as part of preparing the command buffers prior to issuing the firmware call, placing the page in the 'firmware' state, and once the command completes, firmware modifies the RMP table state to be in the 'reclaim' state. The page will still be 'private' at that point, in the sense it will have the 'assigned' bit set.

The hypervisor can then take it back via additional firmware call SEV_CMD_SNP_PAGE_RECLAIM and subsequent RMPUPDATE to normal/'hypervisor' page. So the transition out of 'firmware' does sort of happen automatically, but the overall reclaim process requires explicit work by the hypervisor.

Quoted 8 lines

while performing the {DE,EN}CRYPT operation. So there's no requirement that the pages be in any specific state, the only "requirement" is that software needs to either prevent concurrent accesses or be prepared to handle spurious RMP #PFs due to the temporarily RMP modifications.

SNP_DBG_DECRYPT does require software to convert the destination to be a firmware
page: "The firmware also checks that the destination page is a Firmware page." But this is about SEV_DBG_{DE,EN}CRYPT, not SNP_DBG_ENCRYPT.

The above behavior isn't really specific to DECRYPT/ENCRYPT, but one example of a general requirement that when issuing legacy SEV commands on an SNP-enabled host, the memory that firmware writes to must undergo the above transitions before/after the actual firmware call:

5.3.9 SEV Legacy Commands
The behavior of the SEV-legacy commands is altered when the SNP firmware is in the INIT state. In this case, the SEV-legacy commands require any page that the SEV-legacy command writes to be a Firmware or Default page.

('Default' state is for ranges that have no corresponding RMP table entries at all, which isn't really applicable for how things are implemented in the kernel currently. The reasoning there though is that those pages can't be used for SNP guests, so legacy firmware commands can't be used as a widget to try to mess with private guest memory).

SNP_DBG_DECRYPT calls it out explicitly since that's how it as always worked, whereas for legacy commands these are additional, SNP-specific operations that happen before/after the legacy commands, so they are documented in a broader fashion.

Quoted 42 lines
Quoted 2 lines

This is not compatible with uses kmalloc() allocations since kernel accesses

As is the statement about kmalloc(). kmalloc() of a PAGE_SIZE is a-ok, because the kernel will always hand out a full page.

Quoted 1 line

to other allocations within that page will trigger an RMP fault and crash the

And it's specificaly about other *accesses*, not simply other allocations.

Quoted 1 line

host. Fix this by moving back to page-based allocations.o

Always forcing full page allocations is overkill for SEV and SEV-ES, and robs us of the opportunity to document that SNP+ is special, which is especially important because AFAICT, none of the specs are so kind as to document this "minor" behavior.

Rather than fully revert, just force PAGE_SIZE allocations if the host supports SNP. I haven't run anywhere near 14 hours, but I was also getting failures on every single run of the test, and I ran the test 100 times without problem.

However, this only addresses the case where KVM is using temporary buffers. For small, nicely aligned operations, a misbehaving userspace could induce a crash by coercing the kernel into accessing the to-be-{de,en}crypted page via a kernel mapping while the crypo op is in-progess. I don't see a less awful option than forcing KVM to use the "slow" path, e.g. if the problem is limited to the dest for a decrypt operation:

x86/kvm/svm/sev.c +2 -1
index 87025d0d2f91..7e3334c90c57 100644
@@ -1406,7 +1406,8 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp,
                sev_clflush_pages(&src_p, 1);
                sev_clflush_pages(&dst_p, 1);
 
-               if (IS_ALIGNED(src, 16) && IS_ALIGNED(dst, 16) && IS_ALIGNED(len, 16))
+               if (IS_ALIGNED(src, 16) && IS_ALIGNED(dst, 16) && IS_ALIGNED(len, 16) &&
+                   (!cc_platform_has(CC_ATTR_HOST_SEV_SNP) || cmd != KVM_SEV_DBG_DECRYPT))
                        ret = sev_issue_dbg_cmd(kvm,
                                                __sme_page_pa(src_p) + s_off,
                                                __sme_page_pa(dst_p) + d_off,
Quoted 4 lines

But if firmware temporarily converts *both* pages, then we're "stuck" because at some point KVM needs to actually target the correct guest-owned page. The only option I can think of is to require capable(CAP_SYS_BOOT), *if* the above doesn't suffice.

Not sure I understand fully, but snp_populate_cmd_buf_desc_list() in the firmware driver requires the transiton for the destination page that we write to both for 'encrypt' as well as 'decrypt'. What the issue with using a temp buffer as the destination for the 'encrypt' case?

Quoted 3 lines

So what exactly is the behavior here? Because I can't find anything in the specs, I can only make educated guesses based on the SNP_DBG_DECRYPT documentation.

Hopefully the above references help somewhat, but I agree there's still an uncomfortable amount of connecting the dots for this flow.

Thanks,

Mike

Quoted 51 lines

---
From: Sean Christopherson <[email protected]>
Date: Tue, 23 Jun 2026 12:21:56 -0700
Subject: [PATCH] KVM: SEV: Allocate full pages for {DE,EN}CRYPT ops on
SNP-enabled hosts

When {de,en}crypting memory of an SEV or SEV-ES guest on an SNP-enabled host via a temporary buffer, allocate a full 4KiB page for the buffer to ensure the kernel won't concurrently access the page that's being {de,en}crypted. On SNP-enabled platforms, firmware modifies the RMP to temporarily take ownership of the to-be-{de,en}crypted page, and so using a sub-allocation results in unexpected (and seemingly spurious) RMP #PF violations due to software attempting to access a firmware-owned page.

Note, it is unclear whether the RMP updates are considered architectural, or an implementation quirk, as none of the documentation for the non-SNP DBG_{DE,EN}CRYPT commands say anything about RMP updates, nor does the SNP specific firmware ABI spec.

Fixes 4c735bf1bc22 ("KVM: SEV: Allocate only as many bytes as needed for temp crypt buffers")
---
 arch/x86/kvm/svm/sev.c | 10 ++++++++++
 1 file changed, 10 insertions(+)
arch/x86/kvm/svm/sev.c +10 -0
index 74fb15551e83..87025d0d2f91 100644
@@ -1280,6 +1280,16 @@ static void *sev_dbg_crypt_slow_alloc(struct page *page, unsigned long __va,
 	if (WARN_ON_ONCE((*pa & PAGE_MASK) != ((*pa + *nr_bytes - 1) & PAGE_MASK)))
 		return NULL;
 
+	/*
+	 * If SNP is enabled, i.e. the RMP is active, allocate a full page to
+	 * prevent concurrent accesses to the page.  Firmware modifies the RMP
+	 * to temporarily take ownership of the page while the {DE,EN}CRYPT
+	 * operation is in-progress, and so concurrent software accesses will
+	 * encounter seemingly spurious RMP #PF violations
+	 */
+	if (cc_platform_has(CC_ATTR_HOST_SEV_SNP))
+		return kmalloc(PAGE_SIZE, GFP_KERNEL);
+
 	return kmalloc(*nr_bytes, GFP_KERNEL);
 }
 

base-commit: 9d4853b044beefa21c4ee3e18c40653601a64ced

author maintainer
#10 lore raw

Re: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls

Quoted 15 lines · Michael Roth
Quoted 5 lines · Sean Christopherson
Quoted 5 lines

From: Michael Roth <[email protected]>
Date: Mon, 22 Jun 2026 22:47:03 -0500
Subject: [PATCH] Revert "KVM: SEV: Allocate only as many bytes as needed for
temp crypt buffers"

Quoted 8 lines
Quoted 3 lines

When SNP is enabled, the data passed to firmware must be contained within pages that have been transferred to firmware ownership via the corresponding RMP table updates.

IIUC, and I'm pretty sure I do at this point, this is wrong, or at least *very* misleading. Nothing in KVM performs RMP table updates, and so saying "the data passed to firmware must be contained within pages that have been transferred to firmware ownership" is completely inaccurate.

Quoted 5 lines

The driver will place it into the 'firmware' state ("5.3 Page States" in SEV-SNP Firmware ABI) prior to issuing the command to firmware, so from a kernel perspective I don't think it is inaccurate. It's true that this is handled transparently to KVM, but I don't think the wording is really in conflict with that.

Ugh, I didn't even consider that possibility, obviously. Well, TIL.

Quoted 2 lines

Just trying to clarify and understand if I'm missing something more subtle here,

Noped, I simply didn't realize snp_map_cmd_buf_desc() and snp_populate_cmd_buf_desc_list() even existed.

Quoted 1 line

since your proposed fix seems nicer and more complete anyway.

It's missing a case though :-(

The final write for ENCRYPT writes to the destination directly:

		r = sev_issue_dbg_cmd(kvm, __sme_set(__pa(buf)), dst_pa,
				      nr_bytes, KVM_SEV_DBG_ENCRYPT, err);

More at the bottom.

Quoted 34 lines
Quoted 2 lines

Piecing together the changelog and diff, and my own observations, my understanding is that **firmware** modifies the RMP to *temporarily* take ownership of the page

rmp_mark_pages_firmware() in drivers/crypto/ccp/sev-dev.c does the RMP table update as part of preparing the command buffers prior to issuing the firmware call, placing the page in the 'firmware' state, and once the command completes, firmware modifies the RMP table state to be in the 'reclaim' state. The page will still be 'private' at that point, in the sense it will have the 'assigned' bit set.

The hypervisor can then take it back via additional firmware call SEV_CMD_SNP_PAGE_RECLAIM and subsequent RMPUPDATE to normal/'hypervisor' page. So the transition out of 'firmware' does sort of happen automatically, but the overall reclaim process requires explicit work by the hypervisor.

Quoted 8 lines

while performing the {DE,EN}CRYPT operation. So there's no requirement that the pages be in any specific state, the only "requirement" is that software needs to either prevent concurrent accesses or be prepared to handle spurious RMP #PFs due to the temporarily RMP modifications.

SNP_DBG_DECRYPT does require software to convert the destination to be a firmware
page: "The firmware also checks that the destination page is a Firmware page." But this is about SEV_DBG_{DE,EN}CRYPT, not SNP_DBG_ENCRYPT.

The above behavior isn't really specific to DECRYPT/ENCRYPT, but one example of a general requirement that when issuing legacy SEV commands on an SNP-enabled host, the memory that firmware writes to must undergo the above transitions before/after the actual firmware call:

5.3.9 SEV Legacy Commands
The behavior of the SEV-legacy commands is altered when the SNP firmware is in the INIT state. In this case, the SEV-legacy commands require any page that the SEV-legacy command writes to be a Firmware or Default page.

Quoted 5 lines

('Default' state is for ranges that have no corresponding RMP table entries at all, which isn't really applicable for how things are implemented in the kernel currently. The reasoning there though is that those pages can't be used for SNP guests, so legacy firmware commands can't be used as a widget to try to mess with private guest memory).

Oooh, got it.

Quoted 4 lines

SNP_DBG_DECRYPT calls it out explicitly since that's how it as always worked, whereas for legacy commands these are additional, SNP-specific operations that happen before/after the legacy commands, so they are documented in a broader fashion.

Thanks Mike! I appreciate the info, a lot!

Quoted 41 lines
Quoted 41 lines
Quoted 1 line

This is not compatible with uses kmalloc() allocations since kernel accesses

As is the statement about kmalloc(). kmalloc() of a PAGE_SIZE is a-ok, because the kernel will always hand out a full page.

Quoted 1 line

to other allocations within that page will trigger an RMP fault and crash the

And it's specificaly about other *accesses*, not simply other allocations.

Quoted 1 line

host. Fix this by moving back to page-based allocations.o

Always forcing full page allocations is overkill for SEV and SEV-ES, and robs us of the opportunity to document that SNP+ is special, which is especially important because AFAICT, none of the specs are so kind as to document this "minor" behavior.

Rather than fully revert, just force PAGE_SIZE allocations if the host supports SNP. I haven't run anywhere near 14 hours, but I was also getting failures on every single run of the test, and I ran the test 100 times without problem.

However, this only addresses the case where KVM is using temporary buffers. For small, nicely aligned operations, a misbehaving userspace could induce a crash by coercing the kernel into accessing the to-be-{de,en}crypted page via a kernel mapping while the crypo op is in-progess. I don't see a less awful option than forcing KVM to use the "slow" path, e.g. if the problem is limited to the dest for a decrypt operation:

x86/kvm/svm/sev.c +2 -1
index 87025d0d2f91..7e3334c90c57 100644
@@ -1406,7 +1406,8 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp,
                sev_clflush_pages(&src_p, 1);
                sev_clflush_pages(&dst_p, 1);
 
-               if (IS_ALIGNED(src, 16) && IS_ALIGNED(dst, 16) && IS_ALIGNED(len, 16))
+               if (IS_ALIGNED(src, 16) && IS_ALIGNED(dst, 16) && IS_ALIGNED(len, 16) &&
+                   (!cc_platform_has(CC_ATTR_HOST_SEV_SNP) || cmd != KVM_SEV_DBG_DECRYPT))
                        ret = sev_issue_dbg_cmd(kvm,
                                                __sme_page_pa(src_p) + s_off,
                                                __sme_page_pa(dst_p) + d_off,
Quoted 9 lines
Quoted 4 lines

But if firmware temporarily converts *both* pages, then we're "stuck" because at some point KVM needs to actually target the correct guest-owned page. The only option I can think of is to require capable(CAP_SYS_BOOT), *if* the above doesn't suffice.

Not sure I understand fully, but snp_populate_cmd_buf_desc_list() in the firmware driver requires the transiton for the destination page that we write to both for 'encrypt' as well as 'decrypt'. What the issue with using a temp buffer as the destination for the 'encrypt' case?

The problem is if KVM does NOT use a temporary buffer, which is the "fast" path where src, dst, and len are all 16-byte aligned. In that case, KVM does inline {de,en}cryption between src and dst. But those pages are also mapped into userspace (they have to be, since KVM pins them via gup()), which means that userspace could feed the same pages to a different chunk of the kernel that reads/write userspace memory via gup() (or gup-like equivalent).

E.g. pread()/read() on shmem-backed memory.

Looking at the full picture, isn't this a bug in the PSP driver that affects all commands? With pre-SNP VMs, "guest owned" isn't really a thing, since nothing guarantees that the destination page is accessible *only* by the guest.

If we rip that out and always bounce-buffer, then KVM won't have to force its own bounce buffering of sub-page lengths for SNP, and that would eliminate the race I described above.

Completely untested (I'll give this a whirl tomorrow).

---
 drivers/crypto/ccp/sev-dev.c | 52 ++++++++++++++----------------------
 1 file changed, 20 insertions(+), 32 deletions(-)
drivers/crypto/ccp/sev-dev.c +20 -32
index 7cd6cd6fdb10..44c34c0e3f6f 100644
@@ -528,15 +528,11 @@ static void *sev_fw_alloc(unsigned long len)
  *              replaced with the address of a bounce buffer.
  *
  * @len: length of buffer located at the address originally stored at @paddr_ptr
- *
- * @guest_owned: true if the address corresponds to guest-owned pages, in which
- *               case bounce buffers are not needed.
  */
 struct cmd_buf_desc {
 	u64 *paddr_ptr;
 	u64 paddr_orig;
 	u32 len;
-	bool guest_owned;
 };
 
 /*
@@ -582,7 +578,6 @@ static void snp_populate_cmd_buf_desc_list(int cmd, void *cmd_buf,
 
 		desc_list[0].paddr_ptr = &data->address;
 		desc_list[0].len = data->len;
-		desc_list[0].guest_owned = true;
 		break;
 	}
 	case SEV_CMD_LAUNCH_UPDATE_VMSA: {
@@ -590,7 +585,6 @@ static void snp_populate_cmd_buf_desc_list(int cmd, void *cmd_buf,
 
 		desc_list[0].paddr_ptr = &data->address;
 		desc_list[0].len = data->len;
-		desc_list[0].guest_owned = true;
 		break;
 	}
 	case SEV_CMD_LAUNCH_MEASURE: {
@@ -605,7 +599,6 @@ static void snp_populate_cmd_buf_desc_list(int cmd, void *cmd_buf,
 
 		desc_list[0].paddr_ptr = &data->guest_address;
 		desc_list[0].len = data->guest_len;
-		desc_list[0].guest_owned = true;
 		break;
 	}
 	case SEV_CMD_DBG_DECRYPT: {
@@ -613,7 +606,6 @@ static void snp_populate_cmd_buf_desc_list(int cmd, void *cmd_buf,
 
 		desc_list[0].paddr_ptr = &data->dst_addr;
 		desc_list[0].len = data->len;
-		desc_list[0].guest_owned = true;
 		break;
 	}
 	case SEV_CMD_DBG_ENCRYPT: {
@@ -621,7 +613,6 @@ static void snp_populate_cmd_buf_desc_list(int cmd, void *cmd_buf,
 
 		desc_list[0].paddr_ptr = &data->dst_addr;
 		desc_list[0].len = data->len;
-		desc_list[0].guest_owned = true;
 		break;
 	}
 	case SEV_CMD_ATTESTATION_REPORT: {
@@ -661,7 +652,6 @@ static void snp_populate_cmd_buf_desc_list(int cmd, void *cmd_buf,
 
 		desc_list[0].paddr_ptr = &data->guest_address;
 		desc_list[0].len = data->guest_len;
-		desc_list[0].guest_owned = true;
 		break;
 	}
 	case SEV_CMD_RECEIVE_UPDATE_VMSA: {
@@ -669,7 +659,6 @@ static void snp_populate_cmd_buf_desc_list(int cmd, void *cmd_buf,
 
 		desc_list[0].paddr_ptr = &data->guest_address;
 		desc_list[0].len = data->guest_len;
-		desc_list[0].guest_owned = true;
 		break;
 	}
 	default:
@@ -680,24 +669,24 @@ static void snp_populate_cmd_buf_desc_list(int cmd, void *cmd_buf,
 static int snp_map_cmd_buf_desc(struct cmd_buf_desc *desc)
 {
 	unsigned int npages;
+	struct page *page;
 
 	if (!desc->len)
 		return 0;
 
-	/* Allocate a bounce buffer if this isn't a guest owned page. */
-	if (!desc->guest_owned) {
-		struct page *page;
-
-		page = alloc_pages(GFP_KERNEL_ACCOUNT, get_order(desc->len));
-		if (!page) {
-			pr_warn("Failed to allocate bounce buffer for SEV legacy command.\n");
-			return -ENOMEM;
-		}
-
-		desc->paddr_orig = *desc->paddr_ptr;
-		*desc->paddr_ptr = __psp_pa(page_to_virt(page));
+	/*
+	 * Allocate a bounce buffer if this isn't a guest owned page, or if the
+	 *
+	  */
+	page = alloc_pages(GFP_KERNEL_ACCOUNT, get_order(desc->len));
+	if (!page) {
+		pr_warn("Failed to allocate bounce buffer for SEV legacy command.\n");
+		return -ENOMEM;
 	}
 
+	desc->paddr_orig = *desc->paddr_ptr;
+	*desc->paddr_ptr = __psp_pa(page_to_virt(page));
+
 	npages = PAGE_ALIGN(desc->len) >> PAGE_SHIFT;
 
 	/* Transition the buffer to firmware-owned. */
@@ -712,6 +701,8 @@ static int snp_map_cmd_buf_desc(struct cmd_buf_desc *desc)
 static int snp_unmap_cmd_buf_desc(struct cmd_buf_desc *desc)
 {
 	unsigned int npages;
+	void *bounce_buf;
+	void *dst_buf;
 
 	if (!desc->len)
 		return 0;
@@ -725,17 +716,14 @@ static int snp_unmap_cmd_buf_desc(struct cmd_buf_desc *desc)
 	}
 
 	/* Copy data from bounce buffer and then free it. */
-	if (!desc->guest_owned) {
-		void *bounce_buf = __va(__sme_clr(*desc->paddr_ptr));
-		void *dst_buf = __va(__sme_clr(desc->paddr_orig));
+	bounce_buf = __va(__sme_clr(*desc->paddr_ptr));
+	dst_buf = __va(__sme_clr(desc->paddr_orig));
 
-		memcpy(dst_buf, bounce_buf, desc->len);
-		__free_pages(virt_to_page(bounce_buf), get_order(desc->len));
-
-		/* Restore the original address in the command buffer. */
-		*desc->paddr_ptr = desc->paddr_orig;
-	}
+	memcpy(dst_buf, bounce_buf, desc->len);
+	__free_pages(virt_to_page(bounce_buf), get_order(desc->len));
 
+	/* Restore the original address in the command buffer. */
+	*desc->paddr_ptr = desc->paddr_orig;
 	return 0;
 }
 

base-commit: 53f2cae5efddf82a426a4e980fd3a2450c68aef0 --

author maintainer
#11 lore raw

Re: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls

Quoted 5 lines · Sean Christopherson
Quoted 2 lines · Michael Roth

On Tue, Jun 23, 2026 at 07:55:27PM +0000, Sean Christopherson wrote:
since your proposed fix seems nicer and more complete anyway.

It's missing a case though :-(

Ah, but so is the revert, and it's not a coincidence that the old code didn't bounce buffer the destination for KVM_SEV_DBG_ENCRYPT.

Quoted 6 lines

The final write for ENCRYPT writes to the destination directly:

		r = sev_issue_dbg_cmd(kvm, __sme_set(__pa(buf)), dst_pa,
				      nr_bytes, KVM_SEV_DBG_ENCRYPT, err);

More at the bottom.

Once more, with feeling...

Quoted 25 lines
Quoted 15 lines
Quoted 15 lines
x86/kvm/svm/sev.c +2 -1
index 87025d0d2f91..7e3334c90c57 100644
@@ -1406,7 +1406,8 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp,
                sev_clflush_pages(&src_p, 1);
                sev_clflush_pages(&dst_p, 1);
 
-               if (IS_ALIGNED(src, 16) && IS_ALIGNED(dst, 16) && IS_ALIGNED(len, 16))
+               if (IS_ALIGNED(src, 16) && IS_ALIGNED(dst, 16) && IS_ALIGNED(len, 16) &&
+                   (!cc_platform_has(CC_ATTR_HOST_SEV_SNP) || cmd != KVM_SEV_DBG_DECRYPT))
                        ret = sev_issue_dbg_cmd(kvm,
                                                __sme_page_pa(src_p) + s_off,
                                                __sme_page_pa(dst_p) + d_off,
Quoted 9 lines
Quoted 4 lines

But if firmware temporarily converts *both* pages, then we're "stuck" because at some point KVM needs to actually target the correct guest-owned page. The only option I can think of is to require capable(CAP_SYS_BOOT), *if* the above doesn't suffice.

Not sure I understand fully, but snp_populate_cmd_buf_desc_list() in the firmware driver requires the transiton for the destination page that we write to both for 'encrypt' as well as 'decrypt'. What the issue with using a temp buffer as the destination for the 'encrypt' case?

Quoted 8 lines

The problem is if KVM does NOT use a temporary buffer, which is the "fast" path where src, dst, and len are all 16-byte aligned. In that case, KVM does inline {de,en}cryption between src and dst. But those pages are also mapped into userspace (they have to be, since KVM pins them via gup()), which means that userspace could feed the same pages to a different chunk of the kernel that reads/write userspace memory via gup() (or gup-like equivalent).

E.g. pread()/read() on shmem-backed memory.

pread() doesn't work, because the source or destination needs to be a userspace address, and so any RMP #PFs will get eaten. But copy_file_range() makes the world go kablooie.

Weirdly, I haven't been able to get a splat of any kind, the host just goes AWOL and reboots. But I'm very confident RMP #PFs are the issue, because doing the same operation via writes in userspace yields:

    sev_dbg_test-24385   [049] d....  1892.853197: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853202: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853204: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853206: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853207: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853209: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853211: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853212: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
Quoted 9 lines

Looking at the full picture, isn't this a bug in the PSP driver that affects all commands? With pre-SNP VMs, "guest owned" isn't really a thing, since nothing guarantees that the destination page is accessible *only* by the guest.

If we rip that out and always bounce-buffer, then KVM won't have to force its own bounce buffering of sub-page lengths for SNP, and that would eliminate the race I described above.

Completely untested (I'll give this a whirl tomorrow).

So this doesn't work. Dredging up some knowledge of how this all works, I'm fairly certain the issue is that the encryption is salted with the physical address, and so encrypting at X instead of Y will yield different ciphertext. I.e. any command that encrypts the contents in the destination (or in theory, decrypts, though AFAICT none of the commands do so) *can't* be bounce buffered.

I don't see a way to salvage SEV/SEV-ES on SNP systems, short of requiring CAP_SYS_BOOT or some other elevated permission to do *anything*. Or redo SEV/SEV-ES support to require guest_memfd for operations that require putting pages into Firmware state.

#12 lore raw

Re: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls

Quoted 9 lines · Sean Christopherson
Quoted 5 lines · Sean Christopherson
Quoted 2 lines · Michael Roth

On Tue, Jun 23, 2026 at 07:55:27PM +0000, Sean Christopherson wrote:
since your proposed fix seems nicer and more complete anyway.

It's missing a case though :-(

Ah, but so is the revert, and it's not a coincidence that the old code didn't bounce buffer the destination for KVM_SEV_DBG_ENCRYPT.

Yah I was realizing this too :( Though I'm not sure why we set 'guest_owned' for DBG_DECRYPT... I'm not even sure it makes sense to write decrypted data into guest memory since it would only be usable if the range was decrypted to begin with... in most cases this likely wouldn't be guest memory.

So maybe the bounce buffer approach would work for DBG_DECRYPT at least, but yah...I think the issue is more general...

Quoted 42 lines
Quoted 7 lines

The final write for ENCRYPT writes to the destination directly:

		r = sev_issue_dbg_cmd(kvm, __sme_set(__pa(buf)), dst_pa,
				      nr_bytes, KVM_SEV_DBG_ENCRYPT, err);

More at the bottom.

Once more, with feeling...

Quoted 32 lines
Quoted 15 lines
Quoted 15 lines
x86/kvm/svm/sev.c +2 -1
index 87025d0d2f91..7e3334c90c57 100644
@@ -1406,7 +1406,8 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp,
                sev_clflush_pages(&src_p, 1);
                sev_clflush_pages(&dst_p, 1);
 
-               if (IS_ALIGNED(src, 16) && IS_ALIGNED(dst, 16) && IS_ALIGNED(len, 16))
+               if (IS_ALIGNED(src, 16) && IS_ALIGNED(dst, 16) && IS_ALIGNED(len, 16) &&
+                   (!cc_platform_has(CC_ATTR_HOST_SEV_SNP) || cmd != KVM_SEV_DBG_DECRYPT))
                        ret = sev_issue_dbg_cmd(kvm,
                                                __sme_page_pa(src_p) + s_off,
                                                __sme_page_pa(dst_p) + d_off,
Quoted 9 lines
Quoted 4 lines

But if firmware temporarily converts *both* pages, then we're "stuck" because at some point KVM needs to actually target the correct guest-owned page. The only option I can think of is to require capable(CAP_SYS_BOOT), *if* the above doesn't suffice.

Not sure I understand fully, but snp_populate_cmd_buf_desc_list() in the firmware driver requires the transiton for the destination page that we write to both for 'encrypt' as well as 'decrypt'. What the issue with using a temp buffer as the destination for the 'encrypt' case?

The problem is if KVM does NOT use a temporary buffer, which is the "fast" path where src, dst, and len are all 16-byte aligned. In that case, KVM does inline {de,en}cryption between src and dst. But those pages are also mapped into userspace (they have to be, since KVM pins them via gup()), which means that userspace could feed the same pages to a different chunk of the kernel that reads/write userspace memory via gup() (or gup-like equivalent).

Quoted 5 lines
Quoted 1 line

E.g. pread()/read() on shmem-backed memory.

pread() doesn't work, because the source or destination needs to be a userspace address, and so any RMP #PFs will get eaten. But copy_file_range() makes the world go kablooie.

I was hoping we could at least rely on kernel accesses going through GUP and maybe doing something like unmapping and freezing folio refcount to block concurrent access to the page, or read-only protections that would force the GUP to fail and just fail the concurrent userspace-requested operation...

copy_file_range() bypasses the GUP, but freezing the refcount or locking the folio would cause it to spin until the folio was released. So maybe that's an option?

This is similar to what's been proposed for gmem hugetlb support when splitting/rebuilding hugepages as part of the conversion path, where we need to guard against concurrent access by both kernel/userspace, so maybe it's not too hacky?

Quoted 23 lines

Weirdly, I haven't been able to get a splat of any kind, the host just goes AWOL and reboots. But I'm very confident RMP #PFs are the issue, because doing the same operation via writes in userspace yields:

    sev_dbg_test-24385   [049] d....  1892.853197: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853202: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853204: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853206: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853207: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853209: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853211: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
    sev_dbg_test-24385   [049] d....  1892.853212: page_fault_user: address=0x7fc63f8fe000 ip=0x41cb44 error_code=0x80000007
Quoted 8 lines

Looking at the full picture, isn't this a bug in the PSP driver that affects all commands? With pre-SNP VMs, "guest owned" isn't really a thing, since nothing guarantees that the destination page is accessible *only* by the guest.

If we rip that out and always bounce-buffer, then KVM won't have to force its own bounce buffering of sub-page lengths for SNP, and that would eliminate the race I described above.

Quoted 7 lines
Quoted 1 line

Completely untested (I'll give this a whirl tomorrow).

So this doesn't work. Dredging up some knowledge of how this all works, I'm fairly certain the issue is that the encryption is salted with the physical address, and so encrypting at X instead of Y will yield different ciphertext. I.e. any command that encrypts the contents in the destination (or in theory, decrypts, though AFAICT none of the commands do so) *can't* be bounce buffered.

Yes, sorry for not noticing that detail sooner. It really throws a wrench in things =\

Quoted 5 lines

I don't see a way to salvage SEV/SEV-ES on SNP systems, short of requiring CAP_SYS_BOOT or some other elevated permission to do *anything*. Or redo SEV/SEV-ES support to require guest_memfd for operations that require putting pages into Firmware state.

Yah, short of maybe the above approach, I don't see any way around it atm :( If you think it's worth pursuing though I can give that a shot on my end.

Thanks,

Mike

author maintainer
#13 lore raw

Re: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls

Quoted 11 lines · Michael Roth
Quoted 9 lines · Sean Christopherson
Quoted 5 lines · Sean Christopherson
Quoted 2 lines · Michael Roth

On Tue, Jun 23, 2026 at 07:55:27PM +0000, Sean Christopherson wrote:
since your proposed fix seems nicer and more complete anyway.

It's missing a case though :-(

Ah, but so is the revert, and it's not a coincidence that the old code didn't bounce buffer the destination for KVM_SEV_DBG_ENCRYPT.

Quoted 8 lines

Yah I was realizing this too :( Though I'm not sure why we set 'guest_owned' for DBG_DECRYPT... I'm not even sure it makes sense to write decrypted data into guest memory since it would only be usable if the range was decrypted to begin with... in most cases this likely wouldn't be guest memory.

So maybe the bounce buffer approach would work for DBG_DECRYPT at least,

Yeah, DECRYPT worked fine, it's all the others that break.

Quoted 10 lines
Quoted 3 lines

pread() doesn't work, because the source or destination needs to be a userspace address, and so any RMP #PFs will get eaten. But copy_file_range() makes the world go kablooie.

I was hoping we could at least rely on kernel accesses going through GUP and maybe doing something like unmapping and freezing folio refcount to block concurrent access to the page, or read-only protections that would force the GUP to fail and just fail the concurrent userspace-requested operation...

Quoted 8 lines

copy_file_range() bypasses the GUP, but freezing the refcount or locking the folio would cause it to spin until the folio was released. So maybe that's an option?

This is similar to what's been proposed for gmem hugetlb support when splitting/rebuilding hugepages as part of the conversion path, where we need to guard against concurrent access by both kernel/userspace, so maybe it's not too hacky?

The big difference is that guest_memfd owns the folio. For SEV/SEV-ES, KVM has no clue what sits behind the destination of the ENCRYPT operation (or the other guest_owned commands). KVM knows it's a refcounted struct page due to pinning the memory, but that's about it. We'd basically have to audit every filesystem to see if a "fix" would actually work.

And the risk isn't limited to copy_file_range(), that just happens to be the first syscall I found that did what I want. E.g. any memory that is kernel allocated and mapped into userspace would be problematic, because that memory could be fed back into KVM. Ha! Now that I think about it, I bet the pages mapped via kvm_vcpu_fault() are problematic, because KVM owns the pages and will access them at will.

So I don't think we can fix this by playing games in KVM. Which is pretty much why we ended up with guest_memfd: the only way to lock down memory to the point where we're 100% certain it's safe to "poison" the pages is if KVM sequesters them away in a dedicated file system.

Quoted 8 lines
Quoted 4 lines

I don't see a way to salvage SEV/SEV-ES on SNP systems, short of requiring CAP_SYS_BOOT or some other elevated permission to do *anything*. Or redo SEV/SEV-ES support to require guest_memfd for operations that require putting pages into Firmware state.

Yah, short of maybe the above approach, I don't see any way around it atm :( If you think it's worth pursuing though I can give that a shot on my end.

I say we wait for Paolo to get back from holiday (in a few weeks) before doing anything drastic. I'll send a patch to fix the existing selftest though, no reason to leave that hole open.

maintainer
#14 lore raw

Re: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls

Quoted 15 lines · Sean Christopherson
Quoted 2 lines · Michael Roth

So maybe the bounce buffer approach would work for DBG_DECRYPT at least,

Yeah, DECRYPT worked fine, it's all the others that break.

Quoted 3 lines
Quoted 3 lines

pread() doesn't work, because the source or destination needs to be a userspace address, and so any RMP #PFs will get eaten. But copy_file_range() makes the world go kablooie.

And the risk isn't limited to copy_file_range(), that just happens to be the first syscall I found that did what I want. E.g. any memory that is kernel allocated and mapped into userspace would be problematic, because that memory could be fed back into KVM.

Quoted 4 lines

So I don't think we can fix this by playing games in KVM. Which is pretty much why we ended up with guest_memfd: the only way to lock down memory to the point where we're 100% certain it's safe to "poison" the pages is if KVM sequesters them away in a dedicated file system.

Yes, I agree that it's basically the same issue that led to using guest_memfd for SNP. Another one would be io_submit().

Quoted 4 lines
Quoted 4 lines
Quoted 4 lines

I'm fairly certain the issue is that the encryption is salted with the physical address, and so encrypting at X instead of Y will yield different ciphertext. I.e. any command that encrypts the contents in the destination (or in theory, decrypts, though AFAICT none of the commands do so) *can't* be bounce buffered.

Yep. :/

Quoted 8 lines
Quoted 8 lines
Quoted 4 lines

I don't see a way to salvage SEV/SEV-ES on SNP systems, short of requiring CAP_SYS_BOOT or some other elevated permission to do *anything*. Or redo SEV/SEV-ES support to require guest_memfd for operations that require putting pages into Firmware state.

Yah, short of maybe the above approach, I don't see any way around it atm :( If you think it's worth pursuing though I can give that a shot on my end.

The unavoidable ones include, if I understand correctly, not just DBG_ENCRYPT but also LAUNCH_UPDATE_DATA?

For encrypt the destination is the guest memory, so how would adding guest_memfd support for SEV/SEV-ES work? The guest doesn't have a page-state-change call and neither does it have RMP nested page faults, so how would you communicate the private<->shared switch to userspace?

Quoted 3 lines

I say we wait for Paolo to get back from holiday (in a few weeks) before doing anything drastic. I'll send a patch to fix the existing selftest though, no reason to leave that hole open.

Well, everything is drastic. "Fortunately" AMD helped us with the firmware update that already hides SEV-ES on SNP machines (CVE-2025-48514), so at least there's a precedent.

Could we say CAP_SYS_BOOT is only be required to create the VM, after which it's up to userspace to not screw up? Probably not, because userspace can then drop privileges and operate on the file descriptor; which is the actual dangerous part.

The only not-horrible alternative would be a default-disabled module parameter that allows SEV/SEV-ES on SNP systems but adds TAINT_USER when you create one. And downstreams that don't like the idea can just rip out the taint.

Paolo

#15 lore raw

Re: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls

Quoted 36 lines · Paolo Bonzini
Quoted 15 lines · Sean Christopherson
Quoted 2 lines · Michael Roth

So maybe the bounce buffer approach would work for DBG_DECRYPT at least,

Yeah, DECRYPT worked fine, it's all the others that break.

Quoted 3 lines
Quoted 3 lines

pread() doesn't work, because the source or destination needs to be a userspace address, and so any RMP #PFs will get eaten. But copy_file_range() makes the world go kablooie.

And the risk isn't limited to copy_file_range(), that just happens to be the first syscall I found that did what I want. E.g. any memory that is kernel allocated and mapped into userspace would be problematic, because that memory could be fed back into KVM.

Quoted 4 lines

So I don't think we can fix this by playing games in KVM. Which is pretty much why we ended up with guest_memfd: the only way to lock down memory to the point where we're 100% certain it's safe to "poison" the pages is if KVM sequesters them away in a dedicated file system.

Yes, I agree that it's basically the same issue that led to using guest_memfd for SNP. Another one would be io_submit().

Quoted 4 lines
Quoted 4 lines
Quoted 4 lines

I'm fairly certain the issue is that the encryption is salted with the physical address, and so encrypting at X instead of Y will yield different ciphertext. I.e. any command that encrypts the contents in the destination (or in theory, decrypts, though AFAICT none of the commands do so) *can't* be bounce buffered.

Yep. :/

Quoted 5 lines
Quoted 5 lines
Quoted 4 lines

I don't see a way to salvage SEV/SEV-ES on SNP systems, short of requiring CAP_SYS_BOOT or some other elevated permission to do *anything*. Or redo SEV/SEV-ES support to require guest_memfd for operations that require putting pages into Firmware state.

Quoted 6 lines
Quoted 3 lines
Quoted 3 lines

Yah, short of maybe the above approach, I don't see any way around it atm :( If you think it's worth pursuing though I can give that a shot on my end.

The unavoidable ones include, if I understand correctly, not just DBG_ENCRYPT but also LAUNCH_UPDATE_DATA?

That was my understanding, e.g. a user could try to use the HVA to trigger the copy_file_range() path right after snp_map_cmd_buf_desc()->rmp_mark_pages_firmware() is performed as part of servicing the KVM_SEV_LAUNCH_UPDATE_DATA request, and it seems like that would trigger the same issue. And bounce buffers wouldn't work for that one either.

and it's kinda important =/

Quoted 5 lines

For encrypt the destination is the guest memory, so how would adding guest_memfd support for SEV/SEV-ES work? The guest doesn't have a page-state-change call and neither does it have RMP nested page faults, so how would you communicate the private<->shared switch to userspace?

During early SNP hypervisor development (when guest_memfd was called "restricted_mem"), the patches were actually based on top of some patches from Nikunj that added guest_memfd/restricted_mem support for SEV-ES. One nice thing about that is it brought about support for lazy page allocation instead of relying on KVM_MEMORY_ENCRYPT_REG_REGION.

The support piggybacks off the KVM_HC_MAP_GPA RANGE hypercall that was added on the guest side to enable SEV live migration so the VMM could distinguish between shared/private (and punt private page handling to something else). By coincedence, that's also what SNP/TDX ended up using to forward conversion requests to userspace.

However, since SEV live migration never became a thing upstream there's potential that the KVM_HC_MAP_GPA_RANGE handling might miss some of the newer cases, which would lead to silent corruption (though with SNP enabled we might still get an indicator of whether or not the guest had the C-bit set, so we could detect a mismatch that way and maybe even be able to handle implicit conversion requests).

The big issue with this approach though is hugepages: we'd be dropping existing SEV support for both hugetlb/THP unless we implemented in-place conversion support for SEV-ES so it could eventually benefit from the hugetlb patches at least, but if in the meantime it's 4K-only I'm not sure anyone is going to get much use out of that, or still care about this support when gmem+hugepages is eventually enabled.

Quoted 14 lines
Quoted 4 lines

I say we wait for Paolo to get back from holiday (in a few weeks) before doing anything drastic. I'll send a patch to fix the existing selftest though, no reason to leave that hole open.

Well, everything is drastic. "Fortunately" AMD helped us with the firmware update that already hides SEV-ES on SNP machines (CVE-2025-48514), so at least there's a precedent.

Could we say CAP_SYS_BOOT is only be required to create the VM, after which it's up to userspace to not screw up? Probably not, because userspace can then drop privileges and operate on the file descriptor; which is the actual dangerous part.

Quoted 4 lines

The only not-horrible alternative would be a default-disabled module parameter that allows SEV/SEV-ES on SNP systems but adds TAINT_USER when you create one. And downstreams that don't like the idea can just rip out the taint.

It might not be as elegant, but either of these seems more useful as a means to allow users to continue to be able to run existing workloads if they trust their userspace/VMMs. I'm worried the guest_memfd support would end up being mostly-wasted effort at this point since it's not a straightforward/drop-in replacement.

-Mike

Quoted 3 lines

Paolo

author maintainer
#16 lore raw

Re: [PATCH v2 2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls

Quoted 13 lines · Michael Roth
Quoted 11 lines · Paolo Bonzini
Quoted 8 lines
Quoted 8 lines
Quoted 4 lines

I don't see a way to salvage SEV/SEV-ES on SNP systems, short of requiring CAP_SYS_BOOT or some other elevated permission to do *anything*. Or redo SEV/SEV-ES support to require guest_memfd for operations that require putting pages into Firmware state.

Yah, short of maybe the above approach, I don't see any way around it atm :( If you think it's worth pursuing though I can give that a shot on my end.

The unavoidable ones include, if I understand correctly, not just DBG_ENCRYPT but also LAUNCH_UPDATE_DATA?

Quoted 13 lines

That was my understanding, e.g. a user could try to use the HVA to trigger the copy_file_range() path right after snp_map_cmd_buf_desc()->rmp_mark_pages_firmware() is performed as part of servicing the KVM_SEV_LAUNCH_UPDATE_DATA request, and it seems like that would trigger the same issue. And bounce buffers wouldn't work for that one either.

and it's kinda important =/

Quoted 4 lines

For encrypt the destination is the guest memory, so how would adding guest_memfd support for SEV/SEV-ES work? The guest doesn't have a page-state-change call and neither does it have RMP nested page faults, so how would you communicate the private<->shared switch to userspace?

Yeah, I was spitballing, I don't think guest_memfd has a viable path forward.

Quoted 19 lines

During early SNP hypervisor development (when guest_memfd was called "restricted_mem"), the patches were actually based on top of some patches from Nikunj that added guest_memfd/restricted_mem support for SEV-ES. One nice thing about that is it brought about support for lazy page allocation instead of relying on KVM_MEMORY_ENCRYPT_REG_REGION.

The support piggybacks off the KVM_HC_MAP_GPA RANGE hypercall that was added on the guest side to enable SEV live migration so the VMM could distinguish between shared/private (and punt private page handling to something else). By coincedence, that's also what SNP/TDX ended up using to forward conversion requests to userspace.

However, since SEV live migration never became a thing upstream there's potential that the KVM_HC_MAP_GPA_RANGE handling might miss some of the newer cases, which would lead to silent corruption (though with SNP enabled we might still get an indicator of whether or not the guest had the C-bit set, so we could detect a mismatch that way and maybe even be able to handle implicit conversion requests).

Quoted 20 lines

The big issue with this approach though is hugepages: we'd be dropping existing SEV support for both hugetlb/THP unless we implemented in-place conversion support for SEV-ES so it could eventually benefit from the hugetlb patches at least, but if in the meantime it's 4K-only I'm not sure anyone is going to get much use out of that, or still care about this support when gmem+hugepages is eventually enabled.

Quoted 13 lines
Quoted 4 lines

I say we wait for Paolo to get back from holiday (in a few weeks) before doing anything drastic. I'll send a patch to fix the existing selftest though, no reason to leave that hole open.

Well, everything is drastic. "Fortunately" AMD helped us with the firmware update that already hides SEV-ES on SNP machines (CVE-2025-48514), so at least there's a precedent.

Could we say CAP_SYS_BOOT is only be required to create the VM, after which it's up to userspace to not screw up? Probably not, because userspace can then drop privileges and operate on the file descriptor; which is the actual dangerous part.

Hmm, but there may be a path forward with CAP_SYS_BOOT. It might require some heavy lifting in userspace, but IIRC, LAUNCH_FINISH is typically called before doing KVM_RUN on any vCPU. So while it's not simply KVM_CREATE_VM, I do think that "users" that care deeply enough about security/stability could implement a VM builder that runs with elevated privileges, and then drops priveleges (or hands off the VM) to actually run the guest.

Quoted 4 lines
Quoted 4 lines

The only not-horrible alternative would be a default-disabled module parameter that allows SEV/SEV-ES on SNP systems but adds TAINT_USER when you create one. And downstreams that don't like the idea can just rip out the taint.

What if we have the module param require CAP_SYS_BOOT for the affected iotcls? Then TAINT_USER if it's disabled. That way, if someone is sufficiently motivated to harden their runtime-VMM, they can implement the builder without having to manually remove the taint, and without having to completely trust the VMM (because it's more than just a potential DoS; I wouldn't be at all surprised if someone can turn this in a local privilege escalation).

Quoted 5 lines

It might not be as elegant, but either of these seems more useful as a means to allow users to continue to be able to run existing workloads if they trust their userspace/VMMs. I'm worried the guest_memfd support would end up being mostly-wasted effort at this point since it's not a straightforward/drop-in replacement.

author maintainer
#17 lore raw

PATCH v2 3/6 KVM: SEV: Explicitly validate the dst buffer for debug operations

In reply to Sean Christopherson

When encrypting/decrypting guest memory, explicitly check that the destination is non-NULL and doesn't wrap instead of subtly relying on sev_pin_memory() to perform the check. This will allow adding and using a more focused single-page pinning helper.

---
 arch/x86/kvm/svm/sev.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)
arch/x86/kvm/svm/sev.c +4 -2
index b9d7bd868e0b..9d2044fd910d 100644
@@ -1357,9 +1357,11 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp, bool dec)
 	if (copy_from_user(&debug, u64_to_user_ptr(argp->data), sizeof(debug)))
 		return -EFAULT;
 
-	if (!debug.len || debug.src_uaddr + debug.len < debug.src_uaddr)
+	if (!debug.len || !debug.src_uaddr || !debug.dst_uaddr)
 		return -EINVAL;
-	if (!debug.dst_uaddr)
+
+	if (debug.src_uaddr + debug.len < debug.src_uaddr ||
+	    debug.dst_uaddr + debug.len < debug.dst_uaddr)
 		return -EINVAL;
 
 	vaddr = debug.src_uaddr;
Signature · 1 line
2.54.0.545.g6539524ca2-goog
author maintainer
#18 lore raw

PATCH v2 4/6 KVM: SEV: Add helper function to pin/unpin a single page

In reply to Sean Christopherson

Add helpers to pin/unpin a single page, and use it in all flows that pin exactly one page. None of the single-page users actually check that the correct number of pages was pinned, which is functionally ok, but visually jarring, especially in the decrypt/encrypt flow, which separately pins two pages, but uses a single variable to track how pages were pinned each time. Again, it's functionally ok since core mm guarantees exactly one page will be pinned on success, but it's ugly.

Opportunistically use page_to_phys() instead of open coding an equivalent via page_to_pfn().

Note, all users of the single-page helper pre-validate the address and length, i.e. don't rely on the sanity check in sev_pin_memory().

No functional change intended.

---
 arch/x86/kvm/svm/sev.c | 145 +++++++++++++++++++++++++----------------
 1 file changed, 90 insertions(+), 55 deletions(-)
arch/x86/kvm/svm/sev.c +90 -55
index 9d2044fd910d..4146c91788fb 100644
@@ -720,14 +720,50 @@ static int sev_launch_start(struct kvm *kvm, struct kvm_sev_cmd *argp)
 	return ret;
 }
 
+static int sev_check_pin_count(struct kvm *kvm, unsigned long npages)
+{
+	unsigned long total_npages, lock_limit;
+
+	total_npages = to_kvm_sev_info(kvm)->pages_locked + npages;
+	if (total_npages > totalram_pages())
+		return -EINVAL;
+
+	lock_limit = rlimit(RLIMIT_MEMLOCK) >> PAGE_SHIFT;
+	if (total_npages > lock_limit && !capable(CAP_IPC_LOCK)) {
+		pr_err_ratelimited("SEV: %lu total pages would exceed the lock limit of %lu.\n",
+				   total_npages, lock_limit);
+		return -ENOMEM;
+	}
+
+	return 0;
+}
+
+static int sev_pin_user_pages(struct kvm *kvm, unsigned long addr, int npages,
+			      unsigned int gup_flags, struct page **pages)
+{
+	int npinned;
+
+	lockdep_assert_held(&kvm->lock);
+
+	npinned = pin_user_pages_fast(addr, npages, gup_flags, pages);
+	if (npinned != npages) {
+		if (npinned > 0)
+			unpin_user_pages(pages, npinned);
+		pr_err_ratelimited("SEV: Failure locking %u pages.\n", npages);
+		return -ENOMEM;
+	}
+
+	to_kvm_sev_info(kvm)->pages_locked += npages;
+	return 0;
+}
+
 static struct page **sev_pin_memory(struct kvm *kvm, unsigned long uaddr,
 				    unsigned long ulen, unsigned long *n,
 				    unsigned int flags)
 {
-	struct kvm_sev_info *sev = to_kvm_sev_info(kvm);
-	unsigned long npages, total_npages, lock_limit;
+	unsigned long npages;
 	struct page **pages;
-	int npinned, ret;
+	int ret;
 
 	lockdep_assert_held(&kvm->lock);
 
@@ -743,16 +779,9 @@ static struct page **sev_pin_memory(struct kvm *kvm, unsigned long uaddr,
 	if (npages > INT_MAX)
 		return ERR_PTR(-EINVAL);
 
-	total_npages = sev->pages_locked + npages;
-	if (total_npages > totalram_pages())
-		return ERR_PTR(-EINVAL);
-
-	lock_limit = rlimit(RLIMIT_MEMLOCK) >> PAGE_SHIFT;
-	if (total_npages > lock_limit && !capable(CAP_IPC_LOCK)) {
-		pr_err("SEV: %lu total pages would exceed the lock limit of %lu.\n",
-		       total_npages, lock_limit);
-		return ERR_PTR(-ENOMEM);
-	}
+	ret = sev_check_pin_count(kvm, npages);
+	if (ret)
+		return ERR_PTR(ret);
 
 	/*
 	 * Don't WARN if the kernel (rightly) thinks the total size is absurd,
@@ -764,25 +793,14 @@ static struct page **sev_pin_memory(struct kvm *kvm, unsigned long uaddr,
 	if (!pages)
 		return ERR_PTR(-ENOMEM);
 
-	/* Pin the user virtual address. */
-	npinned = pin_user_pages_fast(uaddr, npages, flags, pages);
-	if (npinned != npages) {
-		pr_err("SEV: Failure locking %lu pages.\n", npages);
-		ret = -ENOMEM;
-		goto err;
+	ret = sev_pin_user_pages(kvm, uaddr, npages, flags, pages);
+	if (ret) {
+		kvfree(pages);
+		return ERR_PTR(ret);
 	}
 
 	*n = npages;
-	sev->pages_locked = total_npages;
-
 	return pages;
-
-err:
-	if (npinned > 0)
-		unpin_user_pages(pages, npinned);
-
-	kvfree(pages);
-	return ERR_PTR(ret);
 }
 
 static void sev_unpin_memory(struct kvm *kvm, struct page **pages,
@@ -793,6 +811,29 @@ static void sev_unpin_memory(struct kvm *kvm, struct page **pages,
 	to_kvm_sev_info(kvm)->pages_locked -= npages;
 }
 
+static struct page *sev_pin_page(struct kvm *kvm, unsigned long addr,
+				 unsigned int flags)
+{
+	struct page *page;
+	int r;
+
+	r = sev_check_pin_count(kvm, 1);
+	if (r)
+		return ERR_PTR(r);
+
+	r = sev_pin_user_pages(kvm, addr, 1, flags, &page);
+	if (r)
+		return ERR_PTR(r);
+
+	return page;
+}
+
+static void sev_unpin_page(struct kvm *kvm, struct page *page)
+{
+	unpin_user_pages(&page, 1);
+	to_kvm_sev_info(kvm)->pages_locked -= 1;
+}
+
 static void sev_clflush_pages(struct page *pages[], unsigned long npages)
 {
 	uint8_t *page_virtual;
@@ -1345,9 +1386,8 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp, bool dec)
 {
 	unsigned long vaddr, vaddr_end, next_vaddr;
 	unsigned long dst_vaddr;
-	struct page **src_p, **dst_p;
+	struct page *src_p, *dst_p;
 	struct kvm_sev_dbg debug;
-	unsigned long n;
 	unsigned int size;
 	int ret;
 
@@ -1373,13 +1413,13 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp, bool dec)
 		int len, s_off, d_off;
 
 		/* lock userspace source and destination page */
-		src_p = sev_pin_memory(kvm, vaddr & PAGE_MASK, PAGE_SIZE, &n, 0);
+		src_p = sev_pin_page(kvm, vaddr & PAGE_MASK, 0);
 		if (IS_ERR(src_p))
 			return PTR_ERR(src_p);
 
-		dst_p = sev_pin_memory(kvm, dst_vaddr & PAGE_MASK, PAGE_SIZE, &n, FOLL_WRITE);
+		dst_p = sev_pin_page(kvm, dst_vaddr & PAGE_MASK, FOLL_WRITE);
 		if (IS_ERR(dst_p)) {
-			sev_unpin_memory(kvm, src_p, n);
+			sev_unpin_page(kvm, src_p);
 			return PTR_ERR(dst_p);
 		}
 
@@ -1388,8 +1428,8 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp, bool dec)
 		 * the pages; flush the destination too so that future accesses do not
 		 * see stale data.
 		 */
-		sev_clflush_pages(src_p, 1);
-		sev_clflush_pages(dst_p, 1);
+		sev_clflush_pages(&src_p, 1);
+		sev_clflush_pages(&dst_p, 1);
 
 		/*
 		 * Since user buffer may not be page aligned, calculate the
@@ -1402,20 +1442,20 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp, bool dec)
 
 		if (dec)
 			ret = __sev_dbg_decrypt_user(kvm,
-						     __sme_page_pa(src_p[0]) + s_off,
+						     __sme_page_pa(src_p) + s_off,
 						     (void __user *)dst_vaddr,
-						     __sme_page_pa(dst_p[0]) + d_off,
+						     __sme_page_pa(dst_p) + d_off,
 						     len, &argp->error);
 		else
 			ret = __sev_dbg_encrypt_user(kvm,
-						     __sme_page_pa(src_p[0]) + s_off,
+						     __sme_page_pa(src_p) + s_off,
 						     (void __user *)vaddr,
-						     __sme_page_pa(dst_p[0]) + d_off,
+						     __sme_page_pa(dst_p) + d_off,
 						     (void __user *)dst_vaddr,
 						     len, &argp->error);
 
-		sev_unpin_memory(kvm, src_p, n);
-		sev_unpin_memory(kvm, dst_p, n);
+		sev_unpin_page(kvm, src_p);
+		sev_unpin_page(kvm, dst_p);
 
 		if (ret)
 			goto err;
@@ -1698,8 +1738,7 @@ static int sev_send_update_data(struct kvm *kvm, struct kvm_sev_cmd *argp)
 	struct sev_data_send_update_data data;
 	struct kvm_sev_send_update_data params;
 	void *hdr, *trans_data;
-	struct page **guest_page;
-	unsigned long n;
+	struct page *guest_page;
 	int ret, offset;
 
 	if (!sev_guest(kvm))
@@ -1723,8 +1762,7 @@ static int sev_send_update_data(struct kvm *kvm, struct kvm_sev_cmd *argp)
 		return -EINVAL;
 
 	/* Pin guest memory */
-	guest_page = sev_pin_memory(kvm, params.guest_uaddr & PAGE_MASK,
-				    PAGE_SIZE, &n, 0);
+	guest_page = sev_pin_page(kvm, params.guest_uaddr & PAGE_MASK, 0);
 	if (IS_ERR(guest_page))
 		return PTR_ERR(guest_page);
 
@@ -1745,7 +1783,7 @@ static int sev_send_update_data(struct kvm *kvm, struct kvm_sev_cmd *argp)
 	data.trans_len = params.trans_len;
 
 	/* The SEND_UPDATE_DATA command requires C-bit to be always set. */
-	data.guest_address = (page_to_pfn(guest_page[0]) << PAGE_SHIFT) + offset;
+	data.guest_address = page_to_phys(guest_page) + offset;
 	data.guest_address |= sev_me_mask;
 	data.guest_len = params.guest_len;
 	data.handle = to_kvm_sev_info(kvm)->handle;
@@ -1772,8 +1810,7 @@ static int sev_send_update_data(struct kvm *kvm, struct kvm_sev_cmd *argp)
 e_free_hdr:
 	kfree(hdr);
 e_unpin:
-	sev_unpin_memory(kvm, guest_page, n);
-
+	sev_unpin_page(kvm, guest_page);
 	return ret;
 }
 
@@ -1878,8 +1915,7 @@ static int sev_receive_update_data(struct kvm *kvm, struct kvm_sev_cmd *argp)
 	struct kvm_sev_receive_update_data params;
 	struct sev_data_receive_update_data data;
 	void *hdr = NULL, *trans = NULL;
-	struct page **guest_page;
-	unsigned long n;
+	struct page *guest_page;
 	int ret, offset;
 
 	if (!sev_guest(kvm))
@@ -1916,8 +1952,7 @@ static int sev_receive_update_data(struct kvm *kvm, struct kvm_sev_cmd *argp)
 	data.trans_len = params.trans_len;
 
 	/* Pin guest memory */
-	guest_page = sev_pin_memory(kvm, params.guest_uaddr & PAGE_MASK,
-				    PAGE_SIZE, &n, FOLL_WRITE);
+	guest_page = sev_pin_page(kvm, params.guest_uaddr & PAGE_MASK, FOLL_WRITE);
 	if (IS_ERR(guest_page)) {
 		ret = PTR_ERR(guest_page);
 		goto e_free_trans;
@@ -1928,10 +1963,10 @@ static int sev_receive_update_data(struct kvm *kvm, struct kvm_sev_cmd *argp)
 	 * encrypts the written data with the guest's key, and the cache may
 	 * contain dirty, unencrypted data.
 	 */
-	sev_clflush_pages(guest_page, n);
+	sev_clflush_pages(&guest_page, 1);
 
 	/* The RECEIVE_UPDATE_DATA command requires C-bit to be always set. */
-	data.guest_address = (page_to_pfn(guest_page[0]) << PAGE_SHIFT) + offset;
+	data.guest_address = page_to_phys(guest_page) + offset;
 	data.guest_address |= sev_me_mask;
 	data.guest_len = params.guest_len;
 	data.handle = to_kvm_sev_info(kvm)->handle;
@@ -1939,7 +1974,7 @@ static int sev_receive_update_data(struct kvm *kvm, struct kvm_sev_cmd *argp)
 	ret = sev_issue_cmd(kvm, SEV_CMD_RECEIVE_UPDATE_DATA, &data,
 				&argp->error);
 
-	sev_unpin_memory(kvm, guest_page, n);
+	sev_unpin_page(kvm, guest_page);
 
 e_free_trans:
 	kfree(trans);
Signature · 1 line
2.54.0.545.g6539524ca2-goog
author maintainer
#19 lore raw

PATCH v2 5/6 KVM: SEV: Rewrite logic to {de,en}crypt memory for debug

In reply to Sean Christopherson

Wholesale rewrite the guts of the debug {de,en}crypt flows, as the existing code is broken, e.g. doesn't handle cases where the access isn't naturally sized and aligned, and is so wildly flawed that attempting to salvage the current code in an iterative fashion would be more risky than a rewrite.

E.g. when encrypting 9 bytes at offset 8, KVM needs to _decrypt_ destination[31:0] into a temporary buffer, buffer[31:0], then copy 9 bytes from source[8:0] to buffer[16:8], then encrypt buffer[31:0] back into destination[31:0]. The current code only ever copies 16 bytes, and bizarrely uses a temporary buffer for the source as well.

To keep the code easier to read and maintain, send the unaligned cases down dedicated "slow" paths instead of trying to mix and match the possible combinations in one helper.

For now, preserve the basic approach of the current code, e.g. allocate an entire page for the temporary buffer, to minimize unwanted changes in functionality.

---
 arch/x86/kvm/svm/sev.c | 306 +++++++++++++++++++----------------------
 1 file changed, 139 insertions(+), 167 deletions(-)
arch/x86/kvm/svm/sev.c +139 -167
index 4146c91788fb..89586f821c9c 100644
@@ -1237,159 +1237,140 @@ static int sev_guest_status(struct kvm *kvm, struct kvm_sev_cmd *argp)
 	return ret;
 }
 
-static int __sev_issue_dbg_cmd(struct kvm *kvm, unsigned long src,
-			       unsigned long dst, int size,
-			       int *error, bool enc)
+static int sev_issue_dbg_cmd(struct kvm *kvm, unsigned long src_pa,
+			     unsigned long dst_pa, unsigned int size,
+			     unsigned int ioctl, int *error)
 {
-	struct sev_data_dbg data;
+	int cmd = ioctl == KVM_SEV_DBG_DECRYPT ? SEV_CMD_DBG_DECRYPT :
+						 SEV_CMD_DBG_ENCRYPT;
+	struct sev_data_dbg data = {
+		.handle = to_kvm_sev_info(kvm)->handle,
+		.dst_addr = dst_pa,
+		.src_addr = src_pa,
+		.len = size,
+	};
+
+	return sev_issue_cmd(kvm, cmd, &data, error);
+}
+
+static struct page *sev_alloc_dbg_buffer(void **buf)
+{
+	struct page *buf_p;
 
-	data.reserved = 0;
-	data.handle = to_kvm_sev_info(kvm)->handle;
-	data.dst_addr = dst;
-	data.src_addr = src;
-	data.len = size;
+	buf_p = alloc_page(GFP_KERNEL);
+	if (!buf_p)
+		return NULL;
+
+	*buf = kmap_local_page(buf_p);
+	return buf_p;
+}
 
-	return sev_issue_cmd(kvm,
-			     enc ? SEV_CMD_DBG_ENCRYPT : SEV_CMD_DBG_DECRYPT,
-			     &data, error);
+static void sev_free_dbg_buffer(struct page *buf_p, void *buf)
+{
+	kunmap_local(buf);
+	__free_page(buf_p);
 }
 
-static int __sev_dbg_decrypt(struct kvm *kvm, unsigned long src_paddr,
-			     unsigned long dst_paddr, int sz, int *err)
+static unsigned int sev_dbg_crypt_slow_addr_and_size(struct page *page,
+						     unsigned long __va,
+						     unsigned int len,
+						     unsigned long *pa)
 {
-	int offset;
+	/* The number of bytes to {de,en}crypt must be 16-byte aligned. */
+	unsigned int nr_bytes = round_up(len, 16);
+	unsigned long va = ALIGN_DOWN(__va, 16);
+
+	/*
+	 * Increase the number of bytes to {de,en}crypt by one chunk (16 bytes)
+	 * if the aligned address and length doesn't cover the unaligned range,
+	 * e.g. if the address is unaligned _and_ the access will split a chunk
+	 * at the tail.
+	 */
+	if (va + nr_bytes < __va + len)
+		nr_bytes += 16;
+
+	*pa = __sme_page_pa(page) + (va & ~PAGE_MASK);
 
 	/*
-	 * Its safe to read more than we are asked, caller should ensure that
-	 * destination has enough space.
+	 * Sanity check that the new access won't split a page.  This should
+	 * never happen; just squash the access and let the firmware command
+	 * fail.
 	 */
-	offset = src_paddr & 15;
-	src_paddr = round_down(src_paddr, 16);
-	sz = round_up(sz + offset, 16);
+	if (WARN_ON_ONCE((*pa & PAGE_MASK) != ((*pa + nr_bytes - 1) & PAGE_MASK)))
+		return 0;
 
-	return __sev_issue_dbg_cmd(kvm, src_paddr, dst_paddr, sz, err, false);
+	return nr_bytes;
 }
 
-static int __sev_dbg_decrypt_user(struct kvm *kvm, unsigned long paddr,
-				  void __user *dst_uaddr,
-				  unsigned long dst_paddr,
-				  int size, int *err)
+static int sev_dbg_decrypt_slow(struct kvm *kvm, unsigned long src,
+				struct page *src_p, unsigned long dst,
+				unsigned int len, int *err)
 {
-	struct page *tpage = NULL;
-	int ret, offset;
-
-	/* if inputs are not 16-byte then use intermediate buffer */
-	if (!IS_ALIGNED(dst_paddr, 16) ||
-	    !IS_ALIGNED(paddr,     16) ||
-	    !IS_ALIGNED(size,      16)) {
-		tpage = (void *)alloc_page(GFP_KERNEL_ACCOUNT | __GFP_ZERO);
-		if (!tpage)
-			return -ENOMEM;
-
-		dst_paddr = __sme_page_pa(tpage);
-	}
-
-	ret = __sev_dbg_decrypt(kvm, paddr, dst_paddr, size, err);
-	if (ret)
-		goto e_free;
-
-	if (tpage) {
-		offset = paddr & 15;
-		if (copy_to_user(dst_uaddr, page_address(tpage) + offset, size))
-			ret = -EFAULT;
-	}
-
-e_free:
-	if (tpage)
-		__free_page(tpage);
-
-	return ret;
+	unsigned int nr_bytes;
+	unsigned long src_pa;
+	struct page *buf_p;
+	void *buf;
+	int r;
+
+	buf_p = sev_alloc_dbg_buffer(&buf);
+	if (!buf_p)
+		return -ENOMEM;
+
+	nr_bytes = sev_dbg_crypt_slow_addr_and_size(src_p, src, len, &src_pa);
+
+	r = sev_issue_dbg_cmd(kvm, src_pa, __sme_page_pa(buf_p),
+			      nr_bytes, KVM_SEV_DBG_DECRYPT, err);
+	if (r)
+		goto out;
+
+	if (copy_to_user((void __user *)dst, buf + (src & 15), len))
+		r = -EFAULT;
+out:
+	sev_free_dbg_buffer(buf_p, buf);
+	return r;
 }
 
-static int __sev_dbg_encrypt_user(struct kvm *kvm, unsigned long paddr,
-				  void __user *vaddr,
-				  unsigned long dst_paddr,
-				  void __user *dst_vaddr,
-				  int size, int *error)
+static int sev_dbg_encrypt_slow(struct kvm *kvm, unsigned long src,
+				unsigned long dst, struct page *dst_p,
+				unsigned int len, int *err)
 {
-	struct page *src_tpage = NULL;
-	struct page *dst_tpage = NULL;
-	int ret, len = size;
+	unsigned int nr_bytes;
+	unsigned long dst_pa;
+	struct page *buf_p;
+	void *buf;
+	int r;
 
-	/* If source buffer is not aligned then use an intermediate buffer */
-	if (!IS_ALIGNED((unsigned long)vaddr, 16)) {
-		src_tpage = alloc_page(GFP_KERNEL_ACCOUNT);
-		if (!src_tpage)
-			return -ENOMEM;
+	buf_p = sev_alloc_dbg_buffer(&buf);
+	if (!buf_p)
+		return -ENOMEM;
 
-		if (copy_from_user(page_address(src_tpage), vaddr, size)) {
-			__free_page(src_tpage);
-			return -EFAULT;
-		}
+	/* Decrypt the _destination_ to do a RMW on plaintext. */
+	nr_bytes = sev_dbg_crypt_slow_addr_and_size(dst_p, dst, len, &dst_pa);
 
-		paddr = __sme_page_pa(src_tpage);
-	}
+	r = sev_issue_dbg_cmd(kvm, dst_pa, __sme_page_pa(buf_p),
+			      nr_bytes, KVM_SEV_DBG_DECRYPT, err);
+	if (r)
+		goto out;
 
 	/*
-	 *  If destination buffer or length is not aligned then do read-modify-write:
-	 *   - decrypt destination in an intermediate buffer
-	 *   - copy the source buffer in an intermediate buffer
-	 *   - use the intermediate buffer as source buffer
+	 * Copy from the source into the intermediate buffer, and then
+	 * re-encrypt the buffer into the destination.
 	 */
-	if (!IS_ALIGNED((unsigned long)dst_vaddr, 16) || !IS_ALIGNED(size, 16)) {
-		int dst_offset;
-
-		dst_tpage = alloc_page(GFP_KERNEL_ACCOUNT);
-		if (!dst_tpage) {
-			ret = -ENOMEM;
-			goto e_free;
-		}
-
-		ret = __sev_dbg_decrypt(kvm, dst_paddr,
-					__sme_page_pa(dst_tpage), size, error);
-		if (ret)
-			goto e_free;
-
-		/*
-		 *  If source is kernel buffer then use memcpy() otherwise
-		 *  copy_from_user().
-		 */
-		dst_offset = dst_paddr & 15;
-
-		if (src_tpage)
-			memcpy(page_address(dst_tpage) + dst_offset,
-			       page_address(src_tpage), size);
-		else {
-			if (copy_from_user(page_address(dst_tpage) + dst_offset,
-					   vaddr, size)) {
-				ret = -EFAULT;
-				goto e_free;
-			}
-		}
-
-		paddr = __sme_page_pa(dst_tpage);
-		dst_paddr = round_down(dst_paddr, 16);
-		len = round_up(size, 16);
-	}
-
-	ret = __sev_issue_dbg_cmd(kvm, paddr, dst_paddr, len, error, true);
-
-e_free:
-	if (src_tpage)
-		__free_page(src_tpage);
-	if (dst_tpage)
-		__free_page(dst_tpage);
-	return ret;
+	if (copy_from_user(buf + (dst & 15), (void __user *)src, len))
+		r = -EFAULT;
+	else
+		r = sev_issue_dbg_cmd(kvm, __sme_page_pa(buf_p), dst_pa,
+				      nr_bytes, KVM_SEV_DBG_ENCRYPT, err);
+out:
+	sev_free_dbg_buffer(buf_p, buf);
+	return r;
 }
 
-static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp, bool dec)
+static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp,
+			 unsigned int cmd)
 {
-	unsigned long vaddr, vaddr_end, next_vaddr;
-	unsigned long dst_vaddr;
-	struct page *src_p, *dst_p;
 	struct kvm_sev_dbg debug;
-	unsigned int size;
-	int ret;
+	unsigned int i, len;
 
 	if (!sev_guest(kvm))
 		return -ENOTTY;
@@ -1404,20 +1385,29 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp, bool dec)
 	    debug.dst_uaddr + debug.len < debug.dst_uaddr)
 		return -EINVAL;
 
-	vaddr = debug.src_uaddr;
-	size = debug.len;
-	vaddr_end = vaddr + size;
-	dst_vaddr = debug.dst_uaddr;
+	for (i = 0; i < debug.len; i += len) {
+		unsigned long src = debug.src_uaddr + i;
+		unsigned long dst = debug.dst_uaddr + i;
+		unsigned long s_off = src & ~PAGE_MASK;
+		unsigned long d_off = dst & ~PAGE_MASK;
+		struct page *src_p, *dst_p;
+		int ret;
 
-	for (; vaddr < vaddr_end; vaddr = next_vaddr) {
-		int len, s_off, d_off;
+		/*
+		 * Copy as many remaining bytes as possible while staying in a
+		 * single page for both the source and destination.
+		 */
+		len = min3(debug.len - i, PAGE_SIZE - s_off, PAGE_SIZE - d_off);
 
-		/* lock userspace source and destination page */
-		src_p = sev_pin_page(kvm, vaddr & PAGE_MASK, 0);
+		/*
+		 * Pin the source and destination pages; firmware operates on
+		 * physical addresses.
+		 */
+		src_p = sev_pin_page(kvm, src & PAGE_MASK, 0);
 		if (IS_ERR(src_p))
 			return PTR_ERR(src_p);
 
-		dst_p = sev_pin_page(kvm, dst_vaddr & PAGE_MASK, FOLL_WRITE);
+		dst_p = sev_pin_page(kvm, dst & PAGE_MASK, FOLL_WRITE);
 		if (IS_ERR(dst_p)) {
 			sev_unpin_page(kvm, src_p);
 			return PTR_ERR(dst_p);
@@ -1431,41 +1421,25 @@ static int sev_dbg_crypt(struct kvm *kvm, struct kvm_sev_cmd *argp, bool dec)
 		sev_clflush_pages(&src_p, 1);
 		sev_clflush_pages(&dst_p, 1);
 
-		/*
-		 * Since user buffer may not be page aligned, calculate the
-		 * offset within the page.
-		 */
-		s_off = vaddr & ~PAGE_MASK;
-		d_off = dst_vaddr & ~PAGE_MASK;
-		len = min_t(size_t, (PAGE_SIZE - s_off), size);
-		len = min_t(size_t, len, PAGE_SIZE - d_off);
-
-		if (dec)
-			ret = __sev_dbg_decrypt_user(kvm,
-						     __sme_page_pa(src_p) + s_off,
-						     (void __user *)dst_vaddr,
-						     __sme_page_pa(dst_p) + d_off,
-						     len, &argp->error);
+		if (IS_ALIGNED(src, 16) && IS_ALIGNED(dst, 16) && IS_ALIGNED(len, 16))
+			ret = sev_issue_dbg_cmd(kvm,
+						__sme_page_pa(src_p) + s_off,
+						__sme_page_pa(dst_p) + d_off,
+						len, cmd, &argp->error);
+		else if (cmd == KVM_SEV_DBG_DECRYPT)
+			ret = sev_dbg_decrypt_slow(kvm, src, src_p, dst,
+						   len, &argp->error);
 		else
-			ret = __sev_dbg_encrypt_user(kvm,
-						     __sme_page_pa(src_p) + s_off,
-						     (void __user *)vaddr,
-						     __sme_page_pa(dst_p) + d_off,
-						     (void __user *)dst_vaddr,
-						     len, &argp->error);
+			ret = sev_dbg_encrypt_slow(kvm, src, dst, dst_p,
+						   len, &argp->error);
 
 		sev_unpin_page(kvm, src_p);
 		sev_unpin_page(kvm, dst_p);
 
 		if (ret)
-			goto err;
-
-		next_vaddr = vaddr + len;
-		dst_vaddr = dst_vaddr + len;
-		size -= len;
+			return ret;
 	}
-err:
-	return ret;
+	return 0;
 }
 
 static int sev_launch_secret(struct kvm *kvm, struct kvm_sev_cmd *argp)
@@ -2727,10 +2701,8 @@ int sev_mem_enc_ioctl(struct kvm *kvm, void __user *argp)
 		r = sev_guest_status(kvm, &sev_cmd);
 		break;
 	case KVM_SEV_DBG_DECRYPT:
-		r = sev_dbg_crypt(kvm, &sev_cmd, true);
-		break;
 	case KVM_SEV_DBG_ENCRYPT:
-		r = sev_dbg_crypt(kvm, &sev_cmd, false);
+		r = sev_dbg_crypt(kvm, &sev_cmd, sev_cmd.id);
 		break;
 	case KVM_SEV_LAUNCH_SECRET:
 		r = sev_launch_secret(kvm, &sev_cmd);
Signature · 1 line
2.54.0.545.g6539524ca2-goog
author maintainer
#20 lore raw

PATCH v2 6/6 KVM: SEV: Allocate only as many bytes as needed for temp crypt buffers

In reply to Sean Christopherson

When using a temporary buffer to {de,en}crypt unaligned memory for debug, allocate only the number of bytes that are needed instead of allocating an entire page. The most common case for unaligned accesses will be reading or writing less than 16 bytes.

---
 arch/x86/kvm/svm/sev.c | 69 ++++++++++++++----------------------------
 1 file changed, 22 insertions(+), 47 deletions(-)
arch/x86/kvm/svm/sev.c +22 -47
index 89586f821c9c..0865ce4bcecb 100644
@@ -1253,53 +1253,34 @@ static int sev_issue_dbg_cmd(struct kvm *kvm, unsigned long src_pa,
 	return sev_issue_cmd(kvm, cmd, &data, error);
 }
 
-static struct page *sev_alloc_dbg_buffer(void **buf)
+static void *sev_dbg_crypt_slow_alloc(struct page *page, unsigned long __va,
+				      unsigned int len, unsigned long *pa,
+				      unsigned int *nr_bytes)
 {
-	struct page *buf_p;
-
-	buf_p = alloc_page(GFP_KERNEL);
-	if (!buf_p)
-		return NULL;
-
-	*buf = kmap_local_page(buf_p);
-	return buf_p;
-}
-
-static void sev_free_dbg_buffer(struct page *buf_p, void *buf)
-{
-	kunmap_local(buf);
-	__free_page(buf_p);
-}
-
-static unsigned int sev_dbg_crypt_slow_addr_and_size(struct page *page,
-						     unsigned long __va,
-						     unsigned int len,
-						     unsigned long *pa)
-{
-	/* The number of bytes to {de,en}crypt must be 16-byte aligned. */
-	unsigned int nr_bytes = round_up(len, 16);
 	unsigned long va = ALIGN_DOWN(__va, 16);
 
+	/* The number of bytes to {de,en}crypt must be 16-byte aligned. */
+	*nr_bytes = round_up(len, 16);
+
 	/*
 	 * Increase the number of bytes to {de,en}crypt by one chunk (16 bytes)
 	 * if the aligned address and length doesn't cover the unaligned range,
 	 * e.g. if the address is unaligned _and_ the access will split a chunk
 	 * at the tail.
 	 */
-	if (va + nr_bytes < __va + len)
-		nr_bytes += 16;
+	if (va + *nr_bytes < __va + len)
+		*nr_bytes += 16;
 
 	*pa = __sme_page_pa(page) + (va & ~PAGE_MASK);
 
 	/*
 	 * Sanity check that the new access won't split a page.  This should
-	 * never happen; just squash the access and let the firmware command
-	 * fail.
+	 * never happen; just pretend the allocation failed.
 	 */
-	if (WARN_ON_ONCE((*pa & PAGE_MASK) != ((*pa + nr_bytes - 1) & PAGE_MASK)))
-		return 0;
+	if (WARN_ON_ONCE((*pa & PAGE_MASK) != ((*pa + *nr_bytes - 1) & PAGE_MASK)))
+		return NULL;
 
-	return nr_bytes;
+	return kmalloc(*nr_bytes, GFP_KERNEL);
 }
 
 static int sev_dbg_decrypt_slow(struct kvm *kvm, unsigned long src,
@@ -1308,17 +1289,14 @@ static int sev_dbg_decrypt_slow(struct kvm *kvm, unsigned long src,
 {
 	unsigned int nr_bytes;
 	unsigned long src_pa;
-	struct page *buf_p;
 	void *buf;
 	int r;
 
-	buf_p = sev_alloc_dbg_buffer(&buf);
-	if (!buf_p)
+	buf = sev_dbg_crypt_slow_alloc(src_p, src, len, &src_pa, &nr_bytes);
+	if (!buf)
 		return -ENOMEM;
 
-	nr_bytes = sev_dbg_crypt_slow_addr_and_size(src_p, src, len, &src_pa);
-
-	r = sev_issue_dbg_cmd(kvm, src_pa, __sme_page_pa(buf_p),
+	r = sev_issue_dbg_cmd(kvm, src_pa, __sme_set(__pa(buf)),
 			      nr_bytes, KVM_SEV_DBG_DECRYPT, err);
 	if (r)
 		goto out;
@@ -1326,7 +1304,7 @@ static int sev_dbg_decrypt_slow(struct kvm *kvm, unsigned long src,
 	if (copy_to_user((void __user *)dst, buf + (src & 15), len))
 		r = -EFAULT;
 out:
-	sev_free_dbg_buffer(buf_p, buf);
+	kfree(buf);
 	return r;
 }
 
@@ -1336,18 +1314,15 @@ static int sev_dbg_encrypt_slow(struct kvm *kvm, unsigned long src,
 {
 	unsigned int nr_bytes;
 	unsigned long dst_pa;
-	struct page *buf_p;
 	void *buf;
 	int r;
 
-	buf_p = sev_alloc_dbg_buffer(&buf);
-	if (!buf_p)
-		return -ENOMEM;
-
 	/* Decrypt the _destination_ to do a RMW on plaintext. */
-	nr_bytes = sev_dbg_crypt_slow_addr_and_size(dst_p, dst, len, &dst_pa);
+	buf = sev_dbg_crypt_slow_alloc(dst_p, dst, len, &dst_pa, &nr_bytes);
+	if (!buf)
+		return -ENOMEM;
 
-	r = sev_issue_dbg_cmd(kvm, dst_pa, __sme_page_pa(buf_p),
+	r = sev_issue_dbg_cmd(kvm, dst_pa, __sme_set(__pa(buf)),
 			      nr_bytes, KVM_SEV_DBG_DECRYPT, err);
 	if (r)
 		goto out;
@@ -1359,10 +1334,10 @@ static int sev_dbg_encrypt_slow(struct kvm *kvm, unsigned long src,
 	if (copy_from_user(buf + (dst & 15), (void __user *)src, len))
 		r = -EFAULT;
 	else
-		r = sev_issue_dbg_cmd(kvm, __sme_page_pa(buf_p), dst_pa,
+		r = sev_issue_dbg_cmd(kvm, __sme_set(__pa(buf)), dst_pa,
 				      nr_bytes, KVM_SEV_DBG_ENCRYPT, err);
 out:
-	sev_free_dbg_buffer(buf_p, buf);
+	kfree(buf);
 	return r;
 }
 
Signature · 1 line
2.54.0.545.g6539524ca2-goog
author maintainer Applied
#21 lore raw
In reply to Sean Christopherson
Quoted 9 lines · Sean Christopherson

Disclaimer: I could have sworn I sent this last week, but obviously did not, and I can't remember _why_ I didn't send it. So don't be too surprised if there's some glaring flaw or boneheaded mistake.

Ashutosh's fix for a heap OOB/UAF bug in the debug {de,en}crypt code, now with a selftest to detect the bug (and confirm the fix), and to validate the functionality.

[...]

Applied to kvm-x86 sev, to get as much exposure in -next as possible.

[1/6] KVM: SVM: Fix page overflow in sev_dbg_crypt() for ENCRYPT path

      https://github.com/kvm-x86/linux/commit/78ee2d50185a
[2/6] KVM: selftests: Add a test to verify SEV {en,de}crypt debug ioctls
      https://github.com/kvm-x86/linux/commit/6edd35e77a42
[3/6] KVM: SEV: Explicitly validate the dst buffer for debug operations
      https://github.com/kvm-x86/linux/commit/cb32e895546b
[4/6] KVM: SEV: Add helper function to pin/unpin a single page
      https://github.com/kvm-x86/linux/commit/99694add4af3
[5/6] KVM: SEV: Rewrite logic to {de,en}crypt memory for debug
      https://github.com/kvm-x86/linux/commit/8d6297a3e73e
[6/6] KVM: SEV: Allocate only as many bytes as needed for temp crypt buffers
      https://github.com/kvm-x86/linux/commit/4c735bf1bc22

--
https://github.com/kvm-x86/linux/tree/next