diff --git a/tools/testing/selftests/kvm/Makefile b/tools/testing/selftests/kvm/Makefile
index c4e34717826a..05bff4039890 100644
--- a/tools/testing/selftests/kvm/Makefile
+++ b/tools/testing/selftests/kvm/Makefile
@@ -34,7 +34,7 @@ ifeq ($(ARCH),s390)
 endif
 
 LIBKVM = lib/assert.c lib/elf.c lib/io.c lib/kvm_util.c lib/rbtree.c lib/sparsebit.c lib/test_util.c lib/guest_modes.c lib/perf_test_util.c
-LIBKVM_x86_64 = lib/x86_64/apic.c lib/x86_64/processor.c lib/x86_64/vmx.c lib/x86_64/svm.c lib/x86_64/ucall.c lib/x86_64/handlers.S
+LIBKVM_x86_64 = lib/x86_64/apic.c lib/x86_64/processor.c lib/x86_64/vmx.c lib/x86_64/svm.c lib/x86_64/ucall.c lib/x86_64/handlers.S lib/ucall_common.c
 LIBKVM_aarch64 = lib/aarch64/processor.c lib/aarch64/ucall.c lib/aarch64/handlers.S lib/aarch64/spinlock.c lib/aarch64/gic.c lib/aarch64/gic_v3.c lib/aarch64/vgic.c
 LIBKVM_s390x = lib/s390x/processor.c lib/s390x/ucall.c lib/s390x/diag318_test_handler.c
 
diff --git a/tools/testing/selftests/kvm/include/kvm_util.h b/tools/testing/selftests/kvm/include/kvm_util.h
index c9286811a4cb..2701bf98c0db 100644
--- a/tools/testing/selftests/kvm/include/kvm_util.h
+++ b/tools/testing/selftests/kvm/include/kvm_util.h
@@ -8,6 +8,16 @@
 #define SELFTEST_KVM_UTIL_H
 
 #include "kvm_util_base.h"
+/*
+ * TODO: ucall.h contains arch-specific declarations along with
+ * ucall_common.h. For now only a subset of archs provide the
+ * new header. Once all archs implement the new header the #include for
+ * ucall_common.h can be dropped.
+ */
+#ifdef __x86_64__
+#include "ucall.h"
+#else
 #include "ucall_common.h"
+#endif
 
 #endif /* SELFTEST_KVM_UTIL_H */
diff --git a/tools/testing/selftests/kvm/include/ucall_common.h b/tools/testing/selftests/kvm/include/ucall_common.h
index 9eecc9d40b79..fcd32607dcff 100644
--- a/tools/testing/selftests/kvm/include/ucall_common.h
+++ b/tools/testing/selftests/kvm/include/ucall_common.h
@@ -1,8 +1,12 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 /*
- * tools/testing/selftests/kvm/include/kvm_util.h
+ * Common interfaces related to ucall support.
+ *
+ * A ucall is a hypercall to userspace.
  *
  * Copyright (C) 2018, Google LLC.
+ * Copyright (C) 2018, Red Hat, Inc.
+ * Copyright (C) 2021, Advanced Micro Devices, Inc.
  */
 #ifndef SELFTEST_KVM_UCALL_COMMON_H
 #define SELFTEST_KVM_UCALL_COMMON_H
@@ -14,6 +18,7 @@ enum {
 	UCALL_ABORT,
 	UCALL_DONE,
 	UCALL_UNHANDLED,
+	UCALL_NOT_IMPLEMENTED,
 };
 
 #define UCALL_MAX_ARGS 6
@@ -23,8 +28,18 @@ struct ucall {
 	uint64_t args[UCALL_MAX_ARGS];
 };
 
+struct ucall_ops {
+	const char *name;
+	void (*init)(struct kvm_vm *vm, void *arg);
+	void (*uninit)(struct kvm_vm *vm);
+	void (*send_cmd)(struct ucall *uc);
+	uint64_t (*recv_cmd)(struct kvm_vm *vm, uint32_t vcpu_id, struct ucall *uc);
+};
+
 void ucall_init(struct kvm_vm *vm, void *arg);
 void ucall_uninit(struct kvm_vm *vm);
