Linux KVM/arm64 development list
 help / color / mirror / Atom feed
* [PATCH v3 0/8] arm64: Add Stage-2 MMU and Nested Guest Framework
@ 2026-10-01 15:16 Joey Gouly
  2026-10-01 15:16 ` [PATCH v3 1/8] lib/alloc_page: check for NULL ptr passed to free_pages() Joey Gouly
                   ` (7 more replies)
  0 siblings, 8 replies; 11+ messages in thread
From: Joey Gouly @ 2026-10-01 15:16 UTC (permalink / raw)
  To: kvmarm
  Cc: Jing Zhang, Marc Zyngier, Oliver Upton, Mark Brown, Wei-Lin Chang,
	andrew.jones, Joey Gouly

Hi all,

This is v3 of the guest framework for kvm-unit-tests/arm64, I have confirmation
from Jing that he was ok with me posting v3 on top of his v2 [1].

This series introduces lightweight infrastructure for managing arm64 stage-2
translation tables and running EL1 guests from EL2.

The stage-2 library provides page-table mapping, unmapping and context loading.
The initial implementation is deliberately limited to a 4K granule, 48-bit IPA
space and 8-bit VMIDs.

The series also includes a stage-2 demand-paging test which handles a guest
stage-2 fault by installing the missing mapping and resuming the guest.

The first 2 patches are just preparatory patches and could be merged
earlier.

Changes since v2:
- Fixed free_pages(NULL), ran into a hang and debugged it to this
- Validate the currently supported stage-2 configuration: 4K granules,
  48-bit IPAs and 8-bit VMIDs.
- Store the VTCR_EL2 and VTTBR_EL2 values in the stage-2 mmu.
- Replace s2mmu_enable()/s2mmu_disable() with s2mmu_load(); saving
  of the previous stage-2 context is now handled by the guest world switch.
- Make s2mmu_map() return errors, rather than printing.
- Fix the required barriers for stage-2 TLB invalidation.
- Split guest creation, so that the policy of mapping is in a separate
  identity helper.
- Remove the hardcoded UART mapping from guest creation. This can be added back
  ad-hoc for debugging.
- Move system register state into C, using assembly for GPRs and
  exception vectors.
- Fix the EL1 exception frame so that x29 and x30 are saved and restored
  correctly, make the frame 16-byte aligned, and use generated offsets instead
  of hard-coded values.
- Use the EL12 system-register aliases when accessing guest EL1 state from a
  VHE host.
- Fix the demand-paging test's IPA placement to avoid the identity-mapped guest
  code region and verify the expected data value explicitly.
- Added a new test to test the exception handling.
- Few other small fixes

See the full diff of v3 and v2 at:
  https://gitlab.arm.com/linux-arm/kvm-unit-tests-jg/-/compare/guest_framework_v2..guest_framework_v3

An LLM was used to review the code, and implement the host-state code
and the guest-test.c tests (both were also reviewed by me)

Thanks,
Joey

[1]: https://lore.kernel.org/kvmarm/20260413204630.1149038-1-jingzhangos@google.com/


Jing Zhang (6):
  lib: arm64: Generalize exception vector definitions for EL2 support
  lib: arm64: Add stage2 page table management library
  lib: arm64: Add foundational guest execution framework
  lib: arm64: Add support for guest exit exception handling
  lib: arm64: Add guest-internal exception handling
  arm64: Add Stage-2 MMU demand paging test

Joey Gouly (2):
  lib/alloc_page: check for NULL ptr passed to free_pages()
  arm64: Add basic guest execution tests

 arm/Makefile.arm64         |   5 +
 arm/debug.c                |   6 +-
 arm/gic.c                  |   2 +-
 arm/guest-test.c           | 157 ++++++++++++++
 arm/micro-bench.c          |   4 +-
 arm/mte.c                  |   6 +-
 arm/pl031.c                |   2 +-
 arm/pmu.c                  |   2 +-
 arm/psci.c                 |   2 +-
 arm/selftest.c             |   6 +-
 arm/stage2-mmu-test.c      | 113 +++++++++++
 arm/timer.c                |   6 +-
 lib/alloc_page.c           |   3 +-
 lib/arm64/asm-offsets.c    |  10 +
 lib/arm64/asm/esr.h        |   3 +
 lib/arm64/asm/guest.h      |  99 +++++++++
 lib/arm64/asm/processor.h  |  32 +--
 lib/arm64/asm/stage2_mmu.h |  76 +++++++
 lib/arm64/asm/sysreg.h     |  36 ++++
 lib/arm64/guest.c          | 264 ++++++++++++++++++++++++
 lib/arm64/guest_arch.S     | 223 ++++++++++++++++++++
 lib/arm64/processor.c      |   8 +-
 lib/arm64/stage2_mmu.c     | 406 +++++++++++++++++++++++++++++++++++++
 23 files changed, 1432 insertions(+), 39 deletions(-)
 create mode 100644 arm/guest-test.c
 create mode 100644 arm/stage2-mmu-test.c
 create mode 100644 lib/arm64/asm/guest.h
 create mode 100644 lib/arm64/asm/stage2_mmu.h
 create mode 100644 lib/arm64/guest.c
 create mode 100644 lib/arm64/guest_arch.S
 create mode 100644 lib/arm64/stage2_mmu.c

-- 
2.43.0


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH v3 1/8] lib/alloc_page: check for NULL ptr passed to free_pages()
  2026-10-01 15:16 [PATCH v3 0/8] arm64: Add Stage-2 MMU and Nested Guest Framework Joey Gouly
@ 2026-10-01 15:16 ` Joey Gouly
  2026-10-01 15:42   ` Mark Brown
  2026-10-01 15:16 ` [PATCH v3 2/8] lib: arm64: Generalize exception vector definitions for EL2 support Joey Gouly
                   ` (6 subsequent siblings)
  7 siblings, 1 reply; 11+ messages in thread
From: Joey Gouly @ 2026-10-01 15:16 UTC (permalink / raw)
  To: kvmarm
  Cc: Jing Zhang, Marc Zyngier, Oliver Upton, Mark Brown, Wei-Lin Chang,
	andrew.jones, Joey Gouly

Don't call virt_to_pfn() before checking for NULL.
---
 lib/alloc_page.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/alloc_page.c b/lib/alloc_page.c
