stub: auto-detect console device and append console= to kernel command line
The Linux kernel does not reliably auto-detect serial consoles on
headless systems. While the docs claim serial is used as a fallback
when no VGA card is found, in practice CONFIG_VT's dummy console
(dummycon) registers early and satisfies the kernel's console
requirement, preventing the serial fallback from ever triggering. The
ACPI SPCR table can help on ARM/RISC-V where QEMU generates it, but
x86 QEMU does not produce SPCR, and SPCR cannot describe virtio
consoles at all. This means UKIs booted via sd-stub in headless VMs
produce no visible console output unless console= is explicitly
passed on the kernel command line.
Fix this by having sd-stub auto-detect the console type and append an
appropriate console= argument when one isn't already present.
Detection priority:
1. VirtIO console PCI device (vendor 0x1AF4, device 0x1003): if
exactly one is found, append console=hvc0. This takes highest
priority since a VirtIO console is explicitly configured by the
VMM (e.g. systemd-vmspawn's virtconsole device). If multiple
VirtIO console devices exist, we cannot determine which hvc index
is correct, so we skip this path entirely.
2. EFI Graphics Output Protocol (GOP): if present, don't add any
console= argument. The kernel will use the framebuffer console by
default, and adding a serial console= would redirect the primary
console away from the display.
3. Serial console: first, we count the total number of serial devices
via EFI_SERIAL_IO_PROTOCOL. If there are zero or more than one,
we bail out — with multiple UARTs, the kernel assigns ttyS indices
based on its own enumeration order and we cannot determine which
index the console UART will receive. Only when exactly one serial
device exists (guaranteeing it will be ttyS0) do we proceed to
verify it's actually used as a console by checking for UART device
path nodes (MESSAGING_DEVICE_PATH + MSG_UART_DP). The firmware's
ConOut handle is checked first; if it has no device path (common
with OVMF's ConSplitter virtual handle when using -nographic
-nodefaults), we fall back to enumerating all
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL handles and checking each one's
device path. The architecture-specific console argument is then
appended:
- x86: console=ttyS0
- ARM: console=ttyAMA0
- Others: console=ttyS0 (RISC-V, LoongArch, MIPS all use ttyS0)
Note on OVMF's VirtioSerialDxe: it exposes virtio serial ports with
the same UART device path nodes as real serial ports (ACPI PNP 0x0501
+ MSG_UART_DP), making them indistinguishable from real UARTs via
device path inspection alone. This is why we check for the VirtIO
console PCI device via EFI_PCI_IO_PROTOCOL before falling back to
device path analysis.
Also add a minimal EFI_PCI_IO_PROTOCOL definition (proto/pci-io.h)
with just enough to call Pci.Read for vendor/device ID enumeration,
and add the MSG_UART_DP subtype to the device path header.
Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>