+void ucall_init_ops(struct kvm_vm *vm, void *arg, const struct ucall_ops *ops);
+void ucall_uninit_ops(struct kvm_vm *vm);
 void ucall(uint64_t cmd, int nargs, ...);
 uint64_t get_ucall(struct kvm_vm *vm, uint32_t vcpu_id, struct ucall *uc);
 
diff --git a/tools/testing/selftests/kvm/include/x86_64/ucall.h b/tools/testing/selftests/kvm/include/x86_64/ucall.h
new file mode 100644
index 000000000000..8366bdc9c04e
--- /dev/null
+++ b/tools/testing/selftests/kvm/include/x86_64/ucall.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Arch-specific ucall implementations.
+ *
+ * A ucall is a "hypercall to userspace".
+ *
+ * Copyright (C) 2021 Advanced Micro Devices
+ */
+#ifndef SELFTEST_KVM_UCALL_H
+#define SELFTEST_KVM_UCALL_H
+
+#include "ucall_common.h"
+
+extern const struct ucall_ops ucall_ops_pio;
+
+extern const struct ucall_ops ucall_ops_default;
+
+#endif /* SELFTEST_KVM_UCALL_H */
diff --git a/tools/testing/selftests/kvm/lib/ucall_common.c b/tools/testing/selftests/kvm/lib/ucall_common.c
new file mode 100644
index 000000000000..db0129edcbc1
--- /dev/null
+++ b/tools/testing/selftests/kvm/lib/ucall_common.c
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Common interfaces related to ucall support. A ucall is a hypercall to
+ * userspace.
+ *
+ * Copyright (C) 2018, Red Hat, Inc.
+ * Copyright (C) 2021, Advanced Micro Devices, Inc.
+ */
+#include "kvm_util_base.h"
+#include "ucall_common.h"
+
+extern const struct ucall_ops ucall_ops_default;
+
+/* Some archs rely on a default that is available even without ucall_init(). */
+#if defined(__x86_64__) || defined(__s390x__)
+static const struct ucall_ops *ucall_ops = &ucall_ops_default;
+#else
+static const struct ucall_ops *ucall_ops;
+#endif
+
+void ucall_init_ops(struct kvm_vm *vm, void *arg, const struct ucall_ops *ops)
+{
+	TEST_ASSERT(ops, "ucall ops must be specified");
+	ucall_ops = ops;
+	sync_global_to_guest(vm, ucall_ops);
+
+	if (ucall_ops->init)
+		ucall_ops->init(vm, arg);
+}
+
+void ucall_init(struct kvm_vm *vm, void *arg)
+{
+	ucall_init_ops(vm, arg, &ucall_ops_default);
+}
+
+void ucall_uninit_ops(struct kvm_vm *vm)
+{
+	if (ucall_ops && ucall_ops->uninit)
+		ucall_ops->uninit(vm);
+
+	ucall_ops = NULL;
+	sync_global_to_guest(vm, ucall_ops);
+}
+
+void ucall_uninit(struct kvm_vm *vm)
+{
+	ucall_uninit_ops(vm);
+}
+
+static void ucall_process_args(struct ucall *uc, uint64_t cmd, int nargs, va_list va_args)
+{
+	int i;
+
+	nargs = nargs <= UCALL_MAX_ARGS ? nargs : UCALL_MAX_ARGS;
+	uc->cmd = cmd;
+
+	for (i = 0; i < nargs; ++i)
+		uc->args[i] = va_arg(va_args, uint64_t);
+}
+
+/*
+ * Allocate/populate a ucall buffer from the guest's stack and then generate an
+ * exit to host userspace. ucall_ops->send_cmd should have some way of
+ * communicating the address of the ucall buffer to the host.
+ */
+void ucall(uint64_t cmd, int nargs, ...)
+{
+	struct ucall uc;
+	va_list va;
+
+	if (!ucall_ops->send_cmd)
+		return;
+
+	va_start(va, nargs);
+	ucall_process_args(&uc, cmd, nargs, va);
+	va_end(va);
+
+	ucall_ops->send_cmd(&uc);
+}
+
+/*
+ * Parse the ucall buffer allocated by the guest via ucall() to determine what
+ * ucall message/command was sent by the guest. If 'uc' is provided, copy the
+ * contents of the guest's ucall buffer into it.
+ */
+uint64_t get_ucall(struct kvm_vm *vm, uint32_t vcpu_id, struct ucall *uc)
+{
+	if (!ucall_ops->recv_cmd)
+		return UCALL_NOT_IMPLEMENTED;
+
+	if (uc)
+		memset(uc, 0, sizeof(*uc));
+
+	return ucall_ops->recv_cmd(vm, vcpu_id, uc);
+}
diff --git a/tools/testing/selftests/kvm/lib/x86_64/ucall.c b/tools/testing/selftests/kvm/lib/x86_64/ucall.c
index a3489973e290..f5d9aba0d803 100644
--- a/tools/testing/selftests/kvm/lib/x86_64/ucall.c
+++ b/tools/testing/selftests/kvm/lib/x86_64/ucall.c
@@ -1,48 +1,28 @@
 // SPDX-License-Identifier: GPL-2.0
 /*
- * ucall support. A ucall is a "hypercall to userspace".
+ * Arch-specific ucall implementations.
+ *
+ * A ucall is a "hypercall to userspace".
  *
  * Copyright (C) 2018, Red Hat, Inc.
  */
-#include "kvm_util.h"
+#include "kvm_util_base.h"
+#include "ucall.h"
 
 #define UCALL_PIO_PORT ((uint16_t)0x1000)
 
-void ucall_init(struct kvm_vm *vm, void *arg)
-{
-}
-
-void ucall_uninit(struct kvm_vm *vm)
-{
-}
-
-void ucall(uint64_t cmd, int nargs, ...)
+static void ucall_ops_pio_send_cmd(struct ucall *uc)
 {
-	struct ucall uc = {
-		.cmd = cmd,
-	};
-	va_list va;
-	int i;
-
-	nargs = nargs <= UCALL_MAX_ARGS ? nargs : UCALL_MAX_ARGS;
-
-	va_start(va, nargs);
-	for (i = 0; i < nargs; ++i)
-		uc.args[i] = va_arg(va, uint64_t);
-	va_end(va);
-
 	asm volatile("in %[port], %%al"
-		: : [port] "d" (UCALL_PIO_PORT), "D" (&uc) : "rax", "memory");
+		: : [port] "d" (UCALL_PIO_PORT), "D" (uc) : "rax", "memory");
 }
 
-uint64_t get_ucall(struct kvm_vm *vm, uint32_t vcpu_id, struct ucall *uc)
+static uint64_t ucall_ops_pio_recv_cmd(struct kvm_vm *vm, uint32_t vcpu_id,
+				       struct ucall *uc)
 {
 	struct kvm_run *run = vcpu_state(vm, vcpu_id);
 	struct ucall ucall = {};
 
-	if (uc)
-		memset(uc, 0, sizeof(*uc));
-
 	if (run->exit_reason == KVM_EXIT_IO && run->io.port == UCALL_PIO_PORT) {
 		struct kvm_regs regs;
 
@@ -57,3 +37,11 @@ uint64_t get_ucall(struct kvm_vm *vm, uint32_t vcpu_id, struct ucall *uc)
 
 	return ucall.cmd;
 }
+
+const struct ucall_ops ucall_ops_pio = {
+	.name = "PIO",
+	.send_cmd = ucall_ops_pio_send_cmd,
+	.recv_cmd = ucall_ops_pio_recv_cmd,
+};
+
+const struct ucall_ops ucall_ops_default = ucall_ops_pio;
