[PATCH 2/2] x86/early_printk: Avoid #VE emulation for TDX guest serial output
From: Vishal Verma
Date: Thu Sep 10 2026 - 18:38:44 EST
A TDX guest cannot execute port I/O instructions directly, but
earlyprintk's serial console still issues plain inb()/outb() and lets
each one fault into the #VE handler to be emulated as a TDVMCALL.
While that works, it is a roundabout way to get a character out.
early_serial_putc() polls the LSR, and then writes a byte, but since the
TDX guest can't directly do port I/O, a #VE exception is raised. The #VE
handler must call TDG.VP.VEINFO.GET to find out what faulted, and then
it can issue the TDVMCALL that does the actual work.
This makes #VE a functional mechanism for doing I/O, which is not
desirable, is unnecessarily complicated and fragile, and results in
twice the number of calls into the TDX module.
Instead, issue the TDVMCALL directly. In early_printk.c, port access is
routed through static calls so the MMIO console can substitute its own
accessors. Add a TDX pair and swap them in the same way.
Note that the output does not appear any earlier - "earlyprintk=" is an
early_param(), so the console is still registered from
parse_early_param(). This only changes how the bytes leave the guest
once it is up.
LLMs were used under supervision to create this patch, to help
understand the scope and mechanisms, create testing instrumentation
(throwaway) to count #VEs before/after the change, and to drive lab
machines to do this testing.
Signed-off-by: Vishal Verma <vishal.l.verma@xxxxxxxxx>
---
arch/x86/kernel/early_printk.c | 48 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
diff --git a/arch/x86/kernel/early_printk.c b/arch/x86/kernel/early_printk.c
index cba75306e5b6..4a70799cd80a 100644
--- a/arch/x86/kernel/early_printk.c
+++ b/arch/x86/kernel/early_printk.c
@@ -21,6 +21,8 @@
#include <linux/usb/xhci-dbgp.h>
#include <asm/pci_x86.h>
#include <linux/static_call.h>
+#include <asm/shared/tdx.h>
+#include <asm/vmx.h>
/* Simple VGA output */
#define VGABASE (__ISA_IO_base + 0xb8000)
@@ -111,6 +113,48 @@ ANNOTATE_NOENDBR_SYM(io_serial_out);
DEFINE_STATIC_CALL(serial_in, io_serial_in);
DEFINE_STATIC_CALL(serial_out, io_serial_out);
+#ifdef CONFIG_INTEL_TDX_GUEST
+/*
+ * A TDX guest cannot execute port I/O instructions, so ask the VMM to do it.
+ */
+static __noendbr unsigned int tdx_serial_in(unsigned long addr, int offset)
+{
+ struct tdx_module_args args = {
+ .r10 = TDX_HYPERCALL_STANDARD,
+ .r11 = hcall_func(EXIT_REASON_IO_INSTRUCTION),
+ .r12 = 1, /* One byte */
+ .r13 = TDVMCALL_PORT_READ,
+ .r14 = addr + offset,
+ };
+
+ if (__tdx_hypercall(&args))
+ return UINT_MAX;
+
+ return args.r11;
+}
+ANNOTATE_NOENDBR_SYM(tdx_serial_in);
+
+static __noendbr void tdx_serial_out(unsigned long addr, int offset, int value)
+{
+ /* One byte */
+ _tdx_hypercall(hcall_func(EXIT_REASON_IO_INSTRUCTION), 1,
+ TDVMCALL_PORT_WRITE, addr + offset, value);
+}
+ANNOTATE_NOENDBR_SYM(tdx_serial_out);
+
+/* Substitute the hypercall accessors, but only in an actual TDX guest */
+static __init void early_serial_tdx_init(void)
+{
+ if (!cpu_feature_enabled(X86_FEATURE_TDX_GUEST))
+ return;
+
+ static_call_update(serial_in, tdx_serial_in);
+ static_call_update(serial_out, tdx_serial_out);
+}
+#else
+static inline void early_serial_tdx_init(void) { }
+#endif /* CONFIG_INTEL_TDX_GUEST */
+
static int early_serial_putc(unsigned char ch)
{
unsigned timeout = 0xffff;
@@ -160,6 +204,9 @@ static __init void early_serial_init(char *s)
unsigned long baud = DEFAULT_BAUD;
char *e;
+ /* Must be before early_serial_hw_init(), which does port I/O */
+ early_serial_tdx_init();
+
if (*s == ',')
++s;
@@ -323,6 +370,7 @@ static __init void early_pci_serial_init(char *s)
*/
if ((bar0 & PCI_BASE_ADDRESS_SPACE) == PCI_BASE_ADDRESS_SPACE_IO) {
/* it is IO mapped */
+ early_serial_tdx_init();
early_serial_base = bar0 & PCI_BASE_ADDRESS_IO_MASK;
write_pci_config(bus, slot, func, PCI_COMMAND,
cmdreg|PCI_COMMAND_IO);
--
2.55.0