All of lore.kernel.org
 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.