index 84f01e11..cbada1f7 100644
--- a/lib/alloc_page.c
+++ b/lib/alloc_page.c
@@ -265,13 +265,14 @@ static bool coalesce(struct mem_area *a, u8 order, pfn_t pfn, pfn_t pfn2)
  */
 static void _free_pages(void *mem)
 {
-	pfn_t pfn2, pfn = virt_to_pfn(mem);
+	pfn_t pfn2, pfn;
 	struct mem_area *a = NULL;
 	uintptr_t i, p;
 	u8 order;
 
 	if (!mem)
 		return;
+	pfn = virt_to_pfn(mem);
 	assert(IS_ALIGNED((uintptr_t)mem, PAGE_SIZE));
 
 	/* find which area this pointer belongs to*/
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v3 2/8] lib: arm64: Generalize exception vector definitions for EL2 support
  2026-10-01 15:16 [PATCH v3 0/8] arm64: Add Stage-2 MMU and Nested Guest Framework Joey Gouly
  2026-10-01 15:16 ` [PATCH v3 1/8] lib/alloc_page: check for NULL ptr passed to free_pages() Joey Gouly
@ 2026-10-01 15:16 ` Joey Gouly
  2026-10-01 15:16 ` [PATCH v3 3/8] lib: arm64: Add stage2 page table management library Joey Gouly
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 11+ messages in thread
From: Joey Gouly @ 2026-10-01 15:16 UTC (permalink / raw)
  To: kvmarm
  Cc: Jing Zhang, Marc Zyngier, Oliver Upton, Mark Brown, Wei-Lin Chang,
	andrew.jones, Joey Gouly

From: Jing Zhang <jingzhangos@google.com>

Rename EL1-specific exception vector enumerations to ELx equivalents (e.g.,
EL1H to ELxH and EL0 to ELx_LOW) to make them level-agnostic. This allows
these identifiers to be reused when running at other exception levels, such
as EL2, supporting the upcoming guest management library.

Signed-off-by: Jing Zhang <jingzhangos@google.com>
Signed-off-by: Joey Gouly <joey.gouly@arm.com>
---
 arm/debug.c               |  6 +++---
 arm/gic.c                 |  2 +-
 arm/micro-bench.c         |  4 ++--
 arm/mte.c                 |  6 +++---
 arm/pl031.c               |  2 +-
 arm/pmu.c                 |  2 +-
 arm/psci.c                |  2 +-
 arm/selftest.c            |  6 +++---
 arm/timer.c               |  6 +++---
 lib/arm64/asm/processor.h | 32 ++++++++++++++++----------------
 lib/arm64/processor.c     |  8 ++++----
 11 files changed, 38 insertions(+), 38 deletions(-)

diff --git a/arm/debug.c b/arm/debug.c
index 0299fd28..9ba93ac2 100644
--- a/arm/debug.c
+++ b/arm/debug.c
@@ -272,7 +272,7 @@ static noinline void test_hw_bp(bool migrate)
 		return;
 	}
 
-	install_exception_handler(EL1H_SYNC, ESR_EC_HW_BP_CURRENT, hw_bp_handler);
+	install_exception_handler(ELxH_SYNC, ESR_EC_HW_BP_CURRENT, hw_bp_handler);
 
 	reset_debug_state();
 
@@ -324,7 +324,7 @@ static noinline void test_wp(bool migrate)
 		return;
 	}
 
-	install_exception_handler(EL1H_SYNC, ESR_EC_WP_CURRENT, wp_handler);
+	install_exception_handler(ELxH_SYNC, ESR_EC_WP_CURRENT, wp_handler);
 
 	reset_debug_state();
 
@@ -365,7 +365,7 @@ static noinline void test_ss(bool migrate)
 	extern unsigned char ss_start;
 	uint32_t mdscr;
 
-	install_exception_handler(EL1H_SYNC, ESR_EC_SSTEP_CURRENT, ss_handler);
+	install_exception_handler(ELxH_SYNC, ESR_EC_SSTEP_CURRENT, ss_handler);
 
 	reset_debug_state();
 
diff --git a/arm/gic.c b/arm/gic.c
index 256dd80d..cccc5ae1 100644
--- a/arm/gic.c
+++ b/arm/gic.c
@@ -180,7 +180,7 @@ static void setup_irq(irq_handler_fn handler)
 #ifdef __arm__
 	install_exception_handler(EXCPTN_IRQ, handler);
 #else
-	install_irq_handler(EL1H_IRQ, handler);
+	install_irq_handler(ELxH_IRQ, handler);
 #endif
 	local_irq_enable();
 }
diff --git a/arm/micro-bench.c b/arm/micro-bench.c
index a6a78f20..6ca36c55 100644
--- a/arm/micro-bench.c
+++ b/arm/micro-bench.c
@@ -52,7 +52,7 @@ static void gic_irq_handler(struct pt_regs *regs)
 
 static void gic_secondary_entry(void *data)
 {
-	install_irq_handler(EL1H_IRQ, gic_irq_handler);
+	install_irq_handler(ELxH_IRQ, gic_irq_handler);
 	gic_enable_defaults();
 	local_irq_enable();
 	irq_ready = true;
@@ -212,7 +212,7 @@ static void lpi_exec(void)
 static bool timer_prep(void)
 {
 	gic_enable_defaults();
-	install_irq_handler(EL1H_IRQ, gic_irq_handler);
+	install_irq_handler(ELxH_IRQ, gic_irq_handler);
 	local_irq_enable();
 
 	if (current_level() == CurrentEL_EL1)
diff --git a/arm/mte.c b/arm/mte.c
index 35707fc6..ad71b104 100644
--- a/arm/mte.c
+++ b/arm/mte.c
@@ -192,7 +192,7 @@ static void mte_sync_test(void)
 
 	mte_exception = false;
 
-	install_exception_handler(EL1H_SYNC, ESR_ELx_EC_DABT_CUR, mte_fault_handler);
+	install_exception_handler(ELxH_SYNC, ESR_ELx_EC_DABT_CUR, mte_fault_handler);
 
 	mem_read(tagged(mem, 2), &val);
 
@@ -218,12 +218,12 @@ static void mte_asymm_test(void)
 	mte_set_tcf(MTE_TCF_ASYMM);
 	mte_exception = false;
 
-	install_exception_handler(EL1H_SYNC, ESR_ELx_EC_DABT_CUR, mte_fault_handler);
+	install_exception_handler(ELxH_SYNC, ESR_ELx_EC_DABT_CUR, mte_fault_handler);
 
 	mem_read(tagged(mem, 3), &val);
 	report((val == 0) && mte_exception && (get_clear_tfsr() == 0), "read");
 
-	install_exception_handler(EL1H_SYNC, ESR_ELx_EC_DABT_CUR, NULL);
+	install_exception_handler(ELxH_SYNC, ESR_ELx_EC_DABT_CUR, NULL);
 
 	mem_write(tagged(mem, 4), 0xaaaaaaaa);
 	report((*mem == 0xaaaaaaaa) && (get_clear_tfsr() == TFSR_EL1_TF0), "write");
diff --git a/arm/pl031.c b/arm/pl031.c
index c56805e4..3c739d4d 100644
--- a/arm/pl031.c
+++ b/arm/pl031.c
@@ -162,7 +162,7 @@ static int check_rtc_irq(void)
 	writel(before + seconds_to_wait, &pl031->mr);
 
 #ifdef __aarch64__
-	install_irq_handler(EL1H_IRQ, irq_handler);
+	install_irq_handler(ELxH_IRQ, irq_handler);
 #else
 	install_exception_handler(EXCPTN_IRQ, irq_handler);
 #endif
diff --git a/arm/pmu.c b/arm/pmu.c
index 2fcec71a..90878641 100644
--- a/arm/pmu.c
+++ b/arm/pmu.c
@@ -968,7 +968,7 @@ static void test_overflow_interrupt(bool overflow_at_64bits)
 		return;
 
 	gic_enable_defaults();
-	install_irq_handler(EL1H_IRQ, irq_handler);
+	install_irq_handler(ELxH_IRQ, irq_handler);
 	local_irq_enable();
 	gic_enable_irq(23);
 
diff --git a/arm/psci.c b/arm/psci.c
index 5800c0ac..1f87736a 100644
--- a/arm/psci.c
+++ b/arm/psci.c
@@ -36,7 +36,7 @@ static void install_invalid_function_handler(exception_fn handler)
 #ifdef __arm__
 	install_exception_handler(EXCPTN_UND, handler);
 #else
-	install_exception_handler(EL1H_SYNC, ESR_ELx_EC_UNKNOWN, handler);
+	install_exception_handler(ELxH_SYNC, ESR_ELx_EC_UNKNOWN, handler);
 #endif
 }
 
diff --git a/arm/selftest.c b/arm/selftest.c
index d9f41e8e..ba81d556 100644
--- a/arm/selftest.c
+++ b/arm/selftest.c
@@ -305,7 +305,7 @@ static enum vector check_vector_prep(void)
 	unsigned long daif;
 
 	if (is_user())
-		return EL0_SYNC_64;
+		return ELx_LOW_SYNC_64;
 
 	asm volatile("mrs %0, daif" : "=r" (daif) ::);
 	expected_regs.pstate = daif;
@@ -313,7 +313,7 @@ static enum vector check_vector_prep(void)
 		expected_regs.pstate |= PSR_MODE_EL1h;
 	else
 		expected_regs.pstate |= PSR_MODE_EL2h;
-	return EL1H_SYNC;
+	return ELxH_SYNC;
 }
 
 static void unknown_handler(struct pt_regs *regs, unsigned int esr __unused)
@@ -400,7 +400,7 @@ static void check_vectors(void *arg __unused)
 #ifdef __arm__
 		install_exception_handler(EXCPTN_UND, user_psci_system_off);
 #else
-		install_exception_handler(EL0_SYNC_64, ESR_ELx_EC_UNKNOWN,
+		install_exception_handler(ELx_LOW_SYNC_64, ESR_ELx_EC_UNKNOWN,
 					  user_psci_system_off);
 #endif
 	} else {
diff --git a/arm/timer.c b/arm/timer.c
index 898043f0..ed332b93 100644
--- a/arm/timer.c
+++ b/arm/timer.c
@@ -356,9 +356,9 @@ static void test_init(void)
 		vtimer_info.irq = TIMER_HVTIMER_IRQ;
 	}
 
-	install_exception_handler(EL1H_SYNC, ESR_ELx_EC_UNKNOWN, ptimer_unsupported_handler);
+	install_exception_handler(ELxH_SYNC, ESR_ELx_EC_UNKNOWN, ptimer_unsupported_handler);
 	ptimer_info.read_ctl();
-	install_exception_handler(EL1H_SYNC, ESR_ELx_EC_UNKNOWN, NULL);
+	install_exception_handler(ELxH_SYNC, ESR_ELx_EC_UNKNOWN, NULL);
 
 	if (ptimer_unsupported && !ERRATA(7b6b46311a85)) {
 		report_skip("Skipping ptimer tests. Set ERRATA_7b6b46311a85=y to enable.");
@@ -369,7 +369,7 @@ static void test_init(void)
 
 	gic_enable_defaults();
 
-	install_irq_handler(EL1H_IRQ, irq_handler);
+	install_irq_handler(ELxH_IRQ, irq_handler);
 	set_timer_irq_enabled(&ptimer_info, true);
 	set_timer_irq_enabled(&vtimer_info, true);
 	local_irq_enable();
diff --git a/lib/arm64/asm/processor.h b/lib/arm64/asm/processor.h
index 32ddc1b3..c20dd599 100644
--- a/lib/arm64/asm/processor.h
+++ b/lib/arm64/asm/processor.h
@@ -13,22 +13,22 @@
 #include <asm/barrier.h>
 
 enum vector {
-	EL1T_SYNC,
-	EL1T_IRQ,
-	EL1T_FIQ,
-	EL1T_ERROR,
-	EL1H_SYNC,
-	EL1H_IRQ,
-	EL1H_FIQ,
-	EL1H_ERROR,
-	EL0_SYNC_64,
-	EL0_IRQ_64,
-	EL0_FIQ_64,
-	EL0_ERROR_64,
-	EL0_SYNC_32,
-	EL0_IRQ_32,
-	EL0_FIQ_32,
-	EL0_ERROR_32,
+	ELxT_SYNC,
+	ELxT_IRQ,
+	ELxT_FIQ,
+	ELxT_ERROR,
+	ELxH_SYNC,
+	ELxH_IRQ,
+	ELxH_FIQ,
+	ELxH_ERROR,
+	ELx_LOW_SYNC_64,
+	ELx_LOW_IRQ_64,
+	ELx_LOW_FIQ_64,
+	ELx_LOW_ERROR_64,
+	ELx_LOW_SYNC_32,
+	ELx_LOW_IRQ_32,
+	ELx_LOW_FIQ_32,
+	ELx_LOW_ERROR_32,
 	VECTOR_MAX,
 };
 
diff --git a/lib/arm64/processor.c b/lib/arm64/processor.c
index 86328c01..5e5cdc77 100644
--- a/lib/arm64/processor.c
+++ b/lib/arm64/processor.c
@@ -198,10 +198,10 @@ void default_vector_irq_handler(enum vector v, struct pt_regs *regs,
 
 void vector_handlers_default_init(vector_fn *handlers)
 {
-	handlers[EL1H_SYNC]	= default_vector_sync_handler;
-	handlers[EL1H_IRQ]	= default_vector_irq_handler;
-	handlers[EL0_SYNC_64]	= default_vector_sync_handler;
-	handlers[EL0_IRQ_64]	= default_vector_irq_handler;
+	handlers[ELxH_SYNC]	= default_vector_sync_handler;
+	handlers[ELxH_IRQ]	= default_vector_irq_handler;
+	handlers[ELx_LOW_SYNC_64]	= default_vector_sync_handler;
+	handlers[ELx_LOW_IRQ_64]	= default_vector_irq_handler;
 }
 
 /* Needed to compile with -Wmissing-prototypes */
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v3 3/8] lib: arm64: Add stage2 page table management library
  2026-10-01 15:16 [PATCH v3 0/8] arm64: Add Stage-2 MMU and Nested Guest Framework Joey Gouly
  2026-10-01 15:16 ` [PATCH v3 1/8] lib/alloc_page: check for NULL ptr passed to free_pages() Joey Gouly
  2026-10-01 15:16 ` [PATCH v3 2/8] lib: arm64: Generalize exception vector definitions for EL2 support Joey Gouly
@ 2026-10-01 15:16 ` Joey Gouly
  2026-10-01 15:16 ` [PATCH v3 4/8] lib: arm64: Add foundational guest execution framework Joey Gouly
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 11+ messages in thread
From: Joey Gouly @ 2026-10-01 15:16 UTC (permalink / raw)
  To: kvmarm
  Cc: Jing Zhang, Marc Zyngier, Oliver Upton, Mark Brown, Wei-Lin Chang,
	andrew.jones, Joey Gouly

From: Jing Zhang <jingzhangos@google.com>

Tests running at EL2 (hypervisor level) require the ability to manage Stage
2 translation tables to control Guest Physical Address (IPA) to Host
Physical Address (PA) translation.

Add a generic Stage 2 MMU library that provides software management of
ARM64 Stage-2 translation tables.

The library features include:
- Support for 4K translation granules, further work needed for 16K/64K.
- Dynamic page table allocation using the allocator.
- Basic support for block mappings where applicable.
- APIs for mapping, unmapping, and loading the Stage 2 MMU.
- Basic fault info reporting (ESR, FAR, HPFAR).

Signed-off-by: Jing Zhang <jingzhangos@google.com>
Signed-off-by: Joey Gouly <joey.gouly@arm.com>
---
 arm/Makefile.arm64         |   1 +
 lib/arm64/asm/stage2_mmu.h |  76 +++++++
 lib/arm64/asm/sysreg.h     |  29 +++
 lib/arm64/stage2_mmu.c     | 406 +++++++++++++++++++++++++++++++++++++
 4 files changed, 512 insertions(+)
 create mode 100644 lib/arm64/asm/stage2_mmu.h
 create mode 100644 lib/arm64/stage2_mmu.c

diff --git a/arm/Makefile.arm64 b/arm/Makefile.arm64
index a40c830d..5e50f5ba 100644
--- a/arm/Makefile.arm64
+++ b/arm/Makefile.arm64
@@ -40,6 +40,7 @@ cflatobjs += lib/arm64/stack.o
 cflatobjs += lib/arm64/processor.o
 cflatobjs += lib/arm64/spinlock.o
 cflatobjs += lib/arm64/gic-v3-its.o lib/arm64/gic-v3-its-cmd.o
+cflatobjs += lib/arm64/stage2_mmu.o
 
 ifeq ($(CONFIG_EFI),y)
 cflatobjs += lib/acpi.o
diff --git a/lib/arm64/asm/stage2_mmu.h b/lib/arm64/asm/stage2_mmu.h
new file mode 100644
index 00000000..182225e1
--- /dev/null
+++ b/lib/arm64/asm/stage2_mmu.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2026, Google LLC.
+ * Author: Jing Zhang <jingzhangos@google.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+#ifndef _ASMARM64_STAGE2_MMU_H_
+#define _ASMARM64_STAGE2_MMU_H_
+
+#include <libcflat.h>
+#include <asm/page.h>
+#include <asm/pgtable.h>
+
+#define pte_is_table(pte)	(pte_val(pte) & PTE_TABLE_BIT)
+
+/* Stage-2 Memory Attributes (MemAttr[3:0]) */
+#define S2_MEMATTR_NORMAL	(0xFUL << 2) /* Normal Memory, Outer/Inner Write-Back */
+#define S2_MEMATTR_DEVICE	(0x0UL << 2) /* Device-nGnRnE */
+
+/* Stage-2 Access Permissions (S2AP[1:0]) */
+#define S2AP_NONE	(0UL << 6)
+#define S2AP_RO		(1UL << 6) /* Read-only */
+#define S2AP_WO		(2UL << 6) /* Write-only */
+#define S2AP_RW		(3UL << 6) /* Read-Write */
+
+/* Flags for mapping */
+#define S2_MAP_RW	(S2AP_RW | S2_MEMATTR_NORMAL | PTE_AF | PTE_SHARED)
+#define S2_MAP_DEVICE	(S2AP_RW | S2_MEMATTR_DEVICE | PTE_AF)
+
+enum s2_granule {
+	S2_PAGE_4K,
+	S2_PAGE_16K,
+	S2_PAGE_64K,
+};
+
+enum s2mmu_error {
+	S2MMU_ERR_NOMEM = -1,
+	S2MMU_ERR_MAPPING_CONFLICT = -2,
+};
+
+/* Main Stage-2 MMU Structure */
+struct s2_mmu {
+	pgd_t *pgd;
+	int vmid;
+	unsigned long vtcr_el2;
+	unsigned long vttbr_el2;
+
+	/* Configuration */
+	enum s2_granule granule;
+	bool allow_block_mappings;
+
+	/* Internal helpers calculated from granule & VA_BITS */
+	unsigned int page_shift;
+	unsigned int level_shift;
+	int root_level; /* 0, 1, or 2 */
+	unsigned long page_size;
+	unsigned long block_size;
+};
+
+/* API */
+/* Initialize an s2_mmu struct with specific settings */
+struct s2_mmu *s2mmu_init(int vmid, enum s2_granule granule, bool allow_block_mappings);
+
+/* Management */
+void s2mmu_destroy(struct s2_mmu *mmu);
+int s2mmu_map(struct s2_mmu *mmu, unsigned long ipa, unsigned long pa,
+	      unsigned long size, unsigned long flags);
+void s2mmu_unmap(struct s2_mmu *mmu, unsigned long ipa, unsigned long size);
+
+/* Install this context on the current CPU. */
+void s2mmu_load(const struct s2_mmu *mmu);
+
+/* Debug */
+void s2mmu_print_fault_info(void);
+
+#endif /* _ASMARM64_STAGE2_MMU_H_ */
diff --git a/lib/arm64/asm/sysreg.h b/lib/arm64/asm/sysreg.h
index f2d05018..72767cf0 100644
--- a/lib/arm64/asm/sysreg.h
+++ b/lib/arm64/asm/sysreg.h
@@ -118,6 +118,35 @@ asm(
 #define SCTLR_EL1_TCF0_SHIFT	38
 #define SCTLR_EL1_TCF0_MASK	GENMASK_ULL(39, 38)
 
+#define VTCR_EL2_RES1		_BITULL(31)
+
+#define VTCR_EL2_T0SZ_MASK	GENMASK_ULL(5, 0)
+
+#define VTCR_EL2_SL0_SHIFT	6
+
+#define VTCR_EL2_IRGN0_SHIFT	8
+#define VTCR_EL2_IRGN0_WBWA	(UL(1) << VTCR_EL2_IRGN0_SHIFT)
+
+#define VTCR_EL2_ORGN0_SHIFT	10
+#define VTCR_EL2_ORGN0_WBWA	(UL(1) << VTCR_EL2_ORGN0_SHIFT)
+
+#define VTCR_EL2_SH0_SHIFT	12
+#define VTCR_EL2_SH0_INNER	(UL(3) << VTCR_EL2_SH0_SHIFT)
+
+#define VTCR_EL2_TG0_SHIFT	14
+#define VTCR_EL2_TG0_4K		(UL(0) << VTCR_EL2_TG0_SHIFT)
+#define VTCR_EL2_TG0_64K	(UL(1) << VTCR_EL2_TG0_SHIFT)
+#define VTCR_EL2_TG0_16K	(UL(2) << VTCR_EL2_TG0_SHIFT)
+
+#define VTCR_EL2_PS_SHIFT	16
+#define VTCR_EL2_PS_40BIT	(UL(2) << VTCR_EL2_PS_SHIFT)
+#define VTCR_EL2_PS_48BIT	(UL(5) << VTCR_EL2_PS_SHIFT)
+
+#define VTTBR_EL2_CNP		_BITULL(0)
+#define VTTBR_EL2_BADDR_MASK	GENMASK_ULL(47, 1)
+#define VTTBR_EL2_VMID_SHIFT	48
+#define VTTBR_EL2_VMID_MASK	GENMASK_ULL(63, 48)
+
 #define HCR_EL2_TGE		_BITULL(27)
 #define HCR_EL2_RW		_BITULL(31)
 #define HCR_EL2_E2H		_BITULL(34)
diff --git a/lib/arm64/stage2_mmu.c b/lib/arm64/stage2_mmu.c
new file mode 100644
index 00000000..c1c371f4
--- /dev/null
+++ b/lib/arm64/stage2_mmu.c
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2026, Google LLC.
+ * Author: Jing Zhang <jingzhangos@google.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+#include <libcflat.h>
+#include <alloc.h>
+#include <asm/stage2_mmu.h>
+#include <asm/sysreg.h>
+#include <asm/io.h>
+#include <asm/barrier.h>
+#include <alloc_page.h>
+
+/* Physical Address Size (PS) - Derive from VA_BITS for simplicity or max */
+#if VA_BITS > 40
+#define VTCR_PS_VAL		VTCR_EL2_PS_48BIT
+#else
+#define VTCR_PS_VAL		VTCR_EL2_PS_40BIT
+#endif
+
+#define S2MMU_SUPPORTED_IPA_BITS	48
+#define S2MMU_MAX_VMID			255
+
+#if VA_BITS != S2MMU_SUPPORTED_IPA_BITS
+#error "The stage-2 MMU currently supports only a 48-bit IPA"
+#endif
+
+static unsigned long s2mmu_build_vtcr(const struct s2_mmu *mmu)
+{
+	unsigned long vtcr = VTCR_EL2_RES1 | VTCR_PS_VAL |
+	                     VTCR_EL2_SH0_INNER | VTCR_EL2_ORGN0_WBWA |
+			     VTCR_EL2_IRGN0_WBWA;
+
+	switch (mmu->granule) {
+	case S2_PAGE_4K:
+		vtcr |= VTCR_EL2_TG0_4K;
+		/* SL0 Encodings for 4K: 0=L2, 1=L1, 2=L0 */
+		if (mmu->root_level == 0)
+			vtcr |= (2UL << VTCR_EL2_SL0_SHIFT); /* Start L0 */
+		else if (mmu->root_level == 1)
+			vtcr |= (1UL << VTCR_EL2_SL0_SHIFT); /* Start L1 */
+		else
+			vtcr |= (0UL << VTCR_EL2_SL0_SHIFT); /* Start L2 */
+		break;
+	case S2_PAGE_16K:
+		vtcr |= VTCR_EL2_TG0_16K;
+		/* SL0 Encodings for 16K: 1=L2, 2=L1 */
+		if (mmu->root_level == 1)
+			vtcr |= (2UL << VTCR_EL2_SL0_SHIFT); /* Start L1 */
+		else
+			vtcr |= (1UL << VTCR_EL2_SL0_SHIFT); /* Start L2 */
+		break;
+	case S2_PAGE_64K:
+		vtcr |= VTCR_EL2_TG0_64K;
+		/* SL0 Encodings for 64K: 1=L2, 2=L1 */
+		if (mmu->root_level == 1)
+			vtcr |= (2UL << VTCR_EL2_SL0_SHIFT); /* Start L1 */
+		else
+			vtcr |= (1UL << VTCR_EL2_SL0_SHIFT); /* Start L2 */
+		break;
+	}
+
+	return vtcr | ((64 - VA_BITS) & VTCR_EL2_T0SZ_MASK);
+}
+
+struct s2_mmu *s2mmu_init(int vmid, enum s2_granule granule, bool allow_block_mappings)
+{
+	struct s2_mmu *mmu;
+	int order = 0;
+
+	/*
+	 * Only accept limited configurations for now.
+	 */
+	if (vmid < 0 || vmid > S2MMU_MAX_VMID || granule != S2_PAGE_4K)
+		return NULL;
+
+	mmu = calloc(1, sizeof(*mmu));
+	if (!mmu)
+		return NULL;
+
+	mmu->vmid = vmid;
+	mmu->granule = granule;
+	mmu->allow_block_mappings = allow_block_mappings;
+
+	/* Configure shifts based on granule */
+	switch (granule) {
+	case S2_PAGE_4K:
+		mmu->page_shift = 12;
+		mmu->level_shift = 9;
+		/*
+		 * Determine Root Level for 4K:
+		 * VA_BITS > 39 (e.g. 48) -> Start L0
+		 * VA_BITS <= 39 (e.g. 32, 36) -> Start L1
+		 */
+		mmu->root_level = (VA_BITS > 39) ? 0 : 1;
+		break;
+	case S2_PAGE_16K:
+		mmu->page_shift = 14;
+		mmu->level_shift = 11;
+		/*
+		 * 16K: L1 covers 47 bits. L0 not valid for 16K
+		 * Start L1 for 47 bits. Start L2 for 36 bits.
+		 */
+		mmu->root_level = (VA_BITS > 36) ? 1 : 2;
+		break;
+	case S2_PAGE_64K:
+		mmu->page_shift = 16;
+		mmu->level_shift = 13;
+		/* 64K: L1 covers 52 bits. L2 covers 42 bits. */
+		mmu->root_level = (VA_BITS > 42) ? 1 : 2;
+		break;
+	}
+
+	mmu->page_size = 1UL << mmu->page_shift;
+	mmu->block_size = 1UL << (mmu->page_shift + mmu->level_shift);
+
+	/* Alloc PGD. Use order for allocation size */
+	if (mmu->page_size > PAGE_SIZE) {
+		order = __builtin_ctz(mmu->page_size / PAGE_SIZE);
+	}
+	mmu->pgd = (pgd_t *)alloc_pages(order);
+	if (mmu->pgd) {
+		memset(mmu->pgd, 0, mmu->page_size);
+	} else {
+		free(mmu);
+		return NULL;
+	}
+
+	mmu->vtcr_el2 = s2mmu_build_vtcr(mmu);
+	mmu->vttbr_el2 = (virt_to_phys(mmu->pgd) & VTTBR_EL2_BADDR_MASK) |
+			    (((unsigned long)mmu->vmid << VTTBR_EL2_VMID_SHIFT) &
+			     VTTBR_EL2_VMID_MASK);
+
+	return mmu;
+}
+
+static unsigned long s2mmu_get_addr_mask(struct s2_mmu *mmu)
+{
+	switch (mmu->granule) {
+	case S2_PAGE_16K:
+		return GENMASK_ULL(47, 14);
+	case S2_PAGE_64K:
+		return GENMASK_ULL(47, 16);
+	default:
+		return GENMASK_ULL(47, 12); /* 4K */
+	}
+}
+
+static void s2mmu_tlbi_all(void)
+{
+	/* Publish page-table writes before invalidating cached translations. */
+	dsb(ishst);
+	asm volatile("tlbi vmalls12e1is" : : : "memory");
+	dsb(ish);
+	isb();
+}
+
+static void s2mmu_free_tables(struct s2_mmu *mmu, pte_t *table, int level)
+{
+	unsigned long entries = 1UL << mmu->level_shift;
+	unsigned long mask = s2mmu_get_addr_mask(mmu);
+	unsigned long i;
+
+	/*
+	 * Recurse if not leaf level
+	 * Level 3 is always leaf page. Levels 0-2 can be Table or Block.
+	 */
+	if (level < 3) {
+		for (i = 0; i < entries; i++) {
+			pte_t entry = table[i];
+			if ((pte_valid(entry) && pte_is_table(entry))) {
+				pte_t *next = (pte_t *)phys_to_virt(pte_val(entry) & mask);
+				s2mmu_free_tables(mmu, next, level + 1);
+			}
+		}
+	}
+
+	free_pages(table);
+}
+
+void s2mmu_destroy(struct s2_mmu *mmu)
+{
+	if (mmu->pgd)
+		s2mmu_free_tables(mmu, (pte_t *)mmu->pgd, mmu->root_level);
+	free(mmu);
+}
+
+void s2mmu_load(const struct s2_mmu *mmu)
+{
+	write_sysreg(mmu->vtcr_el2, vtcr_el2);
+	write_sysreg(mmu->vttbr_el2, vttbr_el2);
+	isb();
+	s2mmu_tlbi_all();
+}
+
+static int get_pte(struct s2_mmu *mmu, pte_t *table, unsigned long idx,
+		   pte_t **next_table)
+{
+	unsigned long mask = s2mmu_get_addr_mask(mmu);
+	pte_t entry = table[idx];
+	int order = 0;
+
+	if (pte_valid(entry)) {
+		if (!pte_is_table(entry))
+			return S2MMU_ERR_MAPPING_CONFLICT;
+
+		*next_table = (pte_t *)phys_to_virt(pte_val(entry) & mask);
+		return 0;
+	}
+
+	/* Allocate table memory covering the Stage-2 Granule size */
+	if (mmu->page_size > PAGE_SIZE)
+		order = __builtin_ctz(mmu->page_size / PAGE_SIZE);
+
+	*next_table = (pte_t *)alloc_pages(order);
+	if (!*next_table)
+		return S2MMU_ERR_NOMEM;
+
+	memset(*next_table, 0, mmu->page_size);
+
+	pte_val(entry) = virt_to_phys(*next_table) | PTE_TABLE_BIT | PTE_VALID;
+	WRITE_ONCE(table[idx], entry);
+
+	return 0;
+}
+
+int s2mmu_map(struct s2_mmu *mmu, unsigned long ipa, unsigned long pa,
+	      unsigned long size, unsigned long flags)
+{
+	unsigned long level_mask, level_shift, level_size, level;
+	unsigned long start_ipa, end_ipa, idx;
+	pte_t entry, *table, *next_table;
+	bool is_block_level;
+	int ret;
+
+	start_ipa = ipa;
+	end_ipa = ipa + size;
+	level_mask = (1UL << mmu->level_shift) - 1;
+
+	while (start_ipa < end_ipa) {
+		table = (pte_t *)mmu->pgd;
+
+		/* Walk from Root to Leaf */
+		for (level = mmu->root_level; level < 3; level++) {
+			level_shift = mmu->page_shift + (3 - level) * mmu->level_shift;
+			idx = (start_ipa >> level_shift) & level_mask;
+			level_size = 1UL << level_shift;
+
+			/*
+			 * Check for Block Mapping
+			 * Valid Block Levels:
+			 * 4K:  L1 (1G), L2 (2MB)
+			 * 16K: L2 (32MB)
+			 * 64K: L2 (512MB)
+			 */
+			is_block_level = (level == 2) ||
+				(mmu->granule == S2_PAGE_4K && level == 1);
+
+			if (mmu->allow_block_mappings && is_block_level) {
+				if ((start_ipa & (level_size - 1)) == 0 &&
+				    (pa & (level_size - 1)) == 0 &&
+				    (start_ipa + level_size) <= end_ipa) {
+					/* Map Block */
+					pte_val(entry) = (pa & ~(level_size - 1)) |
+							 flags | PTE_VALID;
+					WRITE_ONCE(table[idx], entry);
+					start_ipa += level_size;
+					pa += level_size;
+					goto next_chunk; /* Continue outer loop */
+				}
+			}
+
+			/* Move to next level */
+			ret = get_pte(mmu, table, idx, &next_table);
+			if (ret)
+				return ret;
+			table = next_table;
+		}
+
+		/* Leaf Level (Level 3 PTE) */
+		if (level == 3) {
+			idx = (start_ipa >> mmu->page_shift) & level_mask;
+			pte_val(entry) = (pa & ~(mmu->page_size - 1)) | flags | PTE_TYPE_PAGE;
+			WRITE_ONCE(table[idx], entry);
+			start_ipa += mmu->page_size;
+			pa += mmu->page_size;
+		}
+
+next_chunk:
+		continue;
+	}
+
+	s2mmu_tlbi_all();
+	return 0;
+}
+
+/*
+ * Recursive helper to unmap a range within a specific table.
+ * Returns true if the table at this level is now completely empty
+ * and should be freed by the caller.
+ */
+static bool s2mmu_unmap_level(struct s2_mmu *mmu, pte_t *table,
+			      unsigned long current_ipa, int level,
+			      unsigned long start_ipa, unsigned long end_ipa,
+			      unsigned long mask)
+{
+	unsigned long level_size, entry_ipa, entry_end;
+	bool child_empty, table_empty = true;
+	pte_t entry, *next_table;
+	unsigned int level_shift;
+	unsigned long i;
+
+	/* Calculate shift and size for this level */
+	if (level == 3) {
+		level_shift = mmu->page_shift;
+	} else {
+		level_shift = mmu->page_shift + (3 - level) * mmu->level_shift;
+	}
+	level_size = 1UL << level_shift;
+
+	/* Iterate over all entries in this table */
+	for (i = 0; i < (1UL << mmu->level_shift); i++) {
+		entry = table[i];
+		entry_ipa = current_ipa + (i * level_size);
+		entry_end = entry_ipa + level_size;
+
+		/* Skip entries completely outside our target range */
+		if (entry_end <= start_ipa || entry_ipa >= end_ipa) {
+			if (pte_valid(entry))
+				table_empty = false;
+			continue;
+		}
+
+		/*
+		 * If the entry is fully covered by the unmap range,
+		 * we can clear it (leaf) or recurse and free (table).
+		 */
+		if (entry_ipa >= start_ipa && entry_end <= end_ipa) {
+			if (pte_valid(entry)) {
+				if (pte_is_table(entry) && level < 3) {
+					/* Recurse to free children first */
+					next_table = (pte_t *)phys_to_virt(pte_val(entry) & mask);
+					s2mmu_free_tables(mmu, next_table, level + 1);
+				}
+				/* Invalidate the entry */
+				WRITE_ONCE(table[i], __pte(0));
+			}
+			continue;
+		}
+
+		/*
+		 * Partial overlap: This must be a table (split required).
+		 * If it's a Block, we can't split easily in this context
+		 * without complex logic, so we generally skip or fail.
+		 * Assuming standard breakdown: recurse into the table.
+		 */
+		if (pte_valid(entry) && pte_is_table(entry) && level < 3) {
+			next_table = (pte_t *)phys_to_virt(pte_val(entry) & mask);
+			child_empty = s2mmu_unmap_level(mmu, next_table, entry_ipa, level + 1,
+							start_ipa, end_ipa, mask);
+
+			if (child_empty) {
+				free_pages(next_table);
+				WRITE_ONCE(table[i], __pte(0));
+			} else {
+				table_empty = false;
+			}
+		} else if (pte_valid(entry)) {
+			/*
+			 * Overlap on a leaf/block entry that extends
+			 * beyond the unmap range. We cannot simply clear it.
+			 */
+			table_empty = false;
+		}
+	}
+
+	return table_empty;
+}
+
+void s2mmu_unmap(struct s2_mmu *mmu, unsigned long ipa, unsigned long size)
+{
+	unsigned long end_ipa = ipa + size;
+	unsigned long mask = s2mmu_get_addr_mask(mmu);
+
+	if (!mmu->pgd)
+		return;
+
+	/*
+	 * Start recursion from the root level.
+	 * We rarely free the PGD itself unless destroying the MMU,
+	 * so we ignore the return value here.
+	 */
+	s2mmu_unmap_level(mmu, (pte_t *)mmu->pgd, 0, mmu->root_level,
+			  ipa, end_ipa, mask);
+
+	s2mmu_tlbi_all();
+}
+
+void s2mmu_print_fault_info(void)
+{
+	unsigned long esr = read_sysreg(esr_el2);
+	unsigned long far = read_sysreg(far_el2);
+	unsigned long hpfar = read_sysreg(hpfar_el2);
+	printf("Stage-2 Fault Info: ESR=0x%lx FAR=0x%lx HPFAR=0x%lx\n", esr, far, hpfar);
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v3 4/8] lib: arm64: Add foundational guest execution framework
  2026-10-01 15:16 [PATCH v3 0/8] arm64: Add Stage-2 MMU and Nested Guest Framework Joey Gouly
                   ` (2 preceding siblings ...)
  2026-10-01 15:16 ` [PATCH v3 3/8] lib: arm64: Add stage2 page table management library Joey Gouly
@ 2026-10-01 15:16 ` Joey Gouly
  2026-10-01 15:16 ` [PATCH v3 5/8] lib: arm64: Add support for guest exit exception handling Joey Gouly
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 11+ messages in thread
From: Joey Gouly @ 2026-10-01 15:16 UTC (permalink / raw)
  To: kvmarm
  Cc: Jing Zhang, Marc Zyngier, Oliver Upton, Mark Brown, Wei-Lin Chang,
	andrew.jones, Joey Gouly

From: Jing Zhang <jingzhangos@google.com>

Introduce the infrastructure to manage and execute guest when running at
EL2.

The framework includes:
- 'struct guest': Encapsulates vCPU state (GPRs, EL1/EL2 sysregs) and
  Stage-2 MMU context.
- guest_create() / guest_destroy(): Handle lifecycle management and
  Stage-2 MMU setup.
- guest_create_identity(): helper to create a guest and setup some
  mappings.
- guest_run(): entry point that saves host callee-saved
  registers, caches the guest context pointer in TPIDR_EL2, and
  performs the exception return (ERET) to the guest.

Signed-off-by: Jing Zhang <jingzhangos@google.com>
Signed-off-by: Joey Gouly <joey.gouly@arm.com>
---
 arm/Makefile.arm64      |   2 +
 lib/arm64/asm-offsets.c |   4 ++
 lib/arm64/asm/guest.h   |  53 +++++++++++++++++
 lib/arm64/asm/sysreg.h  |   6 ++
 lib/arm64/guest.c       | 127 ++++++++++++++++++++++++++++++++++++++++
 lib/arm64/guest_arch.S  |  51 ++++++++++++++++
 6 files changed, 243 insertions(+)
 create mode 100644 lib/arm64/asm/guest.h
 create mode 100644 lib/arm64/guest.c
 create mode 100644 lib/arm64/guest_arch.S

diff --git a/arm/Makefile.arm64 b/arm/Makefile.arm64
index 5e50f5ba..9026fd71 100644
--- a/arm/Makefile.arm64
+++ b/arm/Makefile.arm64
@@ -41,6 +41,8 @@ cflatobjs += lib/arm64/processor.o
 cflatobjs += lib/arm64/spinlock.o
 cflatobjs += lib/arm64/gic-v3-its.o lib/arm64/gic-v3-its-cmd.o
 cflatobjs += lib/arm64/stage2_mmu.o
+cflatobjs += lib/arm64/guest.o
+cflatobjs += lib/arm64/guest_arch.o
 
 ifeq ($(CONFIG_EFI),y)
 cflatobjs += lib/acpi.o
diff --git a/lib/arm64/asm-offsets.c b/lib/arm64/asm-offsets.c
index 80de023c..c55e4bb9 100644
--- a/lib/arm64/asm-offsets.c
+++ b/lib/arm64/asm-offsets.c
@@ -8,6 +8,7 @@
 #include <libcflat.h>
 #include <kbuild.h>
 #include <asm/ptrace.h>
+#include <asm/guest.h>
 
 int main(void)
 {
@@ -30,5 +31,8 @@ int main(void)
 	DEFINE(S_FP, sizeof(struct pt_regs));
 	DEFINE(S_FRAME_SIZE, (sizeof(struct pt_regs) + 16));
 
+	OFFSET(GUEST_X_OFFSET, guest, x);
+	OFFSET(GUEST_HCR_OFFSET, guest, hcr_el2);
+
 	return 0;
 }
diff --git a/lib/arm64/asm/guest.h b/lib/arm64/asm/guest.h
new file mode 100644
index 00000000..50fc3338
--- /dev/null
+++ b/lib/arm64/asm/guest.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2026, Google LLC.
+ * Author: Jing Zhang <jingzhangos@google.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+#ifndef _ASMARM64_GUEST_H_
+#define _ASMARM64_GUEST_H_
+
+#include <libcflat.h>
+#include <asm/processor.h>
+#include <asm/stage2_mmu.h>
+
+#define HCR_GUEST_FLAGS (HCR_EL2_VM | HCR_EL2_FMO | HCR_EL2_IMO | \
+			 HCR_EL2_AMO | HCR_EL2_RW | HCR_EL2_E2H)
+/* Guest stack size */
+#define GUEST_STACK_SIZE		SZ_64K
+
+struct guest {
+	/* General Purpose Registers */
+	unsigned long x[31]; /* x0..x30 */
+
+	/* Execution State */
+	unsigned long elr_el2;
+	unsigned long spsr_el2;
+
+	/* Control Registers */
+	unsigned long hcr_el2;
+	unsigned long sctlr_el1;
+	unsigned long sp_el1;
+
+	/* Exit Information */
+	unsigned long esr_el2;
+	unsigned long far_el2;
+	unsigned long hpfar_el2;
+	unsigned long exit_code;
+
+	struct s2_mmu *s2mmu;
+	void *guest_stack;
+};
+
+/*
+ * Create a guest from a stage-2 context and entry IPA.
+ */
+struct guest *guest_create(struct s2_mmu *s2mmu, unsigned long entry_ipa);
+
+/* Helper for tests using identity-mapped host code and a stack. */
+struct guest *guest_create_identity(int vmid, void (*guest_func)(void),
+				    enum s2_granule granule);
+void guest_destroy(struct guest *guest);
+void guest_run(struct guest *guest);
+
+#endif /* _ASMARM64_GUEST_H_ */
diff --git a/lib/arm64/asm/sysreg.h b/lib/arm64/asm/sysreg.h
index 72767cf0..b3b7e221 100644
--- a/lib/arm64/asm/sysreg.h
+++ b/lib/arm64/asm/sysreg.h
@@ -147,6 +147,10 @@ asm(
 #define VTTBR_EL2_VMID_SHIFT	48
 #define VTTBR_EL2_VMID_MASK	GENMASK_ULL(63, 48)
 
+#define HCR_EL2_VM		_BITULL(0)
+#define HCR_EL2_FMO		_BITULL(3)
+#define HCR_EL2_IMO		_BITULL(4)
+#define HCR_EL2_AMO		_BITULL(5)
 #define HCR_EL2_TGE		_BITULL(27)
 #define HCR_EL2_RW		_BITULL(31)
 #define HCR_EL2_E2H		_BITULL(34)
@@ -161,6 +165,8 @@ asm(
 #define SYS_HFGWTR2_EL2		sys_reg(3, 4, 3, 1, 3)
 #define SYS_HFGITR2_EL2		sys_reg(3, 4, 3, 1, 7)
 
+#define SYS_SCTLR_EL12		sys_reg(3, 5, 1, 0, 0)
+
 #define INIT_SCTLR_EL1_MMU_OFF	\
 			(SCTLR_EL1_ITD | SCTLR_EL1_SED | SCTLR_EL1_EOS | \
 			 SCTLR_EL1_TSCXT | SCTLR_EL1_EIS | SCTLR_EL1_SPAN | \
diff --git a/lib/arm64/guest.c b/lib/arm64/guest.c
new file mode 100644
index 00000000..b348e628
--- /dev/null
+++ b/lib/arm64/guest.c
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2026, Google LLC.
+ * Author: Jing Zhang <jingzhangos@google.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+#include <libcflat.h>
+#include <asm/guest.h>
+#include <asm/io.h>
+#include <asm/sysreg.h>
+#include <asm/barrier.h>
+#include <alloc_page.h>
+#include <alloc.h>
+
+extern void __guest_switch_regs(struct guest *guest);
+
+static void guest_restore_state(const struct guest *guest)
+{
+	write_sysreg(guest->elr_el2, elr_el2);
+	write_sysreg(guest->spsr_el2, spsr_el2);
+	write_sysreg_s(guest->sctlr_el1, SYS_SCTLR_EL12);
+	write_sysreg(guest->sp_el1, sp_el1);
+}
+
+static void guest_arch_enter(struct guest *guest)
+{
+	s2mmu_load(guest->s2mmu);
+	guest_restore_state(guest);
+	__guest_switch_regs(guest);
+}
+
+void guest_run(struct guest *guest)
+{
+	guest_arch_enter(guest);
+}
+
+struct guest *guest_create(struct s2_mmu *s2_ctx, unsigned long entry_ipa)
+{
+	struct guest *guest;
+
+	if (!s2_ctx)
+		return NULL;
+
+	guest = calloc(1, sizeof(*guest));
+	if (!guest)
+		return NULL;
+
+	guest->elr_el2 = entry_ipa;
+	guest->spsr_el2 = PSR_MODE_EL1h |
+		PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT;
+	guest->hcr_el2 = HCR_GUEST_FLAGS;
+
+	guest->sctlr_el1 = read_sysreg_s(SYS_SCTLR_EL12);
+	/* Disable guest stage 1 translation */
+	guest->sctlr_el1 &= ~(SCTLR_EL1_M | SCTLR_EL1_C);
+	guest->sctlr_el1 |= SCTLR_EL1_I;
+
+	guest->s2mmu = s2_ctx;
+
+	return guest;
+}
+
+struct guest *guest_create_identity(int vmid, void (*guest_func)(void),
+				    enum s2_granule granule)
+{
+	unsigned long guest_pa, code_base, stack_pa;
+	unsigned long *stack_page;
+	struct guest *guest;
+	struct s2_mmu *ctx;
+	int ret;
+
+	ctx = s2mmu_init(vmid, granule, true);
+	if (!ctx)
+		return NULL;
+
+	/*
+	 * Map the Host's code segment Identity Mapped (IPA=PA).
+	 * To be safe, we map a large chunk (e.g., 2MB) around the function
+	 * to capture any helper functions the compiler might generate calls to.
+	 */
+	guest_pa = virt_to_phys((void *)guest_func);
+	code_base = guest_pa & ~(SZ_2M - 1);
+	ret = s2mmu_map(ctx, code_base, code_base, SZ_2M, S2_MAP_RW);
+	if (ret)
+		goto destroy_s2;
+
+	/*
+	 * Map Stack
+	 * Allocate 16 pages (64K) in Host, get its PA, and map it for Guest.
+	 */
+	stack_page = alloc_pages(get_order(GUEST_STACK_SIZE >> PAGE_SHIFT));
+	if (!stack_page)
+		goto destroy_s2;
+	stack_pa = virt_to_phys(stack_page);
+	/* Identity Map it (IPA = PA) */
+	ret = s2mmu_map(ctx, stack_pa, stack_pa, GUEST_STACK_SIZE, S2_MAP_RW);
+	if (ret)
+		goto free_stack;
+
+	/* Create Guest */
+	/* Entry point is the PA of the function (Identity Mapped) */
+	guest = guest_create(ctx, guest_pa);
+	if (!guest)
+		goto free_stack;
+
+	/*
+	 * Setup Guest Stack Pointer
+	 * Must match where we mapped the stack + Offset
+	 */
+	guest->sp_el1 = stack_pa + GUEST_STACK_SIZE;
+	guest->guest_stack = stack_page;
+
+	return guest;
+
+free_stack:
+	free_pages(stack_page);
+destroy_s2:
+	s2mmu_destroy(ctx);
+	return NULL;
+}
+
+void guest_destroy(struct guest *guest)
+{
+	s2mmu_destroy(guest->s2mmu);
+	free_pages(guest->guest_stack);
+	free(guest);
+}
diff --git a/lib/arm64/guest_arch.S b/lib/arm64/guest_arch.S
new file mode 100644
index 00000000..16b95835
--- /dev/null
+++ b/lib/arm64/guest_arch.S
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2026, Google LLC.
+ * Author: Jing Zhang <jingzhangos@google.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+#define __ASSEMBLY__
+#include <asm/asm-offsets.h>
+#include <asm/sysreg.h>
+
+#define GUEST_XREG_OFFSET(x)	(GUEST_X_OFFSET + 8 * (x))
+
+.global __guest_switch_regs
+__guest_switch_regs:
+	/* x0 = struct guest pointer */
+
+	/* Save Host Callee-Saved Regs */
+	stp	x29, x30, [sp, #-16]!
+	stp	x27, x28, [sp, #-16]!
+	stp	x25, x26, [sp, #-16]!
+	stp	x23, x24, [sp, #-16]!
+	stp	x21, x22, [sp, #-16]!
+	stp	x19, x20, [sp, #-16]!
+
+	/*
+	 * Commit the guest EL2 state only after the world-switch frame is valid.
+	 */
+	ldr	x1, [x0, #GUEST_HCR_OFFSET]
+	msr	tpidr_el2, x0
+	msr	hcr_el2, x1
+	isb
+
+	/* Load Guest GPRs */
+	ldp	x1, x2, [x0, #GUEST_XREG_OFFSET(1)]
+	ldp	x3, x4, [x0, #GUEST_XREG_OFFSET(3)]
+	ldp	x5, x6, [x0, #GUEST_XREG_OFFSET(5)]
+	ldp	x7, x8, [x0, #GUEST_XREG_OFFSET(7)]
+	ldp	x9, x10, [x0, #GUEST_XREG_OFFSET(9)]
+	ldp	x11, x12, [x0, #GUEST_XREG_OFFSET(11)]
+	ldp	x13, x14, [x0, #GUEST_XREG_OFFSET(13)]
+	ldp	x15, x16, [x0, #GUEST_XREG_OFFSET(15)]
+	ldp	x17, x18, [x0, #GUEST_XREG_OFFSET(17)]
+	ldp	x19, x20, [x0, #GUEST_XREG_OFFSET(19)]
+	ldp	x21, x22, [x0, #GUEST_XREG_OFFSET(21)]
+	ldp	x23, x24, [x0, #GUEST_XREG_OFFSET(23)]
+	ldp	x25, x26, [x0, #GUEST_XREG_OFFSET(25)]
+	ldp	x27, x28, [x0, #GUEST_XREG_OFFSET(27)]
+	ldp	x29, x30, [x0, #GUEST_XREG_OFFSET(29)]
+	ldr	x0, [x0, #GUEST_XREG_OFFSET(0)]
+
+	eret
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v3 5/8] lib: arm64: Add support for guest exit exception handling
  2026-10-01 15:16 [PATCH v3 0/8] arm64: Add Stage-2 MMU and Nested Guest Framework Joey Gouly
                   ` (3 preceding siblings ...)
  2026-10-01 15:16 ` [PATCH v3 4/8] lib: arm64: Add foundational guest execution framework Joey Gouly
@ 2026-10-01 15:16 ` Joey Gouly
  2026-10-01 15:16 ` [PATCH v3 6/8] lib: arm64: Add guest-internal " Joey Gouly
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 11+ messages in thread
From: Joey Gouly @ 2026-10-01 15:16 UTC (permalink / raw)
  To: kvmarm
  Cc: Jing Zhang, Marc Zyngier, Oliver Upton, Mark Brown, Wei-Lin Chang,
	andrew.jones, Joey Gouly

From: Jing Zhang <jingzhangos@google.com>

Implement the logic to intercept and handle exceptions from managed
guests at EL2. This completes the context switch loop by providing a
mechanism to save guest state, route exceptions to C handlers, and
either resume the guest or return to the host.

Key additions:
- 'hyp_el2_vectors': An EL2 vector table installed during guest
  execution to trap guest exits (Synchronous, IRQ, etc.).
- 'guest_c_exception_handler': A C-level dispatcher that updates the
  guest context with exception info (ESR, FAR, HPFAR) and invokes
  registered per-vector handlers.
- Integrated save/restore logic: guest_run() now handles the full
  cycle of saving host state, loading guest state, trapping exits,
  and optionally resuming or returning.
- Handler API: Added guest_install_handler() to allow tests to register
  custom logic for specific exception types.

Signed-off-by: Jing Zhang <jingzhangos@google.com>
Signed-off-by: Joey Gouly <joey.gouly@arm.com>
---
 lib/arm64/asm-offsets.c |   1 +
 lib/arm64/asm/guest.h   |  19 +++++++
 lib/arm64/guest.c       |  94 +++++++++++++++++++++++++++++++++--
 lib/arm64/guest_arch.S  | 106 ++++++++++++++++++++++++++++++++++++++--
 4 files changed, 212 insertions(+), 8 deletions(-)

diff --git a/lib/arm64/asm-offsets.c b/lib/arm64/asm-offsets.c
index c55e4bb9..1fc595ff 100644
--- a/lib/arm64/asm-offsets.c
+++ b/lib/arm64/asm-offsets.c
@@ -33,6 +33,7 @@ int main(void)
 
 	OFFSET(GUEST_X_OFFSET, guest, x);
 	OFFSET(GUEST_HCR_OFFSET, guest, hcr_el2);
+	OFFSET(GUEST_EXIT_CODE_OFFSET, guest, exit_code);
 
 	return 0;
 }
diff --git a/lib/arm64/asm/guest.h b/lib/arm64/asm/guest.h
index 50fc3338..9ad344e1 100644
--- a/lib/arm64/asm/guest.h
+++ b/lib/arm64/asm/guest.h
@@ -16,6 +16,19 @@
 /* Guest stack size */
 #define GUEST_STACK_SIZE		SZ_64K
 
+/*
+ * Result from Handler:
+ * RESUME: Keep guest running (ERET immediately)
+ * EXIT:   Return to Host C caller
+ */
+enum guest_handler_result {
+	GUEST_ACTION_RESUME,
+	GUEST_ACTION_EXIT
+};
+
+struct guest;
+typedef enum guest_handler_result (*guest_handler_t)(struct guest *guest);
+
 struct guest {
 	/* General Purpose Registers */
 	unsigned long x[31]; /* x0..x30 */
@@ -35,6 +48,9 @@ struct guest {
 	unsigned long hpfar_el2;
 	unsigned long exit_code;
 
+	/* Exception Handlers in EL2 */
+	guest_handler_t handlers[VECTOR_MAX];
+
 	struct s2_mmu *s2mmu;
 	void *guest_stack;
 };
@@ -50,4 +66,7 @@ struct guest *guest_create_identity(int vmid, void (*guest_func)(void),
 void guest_destroy(struct guest *guest);
 void guest_run(struct guest *guest);
 
+unsigned long guest_c_exception_handler(struct guest *guest);
+void guest_install_handler(struct guest *guest, enum vector v, guest_handler_t handler);
+
 #endif /* _ASMARM64_GUEST_H_ */
diff --git a/lib/arm64/guest.c b/lib/arm64/guest.c
index b348e628..afb91559 100644
--- a/lib/arm64/guest.c
+++ b/lib/arm64/guest.c
@@ -12,7 +12,47 @@
 #include <alloc_page.h>
 #include <alloc.h>
 
-extern void __guest_switch_regs(struct guest *guest);
+/* Host state owned by a guest_run() invocation, not by the guest. */
+struct guest_host_state {
+	unsigned long hcr_el2;
+	unsigned long vtcr_el2;
+	unsigned long vttbr_el2;
+	unsigned long vbar_el2;
+	unsigned long tpidr_el2;
+	unsigned long daif;
+};
+
+extern void __guest_switch_regs(struct guest *guest,
+				struct guest_host_state *host_state);
+
+static void guest_save_host_state(struct guest_host_state *state)
+{
+	state->hcr_el2 = read_sysreg(hcr_el2);
+	state->vtcr_el2 = read_sysreg(vtcr_el2);
+	state->vttbr_el2 = read_sysreg(vttbr_el2);
+	state->vbar_el2 = read_sysreg(vbar_el2);
+	state->tpidr_el2 = read_sysreg(tpidr_el2);
+	state->daif = read_sysreg(daif);
+}
+
+static void guest_restore_host_state(const struct guest_host_state *state)
+{
+	write_sysreg(state->vtcr_el2, vtcr_el2);
+	write_sysreg(state->vttbr_el2, vttbr_el2);
+	write_sysreg(state->vbar_el2, vbar_el2);
+	write_sysreg(state->hcr_el2, hcr_el2);
+	write_sysreg(state->tpidr_el2, tpidr_el2);
+	isb();
+	write_sysreg(state->daif, daif);
+}
+
+static void guest_save_state(struct guest *guest)
+{
+	guest->elr_el2 = read_sysreg(elr_el2);
+	guest->spsr_el2 = read_sysreg(spsr_el2);
+	guest->sctlr_el1 = read_sysreg_s(SYS_SCTLR_EL12);
+	guest->sp_el1 = read_sysreg(sp_el1);
+}
 
 static void guest_restore_state(const struct guest *guest)
 {
@@ -22,18 +62,58 @@ static void guest_restore_state(const struct guest *guest)
 	write_sysreg(guest->sp_el1, sp_el1);
 }
 
-static void guest_arch_enter(struct guest *guest)
+static void guest_arch_enter(struct guest *guest,
+			     struct guest_host_state *host_state)
 {
 	s2mmu_load(guest->s2mmu);
 	guest_restore_state(guest);
-	__guest_switch_regs(guest);
+	__guest_switch_regs(guest, host_state);
+}
+
+void guest_arch_exit(const struct guest_host_state *state)
+{
+	guest_restore_host_state(state);
 }
 
 void guest_run(struct guest *guest)
 {
-	guest_arch_enter(guest);
+	struct guest_host_state host_state;
+
+	guest_save_host_state(&host_state);
+	guest_arch_enter(guest, &host_state);
 }
 
+/*
+ * C-Entry for Exception Handling
+ * Returns 0 to Resume Guest, 1 to Exit to Host Caller
+ */
+unsigned long guest_c_exception_handler(struct guest *guest)
+{
+	enum vector vector;
+
+	/* Save guest system-register state before invoking the exit handler. */
+	guest_save_state(guest);
+	guest->esr_el2 = read_sysreg(esr_el2);
+	guest->far_el2 = read_sysreg(far_el2);
+	guest->hpfar_el2 = read_sysreg(hpfar_el2);
+
+	if (guest->exit_code >= VECTOR_MAX)
+		return 1;
+	vector = (enum vector)guest->exit_code;
+
+	/* Invoke Handler if registered */
+	if (guest->handlers[vector]) {
+		if (guest->handlers[vector](guest) == GUEST_ACTION_RESUME) {
+			guest_restore_state(guest);
+			return 0; /* ASM stub will restore and ERET */
+		}
+	}
+
+	/* Default: Exit to caller */
+	return 1;
+}
+
+
 struct guest *guest_create(struct s2_mmu *s2_ctx, unsigned long entry_ipa)
 {
 	struct guest *guest;
@@ -125,3 +205,9 @@ void guest_destroy(struct guest *guest)
 	free_pages(guest->guest_stack);
 	free(guest);
 }
+
+void guest_install_handler(struct guest *guest, enum vector v, guest_handler_t handler)
+{
+	if (v < VECTOR_MAX)
+		guest->handlers[v] = handler;
+}
diff --git a/lib/arm64/guest_arch.S b/lib/arm64/guest_arch.S
index 16b95835..33d0dc53 100644
--- a/lib/arm64/guest_arch.S
+++ b/lib/arm64/guest_arch.S
@@ -12,7 +12,10 @@
 
 .global __guest_switch_regs
 __guest_switch_regs:
-	/* x0 = struct guest pointer */
+	/*
+	 * x0 = struct guest pointer
+	 * x1 = host system-register state
+	 */
 
 	/* Save Host Callee-Saved Regs */
 	stp	x29, x30, [sp, #-16]!
@@ -21,13 +24,18 @@ __guest_switch_regs:
 	stp	x23, x24, [sp, #-16]!
 	stp	x21, x22, [sp, #-16]!
 	stp	x19, x20, [sp, #-16]!
+	stp	x1, xzr, [sp, #-16]!
 
 	/*
-	 * Commit the guest EL2 state only after the world-switch frame is valid.
+	 * Commit the guest EL2 state only after the exit frame is valid. No
+	 * exception may reach the guest vectors without this frame in place.
 	 */
-	ldr	x1, [x0, #GUEST_HCR_OFFSET]
+	ldr	x2, [x0, #GUEST_HCR_OFFSET]
+	adrp	x3, hyp_el2_vectors
+	add	x3, x3, :lo12:hyp_el2_vectors
 	msr	tpidr_el2, x0
-	msr	hcr_el2, x1
+	msr	vbar_el2, x3
+	msr	hcr_el2, x2
 	isb
 
 	/* Load Guest GPRs */
@@ -49,3 +57,93 @@ __guest_switch_regs:
 	ldr	x0, [x0, #GUEST_XREG_OFFSET(0)]
 
 	eret
+
+/* EL2 Hypervisor Vector Table */
+.macro hyp_el2_vector, vector_num
+	.align 7
+	stp	x0, x1, [sp, #-16]!
+	mrs	x0, tpidr_el2
+	mov	x1, \vector_num
+	str	x1, [x0, #GUEST_EXIT_CODE_OFFSET]
+	b	guest_common_exit
+.endm
+
+	.align 11
+.global hyp_el2_vectors
+hyp_el2_vectors:
+	.skip 0x200
+
+	hyp_el2_vector #4	/* ELxH_SYNC */
+	hyp_el2_vector #5	/* ELxH_IRQ */
+	hyp_el2_vector #6	/* ELxH_FIQ */
+	hyp_el2_vector #7	/* ELxH_ERROR */
+
+	hyp_el2_vector #8	/* ELx_LOW_SYNC_64 */
+	hyp_el2_vector #9	/* ELx_LOW_IRQ_64 */
+	hyp_el2_vector #10	/* ELx_LOW_FIQ_64 */
+	hyp_el2_vector #11	/* ELx_LOW_ERROR_64 */
+
+	.align 11
+
+guest_common_exit:
+	stp	x2, x3, [x0, #GUEST_XREG_OFFSET(2)]
+	stp	x4, x5, [x0, #GUEST_XREG_OFFSET(4)]
+	stp	x6, x7, [x0, #GUEST_XREG_OFFSET(6)]
+	stp	x8, x9, [x0, #GUEST_XREG_OFFSET(8)]
+	stp	x10, x11, [x0, #GUEST_XREG_OFFSET(10)]
+	stp	x12, x13, [x0, #GUEST_XREG_OFFSET(12)]
+	stp	x14, x15, [x0, #GUEST_XREG_OFFSET(14)]
+	stp	x16, x17, [x0, #GUEST_XREG_OFFSET(16)]
+	stp	x18, x19, [x0, #GUEST_XREG_OFFSET(18)]
+	stp	x20, x21, [x0, #GUEST_XREG_OFFSET(20)]
+	stp	x22, x23, [x0, #GUEST_XREG_OFFSET(22)]
+	stp	x24, x25, [x0, #GUEST_XREG_OFFSET(24)]
+	stp	x26, x27, [x0, #GUEST_XREG_OFFSET(26)]
+	stp	x28, x29, [x0, #GUEST_XREG_OFFSET(28)]
+	str	x30, [x0, #GUEST_XREG_OFFSET(30)]
+
+	ldp	x2, x3, [sp], #16
+	stp	x2, x3, [x0, #GUEST_XREG_OFFSET(0)]
+
+	bl	guest_c_exception_handler
+	cbz	x0, guest_resume_guest
+
+	/* EXIT */
+	/*
+	 * Restore the host system registers while this world-switch frame and
+	 * the guest exit vector are still valid.
+	 */
+	ldr	x0, [sp]
+	bl	guest_arch_exit
+	add	sp, sp, #16
+
+	/* Restore Host Callee-Saved Regs */
+	ldp	x19, x20, [sp], #16
+	ldp	x21, x22, [sp], #16
+	ldp	x23, x24, [sp], #16
+	ldp	x25, x26, [sp], #16
+	ldp	x27, x28, [sp], #16
+	ldp	x29, x30, [sp], #16
+	ret
+
+	/* RESUME */
+guest_resume_guest:
+	mrs	x0, tpidr_el2
+
+	ldp	x1, x2, [x0, #GUEST_XREG_OFFSET(1)]
+	ldp	x3, x4, [x0, #GUEST_XREG_OFFSET(3)]
+	ldp	x5, x6, [x0, #GUEST_XREG_OFFSET(5)]
+	ldp	x7, x8, [x0, #GUEST_XREG_OFFSET(7)]
+	ldp	x9, x10, [x0, #GUEST_XREG_OFFSET(9)]
+	ldp	x11, x12, [x0, #GUEST_XREG_OFFSET(11)]
+	ldp	x13, x14, [x0, #GUEST_XREG_OFFSET(13)]
+	ldp	x15, x16, [x0, #GUEST_XREG_OFFSET(15)]
+	ldp	x17, x18, [x0, #GUEST_XREG_OFFSET(17)]
+	ldp	x19, x20, [x0, #GUEST_XREG_OFFSET(19)]
+	ldp	x21, x22, [x0, #GUEST_XREG_OFFSET(21)]
+	ldp	x23, x24, [x0, #GUEST_XREG_OFFSET(23)]
+	ldp	x25, x26, [x0, #GUEST_XREG_OFFSET(25)]
+	ldp	x27, x28, [x0, #GUEST_XREG_OFFSET(27)]
+	ldp	x29, x30, [x0, #GUEST_XREG_OFFSET(29)]
+	ldr	x0, [x0, #GUEST_XREG_OFFSET(0)]
+	eret
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v3 6/8] lib: arm64: Add guest-internal exception handling
  2026-10-01 15:16 [PATCH v3 0/8] arm64: Add Stage-2 MMU and Nested Guest Framework Joey Gouly
                   ` (4 preceding siblings ...)
  2026-10-01 15:16 ` [PATCH v3 5/8] lib: arm64: Add support for guest exit exception handling Joey Gouly
@ 2026-10-01 15:16 ` Joey Gouly
  2026-10-01 15:16 ` [PATCH v3 7/8] arm64: Add Stage-2 MMU demand paging test Joey Gouly
  2026-10-01 15:16 ` [PATCH v3 8/8] arm64: Add basic guest execution tests Joey Gouly
  7 siblings, 0 replies; 11+ messages in thread
