diff --git a/src/boot/console.c b/src/boot/console.c index 81a5641d40..8dcd986aa4 100644 --- a/src/boot/console.c +++ b/src/boot/console.c @@ -1,8 +1,13 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "console.h" +#include "device-path-util.h" #include "efi-log.h" +#include "efi-string.h" #include "proto/graphics-output.h" +#include "proto/pci-io.h" +#include "string-util-fundamental.h" +#include "util.h" #define SYSTEM_FONT_WIDTH 8 #define SYSTEM_FONT_HEIGHT 19 @@ -347,3 +352,251 @@ EFI_STATUS console_query_mode(size_t *x_max, size_t *y_max) { return err; } + +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; + + /* Read PCI vendor ID and device ID (at offsets 0x00 and 0x02 in PCI config space) */ + uint16_t pci_id[2] = {}; + if (pci_io->Pci.Read(pci_io, EfiPciIoWidthUint16, /* offset= */ 0x00, /* count= */ 2, pci_id) != EFI_SUCCESS) + continue; + + log_debug("PCI device %zu: vendor=%04x device=%04x", i, pci_id[0], pci_id[1]); + + if (pci_id[0] == PCI_VENDOR_ID_REDHAT && pci_id[1] == 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 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; +} + +#if defined(__i386__) || defined(__x86_64__) + +/* Walk the device path looking for a UART console and determine the COM port index from the + * ACPI device path node. On x86, the Linux kernel assigns fixed ttyS indices based on I/O port + * addresses (see arch/x86/include/asm/serial.h): + * + * ttyS0=0x3F8, ttyS1=0x2F8, ttyS2=0x3E8, ttyS3=0x2E8 + * + * On standard PC firmware, the ACPI UID for PNP0501 (16550 UART) maps directly to the COM port + * index: UID 0 = COM1 (0x3F8) = ttyS0, UID 1 = COM2 (0x2F8) = ttyS1, etc. + * + * Returns EFI_SUCCESS and sets *ret_index on success, or EFI_NOT_FOUND if no PNP0501 UART + * was found. */ +static EFI_STATUS device_path_get_uart_index(const EFI_DEVICE_PATH *dp, uint32_t *ret_index) { + assert(ret_index); + + for (const EFI_DEVICE_PATH *node = dp; !device_path_is_end(node); node = device_path_next_node(node)) + if (node->Type == ACPI_DEVICE_PATH && + node->SubType == ACPI_DP && + node->Length >= sizeof(ACPI_HID_DEVICE_PATH)) { + const ACPI_HID_DEVICE_PATH *acpi = (const ACPI_HID_DEVICE_PATH *) node; + if (acpi->HID == EISA_PNP_ID(0x0501)) { + *ret_index = acpi->UID; + return EFI_SUCCESS; + } + } + + return EFI_NOT_FOUND; +} + +/* Check if the console output is a serial UART. If so, determine the COM port index from the + * ACPI device path so we can pass the correct console= device to the kernel. */ +static EFI_STATUS find_serial_console_index(uint32_t *ret_index) { + assert(ret_index); + + /* 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_get_uart_index(dp, ret_index) == EFI_SUCCESS) { + log_debug("ConOut is a serial console (port index %u).", *ret_index); + return EFI_SUCCESS; + } + + log_debug("ConOut device path does not contain a PNP0501 UART node."); + return EFI_NOT_FOUND; + } + + /* 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 EFI_NOT_FOUND; + } + + bool found = 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)); + + uint32_t index; + if (device_path_get_uart_index(dp, &index) != EFI_SUCCESS) + continue; + + log_debug("Text output handle %zu is a serial console (port index %u).", i, index); + + if (found && *ret_index != index) { + log_debug("Multiple serial consoles with different port indices found, cannot determine which one to use."); + return EFI_NOT_FOUND; + } + + *ret_index = index; + found = true; + } + + if (!found) { + log_debug("No serial console found among text output handles."); + return EFI_NOT_FOUND; + } + + return EFI_SUCCESS; +} + +static const char16_t *serial_console_arg(uint32_t index) { + /* Use the uart I/O port address format (see Documentation/admin-guide/kernel-parameters.txt) + * instead of ttyS names. This addresses the 8250/16550 UART at the specified I/O port + * directly and switches to the matching ttyS device later. The I/O port addresses for + * the standard COM ports are fixed (see arch/x86/include/asm/serial.h), and the ACPI UID + * for PNP0501 maps directly to the COM port index. */ + static const char16_t *const table[] = { + u"console=uart,io,0x3f8", /* COM1 */ + u"console=uart,io,0x2f8", /* COM2 */ + u"console=uart,io,0x3e8", /* COM3 */ + u"console=uart,io,0x2e8", /* COM4 */ + }; + + if (index >= ELEMENTSOF(table)) + return NULL; + + return table[index]; +} + +#endif /* __i386__ || __x86_64__ */ + +/* 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. On x86, if exactly one serial console exists -> console=uart,io, + * 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. + * + * Serial console auto-detection is restricted to x86 where ACPI PNP0501 UIDs map to fixed + * I/O port addresses for 8250/16550 UARTs. On non-x86 (e.g. ARM), serial device indices are + * assigned dynamically, and the kernel has its own console auto-detection mechanisms + * (DT stdout-path, etc.). + * + * Not TPM-measured because the value is deterministically derived from firmware-reported + * hardware state (PCI device enumeration, GOP presence, serial device paths). */ +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; + } +#if defined(__i386__) || defined(__x86_64__) + else { + uint32_t serial_index; + if (find_serial_console_index(&serial_index) == EFI_SUCCESS) + console_arg = serial_console_arg(serial_index); + } +#endif + + 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); +} diff --git a/src/boot/console.h b/src/boot/console.h index 4d0d1364d8..3a2bc6391c 100644 --- a/src/boot/console.h +++ b/src/boot/console.h @@ -36,3 +36,4 @@ EFI_STATUS console_key_read(uint64_t *ret_key, uint64_t timeout_usec); EFI_STATUS console_set_mode(int64_t mode); EFI_STATUS console_query_mode(size_t *x_max, size_t *y_max); EFI_STATUS query_screen_resolution(uint32_t *ret_width, uint32_t *ret_height); +void cmdline_append_console(char16_t **cmdline); diff --git a/src/boot/proto/device-path.h b/src/boot/proto/device-path.h index 531ff3d003..d81c0e1f8d 100644 --- a/src/boot/proto/device-path.h +++ b/src/boot/proto/device-path.h @@ -27,13 +27,14 @@ enum { HW_MEMMAP_DP = 0x03, + ACPI_DP = 0x01, + MEDIA_HARDDRIVE_DP = 0x01, MEDIA_VENDOR_DP = 0x03, MEDIA_FILEPATH_DP = 0x04, MEDIA_PIWG_FW_FILE_DP = 0x06, MEDIA_PIWG_FW_VOL_DP = 0x07, - MSG_UART_DP = 0x0e, MSG_URI_DP = 24, }; @@ -48,6 +49,15 @@ typedef struct { EFI_GUID Guid; } _packed_ VENDOR_DEVICE_PATH; +/* EISA PNP ID encoding: compressed 3-letter vendor + 16-bit product ID. */ +#define EISA_PNP_ID(Id) ((uint32_t) (((Id) << 16) | 0x41D0)) + +typedef struct { + EFI_DEVICE_PATH Header; + uint32_t HID; + uint32_t UID; +} _packed_ ACPI_HID_DEVICE_PATH; + typedef struct { EFI_DEVICE_PATH Header; uint32_t MemoryType; diff --git a/src/boot/proto/serial-io.h b/src/boot/proto/serial-io.h deleted file mode 100644 index 98690c108c..0000000000 --- a/src/boot/proto/serial-io.h +++ /dev/null @@ -1,7 +0,0 @@ -/* 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 02621b68bb..3c8318a2ad 100644 --- a/src/boot/stub.c +++ b/src/boot/stub.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "boot-secret.h" +#include "console.h" #include "cpio.h" #include "device-path-util.h" #include "devicetree.h" @@ -15,9 +16,6 @@ #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" @@ -1246,219 +1244,6 @@ 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; - - /* Read PCI vendor ID and device ID (at offsets 0x00 and 0x02 in PCI config space) */ - uint16_t pci_id[2] = {}; - if (pci_io->Pci.Read(pci_io, EfiPciIoWidthUint16, /* offset= */ 0x00, /* count= */ 2, pci_id) != EFI_SUCCESS) - continue; - - log_debug("PCI device %zu: vendor=%04x device=%04x", i, pci_id[0], pci_id[1]); - - if (pci_id[0] == PCI_VENDOR_ID_REDHAT && pci_id[1] == 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 = {}; @@ -1521,10 +1306,6 @@ 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);