diff --git a/src/boot/proto/device-path.h b/src/boot/proto/device-path.h index b56c217082..531ff3d003 100644 --- a/src/boot/proto/device-path.h +++ b/src/boot/proto/device-path.h @@ -33,6 +33,7 @@ enum { MEDIA_PIWG_FW_FILE_DP = 0x06, MEDIA_PIWG_FW_VOL_DP = 0x07, + MSG_UART_DP = 0x0e, MSG_URI_DP = 24, }; diff --git a/src/boot/proto/pci-io.h b/src/boot/proto/pci-io.h new file mode 100644 index 0000000000..d05f0a683c --- /dev/null +++ b/src/boot/proto/pci-io.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_PCI_IO_PROTOCOL_GUID \ + GUID_DEF(0x4cf5b200, 0x68b8, 0x4ca5, 0x9e, 0xec, 0xb2, 0x3e, 0x3f, 0x50, 0x02, 0x9a) + +typedef enum { + EfiPciIoWidthUint8 = 0, + EfiPciIoWidthUint16, + EfiPciIoWidthUint32, + EfiPciIoWidthUint64, +} EFI_PCI_IO_PROTOCOL_WIDTH; + +typedef struct EFI_PCI_IO_PROTOCOL EFI_PCI_IO_PROTOCOL; + +typedef EFI_STATUS (EFIAPI *EFI_PCI_IO_PROTOCOL_CONFIG)( + EFI_PCI_IO_PROTOCOL *This, + EFI_PCI_IO_PROTOCOL_WIDTH Width, + uint32_t Offset, + size_t Count, + void *Buffer); + +typedef struct { + EFI_PCI_IO_PROTOCOL_CONFIG Read; + EFI_PCI_IO_PROTOCOL_CONFIG Write; +} EFI_PCI_IO_PROTOCOL_CONFIG_ACCESS; + +/* Minimal definition — only Pci.Read is used. Fields before Pci must be correctly sized + * (one function pointer each for PollMem/PollIo, two for Mem.Read/Write, two for Io.Read/Write) + * to ensure Pci is at the right offset. */ +struct EFI_PCI_IO_PROTOCOL { + void *PollMem; + void *PollIo; + EFI_PCI_IO_PROTOCOL_CONFIG_ACCESS Mem; + EFI_PCI_IO_PROTOCOL_CONFIG_ACCESS Io; + EFI_PCI_IO_PROTOCOL_CONFIG_ACCESS Pci; + /* remaining fields omitted */ +}; + +#define PCI_VENDOR_ID_REDHAT 0x1af4U +#define PCI_DEVICE_ID_VIRTIO_CONSOLE 0x1003U diff --git a/src/boot/proto/serial-io.h b/src/boot/proto/serial-io.h new file mode 100644 index 0000000000..98690c108c --- /dev/null +++ b/src/boot/proto/serial-io.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_SERIAL_IO_PROTOCOL_GUID \ + GUID_DEF(0xbb25cf6f, 0xf1d4, 0x11d2, 0x9a, 0x0c, 0x00, 0x90, 0x27, 0x3f, 0xc1, 0xfd) diff --git a/src/boot/stub.c b/src/boot/stub.c index 66b20805d5..6b5ae0a687 100644 --- a/src/boot/stub.c +++ b/src/boot/stub.c @@ -6,6 +6,7 @@ #include "devicetree.h" #include "efi-efivars.h" #include "efi-log.h" +#include "efi-string.h" #include "export-vars.h" #include "graphics.h" #include "iovec-util-fundamental.h" @@ -14,6 +15,9 @@ #include "memory-util-fundamental.h" #include "part-discovery.h" #include "pe.h" +#include "proto/graphics-output.h" +#include "proto/pci-io.h" +#include "proto/serial-io.h" /* IWYU pragma: keep */ #include "proto/shell-parameters.h" #include "random-seed.h" #include "sbat.h" @@ -1242,6 +1246,220 @@ static void measure_profile(unsigned profile, int *parameters_measured) { combine_measured_flag(parameters_measured, m); } +static bool has_virtio_console_pci_device(void) { + _cleanup_free_ EFI_HANDLE *handles = NULL; + size_t n_handles = 0; + + EFI_STATUS err = BS->LocateHandleBuffer( + ByProtocol, + MAKE_GUID_PTR(EFI_PCI_IO_PROTOCOL), + NULL, + &n_handles, + &handles); + if (err != EFI_SUCCESS) { + log_debug_status(err, "Failed to locate PCI I/O protocol handles, assuming no VirtIO console: %m"); + return false; + } + + if (n_handles == 0) { + log_debug("No PCI devices found, not scanning for VirtIO console."); + return false; + } + + log_debug("Found %zu PCI devices, scanning for VirtIO console...", n_handles); + + size_t n_virtio_console = 0; + + for (size_t i = 0; i < n_handles; i++) { + EFI_PCI_IO_PROTOCOL *pci_io = NULL; + + if (BS->HandleProtocol(handles[i], MAKE_GUID_PTR(EFI_PCI_IO_PROTOCOL), (void **) &pci_io) != EFI_SUCCESS) + continue; + + uint16_t vendor_id = 0, device_id = 0; + if (pci_io->Pci.Read(pci_io, EfiPciIoWidthUint16, 0x00, 1, &vendor_id) != EFI_SUCCESS) + continue; + if (pci_io->Pci.Read(pci_io, EfiPciIoWidthUint16, 0x02, 1, &device_id) != EFI_SUCCESS) + continue; + + log_debug("PCI device %zu: vendor=%04x device=%04x", i, vendor_id, device_id); + + if (vendor_id == PCI_VENDOR_ID_REDHAT && device_id == PCI_DEVICE_ID_VIRTIO_CONSOLE) + n_virtio_console++; + + if (n_virtio_console > 1) { + log_debug("There is more than one VirtIO console PCI device, cannot determine which one is the console."); + return false; + } + } + + if (n_virtio_console == 0) { + log_debug("No VirtIO console PCI device found."); + return false; + } + + log_debug("Found exactly one VirtIO console PCI device."); + return true; +} + +static bool device_path_has_uart(const EFI_DEVICE_PATH *dp) { + for (const EFI_DEVICE_PATH *node = dp; !device_path_is_end(node); node = device_path_next_node(node)) + if (node->Type == MESSAGING_DEVICE_PATH && node->SubType == MSG_UART_DP) + return true; + + return false; +} + +static size_t count_serial_devices(void) { + _cleanup_free_ EFI_HANDLE *handles = NULL; + size_t n_handles = 0; + + if (BS->LocateHandleBuffer( + ByProtocol, + MAKE_GUID_PTR(EFI_SERIAL_IO_PROTOCOL), + NULL, + &n_handles, + &handles) != EFI_SUCCESS) + return 0; + + log_debug("Found %zu serial I/O devices in total.", n_handles); + return n_handles; +} + +static bool has_single_serial_console(void) { + /* Even if we find exactly one serial console, we can only confidently map it to ttyS0 + * if there's only one serial device in the entire system. With multiple UARTs, the + * kernel assigns ttyS indices based on its own discovery order, so the console UART + * might end up as ttyS1 or higher. */ + size_t n_serial_devices = count_serial_devices(); + if (n_serial_devices == 0) { + log_debug("No serial I/O devices found."); + return false; + } + if (n_serial_devices > 1) { + log_debug("Found %zu serial I/O devices, cannot determine ttyS index.", n_serial_devices); + return false; + } + + /* Exactly one serial device in the system. Verify it's actually used as a console + * by checking if ConOut or any text output handle has a UART device path. */ + + /* First try the ConOut handle directly */ + EFI_DEVICE_PATH *dp = NULL; + if (BS->HandleProtocol(ST->ConsoleOutHandle, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &dp) == EFI_SUCCESS) { + _cleanup_free_ char16_t *dp_str = NULL; + (void) device_path_to_str(dp, &dp_str); + log_debug("ConOut device path: %ls", strempty(dp_str)); + + if (device_path_has_uart(dp)) { + log_debug("ConOut device path contains UART node."); + return true; + } + + log_debug("ConOut device path does not contain UART node."); + return false; + } + + /* ConOut handle has no device path (e.g. ConSplitter virtual handle). Enumerate all + * text output handles and check if any of them is a serial console. */ + log_debug("ConOut handle has no device path, enumerating text output handles..."); + + _cleanup_free_ EFI_HANDLE *handles = NULL; + size_t n_handles = 0; + if (BS->LocateHandleBuffer( + ByProtocol, + MAKE_GUID_PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL), + NULL, + &n_handles, + &handles) != EFI_SUCCESS) { + log_debug("Failed to enumerate text output handles."); + return false; + } + + for (size_t i = 0; i < n_handles; i++) { + dp = NULL; + if (BS->HandleProtocol(handles[i], MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &dp) != EFI_SUCCESS) + continue; + + _cleanup_free_ char16_t *dp_str = NULL; + (void) device_path_to_str(dp, &dp_str); + log_debug("Text output handle %zu device path: %ls", i, strempty(dp_str)); + + if (device_path_has_uart(dp)) { + log_debug("Text output handle %zu is a serial console.", i); + return true; + } + } + + log_debug("No serial console found among text output handles."); + return false; +} + +static bool has_graphics_output(void) { + EFI_GRAPHICS_OUTPUT_PROTOCOL *gop = NULL; + EFI_STATUS err; + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_GRAPHICS_OUTPUT_PROTOCOL), NULL, (void **) &gop); + if (err != EFI_SUCCESS) { + log_debug_status(err, "No EFI Graphics Output Protocol found: %m"); + return false; + } + + log_debug("EFI Graphics Output Protocol found."); + return true; +} + +static const char16_t *serial_console_arg(void) { +#if defined(__arm__) || defined(__aarch64__) + return u"console=ttyAMA0"; +#else + return u"console=ttyS0"; +#endif +} + +/* If there's no console= in the command line yet, try to detect the appropriate console device. + * + * Detection order: + * 1. If exactly one VirtIO console PCI device exists → console=hvc0 + * 2. If there's graphical output (GOP) → don't add console=, the kernel defaults are fine + * 3. If exactly one serial console exists → arch-specific serial (ttyS0, ttyAMA0, etc.) + * 4. Otherwise → don't add console=, let the user handle it + * + * VirtIO console takes priority since it's explicitly configured by the VMM. Graphics is + * checked before serial to avoid accidentally redirecting output away from a graphical + * console by adding a serial console= argument. */ +static void cmdline_append_console(char16_t **cmdline) { + assert(cmdline); + + if (*cmdline && (efi_fnmatch(u"console=*", *cmdline) || efi_fnmatch(u"* console=*", *cmdline))) { + log_debug("Kernel command line already contains console=, not adding one."); + return; + } + + const char16_t *console_arg = NULL; + + if (has_virtio_console_pci_device()) + console_arg = u"console=hvc0"; + else if (has_graphics_output()) { + log_debug("Graphical output available, not adding console= to kernel command line."); + return; + } else if (has_single_serial_console()) + console_arg = serial_console_arg(); + + if (!console_arg) { + log_debug("Cannot determine console type, not adding console= to kernel command line."); + return; + } + + log_debug("Appending %ls to kernel command line.", console_arg); + + _cleanup_free_ char16_t *old = TAKE_PTR(*cmdline); + if (isempty(old)) + *cmdline = xstrdup16(console_arg); + else + *cmdline = xasprintf("%ls %ls", old, console_arg); +} + static EFI_STATUS run(EFI_HANDLE image) { int sections_measured = -1, parameters_measured = -1, sysext_measured = -1, confext_measured = -1; _cleanup_(devicetree_cleanup) struct devicetree_state dt_state = {}; @@ -1304,6 +1522,12 @@ static EFI_STATUS run(EFI_HANDLE image) { cmdline_append_and_measure_addons(cmdline_addons, &cmdline, ¶meters_measured); cmdline_append_and_measure_smbios(&cmdline, ¶meters_measured); + /* Console auto-detection is intentionally not TPM-measured. The value is deterministically + * derived from firmware-reported hardware state (PCI device enumeration, GOP presence, serial + * device paths), so it doesn't represent an independent input that could be manipulated + * without also changing the firmware environment that TPM already captures. */ + cmdline_append_console(&cmdline); + export_common_variables(loaded_image); export_stub_variables(loaded_image, profile);