From: Joey Gouly @ 2026-10-01 15:16 UTC (permalink / raw)
  To: kvmarm
  Cc: Jing Zhang, Marc Zyngier, Oliver Upton, Mark Brown, Wei-Lin Chang,
	andrew.jones, Joey Gouly

From: Jing Zhang <jingzhangos@google.com>

Implement the infrastructure for managed guests to handle their own
exceptions at EL1. This allows guests to manage internal traps
independently of the EL2 host.

Changes include:
- 'guest_el1_vectors': A dedicated EL1 vector table for guests,
  supporting synchronous exceptions, IRQs, FIQs, and SErrors.
- 'guest_context': A per-guest metadata structure tracked via
  TPIDR_EL1, containing a dispatch table for guest-internal handlers.
- 'guest_el1_c_handler': C-level dispatcher that executes registered
  guest handlers or triggers an HVC trap back to the host for unhandled
  exceptions.
- Integrated setup: guest_create() now allocates and maps the guest
  context, while the context switch logic ensures VBAR_EL1 and
  TPIDR_EL1 are correctly saved and restored.

Signed-off-by: Jing Zhang <jingzhangos@google.com>
Signed-off-by: Joey Gouly <joey.gouly@arm.com>
---
 lib/arm64/asm-offsets.c |  5 +++
 lib/arm64/asm/guest.h   | 27 +++++++++++++++
 lib/arm64/asm/sysreg.h  |  1 +
 lib/arm64/guest.c       | 51 ++++++++++++++++++++++++++++
 lib/arm64/guest_arch.S  | 74 +++++++++++++++++++++++++++++++++++++++++
 5 files changed, 158 insertions(+)

