diff --git a/s390x/Makefile b/s390x/Makefile
index 8d1cfc7c..67be5360 100644
--- a/s390x/Makefile
+++ b/s390x/Makefile
@@ -42,6 +42,7 @@ tests += $(TEST_DIR)/exittime.elf
 tests += $(TEST_DIR)/ex.elf
 
 pv-tests += $(TEST_DIR)/pv-diags.elf
+pv-tests += $(TEST_DIR)/pv-icptcode.elf
 
 ifneq ($(HOST_KEY_DOCUMENT),)
 ifneq ($(GEN_SE_HEADER),)
@@ -125,6 +126,10 @@ $(TEST_DIR)/spec_ex-sie.elf: snippets = $(SNIPPET_DIR)/c/spec_ex.gbin
 $(TEST_DIR)/pv-diags.elf: pv-snippets += $(SNIPPET_DIR)/asm/pv-diag-yield.gbin
 $(TEST_DIR)/pv-diags.elf: pv-snippets += $(SNIPPET_DIR)/asm/pv-diag-288.gbin
 $(TEST_DIR)/pv-diags.elf: pv-snippets += $(SNIPPET_DIR)/asm/pv-diag-500.gbin
+$(TEST_DIR)/pv-icptcode.elf: pv-snippets += $(SNIPPET_DIR)/asm/pv-icpt-112.gbin
+$(TEST_DIR)/pv-icptcode.elf: pv-snippets += $(SNIPPET_DIR)/asm/icpt-loop.gbin
+$(TEST_DIR)/pv-icptcode.elf: pv-snippets += $(SNIPPET_DIR)/asm/loop.gbin
+$(TEST_DIR)/pv-icptcode.elf: pv-snippets += $(SNIPPET_DIR)/asm/pv-icpt-vir-timing.gbin
 
 ifneq ($(GEN_SE_HEADER),)
 snippets += $(pv-snippets)
