diff --git a/lib/x86/amd_sev.h b/lib/x86/amd_sev.h
index 70f3763fe231..b648fb0e7873 100644
--- a/lib/x86/amd_sev.h
+++ b/lib/x86/amd_sev.h
@@ -84,6 +84,15 @@ struct ghcb {
 
 #define	VMGEXIT()			{ asm volatile("rep; vmmcall\n\r"); }
 
+/* PVALIDATE return codes */
+#define PVALIDATE_FAIL_SIZEMISMATCH	6
+
+/* Software defined (when rFlags.CF = 1) */
+#define PVALIDATE_FAIL_NOUPDATE		255
+
+/* RMP page size */
+#define RMP_PG_SIZE_4K			0
+
 enum es_result {
 	ES_OK,			/* All good */
 	ES_UNSUPPORTED,		/* Requested operation not supported */
@@ -157,6 +166,36 @@ efi_status_t setup_amd_sev(void);
  */
 #define SEV_ES_GHCB_MSR_INDEX 0xc0010130
 
+#define GHCB_DATA_LOW		12
+#define GHCB_MSR_INFO_MASK	(BIT_ULL(GHCB_DATA_LOW) - 1)
+#define GHCB_RESP_CODE(v)	((v) & GHCB_MSR_INFO_MASK)
+
+/*
+ * SNP Page State Change Operation
+ *
+ * GHCBData[55:52] - Page operation:
+ *	0x0001	Page assignment, Private
+ *	0x0002	Page assignment, Shared
+ */
+enum psc_op {
+	SNP_PAGE_STATE_PRIVATE = 1,
+	SNP_PAGE_STATE_SHARED,
+};
+
+#define GHCB_MSR_PSC_REQ	0x14
+#define GHCB_MSR_PSC_REQ_GFN(gfn, op)				\
+	/* GHCBData[55:52] */					\
+	(((u64)((op) & 0xf) << 52) |				\
+	/* GHCBData[51:12] */					\
+	((u64)((gfn) & GENMASK_ULL(39, 0)) << 12) |		\
+	/* GHCBData[11:0] */					\
+	GHCB_MSR_PSC_REQ)
+
+#define GHCB_MSR_PSC_RESP	0x15
+#define GHCB_MSR_PSC_RESP_VAL(val)				\
+	/* GHCBData[63:32] */					\
+	(((u64)(val) & GENMASK_ULL(63, 32)) >> 32)
+
 bool amd_sev_es_enabled(void);
 efi_status_t setup_vc_handler(void);
 bool amd_sev_snp_enabled(void);
@@ -166,6 +205,18 @@ void handle_sev_es_vc(struct ex_regs *regs);
 unsigned long long get_amd_sev_c_bit_mask(void);
 unsigned long long get_amd_sev_addr_upperbound(void);
 
+/*
+ * Macros to generate condition code outputs from inline assembly,
+ * The output operand must be type "bool".
+ */
+#ifdef __GCC_ASM_FLAG_OUTPUTS__
+# define CC_SET(c) "\n\t/* output condition code " #c "*/\n"
+# define CC_OUT(c) "=@cc" #c
+#else
+# define CC_SET(c) "\n\tset" #c " %[_cc_" #c "]\n"
+# define CC_OUT(c)[_cc_ ## c] "=qm"
+#endif
+
 /* GHCB Accessor functions from Linux's include/asm/svm.h */
 #define GHCB_BITMAP_IDX(field)							\
 	(offsetof(struct ghcb_save_area, field) / sizeof(u64))
diff --git a/x86/amd_sev.c b/x86/amd_sev.c
index 23f6e3490546..71d1ee1cef91 100644
--- a/x86/amd_sev.c
+++ b/x86/amd_sev.c
@@ -14,11 +14,14 @@
 #include "x86/processor.h"
 #include "x86/amd_sev.h"
 #include "msr.h"
+#include "x86/vm.h"
+#include "alloc_page.h"
 
 #define EXIT_SUCCESS 0
 #define EXIT_FAILURE 1
 
 #define TESTDEV_IO_PORT 0xe0
+#define SNP_PSC_ALLOC_ORDER 10
 
 static char st1[] = "abcdefghijklmnop";
 
@@ -94,6 +97,140 @@ static efi_status_t find_cc_blob_efi(void)
 	return EFI_SUCCESS;
 }
 
+static inline int pvalidate(u64 vaddr, bool rmp_size,
+			    bool validate)
+{
+	bool rmp_unchanged;
+	int result;
+
+	asm volatile(".byte 0xF2, 0x0F, 0x01, 0xFF\n\t"
+		     CC_SET(c)
+		     : CC_OUT(c) (rmp_unchanged), "=a" (result)
+		     : "a" (vaddr), "c" (rmp_size), "d" (validate)
+		     : "memory", "cc");
+
+	if (rmp_unchanged)
+		return PVALIDATE_FAIL_NOUPDATE;
+
+	return result;
+}
+
+static efi_status_t __sev_set_pages_state_msr_proto(unsigned long vaddr, int npages,
+						    int operation)
+{
+	unsigned long vaddr_end = vaddr + (npages * PAGE_SIZE);
+	unsigned long paddr;
+	int ret;
+	u64 val;
+
+	/*
+	 * We are re-using GHCB MSR value setup by OVMF, so save and
+	 * restore it after PSCs.
+	 */
+	phys_addr_t ghcb_old_msr = rdmsr(SEV_ES_GHCB_MSR_INDEX);
+
+	while (vaddr < vaddr_end) {
+		/*
+		 * Although identity mapped, compute GPA to use guest
+		 * physical frame number (GFN) while requesting an
+		 * explicit page state change.
+		 */
+		paddr = __pa(vaddr);
+
+		if (operation == SNP_PAGE_STATE_SHARED) {
+			/* Page invalidation happens before changing to shared */
+			ret = pvalidate(vaddr, RMP_PG_SIZE_4K, false);
+			if (ret) {
+				printf("Failed to invalidate vaddr: 0x%lx, ret: %d\n",
+				       vaddr, ret);
+				return ES_UNSUPPORTED;
+			}
+		}
+
+		wrmsr(SEV_ES_GHCB_MSR_INDEX,
+		      GHCB_MSR_PSC_REQ_GFN(paddr >> PAGE_SHIFT, operation));
+
+		VMGEXIT();
+
+		val = rdmsr(SEV_ES_GHCB_MSR_INDEX);
+
+		if (GHCB_RESP_CODE(val) != GHCB_MSR_PSC_RESP) {
+			printf("Wrong PSC response code: 0x%x\n",
+			       (unsigned int)GHCB_RESP_CODE(val));
+			return ES_VMM_ERROR;
+		}
+
+		if (GHCB_MSR_PSC_RESP_VAL(val)) {
+			printf("Failed to change page state to %s paddr: 0x%lx error: 0x%llx\n",
+			       operation == SNP_PAGE_STATE_PRIVATE ? "private"
+								   : "shared",
+			       paddr, GHCB_MSR_PSC_RESP_VAL(val));
+			return ES_VMM_ERROR;
+		}
+
+		if (operation == SNP_PAGE_STATE_PRIVATE) {
+			ret = pvalidate(vaddr, RMP_PG_SIZE_4K, true);
+			if (ret) {
+				printf("Failed to validate vaddr: 0x%lx, ret: %d\n",
+				       vaddr, ret);
+				return ES_UNSUPPORTED;
+			}
+		}
+
+		vaddr += PAGE_SIZE;
+	}
+
+	/* Restore old GHCB MSR - setup by OVMF */
+	wrmsr(SEV_ES_GHCB_MSR_INDEX, ghcb_old_msr);
+
+	return ES_OK;
+}
+
+static void set_pte_decrypted(unsigned long vaddr, int npages)
+{
+	pteval_t *pte;
+	unsigned long vaddr_end = vaddr + (npages * PAGE_SIZE);
+
+	while (vaddr < vaddr_end) {
+		pte = get_pte((pgd_t *)read_cr3(), (void *)vaddr);
+
+		if (!pte)
+			assert_msg(pte, "No pte found for vaddr 0x%lx", vaddr);
+
+		/* unset c-bit */
+		*pte &= ~(get_amd_sev_c_bit_mask());
+
+		vaddr += PAGE_SIZE;
+	}
+
+	flush_tlb();
+}
+
+static efi_status_t sev_set_pages_state_msr_proto(unsigned long vaddr, int npages,
+						  int operation)
+{
+	efi_status_t status;
+
+	vaddr = vaddr & PAGE_MASK;
+
+	/*
+	 * If the encryption bit is to be cleared, change the page state
+	 * in the RMP table.
+	 */
+	if (operation == SNP_PAGE_STATE_SHARED) {
+		status = __sev_set_pages_state_msr_proto(vaddr, npages,
+							 operation);
+		if (status != ES_OK) {
+			printf("Page state change (Private->Shared) failure.\n");
+			return status;
+		}
+
+		set_pte_decrypted(vaddr, npages);
+	}
+
+	return ES_OK;
+}
+
 static void test_sev_snp_activation(void)
 {
 	efi_status_t status;
@@ -109,6 +246,51 @@ static void test_sev_snp_activation(void)
 	report(status == EFI_SUCCESS, "SEV-SNP CC-blob presence");
 }
 
+/*
+ * Perform page revalidation to ensure page is in the expected private
+ * state. We can confirm this test to succeed when the pvalidate fails
+ * with a return code of PVALIDATE_FAIL_NOUPDATE.
+ */
+static bool is_validated_private_page(unsigned long vaddr, bool rmp_size,
+				      bool state)
+{
+	int ret;
+
+	/* Attempt a pvalidate here for the provided page size */
+	ret = pvalidate(vaddr, rmp_size, state);
+	if (ret == PVALIDATE_FAIL_NOUPDATE)
+		return true;
+
+	/*
+	 * If PVALIDATE_FAIL_SIZEMISMATCH, Entry in the RMP is a 4K
+	 * entry, and what guest is providing is a 2M entry. Therefore,
+	 * fallback to pvalidating 4K entries within 2M range.
+	 */
+	if (rmp_size && ret == PVALIDATE_FAIL_SIZEMISMATCH) {
+		unsigned long vaddr_end = vaddr + LARGE_PAGE_SIZE;
+
+		for (; vaddr < vaddr_end; vaddr += PAGE_SIZE) {
+			ret = pvalidate(vaddr, RMP_PG_SIZE_4K, state);
+			if (ret != PVALIDATE_FAIL_NOUPDATE)
+				return false;
+		}
+	}
+
+	return ret == PVALIDATE_FAIL_NOUPDATE ? true : false;
+}
+
+static int test_write(unsigned long vaddr, int npages)
+{
+	unsigned long vaddr_end = vaddr + (npages << PAGE_SHIFT);
+
+	while (vaddr < vaddr_end) {
+		memcpy((void *)vaddr, st1, strnlen(st1, PAGE_SIZE));
+		vaddr += PAGE_SIZE;
+	}
+
+	return 0;
+}
+
 static void test_stringio(void)
 {
 	int st1_len = sizeof(st1) - 1;
@@ -126,6 +308,60 @@ static void test_stringio(void)
 	report((got & 0xff00) >> 8 == st1[sizeof(st1) - 2], "outsb up");
 }
 
+static void test_sev_psc_ghcb_msr(void)
+{
+	pteval_t *pte;
+	unsigned long *vaddr;
+	efi_status_t status;
+
+	vaddr = alloc_pages(SNP_PSC_ALLOC_ORDER);
+	if (!vaddr)
+		assert_msg(vaddr, "Page allocation failure at addr: %p", vaddr);
+
+	/*
+	 * Page state changes using GHCB MSR protocol can only happen on
+	 * 4K pages.
+	 */
+	force_4k_page(vaddr);
+
+	/* Use this pte to check the C-bit */
+	pte = get_pte_level((pgd_t *)read_cr3(), (void *)vaddr, 1);
+	if (!pte) {
+		assert_msg(pte, "No pte found for vaddr %p", vaddr);
+		return;
+	}
+
+	if (*pte & get_amd_sev_c_bit_mask()) {
+		/*
+		 * Before performing private->shared test, ensure the
+		 * page is in private and in a validated state.
+		 */
+		report(is_validated_private_page((unsigned long)vaddr,
+						 RMP_PG_SIZE_4K, true),
+		       "Expected page state: Private");
+
+		report_info("Private->Shared conversion test using GHCB MSR");
+
+		/* Perform Private->Shared page state change */
+		status = sev_set_pages_state_msr_proto((unsigned long)vaddr,
+						       1 << SNP_PSC_ALLOC_ORDER,
+						       SNP_PAGE_STATE_SHARED);
+
+		report(status == ES_OK, "Private->Shared Page State Change");
+
+		/*
+		 * Access the now-shared page(s) with C-bit cleared and
+		 * ensure read/writes return expected data.
+		 */
+		report(!test_write((unsigned long)vaddr, 1 << SNP_PSC_ALLOC_ORDER),
+		       "Write to %d unencrypted pages after private->shared conversion",
+		       1 << SNP_PSC_ALLOC_ORDER);
+	}
+
+	/* Cleanup */
+	free_pages_by_order(vaddr, SNP_PSC_ALLOC_ORDER);
+}
+
 int main(void)
 {
 	int rtn;
@@ -134,5 +370,10 @@ int main(void)
 	test_sev_es_activation();
 	test_sev_snp_activation();
 	test_stringio();
+	setup_vm();
+
+	if (amd_sev_snp_enabled())
+		test_sev_psc_ghcb_msr();
+
 	return report_summary();
 }
