From: Mark Kettenis Subject: Re: SEV-SNP: Prepare for SNP page validation 1/4 To: Hans-Jörg Höxer Cc: tech@openbsd.org Date: Tue, 11 Aug 2026 15:19:18 +0200 > Date: Tue, 11 Aug 2026 11:33:30 +0200 > From: Hans-Jörg Höxer This includes the SEV-SNP code unconditionally in all amd64 kernels. We probably want it on RAMDISK_CD and VMBOOT. But I'm not sure about the (small) RAMDISK. > this is patch 1/4 that implemnts the missing pieces for SEV-SNP guest > support. > > One essential functionality of SEV-SNP is page validation: An SNP > enabled guest claims pages using the pvalidate instruction. After > successfully claiming a page, the CPU ensures that only that guest > accesses that page. If the hypervisor or another guest accesses > that page, they will receive a protection fault trap. > > For bounce buffers, the guest explicitly rescinds the page to the > hypervisor. > > This change implements claiming and rescinding pages for SEV-SNP. > It also provides a new GHCB request, "Page Stage Change" (PSC), > that is used alongside with pvalidate. > > Handling of an AMD EPYC cache coherency CPU bug by Stefan Fritsch > and Sebastian Sturm. > > ok? comments? > > Take care, > HJ. > > --- > sys/arch/amd64/amd64/ghcb.c | 58 +++++++ > sys/arch/amd64/amd64/snp.c | 234 ++++++++++++++++++++++++++++ > sys/arch/amd64/conf/files.amd64 | 1 + > sys/arch/amd64/include/ghcb.h | 17 ++ > sys/arch/amd64/include/snp.h | 28 ++++ > sys/arch/amd64/include/specialreg.h | 8 + > sys/arch/amd64/include/vmmvar.h | 3 +- > 7 files changed, 348 insertions(+), 1 deletion(-) > create mode 100644 sys/arch/amd64/amd64/snp.c > create mode 100644 sys/arch/amd64/include/snp.h > > diff --git a/sys/arch/amd64/amd64/ghcb.c b/sys/arch/amd64/amd64/ghcb.c > index a0539c76eb4..0083444de72 100644 > --- a/sys/arch/amd64/amd64/ghcb.c > +++ b/sys/arch/amd64/amd64/ghcb.c > @@ -434,3 +434,61 @@ _ghcb_io_rw(uint16_t port, int valsz, uint32_t *val, bool read) > if (read) > *val = frame.tf_rax; > } > + > +/* > + * ghcb_psc_vmgexit > + * > + * Request a page state change from the hypervisor. > + */ > +int > +ghcb_psc_vmgexit(struct ghcb_psc *psc, size_t sz) > +{ > + struct ghcb_sa *ghcb; > + uint64_t s, sw_exitinfo1, sw_exitinfo2; > + uint8_t valid_bm[GHCB_VB_SZ], expected_bm[GHCB_VB_SZ]; > + int error = 0; > + > + if (sz > sizeof(ghcb->v_sharedbuf)) > + return (EINVAL); > + > + memset(valid_bm, 0, sizeof(valid_bm)); > + memset(expected_bm, 0, sizeof(expected_bm)); > + > + s = intr_disable(); > + > + ghcb = (struct ghcb_sa *)ghcb_vaddr; > + ghcb_clear(ghcb); > + memcpy(ghcb->v_sharedbuf, psc, sz); > + > + ghcb->v_sw_exitcode = SEV_VMGEXIT_PAGE_STATE_CHANGE; > + ghcb->v_sw_exitinfo1 = 0; > + ghcb->v_sw_exitinfo2 = 0; > + ghcb->v_sw_scratch = ghcb_paddr + offsetof(struct ghcb_sa, > + v_sharedbuf); > + ghcb_valbm_set(valid_bm, GHCB_SW_EXITCODE); > + ghcb_valbm_set(valid_bm, GHCB_SW_EXITINFO1); > + ghcb_valbm_set(valid_bm, GHCB_SW_EXITINFO2); > + ghcb_valbm_set(valid_bm, GHCB_SW_SCRATCH); > + ghcb_valbm_set(expected_bm, GHCB_SW_EXITINFO1); > + ghcb_valbm_set(expected_bm, GHCB_SW_EXITINFO2); > + > + memcpy(ghcb->valid_bitmap, valid_bm, sizeof(ghcb->valid_bitmap)); > + > + vmgexit(); > + > + memcpy(valid_bm, ghcb->valid_bitmap, sizeof(valid_bm)); > + sw_exitinfo1 = ghcb->v_sw_exitinfo1; > + sw_exitinfo2 = ghcb->v_sw_exitinfo2; > + ghcb_clear(ghcb); > + > + if (ghcb_verify_bm(valid_bm, expected_bm)) > + panic("invalid hypervisor response"); > + > + if (sw_exitinfo1 != 0) > + panic("page state change failed: 0x%llx 0x%llx", sw_exitinfo1, > + sw_exitinfo2); > + > + intr_restore(s); > + > + return (error); > +} > diff --git a/sys/arch/amd64/amd64/snp.c b/sys/arch/amd64/amd64/snp.c > new file mode 100644 > index 00000000000..64a277ba19a > --- /dev/null > +++ b/sys/arch/amd64/amd64/snp.c > @@ -0,0 +1,234 @@ > +/* $OpenBSD$ */ > + > +/* > + * Copyright (c) 2025 Hans-Joerg Hoexer > + * > + * Permission to use, copy, modify, and distribute this software for any > + * purpose with or without fee is hereby granted, provided that the above > + * copyright notice and this permission notice appear in all copies. > + * > + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES > + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF > + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR > + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES > + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN > + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF > + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. > + */ > + > +#include > +#include > +#include > + > +#include > +#include > + > +#define SNP_PSC_PRIVATE 0x0001 > +#define SNP_PSC_SHARED 0x0002 > + > +/* > + * Workaround for cache coherency CPU bug named CVE-2025-38560 in linux and > + * CVE-2024-36331 in OVMF. It seems this can cause memory corruption even > + * without any attacker, corrupting page tables and causing panics while > + * booting. > + */ > +static void > +snp_evict_cache(vaddr_t va, unsigned int npages) > +{ > + volatile uint8_t dummy; > + uint8_t *ptr = (uint8_t *)va; > + unsigned int i; > + > + if (curcpu()->ci_feature_amdsev_ebx & CPUIDEBX_COHERENCY_SFW_NO) > + return; > + > + for (i = 0; i < npages; i++) { > + /* > + * Reading from the first and last cache line of the page from > + * the guest causes a flush of all cache-lines of that page > + * and is cheaper than a complete clflush. > + */ > + dummy = ptr[i * PAGE_SIZE]; > + dummy = ptr[i * PAGE_SIZE + PAGE_SIZE - 1]; > + } > +} > + > +/* > + * _pvalidate > + * > + * pvalidate single 4K page. > + */ > +static __inline int > +_pvalidate(vaddr_t va, uint32_t state) > +{ > + uint64_t rax, rv; > + uint32_t ecx, edx; > + uint8_t unchanged = 0; > + > + rax = va; > + ecx = 0; /* 4K page */ > + if (state == SNP_PSC_PRIVATE) > + edx = 1; /* set "validated" in RMP */ > + else > + edx = 0; /* clear "validated" RMP */ > + > + __asm volatile( > + " pvalidate ;" > + " setc %b0 ;" > + : "=qm" (unchanged), "=a" (rv) > + : "a" (rax), "c" (ecx), "d" (edx) > + : "memory", "cc"); > + > + /* > + * pvalidate was successful, however, the "validated" entry > + * was already set to "state" (ie. no change). > + */ > + if (rv == 0 && unchanged) > + return (-1); > + > + if (state == SNP_PSC_PRIVATE) > + snp_evict_cache(va, 1); > + > + return (rv); > +} > + > +/* > + * pvalidate > + * > + * pvalidate range in 4K chunks. > + */ > +static int > +pvalidate(paddr_t start, paddr_t end, uint32_t state, int early) > +{ > + paddr_t pa; > + size_t left; > + int error; > + > + left = end - start; > + for (pa = start; pa < end; pa += PAGE_SIZE, left -= PAGE_SIZE) { > + if ((error = _pvalidate(PMAP_DIRECT_MAP(pa), state)) != 0) { > + /* > + * During early bootstrap it is acceptable, > + * when we initially claim a page, that is > + * already private. Therefore the valid > + * flags was not changed in the RMP. > + * > + * However, after bootstrap, this may not happen. > + */ > + if (early && state == SNP_PSC_PRIVATE && error == -1) > + continue; > + return (error); > + } > + } > + > + return (0); > +} > + > +/* > + * _snp_page_state_change > + * > + * Set the host physical page, that is assigned to our VM as guest > + * physical page to either "private" (ie. encrypted) or "shared" (ie. > + * unencrypted). > + * > + * When sharing a page with hypervisor, ie. a page shall be used > + * as bounce buffer, we require the following order of events: > + * > + * 1) Set "validate" in the RMP to 0 using pvalidate. > + * 2) Request the hypervisor to set "assigend" in the RMP to > + * "hypervisor owned". > + * 3) Establish a mapping with the C-bit cleared; with this mapping > + * the page can be accessed unencrypted. > + * > + * When claiming a page, ie. when freeing a bounce buffer page, we > + * require the following order: > + * > + * 1) Remove the mapping for unencryped access. > + * 2) Request the hypervisor to set "assigend" to "guest owned" > + * 3) Set "validated" in the RMP to 1; this requires a mapping with > + * C-bit set; we use the PMAP DIRECT mapping of that page, as > + * these are always encrypted. > + */ > +static void > +_snp_page_state_change(paddr_t start, paddr_t end, int state, int early) > +{ > + struct ghcb_psc psc; > + paddr_t pa; > + int error; > + size_t sz, left; > + > + left = end - start; > + for (pa = start; pa < end; pa += sz, left -= sz) { > + if ((pa & ~L2_FRAME) == 0 && left >= NBPD_L2) > + sz = NBPD_L2; > + else > + sz = NBPD_L1; > + memset(&psc, 0, sizeof(psc)); > + psc.psc_hdr.end_entry = 0; > + psc.psc_entry.cur_page = 0; > + psc.psc_entry.gfn = pa >> PAGE_SHIFT; > + psc.psc_entry.operation = state; > + psc.psc_entry.pagesize = (sz == NBPD_L2); > + > + if (state == SNP_PSC_SHARED && > + (error = pvalidate(pa, pa + sz, state, early)) != 0) { > + panic("pvalidate failed: %d", error); > + } > + > + if ((error = ghcb_psc_vmgexit(&psc, sizeof(psc))) != 0) > + panic("ghcb_psc_vmgexit() failed: %d", error); > + > + if (state == SNP_PSC_PRIVATE && > + (error = pvalidate(pa, pa + sz, state, early)) != 0) { > + panic("pvalidate failed: %d", error); > + } > + } > +} > + > +static void > +_snp_claim(paddr_t start, paddr_t end, int early) > +{ > + _snp_page_state_change(start, end, SNP_PSC_PRIVATE, early); > +} > + > +static void > +_snp_rescind(paddr_t start, paddr_t end, int early) > +{ > + _snp_page_state_change(start, end, SNP_PSC_SHARED, early); > +} > + > + > +void > +snp_claim_early(paddr_t start, paddr_t end) > +{ > + if (!ISSET(cpu_sev_guestmode, SEV_STAT_SNP_ACTIVE)) > + return; > + > + return _snp_claim(start, end, 1); > +} > + > +void > +snp_claim_pages(struct pglist *mlist) > +{ > + struct vm_page *pg; > + > + if (!ISSET(cpu_sev_guestmode, SEV_STAT_SNP_ACTIVE)) > + return; > + > + TAILQ_FOREACH(pg, mlist, pageq) > + _snp_claim(VM_PAGE_TO_PHYS(pg), > + VM_PAGE_TO_PHYS(pg) + PAGE_SIZE, 0); > +} > + > +void > +snp_rescind_pages(struct pglist *mlist) > +{ > + struct vm_page *pg; > + > + if (!ISSET(cpu_sev_guestmode, SEV_STAT_SNP_ACTIVE)) > + return; > + > + TAILQ_FOREACH(pg, mlist, pageq) > + _snp_rescind(VM_PAGE_TO_PHYS(pg), > + VM_PAGE_TO_PHYS(pg) + PAGE_SIZE, 0); > +} > diff --git a/sys/arch/amd64/conf/files.amd64 b/sys/arch/amd64/conf/files.amd64 > index 0c1e224f5c2..5474475bb41 100644 > --- a/sys/arch/amd64/conf/files.amd64 > +++ b/sys/arch/amd64/conf/files.amd64 > @@ -30,6 +30,7 @@ file arch/amd64/amd64/i8259.c > file arch/amd64/amd64/cacheinfo.c > file arch/amd64/amd64/ghcb.c > file arch/amd64/amd64/sev_bus_space.c > +file arch/amd64/amd64/snp.c > file arch/amd64/amd64/vector.S > file arch/amd64/amd64/copy.S > file arch/amd64/amd64/spl.S > diff --git a/sys/arch/amd64/include/ghcb.h b/sys/arch/amd64/include/ghcb.h > index 55e184186ab..a821abdfb23 100644 > --- a/sys/arch/amd64/include/ghcb.h > +++ b/sys/arch/amd64/include/ghcb.h > @@ -111,6 +111,22 @@ struct ghcb_sync { > > #ifndef _LOCORE > > +struct ghcb_psc { > + struct { > + uint16_t cur_entry; > + uint16_t end_entry; > + uint32_t reserved; > + } psc_hdr; > + > + struct { > + uint64_t cur_page:12; > + uint64_t gfn:40; > + uint64_t operation:4; > + uint64_t pagesize:1; > + uint64_t reserved:7; > + } psc_entry; > +}; > + > extern vaddr_t ghcb_vaddr; > extern paddr_t ghcb_paddr; > > @@ -129,6 +145,7 @@ int ghcb_valbm_isset(uint8_t *, int); > int ghcb_verify_bm(uint8_t *, uint8_t *); > int ghcb_valid(struct ghcb_sa *); > int ghcb_empty(struct ghcb_sa *); > +int ghcb_psc_vmgexit(struct ghcb_psc *, size_t); > > void ghcb_sync_val(int, int, struct ghcb_sync *); > void ghcb_sync_out(struct trapframe *, const struct ghcb_extra_regs *, > diff --git a/sys/arch/amd64/include/snp.h b/sys/arch/amd64/include/snp.h > new file mode 100644 > index 00000000000..07dcea3a813 > --- /dev/null > +++ b/sys/arch/amd64/include/snp.h > @@ -0,0 +1,28 @@ > +/* $OpenBSD$ */ > + > +/* > + * Copyright (c) 2025 Hans-Joerg Hoexer > + * > + * Permission to use, copy, modify, and distribute this software for any > + * purpose with or without fee is hereby granted, provided that the above > + * copyright notice and this permission notice appear in all copies. > + * > + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES > + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF > + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR > + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES > + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN > + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF > + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. > + */ > + > +#ifndef _MACHINE_SNP_H_ > +#define _MACHINE_SNP_H_ > + > +#include > + > +void snp_claim_early(paddr_t, paddr_t); > +void snp_claim_pages(struct pglist *); > +void snp_rescind_pages(struct pglist *); > + > +#endif /* !_MACHINE_SNP_H_ */ > diff --git a/sys/arch/amd64/include/specialreg.h b/sys/arch/amd64/include/specialreg.h > index 17bcc8cb29f..1a32da87ed7 100644 > --- a/sys/arch/amd64/include/specialreg.h > +++ b/sys/arch/amd64/include/specialreg.h > @@ -431,6 +431,14 @@ > "\023VTOMMSR" "\024IBSVIRT" "\031VMSARPROT" "\032SMTPROT" \ > "\035SVSMPAGEMSR" "\036NVSMSR" ) > > +/* > + * AMD CPUID function 0x8000001F EBX bits > + */ > +#define CPUIDEBX_COHERENCY_SFW_NO (1ULL << 31) /* no coherency cpu bug */ > + > + /* Number of encrypted guests */ > + #define CPUID_AMDSEV_ECX_BITS ("\20") > + > /* Number of encrypted guests */ > #define CPUID_AMDSEV_ECX_BITS ("\20") > > diff --git a/sys/arch/amd64/include/vmmvar.h b/sys/arch/amd64/include/vmmvar.h > index 25b1618ad1f..215ee20988f 100644 > --- a/sys/arch/amd64/include/vmmvar.h > +++ b/sys/arch/amd64/include/vmmvar.h > @@ -267,10 +267,11 @@ > #define SVM_VMEXIT_INVALID -1 > > /* > - * Additional VMEXIT codes used in SEV-ES/SNP in the GHCB > + * Additional VMEXIT codes used in SEV-ES/SNP in the GHCB > */ > #define SEV_VMGEXIT_MMIO_READ 0x80000001 > #define SEV_VMGEXIT_MMIO_WRITE 0x80000002 > +#define SEV_VMGEXIT_PAGE_STATE_CHANGE 0x80000010 > > #ifndef _LOCORE > > -- > 2.53.0 > > > [2:application/pkcs7-signature Show Save:smime.p7s (6kB)] >