diff --git a/s390x/pv-icptcode.c b/s390x/pv-icptcode.c
new file mode 100644
index 00000000..d7c47d6f
--- /dev/null
+++ b/s390x/pv-icptcode.c
@@ -0,0 +1,376 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * PV virtualization interception tests for intercepts that are not
+ * caused by an instruction.
+ *
+ * Copyright (c) 2023 IBM Corp
+ *
+ * Authors:
+ *  Janosch Frank <frankja@linux.ibm.com>
+ */
+#include <libcflat.h>
+#include <sie.h>
+#include <smp.h>
+#include <sclp.h>
+#include <snippet.h>
+#include <pv_icptdata.h>
+#include <asm/facility.h>
+#include <asm/barrier.h>
+#include <asm/sigp.h>
+#include <asm/uv.h>
+#include <asm/time.h>
+
+static struct vm vm, vm2;
+
+/*
+ * The hypervisor should not be able to decrease the cpu timer by an
+ * amount that is higher than the amount of time spent outside of
+ * SIE.
+ *
+ * Warning: A lot of things influence time so decreasing the timer by
+ * a more significant amount than the difference to have a safety
+ * margin is advised.
+ */
+static void test_validity_timing(void)
+{
+	extern const char SNIPPET_NAME_START(asm, pv_icpt_vir_timing)[];
+	extern const char SNIPPET_NAME_END(asm, pv_icpt_vir_timing)[];
+	extern const char SNIPPET_HDR_START(asm, pv_icpt_vir_timing)[];
+	extern const char SNIPPET_HDR_END(asm, pv_icpt_vir_timing)[];
+	int size_hdr = SNIPPET_HDR_LEN(asm, pv_icpt_vir_timing);
+	int size_gbin = SNIPPET_LEN(asm, pv_icpt_vir_timing);
+	uint64_t time_exit, time_entry, tmp;
+
+	report_prefix_push("manipulated cpu time");
+	snippet_pv_init(&vm, SNIPPET_NAME_START(asm, pv_icpt_vir_timing),
+			SNIPPET_HDR_START(asm, pv_icpt_vir_timing),
+			size_gbin, size_hdr, SNIPPET_UNPACK_OFF);
+
+	sie(&vm);
+	report(pv_icptdata_check_diag(&vm, 0x44), "spt done");
+	stck(&time_exit);
+	tmp = vm.sblk->cputm;
+	mb();
+
+	/* Cpu timer counts down so adding a ms should lead to a validity */
+	vm.sblk->cputm += S390_CLOCK_SHIFT_US * 1000;
+	sie_expect_validity(&vm);
+	sie(&vm);
+	report(uv_validity_check(&vm), "validity entry cput > exit cput");
+	vm.sblk->cputm = tmp;
+
+	/*
+	 * We are not allowed to decrement the timer more than the
+	 * time spent outside of SIE
+	 */
+	stck(&time_entry);
+	vm.sblk->cputm -= (time_entry - time_exit) + S390_CLOCK_SHIFT_US * 1000;
+	sie_expect_validity(&vm);
+	sie(&vm);
+	report(uv_validity_check(&vm), "validity entry cput < time spent outside SIE");
+	vm.sblk->cputm = tmp;
+
+	uv_destroy_guest(&vm);
+	report_prefix_pop();
+}
+
+static void run_loop(void)
+{
+	sie(&vm);
+	sigp_retry(stap(), SIGP_STOP, 0, NULL);
+}
+
+static void test_validity_already_running(void)
+{
+	extern const char SNIPPET_NAME_START(asm, loop)[];
+	extern const char SNIPPET_NAME_END(asm, loop)[];
+	extern const char SNIPPET_HDR_START(asm, loop)[];
+	extern const char SNIPPET_HDR_END(asm, loop)[];
+	int size_hdr = SNIPPET_HDR_LEN(asm, loop);
+	int size_gbin = SNIPPET_LEN(asm, loop);
+	struct psw psw = {
+		.mask = PSW_MASK_64,
+		.addr = (uint64_t)run_loop,
+	};
+
+	report_prefix_push("already running");
+	if (smp_query_num_cpus() < 3) {
+		report_skip("need at least 3 cpus for this test");
+		goto out;
+	}
+
+	snippet_pv_init(&vm, SNIPPET_NAME_START(asm, loop),
+			SNIPPET_HDR_START(asm, loop),
+			size_gbin, size_hdr, SNIPPET_UNPACK_OFF);
+
+	smp_cpu_setup(1, psw);
+	sie_expect_validity(&vm);
+	smp_cpu_setup(2, psw);
+	while (vm.sblk->icptcode != ICPT_VALIDITY) {
+		mb();
+	}
+
+	/*
+	 * One cpu will enter SIE and one will receive the validity.
+	 * We rely on the expectation that the cpu in SIE won't exit
+	 * until we had a chance to observe the validity as the exit
+	 * would overwrite the validity.
+	 *
+	 * In general that expectation is valid but HW/FW can in
+	 * theory still exit to handle their interrupts.
+	 */
+	report(uv_validity_check(&vm), "validity");
+	smp_cpu_stop(1);
+	smp_cpu_stop(2);
+	uv_destroy_guest(&vm);
+
+out:
+	report_prefix_pop();
+}
+
+/* Tests if a vcpu handle from another configuration results in a validity intercept. */
+static void test_validity_handle_not_in_config(void)
+{
+	extern const char SNIPPET_NAME_START(asm, icpt_loop)[];
+	extern const char SNIPPET_NAME_END(asm, icpt_loop)[];
+	extern const char SNIPPET_HDR_START(asm, icpt_loop)[];
+	extern const char SNIPPET_HDR_END(asm, icpt_loop)[];
+	int size_hdr = SNIPPET_HDR_LEN(asm, icpt_loop);
+	int size_gbin = SNIPPET_LEN(asm, icpt_loop);
+
+	report_prefix_push("handle not in config");
+	/* Setup our primary vm */
+	snippet_pv_init(&vm, SNIPPET_NAME_START(asm, icpt_loop),
+			SNIPPET_HDR_START(asm, icpt_loop),
+			size_gbin, size_hdr, SNIPPET_UNPACK_OFF);
+
+	/* Setup secondary vm */
+	snippet_setup_guest(&vm2, true);
+	snippet_pv_init(&vm2, SNIPPET_NAME_START(asm, icpt_loop),
+			SNIPPET_HDR_START(asm, icpt_loop),
+			size_gbin, size_hdr, SNIPPET_UNPACK_OFF);
+
+	vm.sblk->pv_handle_cpu = vm2.sblk->pv_handle_cpu;
+	sie_expect_validity(&vm);
+	sie(&vm);
+	report(uv_validity_check(&vm), "switched cpu handle");
+	vm.sblk->pv_handle_cpu = vm.uv.vcpu_handle;
+
+	vm.sblk->pv_handle_config = vm2.uv.vm_handle;
+	sie_expect_validity(&vm);
+	sie(&vm);
+	report(uv_validity_check(&vm), "switched configuration handle");
+	vm.sblk->pv_handle_config = vm.uv.vm_handle;
+
+	/* Destroy the second vm, since we don't need it for further tests */
+	uv_destroy_guest(&vm2);
+	sie_guest_destroy(&vm2);
+
+	uv_destroy_guest(&vm);
+	report_prefix_pop();
+}
+
+/* Tests if a wrong vm or vcpu handle results in a validity intercept. */
+static void test_validity_seid(void)
+{
+	extern const char SNIPPET_NAME_START(asm, icpt_loop)[];
+	extern const char SNIPPET_NAME_END(asm, icpt_loop)[];
+	extern const char SNIPPET_HDR_START(asm, icpt_loop)[];
+	extern const char SNIPPET_HDR_END(asm, icpt_loop)[];
+	int size_hdr = SNIPPET_HDR_LEN(asm, icpt_loop);
+	int size_gbin = SNIPPET_LEN(asm, icpt_loop);
+	int fails = 0;
+	int i;
+
+	report_prefix_push("handles");
+	snippet_pv_init(&vm, SNIPPET_NAME_START(asm, icpt_loop),
+			SNIPPET_HDR_START(asm, icpt_loop),
+			size_gbin, size_hdr, SNIPPET_UNPACK_OFF);
+
+	for (i = 0; i < 64; i++) {
+		vm.sblk->pv_handle_config ^= 1UL << i;
+		sie_expect_validity(&vm);
+		sie(&vm);
+		if (!uv_validity_check(&vm)) {
+			report_fail("SIE accepted wrong VM SEID, changed bit %d",
+				    63 - i);
+			fails++;
+		}
+		vm.sblk->pv_handle_config ^= 1UL << i;
+	}
+	report(!fails, "No wrong vm handle accepted");
+
+	fails = 0;
+	for (i = 0; i < 64; i++) {
+		vm.sblk->pv_handle_cpu ^= 1UL << i;
+		sie_expect_validity(&vm);
+		sie(&vm);
+		if (!uv_validity_check(&vm)) {
+			report_fail("SIE accepted wrong CPU SEID, changed bit %d",
+				    63 - i);
+			fails++;
+		}
+		vm.sblk->pv_handle_cpu ^= 1UL << i;
+	}
+	report(!fails, "No wrong cpu handle accepted");
+
+	uv_destroy_guest(&vm);
+	report_prefix_pop();
+}
+
+/*
+ * Tests if we get a validity intercept if the CR1 asce at SIE entry
+ * is not the same as the one given at the UV creation of the VM.
+ */
+static void test_validity_asce(void)
+{
+	extern const char SNIPPET_NAME_START(asm, pv_icpt_112)[];
+	extern const char SNIPPET_NAME_END(asm, pv_icpt_112)[];
+	extern const char SNIPPET_HDR_START(asm, pv_icpt_112)[];
+	extern const char SNIPPET_HDR_END(asm, pv_icpt_112)[];
+	int size_hdr = SNIPPET_HDR_LEN(asm, pv_icpt_112);
+	int size_gbin = SNIPPET_LEN(asm, pv_icpt_112);
+	uint64_t asce_old, asce_new;
+	void *pgd_new, *pgd_old;
+
+	report_prefix_push("asce");
+	snippet_pv_init(&vm, SNIPPET_NAME_START(asm, pv_icpt_112),
+			SNIPPET_HDR_START(asm, pv_icpt_112),
+			size_gbin, size_hdr, SNIPPET_UNPACK_OFF);
+
+	asce_old = vm.save_area.guest.asce;
+	pgd_new = memalign_pages_flags(PAGE_SIZE, PAGE_SIZE * 4, 0);
+	pgd_old = (void *)(asce_old & PAGE_MASK);
+
+	/* Copy the contents of the top most table */
+	memcpy(pgd_new, pgd_old, PAGE_SIZE * 4);
+
+	/* Create the replacement ASCE */
+	asce_new = __pa(pgd_new) | ASCE_DT_REGION1 | REGION_TABLE_LENGTH | ASCE_P;
+	vm.save_area.guest.asce = asce_new;
+
+	sie_expect_validity(&vm);
+	sie(&vm);
+	report(uv_validity_check(&vm), "wrong CR1 validity");
+
+	/* Restore the old ASCE */
+	vm.save_area.guest.asce = asce_old;
+
+	/* Try if we can still do an entry with the correct asce */
+	sie(&vm);
+	report(pv_icptdata_check_diag(&vm, 0x44), "re-entry with valid CR1");
+	uv_destroy_guest(&vm);
+	free_pages(pgd_new);
+	report_prefix_pop();
+}
+
+static void run_icpt_122_tests(unsigned long lc_off)
+{
+	uv_export(vm.sblk->mso + lc_off);
+	sie(&vm);
+	report(vm.sblk->icptcode == ICPT_PV_PREF, "Intercept 112 for page 0");
+	uv_import(vm.uv.vm_handle, vm.sblk->mso + lc_off);
+
+	uv_export(vm.sblk->mso + lc_off + PAGE_SIZE);
+	sie(&vm);
+	report(vm.sblk->icptcode == ICPT_PV_PREF, "Intercept 112 for page 1");
+	uv_import(vm.uv.vm_handle, vm.sblk->mso + lc_off + PAGE_SIZE);
+}
+
+static void run_icpt_122_tests_prefix(unsigned long prefix)
+{
+	uint32_t *ptr = 0;
+
+	report_prefix_pushf("0x%lx", prefix);
+	report_prefix_push("unshared");
+	run_icpt_122_tests(prefix);
+	report_prefix_pop();
+
+	/*
+	 * Guest will share the lowcore and we need to check if that
+	 * makes a difference (which it should not).
+	 */
+	report_prefix_push("shared");
+
+	sie(&vm);
+	/* Guest indicates that it has been setup via the diag 0x44 */
+	assert(pv_icptdata_check_diag(&vm, 0x44));
+	/* If the pages have not been shared these writes will cause exceptions */
+	ptr = (uint32_t *)prefix;
+	WRITE_ONCE(ptr, 0);
+	ptr = (uint32_t *)(prefix + offsetof(struct lowcore, ars_sa[0]));
+	WRITE_ONCE(ptr, 0);
+
+	run_icpt_122_tests(prefix);
+
+	/* shared*/
+	report_prefix_pop();
+	/* prefix hex value */
+	report_prefix_pop();
+}
+
+static void test_icpt_112(void)
+{
+	extern const char SNIPPET_NAME_START(asm, pv_icpt_112)[];
+	extern const char SNIPPET_NAME_END(asm, pv_icpt_112)[];
+	extern const char SNIPPET_HDR_START(asm, pv_icpt_112)[];
+	extern const char SNIPPET_HDR_END(asm, pv_icpt_112)[];
+	int size_hdr = SNIPPET_HDR_LEN(asm, pv_icpt_112);
+	int size_gbin = SNIPPET_LEN(asm, pv_icpt_112);
+
+	unsigned long lc_off = 0;
+
+	report_prefix_push("prefix");
+
+	snippet_pv_init(&vm, SNIPPET_NAME_START(asm, pv_icpt_112),
+			SNIPPET_HDR_START(asm, pv_icpt_112),
+			size_gbin, size_hdr, SNIPPET_UNPACK_OFF);
+
+	/* Setup of the guest's state for 0x0 prefix */
+	sie(&vm);
+	assert(pv_icptdata_check_diag(&vm, 0x44));
+
+	/* Test on standard 0x0 prefix */
+	run_icpt_122_tests_prefix(0);
+
+	/* Setup of the guest's state for 0x8000 prefix */
+	lc_off = 0x8000;
+	uv_import(vm.uv.vm_handle, vm.sblk->mso + lc_off);
+	uv_import(vm.uv.vm_handle, vm.sblk->mso + lc_off + PAGE_SIZE);
+	/* Guest will set prefix to 0x8000 */
+	sie(&vm);
+	/* SPX generates a PV instruction notification */
+	assert(vm.sblk->icptcode == ICPT_PV_NOTIFY && vm.sblk->ipa == 0xb210);
+	assert(*(u32 *)vm.sblk->sidad == 0x8000);
+
+	/* Test on 0x8000 prefix */
+	run_icpt_122_tests_prefix(0x8000);
+
+	/* Try a re-entry after everything has been imported again */
+	sie(&vm);
+	report(pv_icptdata_check_diag(&vm, 0x9c) &&
+	       vm.save_area.guest.grs[0] == 42,
+	       "re-entry successful");
+	report_prefix_pop();
+	uv_destroy_guest(&vm);
+}
+
+int main(void)
+{
+	report_prefix_push("pv-icpts");
+	if (!uv_host_requirement_checks())
+		goto done;
+
+	snippet_setup_guest(&vm, true);
+	test_icpt_112();
+	test_validity_asce();
+	test_validity_seid();
+	test_validity_handle_not_in_config();
+	test_validity_already_running();
+	test_validity_timing();
+	sie_guest_destroy(&vm);
+
+done:
+	report_prefix_pop();
+	return report_summary();
+}
diff --git a/s390x/snippets/asm/icpt-loop.S b/s390x/snippets/asm/icpt-loop.S
new file mode 100644
index 00000000..2aa59c01
--- /dev/null
+++ b/s390x/snippets/asm/icpt-loop.S
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Infinite loop snippet which can be used to test manipulated SIE
+ * control block intercepts. E.g. when manipulating the PV handles.
+ *
+ * Copyright (c) 2023 IBM Corp
+ *
+ * Authors:
+ *  Janosch Frank <frankja@linux.ibm.com>
+ */
+.section .text
+xgr	%r0, %r0
+retry:
+diag	0,0,0x44
+j 	retry
diff --git a/s390x/snippets/asm/loop.S b/s390x/snippets/asm/loop.S
new file mode 100644
index 00000000..a75bf00d
--- /dev/null
+++ b/s390x/snippets/asm/loop.S
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Infinite loop snippet with no exit
+ *
+ * Copyright (c) 2023 IBM Corp
+ *
+ * Authors:
+ *  Janosch Frank <frankja@linux.ibm.com>
+ */
+.section .text
+
+retry:
+j 	retry
diff --git a/s390x/snippets/asm/pv-icpt-112.S b/s390x/snippets/asm/pv-icpt-112.S
new file mode 100644
index 00000000..d9545fff
--- /dev/null
+++ b/s390x/snippets/asm/pv-icpt-112.S
@@ -0,0 +1,81 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Intercept 112 PV snippet
+ *
+ * We setup and share a prefix at 0x0 and 0x8000 which the hypervisor
+ * test will try to export and then execute a SIE entry which
+ * should result in a 112 SIE intercept.
+ *
+ * Copyright (c) 2023 IBM Corp
+ *
+ * Authors:
+ *  Janosch Frank <frankja@linux.ibm.com>
+ */
+#include <asm/asm-offsets.h>
+
+.section .text
+xgr	%r0, %r0
+xgr	%r1, %r1
+
+/* Let's tell the hypervisor we're ready to start */
+diag	0,0,0x44
+
+/*
+ * Hypervisor will export the lowcore and try a SIE entry which should
+ * result in a 112. It will then import the lowcore again and we
+ * should continue with the code below.
+ */
+
+/* Share the lowcore */
+larl	%r1, share
+.insn rrf,0xB9A40000,0,1,0,0
+xgr	%r1, %r1
+
+/*  Let's tell the hypervisor we're ready to start shared testing */
+diag	0,0,0x44
+
+/* Host: icpt:  PV instruction diag 0x44 */
+/* Host: icpt:  112 */
+
+/* Copy the invalid PGM new PSW to the new lowcore */
+larl	%r1, prfx
+l	%r2, 0(%r1)
+mvc     GEN_LC_PGM_NEW_PSW(16, %r2), GEN_LC_PGM_NEW_PSW(%r0)
+
+/* Change the prefix to 0x8000 and re-try */
+xgr	%r1, %r1
+xgr	%r2, %r2
+larl	%r2, prfx
+spx	0(%r2)
+
+/* Host: icpt:  PV instruction notification SPX*/
+/* Host: icpt:  112 */
+
+/* Share the new lowcore */
+larl	%r3, share_addr
+stg	%r2, 0(%r3)
+larl	%r2, share
+.insn rrf,0xB9A40000,0,2,0,0
+
+/* Let's tell the hypervisor we're ready to start shared testing */
+diag	0,0,0x44
+
+/* Host: icpt:  PV instruction diag 0x44 */
+/* Host: icpt:  112 */
+
+/* Test re-entry */
+lghi	%r1, 42
+diag	1,0,0x9c
+
+/* Host: icpt:  PV instruction diag 0x9c */
+
+.align 8
+share:
+	.quad 0x0030100000000000
+	.quad 0x0, 0x0, 0x0
+share_addr:
+	.quad 0x0
+	.quad 0x0
+.align 4
+prfx:
+	.long 0x00008000
diff --git a/s390x/snippets/asm/pv-icpt-vir-timing.S b/s390x/snippets/asm/pv-icpt-vir-timing.S
new file mode 100644
index 00000000..939134a2
--- /dev/null
+++ b/s390x/snippets/asm/pv-icpt-vir-timing.S
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Sets a cpu timer which the host can manipulate to check if it will
+ * receive a validity
+ *
+ * Copyright (c) 2023 IBM Corp
+ *
+ * Authors:
+ *  Janosch Frank <frankja@linux.ibm.com>
+ */
+.section .text
+larl	%r1, time_val
+spt	0 (%r1)
+diag    0, 0, 0x44
+lghi	%r1, 42
+diag	1, 0, 0x9c
+
+
+.align 8
+time_val:
+	.quad 0x280de80000
diff --git a/s390x/unittests.cfg b/s390x/unittests.cfg
index b61faf07..e2d3478e 100644
--- a/s390x/unittests.cfg
+++ b/s390x/unittests.cfg
@@ -218,3 +218,8 @@ extra_params = -append '--parallel'
 
 [execute]
 file = ex.elf
+
+[pv-icptcode]
+file = pv-icptcode.elf
+smp = 3
+extra_params = -m 2200