diff --git a/lib/arm64/asm-offsets.c b/lib/arm64/asm-offsets.c
index 1fc595ff..5081f1df 100644
--- a/lib/arm64/asm-offsets.c
+++ b/lib/arm64/asm-offsets.c
@@ -35,5 +35,10 @@ int main(void)
 	OFFSET(GUEST_HCR_OFFSET, guest, hcr_el2);
 	OFFSET(GUEST_EXIT_CODE_OFFSET, guest, exit_code);
 
+	OFFSET(GUEST_EL1_X_OFFSET, guest_el1_regs, regs);
+	OFFSET(GUEST_EL1_ELR_OFFSET, guest_el1_regs, elr);
+	OFFSET(GUEST_EL1_SPSR_OFFSET, guest_el1_regs, spsr);
+	DEFINE(GUEST_EL1_REGS_SIZE, sizeof(struct guest_el1_regs));
+
 	return 0;
 }
diff --git a/lib/arm64/asm/guest.h b/lib/arm64/asm/guest.h
index 9ad344e1..d62fdc65 100644
--- a/lib/arm64/asm/guest.h
+++ b/lib/arm64/asm/guest.h
@@ -29,6 +29,27 @@ enum guest_handler_result {
 struct guest;
 typedef enum guest_handler_result (*guest_handler_t)(struct guest *guest);
 
+/*
+ * Guest EL1 Exception Frame (pushed to guest stack by asm stub)
+ * We use a simplified frame: x0-x30, elr, spsr. size = 34*8
+ */
+struct guest_el1_regs {
+	unsigned long regs[31];
+	unsigned long elr;
+	unsigned long spsr;
+	unsigned long reserved;
+};
+
+typedef void (*guest_el1_handler_t)(struct guest_el1_regs *regs, unsigned int esr);
+
+/*
+ * Guest Context Structure
+ * This will be pointed to by TPIDR_EL1 while the guest is running.
+ */
+struct guest_context {
+	guest_el1_handler_t handlers[VECTOR_MAX];
+};
+
 struct guest {
 	/* General Purpose Registers */
 	unsigned long x[31]; /* x0..x30 */
@@ -41,15 +62,18 @@ struct guest {
 	unsigned long hcr_el2;
 	unsigned long sctlr_el1;
 	unsigned long sp_el1;
+	unsigned long vbar_el1;
 
 	/* Exit Information */
 	unsigned long esr_el2;
 	unsigned long far_el2;
 	unsigned long hpfar_el2;
 	unsigned long exit_code;
+	unsigned long tpidr_el1;
 
 	/* Exception Handlers in EL2 */
 	guest_handler_t handlers[VECTOR_MAX];
+	struct guest_context *guest_context;
 
 	struct s2_mmu *s2mmu;
 	void *guest_stack;
@@ -69,4 +93,7 @@ void guest_run(struct guest *guest);
 unsigned long guest_c_exception_handler(struct guest *guest);
 void guest_install_handler(struct guest *guest, enum vector v, guest_handler_t handler);
 
+void guest_el1_c_handler(struct guest_el1_regs *regs, unsigned int vector);
+void guest_install_el1_handler(struct guest *guest, enum vector v, guest_el1_handler_t handler);
+
 #endif /* _ASMARM64_GUEST_H_ */
diff --git a/lib/arm64/asm/sysreg.h b/lib/arm64/asm/sysreg.h
index b3b7e221..8903b630 100644
--- a/lib/arm64/asm/sysreg.h
+++ b/lib/arm64/asm/sysreg.h
@@ -166,6 +166,7 @@ asm(
 #define SYS_HFGITR2_EL2		sys_reg(3, 4, 3, 1, 7)
 
 #define SYS_SCTLR_EL12		sys_reg(3, 5, 1, 0, 0)
+#define SYS_VBAR_EL12		sys_reg(3, 5, 12, 0, 0)
 
 #define INIT_SCTLR_EL1_MMU_OFF	\
 			(SCTLR_EL1_ITD | SCTLR_EL1_SED | SCTLR_EL1_EOS | \
diff --git a/lib/arm64/guest.c b/lib/arm64/guest.c
index afb91559..821f281d 100644
--- a/lib/arm64/guest.c
+++ b/lib/arm64/guest.c
@@ -24,6 +24,7 @@ struct guest_host_state {
 
 extern void __guest_switch_regs(struct guest *guest,
 				struct guest_host_state *host_state);
+void guest_arch_exit(const struct guest_host_state *state);
 
 static void guest_save_host_state(struct guest_host_state *state)
 {
@@ -52,6 +53,8 @@ static void guest_save_state(struct guest *guest)
 	guest->spsr_el2 = read_sysreg(spsr_el2);
 	guest->sctlr_el1 = read_sysreg_s(SYS_SCTLR_EL12);
 	guest->sp_el1 = read_sysreg(sp_el1);
+	guest->vbar_el1 = read_sysreg_s(SYS_VBAR_EL12);
+	guest->tpidr_el1 = read_sysreg(tpidr_el1);
 }
 
 static void guest_restore_state(const struct guest *guest)
@@ -60,6 +63,8 @@ static void guest_restore_state(const struct guest *guest)
 	write_sysreg(guest->spsr_el2, spsr_el2);
 	write_sysreg_s(guest->sctlr_el1, SYS_SCTLR_EL12);
 	write_sysreg(guest->sp_el1, sp_el1);
+	write_sysreg_s(guest->vbar_el1, SYS_VBAR_EL12);
+	write_sysreg(guest->tpidr_el1, tpidr_el1);
 }
 
 static void guest_arch_enter(struct guest *guest,
@@ -113,10 +118,32 @@ unsigned long guest_c_exception_handler(struct guest *guest)
 	return 1;
 }
 
+void guest_install_el1_handler(struct guest *guest, enum vector v, guest_el1_handler_t handler)
+{
+	if (guest && guest->guest_context && v < VECTOR_MAX)
+		guest->guest_context->handlers[v] = handler;
+}
+
+void guest_el1_c_handler(struct guest_el1_regs *regs, unsigned int vector)
+{
+	struct guest_context *ctx = (struct guest_context *)read_sysreg(tpidr_el1);
+	unsigned int esr = read_sysreg(esr_el1);
+
+	if (ctx && vector < VECTOR_MAX && ctx->handlers[vector]) {
+		ctx->handlers[vector](regs, esr);
+	} else {
+		asm volatile("hvc #0xFFFF");
+	}
+}
+
+extern void guest_el1_vectors(void);
 
 struct guest *guest_create(struct s2_mmu *s2_ctx, unsigned long entry_ipa)
 {
 	struct guest *guest;
+	struct guest_context *guest_ctx;
+	unsigned long guest_ctx_pa;
+	int ret;
 
 	if (!s2_ctx)
 		return NULL;
@@ -125,6 +152,21 @@ struct guest *guest_create(struct s2_mmu *s2_ctx, unsigned long entry_ipa)
 	if (!guest)
 		return NULL;
 
+	/* Allocate the internal context table */
+	guest_ctx = (void *)alloc_page();
+	if (!guest_ctx)
+		goto free_guest;
+	memset(guest_ctx, 0, PAGE_SIZE);
+	guest->guest_context = guest_ctx;
+
+	guest_ctx_pa = virt_to_phys(guest_ctx);
+	ret = s2mmu_map(s2_ctx, guest_ctx_pa, guest_ctx_pa, PAGE_SIZE,
+			S2_MAP_RW);
+	if (ret)
+		goto free_guest_ctx;
+
+	guest->tpidr_el1 = guest_ctx_pa;
+
 	guest->elr_el2 = entry_ipa;
 	guest->spsr_el2 = PSR_MODE_EL1h |
 		PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT;
@@ -135,9 +177,16 @@ struct guest *guest_create(struct s2_mmu *s2_ctx, unsigned long entry_ipa)
 	guest->sctlr_el1 &= ~(SCTLR_EL1_M | SCTLR_EL1_C);
 	guest->sctlr_el1 |= SCTLR_EL1_I;
 
+	guest->vbar_el1 = virt_to_phys((void *)guest_el1_vectors);
 	guest->s2mmu = s2_ctx;
 
 	return guest;
+
+free_guest_ctx:
+	free_page(guest_ctx);
+free_guest:
+	free(guest);
+	return NULL;
 }
 
 struct guest *guest_create_identity(int vmid, void (*guest_func)(void),
@@ -203,6 +252,8 @@ void guest_destroy(struct guest *guest)
 {
 	s2mmu_destroy(guest->s2mmu);
 	free_pages(guest->guest_stack);
+	if (guest->guest_context)
+		free_page(guest->guest_context);
 	free(guest);
 }
 
diff --git a/lib/arm64/guest_arch.S b/lib/arm64/guest_arch.S
index 33d0dc53..1ccba114 100644
--- a/lib/arm64/guest_arch.S
+++ b/lib/arm64/guest_arch.S
@@ -9,6 +9,7 @@
 #include <asm/sysreg.h>
 
 #define GUEST_XREG_OFFSET(x)	(GUEST_X_OFFSET + 8 * (x))
+#define GUEST_EL1_XREG_OFFSET(x)	(GUEST_EL1_X_OFFSET + 8 * (x))
 
 .global __guest_switch_regs
 __guest_switch_regs:
@@ -147,3 +148,76 @@ guest_resume_guest:
 	ldp	x29, x30, [x0, #GUEST_XREG_OFFSET(29)]
 	ldr	x0, [x0, #GUEST_XREG_OFFSET(0)]
 	eret
+
+/* EL1 Vector Table */
+.macro guest_el1_vector, vector_num
+	.align 7
+	sub	sp, sp, #GUEST_EL1_REGS_SIZE
+	/* Save x29 and x30 before using x29 for the vector number. */
+	stp	x29, x30, [sp, #GUEST_EL1_XREG_OFFSET(29)]
+	mov	x29, \vector_num
+	b       guest_el1_common
+.endm
+
+.align 11
+.global guest_el1_vectors
+guest_el1_vectors:
+	.skip 0x200
+
+	guest_el1_vector #4	/* Sync */
+	guest_el1_vector #5	/* IRQ */
+	guest_el1_vector #6	/* FIQ */
+	guest_el1_vector #7	/* SError */
+
+	.align 11
+
+guest_el1_common:
+	stp	x0, x1, [sp, #GUEST_EL1_XREG_OFFSET(0)]
+	stp	x2, x3, [sp, #GUEST_EL1_XREG_OFFSET(2)]
+	stp	x4, x5, [sp, #GUEST_EL1_XREG_OFFSET(4)]
+	stp	x6, x7, [sp, #GUEST_EL1_XREG_OFFSET(6)]
+	stp	x8, x9, [sp, #GUEST_EL1_XREG_OFFSET(8)]
+	stp	x10, x11, [sp, #GUEST_EL1_XREG_OFFSET(10)]
+	stp	x12, x13, [sp, #GUEST_EL1_XREG_OFFSET(12)]
+	stp	x14, x15, [sp, #GUEST_EL1_XREG_OFFSET(14)]
+	stp	x16, x17, [sp, #GUEST_EL1_XREG_OFFSET(16)]
+	stp	x18, x19, [sp, #GUEST_EL1_XREG_OFFSET(18)]
+	stp	x20, x21, [sp, #GUEST_EL1_XREG_OFFSET(20)]
+	stp	x22, x23, [sp, #GUEST_EL1_XREG_OFFSET(22)]
+	stp	x24, x25, [sp, #GUEST_EL1_XREG_OFFSET(24)]
+	stp	x26, x27, [sp, #GUEST_EL1_XREG_OFFSET(26)]
+	str	x28, [sp, #GUEST_EL1_XREG_OFFSET(28)]
+
+	mrs	x0, elr_el1
+	str	x0, [sp, #GUEST_EL1_ELR_OFFSET]
+	mrs	x0, spsr_el1
+	str	x0, [sp, #GUEST_EL1_SPSR_OFFSET]
+
+	mov	x0, sp
+	mov	x1, x29
+	bl	guest_el1_c_handler
+
+	ldr	x0, [sp, #GUEST_EL1_ELR_OFFSET]
+	msr	elr_el1, x0
+	ldr	x0, [sp, #GUEST_EL1_SPSR_OFFSET]
+	msr	spsr_el1, x0
+
+	ldp	x0, x1, [sp, #GUEST_EL1_XREG_OFFSET(0)]
+	ldp	x2, x3, [sp, #GUEST_EL1_XREG_OFFSET(2)]
+	ldp	x4, x5, [sp, #GUEST_EL1_XREG_OFFSET(4)]
+	ldp	x6, x7, [sp, #GUEST_EL1_XREG_OFFSET(6)]
+	ldp	x8, x9, [sp, #GUEST_EL1_XREG_OFFSET(8)]
+	ldp	x10, x11, [sp, #GUEST_EL1_XREG_OFFSET(10)]
+	ldp	x12, x13, [sp, #GUEST_EL1_XREG_OFFSET(12)]
+	ldp	x14, x15, [sp, #GUEST_EL1_XREG_OFFSET(14)]
+	ldp	x16, x17, [sp, #GUEST_EL1_XREG_OFFSET(16)]
+	ldp	x18, x19, [sp, #GUEST_EL1_XREG_OFFSET(18)]
+	ldp	x20, x21, [sp, #GUEST_EL1_XREG_OFFSET(20)]
+	ldp	x22, x23, [sp, #GUEST_EL1_XREG_OFFSET(22)]
+	ldp	x24, x25, [sp, #GUEST_EL1_XREG_OFFSET(24)]
+	ldp	x26, x27, [sp, #GUEST_EL1_XREG_OFFSET(26)]
+	ldp	x28, x29, [sp, #GUEST_EL1_XREG_OFFSET(28)]
+	ldr	x30, [sp, #GUEST_EL1_XREG_OFFSET(30)]
+
+	add	sp, sp, #GUEST_EL1_REGS_SIZE
+	eret
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v3 7/8] arm64: Add Stage-2 MMU demand paging test
  2026-10-01 15:16 [PATCH v3 0/8] arm64: Add Stage-2 MMU and Nested Guest Framework Joey Gouly
                   ` (5 preceding siblings ...)
  2026-10-01 15:16 ` [PATCH v3 6/8] lib: arm64: Add guest-internal " Joey Gouly
@ 2026-10-01 15:16 ` Joey Gouly
  2026-10-01 15:16 ` [PATCH v3 8/8] arm64: Add basic guest execution tests Joey Gouly
  7 siblings, 0 replies; 11+ messages in thread
From: Joey Gouly @ 2026-10-01 15:16 UTC (permalink / raw)
  To: kvmarm
  Cc: Jing Zhang, Marc Zyngier, Oliver Upton, Mark Brown, Wei-Lin Chang,
	andrew.jones, Joey Gouly

From: Jing Zhang <jingzhangos@google.com>

Introduce a new test case to validate Stage-2 MMU fault handling. The
test verifies that the hypervisor correctly identifies and handles
Stage-2 data aborts triggered by a guest accessing unmapped memory.

The test performs the following:
- Sets up a guest with Stage-1 disabled, using identity-mapped host
   code and shared data in the Stage-2 page tables.
- Triggers a Stage-2 data abort by accessing a specific unmapped IPA.
- Catches the exception in the host, verifies the fault address,
   and dynamically maps a new page to resolve the fault.
- Resumes the guest to confirm the memory access completes successfully
   and the fault handler functioned as expected.

Signed-off-by: Jing Zhang <jingzhangos@google.com>
Signed-off-by: Joey Gouly <joey.gouly@arm.com>
---
 arm/Makefile.arm64    |   1 +
 arm/stage2-mmu-test.c | 113 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 114 insertions(+)
 create mode 100644 arm/stage2-mmu-test.c

diff --git a/arm/Makefile.arm64 b/arm/Makefile.arm64
index 9026fd71..e547f92d 100644
--- a/arm/Makefile.arm64
+++ b/arm/Makefile.arm64
@@ -67,6 +67,7 @@ tests += $(TEST_DIR)/cache.$(exe)
 tests += $(TEST_DIR)/debug.$(exe)
 tests += $(TEST_DIR)/fpu.$(exe)
 tests += $(TEST_DIR)/mte.$(exe)
+tests += $(TEST_DIR)/stage2-mmu-test.$(exe)
 
 include $(SRCDIR)/$(TEST_DIR)/Makefile.common
 
diff --git a/arm/stage2-mmu-test.c b/arm/stage2-mmu-test.c
new file mode 100644
index 00000000..91d3de7e
--- /dev/null
+++ b/arm/stage2-mmu-test.c
@@ -0,0 +1,113 @@
+/*
+ * ARM64 Stage-2 MMU Demand Paging Test
+ *
+ * This test validates stage-2 data abort handling by purposefully
+ * accessing unmapped memory in the guest and verifying that the
+ * host correctly handles the fault by mapping the page.
+ *
+ * Copyright (C) 2026 Google LLC.
+ * Author: Jing Zhang <jingzhangos@google.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+#include <libcflat.h>
+#include <alloc_page.h>
+#include <asm/io.h>
+#include <asm/smp.h>
+#include <asm/guest.h>
+#include <asm/stage2_mmu.h>
+
+#define TEST_PAGE_IPA		0x50000000UL
+#define FAULT_ADDR_IPA		0x60000000UL
+#define TEST_DATA		0xBEEFCAFEUL
+
+static volatile bool handled = false;
+
+static void guest_code(void)
+{
+	volatile unsigned long *test_va = (void *)TEST_PAGE_IPA;
+	volatile unsigned long *fault_va = (void *)FAULT_ADDR_IPA;
+
+	*fault_va = *test_va;
+
+	if (*fault_va == *test_va && *fault_va == TEST_DATA)
+		handled = true;
+
+	asm("hvc #0");
+}
+
+static enum guest_handler_result guest_exception_handler(struct guest *guest)
+{
+	unsigned long far, ec;
+	unsigned long *fixup_page;
+
+	ec = guest->esr_el2 >> ESR_ELx_EC_SHIFT;
+
+	if (ec == ESR_ELx_EC_HVC64) {
+		report_info("CPU%d: Guest exited via HVC.", smp_processor_id());
+		return GUEST_ACTION_EXIT;
+	}
+
+	if (ec == ESR_ELx_EC_DABT_LOW) {
+		far = guest->far_el2;
+		if (far == FAULT_ADDR_IPA) {
+			fixup_page = alloc_page();
+			s2mmu_map(guest->s2mmu, FAULT_ADDR_IPA,
+				  virt_to_phys(fixup_page), PAGE_SIZE, S2_MAP_RW);
+			report(true, "Caught stage-2 fault at 0x%lx", far);
+		} else {
+			report(false, "Unexpected fault address: 0x%lx", far);
+		}
+	} else {
+		report(false, "Unexpected exception class: 0x%lx", ec);
+	}
+
+	return GUEST_ACTION_RESUME;
+}
+
+int main(int argc, char **argv)
+{
+	struct guest *guest;
+	unsigned long *test_page;
+	unsigned long code_va_base, code_pa_base, data_base;
+
+	report_prefix_push("stage2-mmu");
+
+	if (current_level() != CurrentEL_EL2) {
+		report_skip("EL2 is required");
+		return report_summary();
+	}
+
+	guest = guest_create_identity(smp_processor_id(), guest_code,
+				      S2_PAGE_4K);
+
+	/* Map host code: IPA(VA) -> PA */
+	/* We use the host VA as the Guest IPA because guest stage 1 is disabled. */
+	code_va_base = (unsigned long)guest_code;
+	code_pa_base = virt_to_phys((void *)guest_code);
+
+	/* Align to 2MB to use block descriptors where possible */
+	code_va_base = code_va_base & ~(SZ_2M - 1);
+	code_pa_base = code_pa_base & ~(SZ_2M - 1);
+	s2mmu_map(guest->s2mmu, code_va_base, code_pa_base, SZ_2M, S2_MAP_RW);
+
+	/* Identity map the shared variable */
+	data_base = virt_to_phys((void *)&handled) & PAGE_MASK;
+	s2mmu_map(guest->s2mmu, data_base, data_base, PAGE_SIZE, S2_MAP_RW);
+
+	/* Map test data page */
+	test_page = alloc_page();
+	*test_page = TEST_DATA;
+	s2mmu_map(guest->s2mmu, TEST_PAGE_IPA, virt_to_phys(test_page), PAGE_SIZE, S2_MAP_RW);
+
+	guest_install_handler(guest, ELx_LOW_SYNC_64, guest_exception_handler);
+
+	report_info("CPU%d: entering guest...", smp_processor_id());
+
+	guest_run(guest);
+
+	report(handled, "Stage-2 fault handling test completed");
+	guest_destroy(guest);
+
+	return report_summary();
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v3 8/8] arm64: Add basic guest execution tests
  2026-10-01 15:16 [PATCH v3 0/8] arm64: Add Stage-2 MMU and Nested Guest Framework Joey Gouly
                   ` (6 preceding siblings ...)
  2026-10-01 15:16 ` [PATCH v3 7/8] arm64: Add Stage-2 MMU demand paging test Joey Gouly
@ 2026-10-01 15:16 ` Joey Gouly
  7 siblings, 0 replies; 11+ messages in thread
From: Joey Gouly @ 2026-10-01 15:16 UTC (permalink / raw)
  To: kvmarm
  Cc: Jing Zhang, Marc Zyngier, Oliver Upton, Mark Brown, Wei-Lin Chang,
	andrew.jones, Joey Gouly

Add tests that verify that EL2 and EL1 exits can inspect and modify
guest registers.

Assisted-by: LLM
Signed-off-by: Joey Gouly <joey.gouly@arm.com>
---
 arm/Makefile.arm64  |   1 +
 arm/guest-test.c    | 157 ++++++++++++++++++++++++++++++++++++++++++++
 lib/arm64/asm/esr.h |   3 +
 3 files changed, 161 insertions(+)
 create mode 100644 arm/guest-test.c

diff --git a/arm/Makefile.arm64 b/arm/Makefile.arm64
index e547f92d..5c329588 100644
--- a/arm/Makefile.arm64
+++ b/arm/Makefile.arm64
@@ -68,6 +68,7 @@ tests += $(TEST_DIR)/debug.$(exe)
 tests += $(TEST_DIR)/fpu.$(exe)
 tests += $(TEST_DIR)/mte.$(exe)
 tests += $(TEST_DIR)/stage2-mmu-test.$(exe)
+tests += $(TEST_DIR)/guest-test.$(exe)
 
 include $(SRCDIR)/$(TEST_DIR)/Makefile.common
 
diff --git a/arm/guest-test.c b/arm/guest-test.c
new file mode 100644
index 00000000..90676bdf
--- /dev/null
+++ b/arm/guest-test.c
@@ -0,0 +1,157 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ *
+ * Copyright (C) 2026 Arm Ltd.
+ */
+#include <libcflat.h>
+#include <asm/guest.h>
+#include <asm/io.h>
+#include <asm/smp.h>
+#include <asm/sysreg.h>
+
+#define HVC_FIRST_EXIT		0x101
+#define HVC_SECOND_EXIT		0x102
+#define HVC_EL1_DONE		0x103
+#define HVC_BAD_EXIT		0x104
+#define EL1_BRK_IMM		0x321
+#define INITIAL_VALUE		0x123
+#define RESUME_VALUE		0x456
+#define EL1_RESUME_VALUE	0x789
+#define TPIDR_EL2_VALUE		0x12345678
+
+static void guest_resume_entry(void)
+{
+	unsigned long value;
+
+	/* Place a specific value in x0, and re-read x0 after the HVC */
+	asm volatile(
+		"mov x0, %1\n"
+		"hvc %2\n"
+		"mov %0, x0"
+		: "=r" (value)
+		: "r" (INITIAL_VALUE), "i" (HVC_FIRST_EXIT)
+		: "x0", "memory");
+
+	if (value == RESUME_VALUE)
+		asm volatile("hvc %0" :: "i" (HVC_SECOND_EXIT));
+	else
+		asm volatile("hvc %0" :: "i" (HVC_BAD_EXIT));
+}
+
+static void guest_el1_exception_entry(void)
+{
+	unsigned long value;
+
+	// BRK, and check that the exception handler modified x0
+	asm volatile(
+		"brk %1\n"
+		"mov %0, x0"
+		: "=r" (value)
+		: "i" (EL1_BRK_IMM)
+		: "x0", "memory");
+
+	if (value == EL1_RESUME_VALUE) {
+		asm volatile(
+			"mov x0, %0\n"
+			"hvc %1"
+			:
+			: "r" (value), "i" (HVC_EL1_DONE)
+			: "x0", "memory");
+	} else {
+		asm volatile("hvc %0" :: "i" (HVC_BAD_EXIT));
+	}
+}
+
+static bool guest_exited_with_hvc(const struct guest *guest,
+				  unsigned long expected_imm)
+{
+	unsigned long ec = guest->esr_el2 >> ESR_ELx_EC_SHIFT;
+	unsigned long imm = guest->esr_el2 & ESR_ELx_ISS_IMM_MASK;
+
+	return ec == ESR_ELx_EC_HVC64 && imm == expected_imm;
+}
+
+static enum guest_handler_result guest_sync_handler(struct guest *guest)
+{
+	if (guest_exited_with_hvc(guest, HVC_FIRST_EXIT) &&
+	    guest->x[0] == INITIAL_VALUE)
+		guest->x[0] = RESUME_VALUE;
+
+	return GUEST_ACTION_EXIT;
+}
+
+static void guest_el1_sync_handler(struct guest_el1_regs *regs,
+				   unsigned int esr)
+{
+	unsigned long ec = esr >> ESR_ELx_EC_SHIFT;
+	unsigned long imm = esr & ESR_ELx_ISS_IMM_MASK;
+
+	if (ec == ESR_ELx_EC_BRK64 && imm == EL1_BRK_IMM)
+		regs->regs[0] = EL1_RESUME_VALUE;
+	else
+		regs->regs[0] = 0;
+
+	regs->elr += 4;
+}
+
+static void test_guest_resume(void)
+{
+	unsigned long old_tpidr_el2 = read_sysreg(tpidr_el2);
+	struct guest *guest;
+	bool tpidr_restored;
+
+	guest = guest_create_identity(smp_processor_id(), guest_resume_entry,
+				      S2_PAGE_4K);
+	assert(guest);
+	guest_install_handler(guest, ELx_LOW_SYNC_64, guest_sync_handler);
+
+	guest_run(guest);
+	report(guest_exited_with_hvc(guest, HVC_FIRST_EXIT) &&
+	       guest->x[0] == RESUME_VALUE,
+	       "exit handler changes guest register state");
+
+	write_sysreg(0, elr_el2);
+	write_sysreg(TPIDR_EL2_VALUE, tpidr_el2);
+	guest_run(guest);
+	tpidr_restored = read_sysreg(tpidr_el2) == TPIDR_EL2_VALUE;
+	write_sysreg(old_tpidr_el2, tpidr_el2);
+
+	report(guest_exited_with_hvc(guest, HVC_SECOND_EXIT),
+	       "guest resumes with its saved PC and register state");
+	report(tpidr_restored, "guest run restores host TPIDR_EL2");
+
+	guest_destroy(guest);
+}
+
+static void test_guest_el1_exception(void)
+{
+	struct guest *guest;
+
+	guest = guest_create_identity(smp_processor_id(),
+				      guest_el1_exception_entry, S2_PAGE_4K);
+	assert(guest);
+	guest_install_handler(guest, ELx_LOW_SYNC_64, guest_sync_handler);
+	guest_install_el1_handler(guest, ELxH_SYNC, guest_el1_sync_handler);
+
+	guest_run(guest);
+	report(guest_exited_with_hvc(guest, HVC_EL1_DONE) &&
+	       guest->x[0] == EL1_RESUME_VALUE,
+	       "guest handles an EL1 synchronous exception");
+
+	guest_destroy(guest);
+}
+
+int main(int argc, char **argv)
+{
+	report_prefix_push("guest");
+
+	if (current_level() != CurrentEL_EL2) {
+		report_skip("EL2 is required");
+		return report_summary();
+	}
+
+	test_guest_resume();
+	test_guest_el1_exception();
+
+	return report_summary();
+}
diff --git a/lib/arm64/asm/esr.h b/lib/arm64/asm/esr.h
index 3860eb9d..02a27c24 100644
--- a/lib/arm64/asm/esr.h
+++ b/lib/arm64/asm/esr.h
@@ -72,4 +72,7 @@
 #define ESR_ELx_FSC		(0x3F)
 #define ESR_ELx_FSC_EXTABT	(0x10)
 
+/* Immediate field for HVC, SVC, and BRK instructions. */
+#define ESR_ELx_ISS_IMM_MASK	GENMASK(15, 0)
+
 #endif /* _ASMARM64_ESR_H_ */
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [PATCH v3 1/8] lib/alloc_page: check for NULL ptr passed to free_pages()
  2026-10-01 15:16 ` [PATCH v3 1/8] lib/alloc_page: check for NULL ptr passed to free_pages() Joey Gouly
@ 2026-10-01 15:42   ` Mark Brown
  2026-10-01 15:53     ` Joey Gouly
  0 siblings, 1 reply; 11+ messages in thread
From: Mark Brown @ 2026-10-01 15:42 UTC (permalink / raw)
  To: Joey Gouly
  Cc: kvmarm, Jing Zhang, Marc Zyngier, Oliver Upton, Wei-Lin Chang,
	andrew.jones

[-- Attachment #1: Type: text/plain, Size: 223 bytes --]

On Thu, Oct 01, 2026 at 04:16:38PM +0100, Joey Gouly wrote:
> Don't call virt_to_pfn() before checking for NULL.
> ---
>  lib/alloc_page.c | 3 ++-
>  1 file changed, 2 insertions(+), 1 deletion(-)

There's no signoff here.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH v3 1/8] lib/alloc_page: check for NULL ptr passed to free_pages()
  2026-10-01 15:42   ` Mark Brown
@ 2026-10-01 15:53     ` Joey Gouly
  0 siblings, 0 replies; 11+ messages in thread
From: Joey Gouly @ 2026-10-01 15:53 UTC (permalink / raw)
  To: Mark Brown
  Cc: kvmarm, Jing Zhang, Marc Zyngier, Oliver Upton, Wei-Lin Chang,
	andrew.jones

On Thu, Oct 01, 2026 at 04:42:33PM +0100, Mark Brown wrote:
> On Thu, Oct 01, 2026 at 04:16:38PM +0100, Joey Gouly wrote:
> > Don't call virt_to_pfn() before checking for NULL.
> > ---
> >  lib/alloc_page.c | 3 ++-
> >  1 file changed, 2 insertions(+), 1 deletion(-)
> 
> There's no signoff here.

Agh, relatively new setup and clearly some things still not setup
correctly! 

Just in case:

Signed-off-by: Joey Gouly <joey.gouly@arm.com>

Thanks,
Joey

^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-10-01 15:53 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-01 15:16 [PATCH v3 0/8] arm64: Add Stage-2 MMU and Nested Guest Framework Joey Gouly
2026-10-01 15:16 ` [PATCH v3 1/8] lib/alloc_page: check for NULL ptr passed to free_pages() Joey Gouly
2026-10-01 15:42   ` Mark Brown
2026-10-01 15:53     ` Joey Gouly
2026-10-01 15:16 ` [PATCH v3 2/8] lib: arm64: Generalize exception vector definitions for EL2 support Joey Gouly
2026-10-01 15:16 ` [PATCH v3 3/8] lib: arm64: Add stage2 page table management library Joey Gouly
2026-10-01 15:16 ` [PATCH v3 4/8] lib: arm64: Add foundational guest execution framework Joey Gouly
2026-10-01 15:16 ` [PATCH v3 5/8] lib: arm64: Add support for guest exit exception handling Joey Gouly
2026-10-01 15:16 ` [PATCH v3 6/8] lib: arm64: Add guest-internal " Joey Gouly
2026-10-01 15:16 ` [PATCH v3 7/8] arm64: Add Stage-2 MMU demand paging test Joey Gouly
2026-10-01 15:16 ` [PATCH v3 8/8] arm64: Add basic guest execution tests Joey Gouly

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox