diff --git a/lib/x86/amd_sev.c b/lib/x86/amd_sev.c
index 5cbdeb35bba8..e2f99bc8eded 100644
--- a/lib/x86/amd_sev.c
+++ b/lib/x86/amd_sev.c
@@ -346,3 +346,171 @@ enum es_result __sev_set_pages_state_msr_proto(unsigned long vaddr, int npages,
 
 	return ES_OK;
 }
+
+static void pvalidate_pages(struct snp_psc_desc *desc, unsigned long *vaddr_arr)
+{
+	struct psc_entry *entry;
+	int ret, i;
+	unsigned long vaddr;
+	bool validate;
+
+	for (i = 0; i <= desc->hdr.end_entry; i++) {
+		vaddr = vaddr_arr[i];
+		entry = &desc->entries[i];
+		validate = entry->operation == SNP_PAGE_STATE_PRIVATE ? true : false;
+
+		ret = pvalidate(vaddr, entry->pagesize, validate);
+		if (ret == PVALIDATE_FAIL_SIZEMISMATCH) {
+			assert(entry->pagesize == RMP_PG_SIZE_2M);
+			unsigned long vaddr_end = vaddr + LARGE_PAGE_SIZE;
+
+			for (; vaddr < vaddr_end; vaddr += PAGE_SIZE) {
+				ret = pvalidate(vaddr, RMP_PG_SIZE_4K, validate);
+				if (ret)
+					break;
+			}
+		}
+		assert(!ret);
+	}
+}
+
+static int verify_exception(struct ghcb *ghcb)
+{
+	return ghcb->save.sw_exit_info_1 & GENMASK_ULL(31, 0);
+}
+
+static int sev_ghcb_hv_call(struct ghcb *ghcb, u64 exit_code,
+			    u64 exit_info_1, u64 exit_info_2)
+{
+	ghcb->version = GHCB_PROTOCOL_MAX;
+	ghcb->ghcb_usage = GHCB_DEFAULT_USAGE;
+
+	ghcb_set_sw_exit_code(ghcb, exit_code);
+	ghcb_set_sw_exit_info_1(ghcb, exit_info_1);
+	ghcb_set_sw_exit_info_2(ghcb, exit_info_2);
+
+	VMGEXIT();
+
+	return verify_exception(ghcb);
+}
+
+static int vmgexit_psc(struct snp_psc_desc *desc, struct ghcb *ghcb)
+{
+	int cur_entry, end_entry, ret = 0;
+	struct snp_psc_desc *data;
+
+	/* Ensure end_entry is within bounds */
+	assert(desc->hdr.end_entry < VMGEXIT_PSC_MAX_ENTRY);
+
+	vc_ghcb_invalidate(ghcb);
+
+	data = (struct snp_psc_desc *)ghcb->shared_buffer;
+	memcpy(ghcb->shared_buffer, desc, GHCB_SHARED_BUF_SIZE);
+
+	cur_entry = data->hdr.cur_entry;
+	end_entry = data->hdr.end_entry;
+
+	while (data->hdr.cur_entry <= data->hdr.end_entry) {
+		ghcb_set_sw_scratch(ghcb, (u64)__pa(data));
+
+		ret = sev_ghcb_hv_call(ghcb, SVM_VMGEXIT_PSC, 0, 0);
+
+		if (ret) {
+			report_info("SNP: PSC failed with ret: %d\n", ret);
+			ret = 1;
+			break;
+		}
+
+		if (cur_entry > data->hdr.cur_entry) {
+			report_info("SNP: PSC processing going backward, cur_entry %d (got %d)\n",
+				    cur_entry, data->hdr.cur_entry);
+			ret = 1;
+			break;
+		}
+
+		if (data->hdr.end_entry != end_entry) {
+			report_info("End entry mismatch: end_entry %d (got %d)\n",
+				    end_entry, data->hdr.end_entry);
+			ret = 1;
+			break;
+		}
+
+		if (data->hdr.reserved) {
+			report_info("Reserved bit is set in the PSC header\n");
+			ret = 1;
+			break;
+		}
+	}
+
+	/* Copy the output in shared buffer back to desc */
+	memcpy(desc, ghcb->shared_buffer, GHCB_SHARED_BUF_SIZE);
+
+	return ret;
+}
+
+static void add_psc_entry(struct snp_psc_desc *desc, u8 idx, u8 op, unsigned long vaddr,
+			  bool large_entry, u16 cur_page_offset)
+{
+	struct psc_hdr *hdr = &desc->hdr;
+	struct psc_entry *entry = &desc->entries[idx];
+
+	assert_msg(!large_entry || IS_ALIGNED(vaddr, LARGE_PAGE_SIZE),
+		   "Must use 2M-aligned addresses for large PSC entries");
+
+	entry->gfn = pgtable_va_to_pa(vaddr) >> PAGE_SHIFT;
+	entry->operation = op;
+	entry->pagesize = large_entry;
+	entry->cur_page = cur_page_offset;
+	hdr->end_entry = idx;
+}
+
+unsigned long __sev_set_pages_state(struct snp_psc_desc *desc, unsigned long vaddr,
+				    unsigned long vaddr_end, int op,
+				    struct ghcb *ghcb, bool large_entry)
+{
+	unsigned long vaddr_arr[VMGEXIT_PSC_MAX_ENTRY];
+	int ret, iter = 0, iter2 = 0;
+	u8 page_size;
+
+	memset(desc, 0, sizeof(*desc));
+
+	report_info("%s: address start %lx end %lx op %d large %d",
+		    __func__, vaddr, vaddr_end, op, large_entry);
+
+	while (vaddr < vaddr_end && iter < ARRAY_SIZE(desc->entries)) {
+		vaddr_arr[iter] = vaddr;
+
+		if (large_entry && IS_ALIGNED(vaddr, LARGE_PAGE_SIZE) &&
+		    (vaddr_end - vaddr) >= LARGE_PAGE_SIZE) {
+			add_psc_entry(desc, iter, op, vaddr, true, 0);
+			vaddr += LARGE_PAGE_SIZE;
+		} else {
+			add_psc_entry(desc, iter, op, vaddr, false, 0);
+			vaddr += PAGE_SIZE;
+		}
+
+		iter++;
+	}
+
+	if (op == SNP_PAGE_STATE_SHARED)
+		pvalidate_pages(desc, vaddr_arr);
+
+	ret = vmgexit_psc(desc, ghcb);
+	assert_msg(!ret, "VMGEXIT failed with ret value: %d", ret);
+
+	if (op == SNP_PAGE_STATE_PRIVATE)
+		pvalidate_pages(desc, vaddr_arr);
+
+	for (iter2 = 0; iter2 < iter; iter2++) {
+		page_size = desc->entries[iter2].pagesize;
+
+		if (page_size == RMP_PG_SIZE_2M)
+			assert_msg(desc->entries[iter2].cur_page == 512,
+				   "Failed to process sub-entries within 2M range");
+		else if (page_size == RMP_PG_SIZE_4K)
+			assert_msg(desc->entries[iter2].cur_page == 1,
+				   "Failed to process 4K entry");
+	}
+
+	return vaddr;
+}
diff --git a/lib/x86/amd_sev.h b/lib/x86/amd_sev.h
index 04c569be57eb..bf065ef613b7 100644
--- a/lib/x86/amd_sev.h
+++ b/lib/x86/amd_sev.h
@@ -208,6 +208,29 @@ enum psc_op {
 	/* GHCBData[63:32] */					\
 	(((u64)(val) & GENMASK_ULL(63, 32)) >> 32)
 
+struct psc_hdr {
+	u16 cur_entry;
+	u16 end_entry;
+	u32 reserved;
+};
+
+struct psc_entry {
+	u64 cur_page	: 12,
+	    gfn		: 40,
+	    operation	: 4,
+	    pagesize	: 1,
+	    reserved	: 7;
+};
+
+#define VMGEXIT_PSC_MAX_ENTRY					\
+	((GHCB_SHARED_BUF_SIZE - sizeof(struct psc_hdr)) /	\
+	 sizeof(struct psc_entry))
+
+struct snp_psc_desc {
+	struct psc_hdr hdr;
+	struct psc_entry entries[VMGEXIT_PSC_MAX_ENTRY];
+};
+
 bool amd_sev_es_enabled(void);
 efi_status_t setup_vc_handler(void);
 bool amd_sev_snp_enabled(void);
@@ -219,6 +242,10 @@ void set_pte_encrypted(unsigned long vaddr, int npages);
 bool is_validated_private_page(unsigned long vaddr, bool rmp_size);
 enum es_result  __sev_set_pages_state_msr_proto(unsigned long vaddr,
 					        int npages, int operation);
+unsigned long __sev_set_pages_state(struct snp_psc_desc *desc, unsigned long vaddr,
+				    unsigned long vaddr_end, int op,
+				    struct ghcb *ghcb, bool large_entry);
+void vc_ghcb_invalidate(struct ghcb *ghcb);
 
 unsigned long long get_amd_sev_c_bit_mask(void);
 unsigned long long get_amd_sev_addr_upperbound(void);
diff --git a/lib/x86/amd_sev_vc.c b/lib/x86/amd_sev_vc.c
index 77892edd4678..cdbd7c0bc39c 100644
--- a/lib/x86/amd_sev_vc.c
+++ b/lib/x86/amd_sev_vc.c
@@ -11,7 +11,7 @@
 
 extern phys_addr_t ghcb_addr;
 
-static void vc_ghcb_invalidate(struct ghcb *ghcb)
+void vc_ghcb_invalidate(struct ghcb *ghcb)
 {
 	ghcb->save.sw_exit_code = 0;
 	memset(ghcb->save.valid_bitmap, 0, sizeof(ghcb->save.valid_bitmap));
diff --git a/lib/x86/svm.h b/lib/x86/svm.h
index e0aafe80a290..77061c0a1980 100644
--- a/lib/x86/svm.h
+++ b/lib/x86/svm.h
@@ -372,6 +372,7 @@ struct __attribute__ ((__packed__)) vmcb {
 #define SVM_EXIT_NPF  		0x400
 
 #define SVM_EXIT_ERR		-1
+#define SVM_VMGEXIT_PSC		0x80000010
 
 #define SVM_CR0_SELECTIVE_MASK (X86_CR0_TS | X86_CR0_MP)
 
diff --git a/lib/x86/vm.c b/lib/x86/vm.c
index cfca452bb110..3547a1c26869 100644
--- a/lib/x86/vm.c
+++ b/lib/x86/vm.c
@@ -370,3 +370,29 @@ unsigned long pgtable_va_to_pa(unsigned long va)
 
 	__builtin_unreachable();
 }
+
+void *vmalloc_pages(int num_pages, int order, bool large_page)
+{
+	unsigned long length = num_pages * PAGE_SIZE;
+	pgd_t *cr3 = (pgd_t *)read_cr3();
+	void *vaddr, *paddr;
+
+	/* Allocate physical pages */
+	paddr = alloc_pages(order);
+	assert(paddr);
+
+	/* Allocate virtual pages */
+	vaddr = alloc_vpages_aligned(num_pages, large_page ? ORDER_2M : ORDER_4K);
+	assert(vaddr);
+
+	/*
+	 * Create pagetable entries that map the newly assigned virtual
+	 * pages to physical pages
+	 */
+	if (!large_page)
+		install_pages(cr3, __pa(paddr), length, vaddr);
+	else
+		install_large_pages(cr3, __pa(paddr), length, vaddr);
+
+	return vaddr;
+}
diff --git a/lib/x86/vm.h b/lib/x86/vm.h
index 0216ea1f37f9..dc77d3fcaa1c 100644
--- a/lib/x86/vm.h
+++ b/lib/x86/vm.h
@@ -61,6 +61,7 @@ static inline void *current_page_table(void)
 void split_large_page(unsigned long *ptep, int level);
 void force_4k_page(void *addr);
 unsigned long pgtable_va_to_pa(unsigned long vaddr);
+void *vmalloc_pages(int num_pages, int order, bool large_page);
 
 struct vm_vcpu_info {
         u64 cr3;
diff --git a/x86/amd_sev.c b/x86/amd_sev.c
index 3b1593e42634..15281835d0ef 100644
--- a/x86/amd_sev.c
+++ b/x86/amd_sev.c
@@ -173,6 +173,37 @@ static int test_write(unsigned long vaddr, int npages)
 	return 0;
 }
 
+static void sev_set_pages_state(unsigned long vaddr, int npages, int op,
+				struct ghcb *ghcb)
+{
+	struct snp_psc_desc desc;
+	unsigned long vaddr_end;
+	bool large_entry;
+
+	vaddr &= PAGE_MASK;
+	vaddr_end = vaddr + (npages << PAGE_SHIFT);
+
+	if (IS_ALIGNED(vaddr, LARGE_PAGE_SIZE))
+		large_entry = true;
+
+	while (vaddr < vaddr_end) {
+		vaddr = __sev_set_pages_state(&desc, vaddr, vaddr_end,
+					      op, ghcb, large_entry);
+	}
+}
+
+static void snp_free_pages(int order, int npages, unsigned long vaddr,
+			   struct ghcb *ghcb)
+{
+	set_pte_encrypted(vaddr, SEV_ALLOC_PAGE_COUNT);
+
+	/* Convert pages back to default guest-owned state */
+	sev_set_pages_state(vaddr, npages, SNP_PAGE_STATE_PRIVATE, ghcb);
+
+	/* Free all the associated physical pages */
+	free_pages_by_order((void *)pgtable_va_to_pa(vaddr), order);
+}
+
 static void test_sev_psc_ghcb_msr(void)
 {
 	void *vaddr;
@@ -210,6 +241,44 @@ static void test_sev_psc_ghcb_msr(void)
 	free_pages_by_order(vaddr, SEV_ALLOC_ORDER);
 }
 
+static void init_vpages(void)
+{
+	/*
+	 * alloc_vpages_aligned() allocates contiguous virtual
+	 * pages that grow downward from vfree_top, 0, and this is
+	 * problematic for SNP related PSC tests because
+	 * vaddr < vaddr_end using unsigned values causes an issue
+	 * (vaddr_end is 0x0). To avoid this, allocate a dummy virtual
+	 * page.
+	 */
+	alloc_vpages_aligned(1, 0);
+}
+
+static void test_sev_psc_ghcb_nae(void)
+{
+	unsigned long vaddr;
+	struct ghcb *ghcb = (struct ghcb *)rdmsr(SEV_ES_GHCB_MSR_INDEX);
+
+	report_info("TEST: GHCB Protocol based page state change test");
+
+	vaddr = (unsigned long)vmalloc_pages(SEV_ALLOC_PAGE_COUNT,
+					     SEV_ALLOC_ORDER, RMP_PG_SIZE_2M);
+
+	report(is_validated_private_page(vaddr, RMP_PG_SIZE_2M),
+	       "Expected page state: Private");
+
+	sev_set_pages_state(vaddr, SEV_ALLOC_PAGE_COUNT, SNP_PAGE_STATE_SHARED,
+			    ghcb);
+
+	set_pte_decrypted(vaddr, SEV_ALLOC_PAGE_COUNT);
+
+	report(!test_write((unsigned long)vaddr, SEV_ALLOC_PAGE_COUNT),
+	       "Write to %d unencrypted 2M pages after private->shared conversion",
+	       (SEV_ALLOC_PAGE_COUNT) / (1 << ORDER_2M));
+
+	snp_free_pages(SEV_ALLOC_ORDER, SEV_ALLOC_PAGE_COUNT, vaddr, ghcb);
+}
+
 int main(void)
 {
 	int rtn;
@@ -221,8 +290,15 @@ int main(void)
 
 	/* Setup a new page table via setup_vm() */
 	setup_vm();
-	if (amd_sev_snp_enabled())
+	if (amd_sev_snp_enabled()) {
+		/*
+		 * call init_vpages() before running any of SEV-SNP
+		 * related PSC tests.
+		 */
+		init_vpages();
 		test_sev_psc_ghcb_msr();
+		test_sev_psc_ghcb_nae();
+	}
 
 	return report_summary();
 }
