From: Daan De Meyer Date: Wed, 1 Apr 2026 18:24:01 +0000 (+0000) Subject: stub: Determine the correct serial console from the ACPI device path X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=e306f4d1234d55e23f55048167d7554d15d31fbb;p=thirdparty%2Fsystemd.git stub: Determine the correct serial console from the ACPI device path Instead of requiring exactly one serial device and assuming ttyS0, extract the COM port index from the ACPI device path and use the uart I/O port address format for the console= kernel argument. On x86, the ACPI UID for PNP0501 (16550 UART) maps directly to the COM port number: UID 0 = COM1 (0x3F8), UID 1 = COM2 (0x2F8), etc. The I/O port addresses are fixed in the kernel (see arch/x86/include/asm/serial.h). Using the console=uart,io, format (see Documentation/admin-guide/kernel-parameters.txt) addresses the UART by I/O port directly rather than relying on ttyS naming, and also provides early console output before the full serial driver loads. Restrict the entire serial console auto-detection to x86. On non-x86 (e.g. ARM with PL011 UARTs), displays may be available without GOP (e.g. simple-framebuffer via device tree), serial device indices are assigned dynamically during probe rather than being fixed to I/O port addresses, and the kernel has its own console auto-detection via DT stdout-path. When ConOut has no device path (ConSplitter), all text output handles are enumerated. If multiple handles have PNP0501 UART nodes with different UIDs, bail out rather than guessing. Add ACPI_DP device path subtype, ACPI_HID_DEVICE_PATH struct, and EISA_PNP_ID() macro to device-path.h for parsing ACPI device path nodes. Remove MSG_UART_DP, device_path_has_uart(), count_serial_devices() and proto/serial-io.h (no longer needed). Move all the console logic to console.c as well. --- diff --git a/src/boot/console.c b/src/boot/console.c index 81a5641d405..8dcd986aa4f 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 4d0d1364d8f..3a2bc6391cd 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 531ff3d003b..d81c0e1f8dd 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 98690c108c2..00000000000 --- 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 02621b68bb9..3c8318a2adf 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);