[PATCH 2/2] KVM: selftests: Add test case for readonly memslots on x86

From: Yohei Kojima

Date: Thu Feb 26 2026 - 02:45:56 EST


Extend set_memory_region_test to verify the following properties:

* read on RO-memslot succeeds,
* execute on RO-memslot backed by executable memory succeeds, and
* write on RO-memslot fails with mmio fault.

Signed-off-by: Yohei Kojima <yohei.kojima@xxxxxxxx>
---
.../selftests/kvm/set_memory_region_test.c | 100 ++++++++++++++++++
1 file changed, 100 insertions(+)

diff --git a/tools/testing/selftests/kvm/set_memory_region_test.c b/tools/testing/selftests/kvm/set_memory_region_test.c
index 8d4fd713347c..2f21bfcbc821 100644
--- a/tools/testing/selftests/kvm/set_memory_region_test.c
+++ b/tools/testing/selftests/kvm/set_memory_region_test.c
@@ -28,7 +28,10 @@
* Somewhat arbitrary location and slot, intended to not overlap anything.
*/
#define MEM_REGION_GPA 0xc0000000
+#define MEM_REGION_RO_GPA 0xd0000000
+
#define MEM_REGION_SLOT 10
+#define MEM_REGION_RO_SLOT 11

static const uint64_t MMIO_VAL = 0xbeefull;

@@ -49,6 +52,8 @@ static inline uint64_t guest_spin_on_val(uint64_t spin_val)
return val;
}

+static int allow_mmio_fault;
+
static void *vcpu_worker(void *data)
{
struct kvm_vcpu *vcpu = data;
@@ -76,6 +81,13 @@ static void *vcpu_worker(void *data)
if (run->exit_reason != KVM_EXIT_MMIO)
break;

+ if (allow_mmio_fault && run->mmio.is_write)
+ /*
+ * in this case, skip checking mmio-related assertions
+ * and exit status
+ */
+ return NULL;
+
TEST_ASSERT(!run->mmio.is_write, "Unexpected exit mmio write");
TEST_ASSERT(run->mmio.len == 8,
"Unexpected exit mmio size = %u", run->mmio.len);
@@ -336,6 +348,92 @@ static void test_delete_memory_region(bool disable_slot_zap_quirk)
kvm_vm_free(vm);
}

+static void guest_code_ro_memory_region(void)
+{
+ uint64_t val;
+ unsigned char c;
+ void *instruction_addr;
+
+ GUEST_SYNC(0);
+
+ val = guest_spin_on_val(0);
+ __GUEST_ASSERT(val == 1, "Expected '1', got '%lx'", val);
+
+ /* RO memory read; should succeed if the backing memory is readable */
+ c = *(unsigned char *)MEM_REGION_RO_GPA;
+ __GUEST_ASSERT(c == 0xab, "Expected '0xab', got '0x%x'", c);
+
+ /* RO memory exec; should succeed if the backing memory is executable */
+ instruction_addr = ((unsigned char *)MEM_REGION_RO_GPA) + 8;
+ val = ((uint32_t (*)(void))instruction_addr)();
+ __GUEST_ASSERT(val == 0xbeef,
+ "Expected 0xbeef, but got '%lx'", val);
+
+ /* Spin until the mmio fault is allowed for RO-memslot write */
+ val = guest_spin_on_val(1);
+ __GUEST_ASSERT(val == 2, "Expected '2', got '%lx'", val);
+
+ /* RO memory write; should fail */
+ WRITE_ONCE(*((uint64_t *)MEM_REGION_RO_GPA), 0x12);
+ __GUEST_ASSERT(0, "RO memory write is expected to fail, but it didn't");
+}
+
+/*
+ * On x86 environment, write access to the readonly memslots are trapped as
+ * a special MMIO fault. This test verifies that write access on the readonly
+ * memslot is blocked, and read/exec access isn't.
+ */
+static void test_ro_memory_region(void)
+{
+ pthread_t vcpu_thread;
+ uint64_t *hva, *hva_ro;
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+
+ /*
+ * Equivalent C function (assuming SysV ABI):
+ * uint32_t some_function(void) {
+ * return 0xbeef;
+ * }
+ */
+ unsigned char inst_bytes[] = {
+ 0x48, 0xc7, 0xc0, 0xef, 0xbe, 0x00, 0x00, // mov %eax, $0xbeef
+ 0xc3, // ret
+ };
+
+ pr_info("Testing write on RO memslot\n");
+
+ vm = spawn_vm(&vcpu, &vcpu_thread, guest_code_ro_memory_region);
+
+ vm_userspace_mem_region_add_map(vm,
+ MEM_REGION_RO_GPA,
+ MEM_REGION_RO_SLOT,
+ MEM_REGION_SIZE,
+ KVM_MEM_READONLY);
+
+ hva = addr_gpa2hva(vm, MEM_REGION_GPA);
+ hva_ro = addr_gpa2hva(vm, MEM_REGION_RO_GPA);
+
+ memset(hva_ro, 0xcccccccc, 0x2000);
+ WRITE_ONCE(*hva_ro, 0xab);
+ memcpy(((unsigned char *)hva_ro) + 8, inst_bytes, sizeof(inst_bytes));
+
+ WRITE_ONCE(*hva, 1);
+
+ /* Wait the vcpu thread to complete read/exec on ro memory */
+ usleep(100000);
+
+ allow_mmio_fault = true;
+ WRITE_ONCE(*hva, 2);
+
+ wait_for_vcpu();
+
+ pthread_join(vcpu_thread, NULL);
+
+ kvm_vm_free(vm);
+ allow_mmio_fault = false;
+}
+
static void test_zero_memory_regions(void)
{
struct kvm_vcpu *vcpu;
@@ -629,6 +727,8 @@ int main(int argc, char *argv[])
*/
test_zero_memory_regions();
test_mmio_during_vectoring();
+
+ test_ro_memory_region();
#endif

test_invalid_memory_region_flags();
--
2.43.0