diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 33cd30996e47..f8dac4a58327 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -1543,6 +1543,7 @@ Kernel parameters
earlyprintk=serial[,ttySn[,baudrate]]
earlyprintk=serial[,0x...[,baudrate]]
earlyprintk=ttySn[,baudrate]
+ earlyprintk=tdx
earlyprintk=dbgp[debugController#]
earlyprintk=mmio32,membase[,{nocfg|baudrate}]
earlyprintk=pciserial[,force],bus:device.function[,{nocfg|baudrate}]@@ -1556,6 +1557,14 @@ Kernel parameters
Use "nocfg" to skip UART configuration, assume
BIOS/firmware has configured UART correctly.
+ On x86, "tdx" is a fixed COM1 console, and takes no
+ options. In a TDX guest the port is driven with
+ TDVMCALLs rather than port I/O instructions, avoiding
+ the #VE exception that would otherwise be taken to
+ emulate every access. Outside a TDX guest it falls back
+ to port I/O, behaving like "earlyprintk=serial". If
+ CONFIG_INTEL_TDX_GUEST is not set, it is ignored.
+
Append ",keep" to not disable it when the real console
takes over.
diff --git a/arch/x86/include/asm/tdx.h b/arch/x86/include/asm/tdx.h
index 89e97d5761d8..325dd7c5929f 100644
--- a/arch/x86/include/asm/tdx.h
+++ b/arch/x86/include/asm/tdx.h
@@ -83,6 +83,9 @@ int tdx_mcall_extend_rtmr(u8 index, u8 *data);
u64 tdx_hcall_get_quote(u8 *buf, size_t size);
+u8 tdx_inb(u16 port);
+void tdx_outb(u8 value, u16 port);
+
void __init tdx_dump_attributes(u64 td_attr);
void __init tdx_dump_td_ctls(u64 td_ctls);
diff --git a/arch/x86/coco/tdx/tdx.c b/arch/x86/coco/tdx/tdx.c
index 7d1a93ee2534..d8887a2a524b 100644
--- a/arch/x86/coco/tdx/tdx.c
+++ b/arch/x86/coco/tdx/tdx.c
@@ -194,6 +194,45 @@ u64 tdx_hcall_get_quote(u8 *buf, size_t size)
}
EXPORT_SYMBOL_GPL(tdx_hcall_get_quote);
+/**
+ * tdx_inb() - Read a byte from an I/O port without a #VE
+ * @port: I/O port to read from
+ *
+ * Ask the VMM to perform the read with TDG.VP.VMCALL<Instruction.IO>, rather
+ * than executing an IN instruction and having the resulting #VE emulate it.
+ *
+ * Return: the byte read, or 0xFF if the hypercall failed.
+ */
+u8 tdx_inb(u16 port)
+{
+ struct tdx_module_args args = {
+ .r10 = TDX_HYPERCALL_STANDARD,
+ .r11 = hcall_func(EXIT_REASON_IO_INSTRUCTION),
+ .r12 = 1,
+ .r13 = TDVMCALL_PORT_READ,
+ .r14 = port,
+ };
+
+ if (__tdx_hypercall(&args))
+ return 0xFF;
+
+ return args.r11;
+}
+
+/**
+ * tdx_outb() - Write a byte to an I/O port without a #VE
+ * @value: byte to write
+ * @port: I/O port to write to
+ *
+ * Ask the VMM to perform the write with TDG.VP.VMCALL<Instruction.IO>, rather
+ * than executing an OUT instruction and having the resulting #VE emulate it.
+ */
+void tdx_outb(u8 value, u16 port)
+{
+ _tdx_hypercall(hcall_func(EXIT_REASON_IO_INSTRUCTION), 1,
+ TDVMCALL_PORT_WRITE, port, value);
+}
+
static void __noreturn tdx_panic(const char *msg)
{
struct tdx_module_args args = {diff --git a/arch/x86/kernel/early_printk.c b/arch/x86/kernel/early_printk.c
index cba75306e5b6..194737a3e9a0 100644
--- a/arch/x86/kernel/early_printk.c
+++ b/arch/x86/kernel/early_printk.c
@@ -21,6 +21,7 @@
#include <linux/usb/xhci-dbgp.h>
#include <asm/pci_x86.h>
#include <linux/static_call.h>
+#include <asm/tdx.h>
/* Simple VGA output */
#define VGABASE (__ISA_IO_base + 0xb8000)
@@ -111,6 +112,23 @@ 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)
+{
+ return tdx_inb(addr + offset);
+}
+ANNOTATE_NOENDBR_SYM(tdx_serial_in);
+
+static __noendbr void tdx_serial_out(unsigned long addr, int offset, int value)
+{
+ tdx_outb(value, addr + offset);
+}
+ANNOTATE_NOENDBR_SYM(tdx_serial_out);
+#endif /* CONFIG_INTEL_TDX_GUEST */
+
static int early_serial_putc(unsigned char ch)
{
unsigned timeout = 0xffff;@@ -196,6 +214,22 @@ static __init void early_serial_init(char *s)
early_serial_hw_init(divisor);
}
+#ifdef CONFIG_INTEL_TDX_GUEST
+/*
+ * Use early_serial_base (COM1) and same default baud rate as
+ * "earlyprintk=serial"
+ */
+static __init void early_serial_tdx_init(void)
+{
+ if (cpu_feature_enabled(X86_FEATURE_TDX_GUEST)) {
+ static_call_update(serial_in, tdx_serial_in);
+ static_call_update(serial_out, tdx_serial_out);
+ }
+
+ early_serial_hw_init(115200 / DEFAULT_BAUD);
+}
+#endif /* CONFIG_INTEL_TDX_GUEST */
+
static __noendbr void mem32_serial_out(unsigned long addr, int offset, int value)
{
u32 __iomem *vaddr = (u32 __iomem *)addr;@@ -414,6 +448,13 @@ static int __init setup_early_printk(char *buf)
early_serial_init(buf + 4);
early_console_register(&early_serial_console, keep);
}
+#ifdef CONFIG_INTEL_TDX_GUEST
+ if (!strncmp(buf, "tdx", 3)) {
+ early_serial_tdx_init();
+ early_console_register(&early_serial_console, keep);
+ break;
+ }
+#endif
#ifdef CONFIG_PCI
if (!strncmp(buf, "pciserial", 9)) {
buf += 9; /* Keep from match the above "pciserial" */
--
2.55.0