* [PATCH v5 0/2] x86/tdx: Use TDVMCALLs directly for earlyprintk
@ 2026-11-02 22:51 Vishal Verma
2026-11-02 22:51 ` [PATCH v5 1/2] x86/tdx: Move port I/O definitions to a shared header Vishal Verma
2026-11-02 22:51 ` [PATCH v5 2/2] x86/early_printk: Add earlyprintk=tdx to drive the UART with TDVMCALLs Vishal Verma
0 siblings, 2 replies; 4+ messages in thread
From: Vishal Verma @ 2026-11-02 22:51 UTC (permalink / raw)
To: x86, Dave Hansen, Kiryl Shutsemau, Rick Edgecombe,
Jonathan Corbet, Shuah Khan, Randy Dunlap
Cc: linux-kernel, linux-coco, kvm, linux-doc, Vishal Verma,
Nikolay Borisov
Background
==========
TDX guests have early_printk support, but it is through a roundabout way
using a #VE exception as a functional mechanism. The exception handler
has to use TDG.VP.VEINFO.GET to find out what faulted, and then issue
the TDVMCALL. Simplify this by using a TDVMCALL from the guest which can
directly perform the port I/O.
Patch details
=============
Patch 1 is a cleanup that moves the port I/O direction constants to
arch/x86/include/asm/shared/tdx.h.
Patch 2: Add a new earlyprintk=tdx option, which calls the new
tdx_serial_in() / tdx_serial_out() helpers to perform the port I/O
directly using TDVMCALLs.
The serial earlyprintk path is left as is - TD guests using this will
still take a #VE exception, and the exception handler will perform the
I/O.
Testing
=======
Added a counter for the number of COM1 port I/O #VEs handled:
no earlyprintk - ~63000
earlyprintk=ttyS0 - ~61000
earlyprintk=tdx - ~35000
The new tdx option nearly halves the number of #VEs handled.
Additionally, ran the usual suite of CI testing, which covers KVM self
tests, KVM unit tests, and Avocado KVM tests without any regressions.
LLM Use
=======
LLMs were used under supervision to generate some of the changes, to
help understand the scope and mechanisms, create testing instrumentation
(throwaway) to count #VEs in the serial vs tdx earlyprintk setups, and
to drive lab machines to do this testing.
Signed-off-by: Vishal Verma <vishal.l.verma@intel.com>
---
Changes in v5:
- Collect review tags (Rick, Kiryl, Nikolay)
- Move LLM attestation notes from the commit message to cover letter
- Rebase to v7.4-rc1
- Link to v4: https://patch.msgid.link/20261005-b4-tdx_earlyprintk_tdcalls-v4-0-3f7c56dd568a@intel.com
Changes in v4:
- Rename the tdx_inb()/tdx_outb() helpers directly to the serial
accessors with __noendbr annotations, and remove the config ifdefs
from early_printk.c. (Kiryl)
- Rebase to v7.3-rc6
- Link to v3: https://patch.msgid.link/20260922-b4-tdx_earlyprintk_tdcalls-v3-0-6ebaa599df10@intel.com
Changes in v3:
- Drop the ttySN and baudrate option parsing for earlyprintk=tdx (Dave Hansen)
- Link to v2: https://patch.msgid.link/20260921-b4-tdx_earlyprintk_tdcalls-v2-0-ad1fde3f25fd@intel.com
Changes in v2:
- Rebase to v7.3-rc4
- Move the TDX hypercall helpers into coco/tdx/tdx.c (Kiryl)
- Add a new earlyprintk=tdx option that implements the direct TDVMCALL
path (Dave Hansen)
- Link to v1: https://patch.msgid.link/20260910-b4-tdx_earlyprintk_tdcalls-v1-0-4b2b1bf9001b@intel.com
---
Vishal Verma (2):
x86/tdx: Move port I/O definitions to a shared header
x86/early_printk: Add earlyprintk=tdx to drive the UART with TDVMCALLs
Documentation/admin-guide/kernel-parameters.txt | 8 ++++++
arch/x86/include/asm/shared/tdx.h | 6 ++++
arch/x86/include/asm/tdx.h | 4 +++
arch/x86/boot/compressed/tdx.c | 4 +--
arch/x86/coco/tdx/tdx.c | 37 +++++++++++++++++++++----
arch/x86/kernel/early_printk.c | 20 +++++++++++++
6 files changed, 71 insertions(+), 8 deletions(-)
---
base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4
change-id: 20260903-b4-tdx_earlyprintk_tdcalls-40d1b21a8ba2
Best regards,
--
Vishal Verma <vishal.l.verma@intel.com>
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v5 1/2] x86/tdx: Move port I/O definitions to a shared header
2026-11-02 22:51 [PATCH v5 0/2] x86/tdx: Use TDVMCALLs directly for earlyprintk Vishal Verma
@ 2026-11-02 22:51 ` Vishal Verma
2026-11-02 22:51 ` [PATCH v5 2/2] x86/early_printk: Add earlyprintk=tdx to drive the UART with TDVMCALLs Vishal Verma
1 sibling, 0 replies; 4+ messages in thread
From: Vishal Verma @ 2026-11-02 22:51 UTC (permalink / raw)
To: x86, Dave Hansen, Kiryl Shutsemau, Rick Edgecombe,
Jonathan Corbet, Shuah Khan, Randy Dunlap
Cc: linux-kernel, linux-coco, kvm, linux-doc, Vishal Verma,
Nikolay Borisov
In preparation for adding a 'tdx' option for early_printk which uses
direct TDVMCALLs for I/O, factor out the PORT_READ and PORT_WRITE
definitions from arch/x86/coco/tdx/tdx.c into
arch/x86/include/asm/shared/tdx.h, and namespace them with 'TDVMCALL_'.
The decompressor also used 0/1 literals in its tdx_io_{in,out} helpers -
switch those to use the new definitions.
Signed-off-by: Vishal Verma <vishal.l.verma@intel.com>
Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
Reviewed-by: Rick Edgecombe <rick.p.edgecombe@intel.com>
Reviewed-by: Nikolay Borisov <nik.borisov@suse.com>
Cc: Rick Edgecombe <rick.p.edgecombe@intel.com>
---
arch/x86/include/asm/shared/tdx.h | 6 ++++++
arch/x86/boot/compressed/tdx.c | 4 ++--
arch/x86/coco/tdx/tdx.c | 8 ++------
3 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/arch/x86/include/asm/shared/tdx.h b/arch/x86/include/asm/shared/tdx.h
index 56d741065783b..3d1656a44f75e 100644
--- a/arch/x86/include/asm/shared/tdx.h
+++ b/arch/x86/include/asm/shared/tdx.h
@@ -85,6 +85,12 @@
#define TDVMCALL_STATUS_ALIGN_ERROR 0x8000000000000002ULL
#define TDVMCALL_STATUS_SUBFUNC_UNSUPPORTED 0x8000000000000003ULL
+/*
+ * TDG.VP.VMCALL<Instruction.IO> direction (passed in R13)
+ */
+#define TDVMCALL_PORT_READ 0
+#define TDVMCALL_PORT_WRITE 1
+
/*
* Bitmasks of exposed registers (with VMM).
*/
diff --git a/arch/x86/boot/compressed/tdx.c b/arch/x86/boot/compressed/tdx.c
index 8451d6a1030c6..ed278bac3c939 100644
--- a/arch/x86/boot/compressed/tdx.c
+++ b/arch/x86/boot/compressed/tdx.c
@@ -22,7 +22,7 @@ static inline unsigned int tdx_io_in(int size, u16 port)
.r10 = TDX_HYPERCALL_STANDARD,
.r11 = hcall_func(EXIT_REASON_IO_INSTRUCTION),
.r12 = size,
- .r13 = 0,
+ .r13 = TDVMCALL_PORT_READ,
.r14 = port,
};
@@ -38,7 +38,7 @@ static inline void tdx_io_out(int size, u16 port, u32 value)
.r10 = TDX_HYPERCALL_STANDARD,
.r11 = hcall_func(EXIT_REASON_IO_INSTRUCTION),
.r12 = size,
- .r13 = 1,
+ .r13 = TDVMCALL_PORT_WRITE,
.r14 = port,
.r15 = value,
};
diff --git a/arch/x86/coco/tdx/tdx.c b/arch/x86/coco/tdx/tdx.c
index 8bacbef62891f..9531490c9c246 100644
--- a/arch/x86/coco/tdx/tdx.c
+++ b/arch/x86/coco/tdx/tdx.c
@@ -24,10 +24,6 @@
#define EPT_READ 0
#define EPT_WRITE 1
-/* Port I/O direction */
-#define PORT_READ 0
-#define PORT_WRITE 1
-
/* See Exit Qualification for I/O Instructions in VMX documentation */
#define VE_IS_IO_IN(e) ((e) & BIT(3))
#define VE_GET_IO_SIZE(e) (((e) & GENMASK(2, 0)) + 1)
@@ -673,7 +669,7 @@ static bool handle_in(struct pt_regs *regs, int size, int port)
.r10 = TDX_HYPERCALL_STANDARD,
.r11 = hcall_func(EXIT_REASON_IO_INSTRUCTION),
.r12 = size,
- .r13 = PORT_READ,
+ .r13 = TDVMCALL_PORT_READ,
.r14 = port,
};
bool success;
@@ -702,7 +698,7 @@ static bool handle_out(struct pt_regs *regs, int size, int port)
* "TDG.VP.VMCALL<Instruction.IO>".
*/
return !_tdx_hypercall(hcall_func(EXIT_REASON_IO_INSTRUCTION), size,
- PORT_WRITE, port, regs->ax & mask);
+ TDVMCALL_PORT_WRITE, port, regs->ax & mask);
}
/*
--
2.56.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v5 2/2] x86/early_printk: Add earlyprintk=tdx to drive the UART with TDVMCALLs
2026-11-02 22:51 [PATCH v5 0/2] x86/tdx: Use TDVMCALLs directly for earlyprintk Vishal Verma
2026-11-02 22:51 ` [PATCH v5 1/2] x86/tdx: Move port I/O definitions to a shared header Vishal Verma
@ 2026-11-02 22:51 ` Vishal Verma
2026-11-02 22:59 ` sashiko-bot
1 sibling, 1 reply; 4+ messages in thread
From: Vishal Verma @ 2026-11-02 22:51 UTC (permalink / raw)
To: x86, Dave Hansen, Kiryl Shutsemau, Rick Edgecombe,
Jonathan Corbet, Shuah Khan, Randy Dunlap
Cc: linux-kernel, linux-coco, kvm, linux-doc, Vishal Verma,
Nikolay Borisov
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, add an earlyprintk=tdx option, which can issue the TDVMCALLs
directly. Add a pair of tdx_serial_in() and tdx_serial_out() accessors
and set them up in this case.
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.
Signed-off-by: Vishal Verma <vishal.l.verma@intel.com>
Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
Reviewed-by: Rick Edgecombe <rick.p.edgecombe@intel.com>
Reviewed-by: Nikolay Borisov <nik.borisov@suse.com>
---
Documentation/admin-guide/kernel-parameters.txt | 8 +++++++
arch/x86/include/asm/tdx.h | 4 ++++
arch/x86/coco/tdx/tdx.c | 29 +++++++++++++++++++++++++
arch/x86/kernel/early_printk.c | 20 +++++++++++++++++
4 files changed, 61 insertions(+)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 0b3c140babd94..da5aa688294cc 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -1547,6 +1547,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}]
@@ -1560,6 +1561,13 @@ 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".
+
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 1fcfaed515fec..c6f6a60fbbac4 100644
--- a/arch/x86/include/asm/tdx.h
+++ b/arch/x86/include/asm/tdx.h
@@ -98,6 +98,10 @@ static inline bool tdx_early_handle_ve(struct pt_regs *regs) { return false; }
#endif /* CONFIG_INTEL_TDX_GUEST */
+/* Only referenced when X86_FEATURE_TDX_GUEST is enabled, no stubs needed */
+__noendbr unsigned int tdx_serial_in(unsigned long addr, int offset);
+__noendbr void tdx_serial_out(unsigned long addr, int offset, int value);
+
#if defined(CONFIG_KVM_GUEST) && defined(CONFIG_INTEL_TDX_GUEST)
long tdx_kvm_hypercall(unsigned int nr, unsigned long p1, unsigned long p2,
unsigned long p3, unsigned long p4);
diff --git a/arch/x86/coco/tdx/tdx.c b/arch/x86/coco/tdx/tdx.c
index 9531490c9c246..74d4e479964dd 100644
--- a/arch/x86/coco/tdx/tdx.c
+++ b/arch/x86/coco/tdx/tdx.c
@@ -211,6 +211,35 @@ int tdx_get_max_quote_size(u64 *max_quote_size)
}
EXPORT_SYMBOL_FOR_MODULES(tdx_get_max_quote_size, "tdx-guest");
+/*
+ * early_printk accessors for a TDX guest. Ask the VMM to do the port I/O with
+ * TDG.VP.VMCALL<Instruction.IO> instead of executing IN/OUT and having the
+ * resulting #VE emulate it.
+ */
+__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,
+ .r13 = TDVMCALL_PORT_READ,
+ .r14 = addr + offset,
+ };
+
+ if (__tdx_hypercall(&args))
+ return 0xFF;
+
+ return args.r11;
+}
+ANNOTATE_NOENDBR_SYM(tdx_serial_in);
+
+__noendbr void tdx_serial_out(unsigned long addr, int offset, int value)
+{
+ _tdx_hypercall(hcall_func(EXIT_REASON_IO_INSTRUCTION), 1,
+ TDVMCALL_PORT_WRITE, addr + offset, value);
+}
+ANNOTATE_NOENDBR_SYM(tdx_serial_out);
+
/*
* The kernel cannot handle #VEs when accessing normal kernel memory. Ensure
* that no #VE will be delivered for accesses to TD-private memory.
diff --git a/arch/x86/kernel/early_printk.c b/arch/x86/kernel/early_printk.c
index cba75306e5b62..04dadcd73b857 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)
@@ -196,6 +197,20 @@ static __init void early_serial_init(char *s)
early_serial_hw_init(divisor);
}
+/*
+ * 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);
+}
+
static __noendbr void mem32_serial_out(unsigned long addr, int offset, int value)
{
u32 __iomem *vaddr = (u32 __iomem *)addr;
@@ -414,6 +429,11 @@ static int __init setup_early_printk(char *buf)
early_serial_init(buf + 4);
early_console_register(&early_serial_console, keep);
}
+ if (!strncmp(buf, "tdx", 3)) {
+ early_serial_tdx_init();
+ early_console_register(&early_serial_console, keep);
+ break;
+ }
#ifdef CONFIG_PCI
if (!strncmp(buf, "pciserial", 9)) {
buf += 9; /* Keep from match the above "pciserial" */
--
2.56.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH v5 2/2] x86/early_printk: Add earlyprintk=tdx to drive the UART with TDVMCALLs
2026-11-02 22:51 ` [PATCH v5 2/2] x86/early_printk: Add earlyprintk=tdx to drive the UART with TDVMCALLs Vishal Verma
@ 2026-11-02 22:59 ` sashiko-bot
0 siblings, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-11-02 22:59 UTC (permalink / raw)
To: Vishal Verma; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] x86/boot: missing earlyprintk=tdx support in parse_earlyprintk()
--
commit f1945c05c983a94ab54fe2daf5b1ddd281e8d948
Author: Vishal Verma <vishal.l.verma@intel.com>
x86/early_printk: Add earlyprintk=tdx to drive the UART with TDVMCALLs
This patch adds an earlyprintk=tdx option along with tdx_serial_in() and
tdx_serial_out() accessors so that a TDX guest can drive the early serial
console directly with TDVMCALLs rather than triggering #VE exceptions on
port I/O instructions.
> diff --git a/arch/x86/kernel/early_printk.c b/arch/x86/kernel/early_printk.c
> index cba75306e5b62..04dadcd73b857 100644
> --- a/arch/x86/kernel/early_printk.c
> +++ b/arch/x86/kernel/early_printk.c
[ ... ]
> @@ -414,6 +429,11 @@ static int __init setup_early_printk(char *buf)
> early_serial_init(buf + 4);
> early_console_register(&early_serial_console, keep);
> }
> + if (!strncmp(buf, "tdx", 3)) {
> + early_serial_tdx_init();
> + early_console_register(&early_serial_console, keep);
> + break;
> + }
[Severity: Medium]
As reported by Sashiko on v4 [1], when booting with earlyprintk=tdx, this
adds support in setup_early_printk(), but parse_earlyprintk() in the boot
and decompression code only checks for "serial" and "ttyS":
arch/x86/boot/early_serial_console.c:parse_earlyprintk() {
if (cmdline_find_option("earlyprintk", arg, sizeof(arg)) > 0) {
char *e;
if (!strncmp(arg, "serial", 6)) {
port = DEFAULT_SERIAL_PORT;
pos += 6;
}
...
}
if (port)
early_serial_init(port, baud);
}
In extract_kernel() -> console_init() -> early_serial_console_init() ->
parse_earlyprintk(), early_tdx_detect() has already wired up the port I/O
operations to use TDVMCALLs in a TDX guest. However, because
parse_earlyprintk() does not recognize "tdx", port remains 0 and
early_serial_init() is skipped, leaving early_serial_base unset during
kernel decompression.
Should parse_earlyprintk() in arch/x86/boot/early_serial_console.c also
handle "tdx" so that decompressor serial console output is not lost when
switching from earlyprintk=serial to earlyprintk=tdx?
[1] https://lore.kernel.org/r/20261005-b4-tdx_earlyprintk_tdcalls-v4-2-3f7c56dd568a@intel.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261102-b4-tdx_earlyprintk_tdcalls-v5-0-b01bc95d44ff@intel.com?part=2
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-11-02 22:59 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-02 22:51 [PATCH v5 0/2] x86/tdx: Use TDVMCALLs directly for earlyprintk Vishal Verma
2026-11-02 22:51 ` [PATCH v5 1/2] x86/tdx: Move port I/O definitions to a shared header Vishal Verma
2026-11-02 22:51 ` [PATCH v5 2/2] x86/early_printk: Add earlyprintk=tdx to drive the UART with TDVMCALLs Vishal Verma
2026-11-02 22:59 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox