From 6a004be0cceab5d669eedb5a2e6ee2feab31d5bd Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Wed, 15 Feb 2023 15:48:31 +0000 Subject: [efi] Support the initrd autodetection mechanism in newer Linux kernels Linux 5.7 added the ability to autodetect an initrd by searching for a handle via a fixed vendor-specific "Linux initrd device path" and then locating and using the EFI_LOAD_FILE2_PROTOCOL instance on that handle. This maps quite naturally onto our existing concept of a "magic initrd" as introduced for EFI in commit e5f0255 ("[efi] Provide an "initrd.magic" file for use by UEFI kernels"). Add an EFI_LOAD_FILE2_PROTOCOL instance to our EFI virtual files (backed by simply calling the existing EFI_SIMPLE_FILE_SYSTEM_PROTOCOL method to read from the file), and install the protocol instance for the "initrd.magic" virtual file onto a new device handle that also provides the Linux initrd device path. The design choice in Linux of using a single fixed device path makes this unfortunately messy to support, since device paths must be unique within a system. When multiple bootloaders are used (e.g. GRUB loading iPXE loading Linux) then only one bootloader can ever install the device path onto a handle. Subsequent bootloaders must locate the existing handle and replace the load file protocol instance with their own. Signed-off-by: Michael Brown --- src/interface/efi/efi_file.c | 190 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/src/interface/efi/efi_file.c b/src/interface/efi/efi_file.c index 7ed3ea5..e8debbb 100644 --- a/src/interface/efi/efi_file.c +++ b/src/interface/efi/efi_file.c @@ -43,14 +43,21 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); #include #include #include +#include #include #include #include +#include #include /** EFI media ID */ #define EFI_MEDIA_ID_MAGIC 0x69505845 +/** Linux initrd fixed device path vendor GUID */ +#define LINUX_INITRD_VENDOR_GUID \ + { 0x5568e427, 0x68fc, 0x4f3d, \ + { 0xac, 0x74, 0xca, 0x55, 0x52, 0x31, 0xcc, 0x68 } } + /** An EFI virtual file reader */ struct efi_file_reader { /** EFI file */ @@ -69,6 +76,8 @@ struct efi_file { struct refcnt refcnt; /** EFI file protocol */ EFI_FILE_PROTOCOL file; + /** EFI load file protocol */ + EFI_LOAD_FILE2_PROTOCOL load; /** Image (if any) */ struct image *image; /** Filename */ @@ -370,6 +379,8 @@ efi_file_open ( EFI_FILE_PROTOCOL *this, EFI_FILE_PROTOCOL **new, ref_init ( &file->refcnt, efi_file_free ); memcpy ( &new_file->file, &efi_file_root.file, sizeof ( new_file->file ) ); + memcpy ( &new_file->load, &efi_file_root.load, + sizeof ( new_file->load ) ); efi_file_image ( new_file, image_get ( image ) ); *new = &new_file->file; DBGC ( new_file, "EFIFILE %s opened\n", efi_file_name ( new_file ) ); @@ -684,6 +695,44 @@ static EFI_STATUS EFIAPI efi_file_flush ( EFI_FILE_PROTOCOL *this ) { return 0; } +/** + * Load file + * + * @v this EFI file loader + * @v path File path + * @v boot Boot policy + * @v len Buffer size + * @v data Buffer, or NULL + * @ret efirc EFI status code + */ +static EFI_STATUS EFIAPI efi_file_load ( EFI_LOAD_FILE2_PROTOCOL *this, + EFI_DEVICE_PATH_PROTOCOL *path __unused, + BOOLEAN boot __unused, UINTN *len, + VOID *data ) { + struct efi_file *file = container_of ( this, struct efi_file, load ); + size_t max_len; + size_t file_len; + EFI_STATUS efirc; + + /* Calculate maximum length */ + max_len = ( data ? *len : 0 ); + DBGC ( file, "EFIFILE %s load at %p+%#zx\n", + efi_file_name ( file ), data, max_len ); + + /* Check buffer size */ + file_len = efi_file_len ( file ); + if ( file_len > max_len ) { + *len = file_len; + return EFI_BUFFER_TOO_SMALL; + } + + /* Read from file */ + if ( ( efirc = efi_file_read ( &file->file, len, data ) ) != 0 ) + return efirc; + + return 0; +} + /** Root directory */ static struct efi_file efi_file_root = { .refcnt = REF_INIT ( ref_no_free ), @@ -700,6 +749,9 @@ static struct efi_file efi_file_root = { .SetInfo = efi_file_set_info, .Flush = efi_file_flush, }, + .load = { + .LoadFile = efi_file_load, + }, .image = NULL, .name = "", }; @@ -720,11 +772,34 @@ static struct efi_file efi_file_initrd = { .SetInfo = efi_file_set_info, .Flush = efi_file_flush, }, + .load = { + .LoadFile = efi_file_load, + }, .image = NULL, .name = "initrd.magic", .read = efi_file_read_initrd, }; +/** Linux initrd fixed device path */ +static struct { + VENDOR_DEVICE_PATH vendor; + EFI_DEVICE_PATH_PROTOCOL end; +} __attribute__ (( packed )) efi_file_initrd_path = { + .vendor = { + .Header = { + .Type = MEDIA_DEVICE_PATH, + .SubType = MEDIA_VENDOR_DP, + .Length[0] = sizeof ( efi_file_initrd_path.vendor ), + }, + .Guid = LINUX_INITRD_VENDOR_GUID, + }, + .end = { + .Type = END_DEVICE_PATH_TYPE, + .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE, + .Length[0] = sizeof ( efi_file_initrd_path.end ), + }, +}; + /** * Open root directory * @@ -834,6 +909,110 @@ static EFI_DISK_IO_PROTOCOL efi_disk_io_protocol = { }; /** + * (Re)install fixed device path file + * + * @v path Device path + * @v load Load file protocol, or NULL to uninstall protocol + * @ret rc Return status code + * + * Linux 5.7 added the ability to autodetect an initrd by searching + * for a handle via a fixed vendor-specific "Linux initrd device path" + * and then locating and using the EFI_LOAD_FILE2_PROTOCOL instance on + * that handle. + * + * The design choice in Linux of using a single fixed device path + * makes this unfortunately messy to support, since device paths must + * be unique within a system. When multiple bootloaders are used + * (e.g. GRUB loading iPXE loading Linux) then only one bootloader can + * ever install the device path onto a handle. Subsequent bootloaders + * must locate the existing handle and replace the load file protocol + * instance with their own. + */ +static int efi_file_path_install ( EFI_DEVICE_PATH_PROTOCOL *path, + EFI_LOAD_FILE2_PROTOCOL *load ) { + EFI_BOOT_SERVICES *bs = efi_systab->BootServices; + EFI_DEVICE_PATH_PROTOCOL *end; + EFI_HANDLE handle; + VOID *path_copy; + VOID *old; + size_t path_len; + EFI_STATUS efirc; + int rc; + + /* Locate or install the handle with this device path */ + end = path; + if ( ( ( efirc = bs->LocateDevicePath ( &efi_device_path_protocol_guid, + &end, &handle ) ) == 0 ) && + ( end->Type == END_DEVICE_PATH_TYPE ) ) { + + /* Exact match: reuse (or uninstall from) this handle */ + if ( load ) { + DBGC ( path, "EFIFILE %s reusing existing handle\n", + efi_devpath_text ( path ) ); + } + + } else { + + /* Allocate a permanent copy of the device path, since + * this handle will survive after this binary is + * unloaded. + */ + path_len = ( efi_path_len ( path ) + sizeof ( *end ) ); + if ( ( efirc = bs->AllocatePool ( EfiBootServicesData, path_len, + &path_copy ) ) != 0 ) { + rc = -EEFI ( efirc ); + DBGC ( path, "EFIFILE %s could not allocate device path: " + "%s\n", efi_devpath_text ( path ), strerror ( rc ) ); + return rc; + } + memcpy ( path_copy, path, path_len ); + + /* Create a new handle with this device path */ + handle = NULL; + if ( ( efirc = bs->InstallMultipleProtocolInterfaces ( + &handle, + &efi_device_path_protocol_guid, path_copy, + NULL ) ) != 0 ) { + rc = -EEFI ( efirc ); + DBGC ( path, "EFIFILE %s could not create handle: %s\n", + efi_devpath_text ( path ), strerror ( rc ) ); + return rc; + } + } + + /* Uninstall existing load file protocol instance, if any */ + if ( ( ( efirc = bs->HandleProtocol ( handle, &efi_load_file2_protocol_guid, + &old ) ) == 0 ) && + ( ( efirc = bs->UninstallMultipleProtocolInterfaces ( + handle, + &efi_load_file2_protocol_guid, old, + NULL ) ) != 0 ) ) { + rc = -EEFI ( efirc ); + DBGC ( path, "EFIFILE %s could not uninstall %s: %s\n", + efi_devpath_text ( path ), + efi_guid_ntoa ( &efi_load_file2_protocol_guid ), + strerror ( rc ) ); + return rc; + } + + /* Install new load file protocol instance, if applicable */ + if ( ( load != NULL ) && + ( ( efirc = bs->InstallMultipleProtocolInterfaces ( + &handle, + &efi_load_file2_protocol_guid, load, + NULL ) ) != 0 ) ) { + rc = -EEFI ( efirc ); + DBGC ( path, "EFIFILE %s could not install %s: %s\n", + efi_devpath_text ( path ), + efi_guid_ntoa ( &efi_load_file2_protocol_guid ), + strerror ( rc ) ); + return rc; + } + + return 0; +} + +/** * Install EFI simple file system protocol * * @v handle EFI handle @@ -903,8 +1082,16 @@ int efi_file_install ( EFI_HANDLE handle ) { } assert ( diskio.diskio == &efi_disk_io_protocol ); + /* Install Linux initrd fixed device path file */ + if ( ( rc = efi_file_path_install ( &efi_file_initrd_path.vendor.Header, + &efi_file_initrd.load ) ) != 0 ) { + goto err_initrd; + } + return 0; + efi_file_path_install ( &efi_file_initrd_path.vendor.Header, NULL ); + err_initrd: bs->CloseProtocol ( handle, &efi_disk_io_protocol_guid, efi_image_handle, handle ); err_open: @@ -930,6 +1117,9 @@ void efi_file_uninstall ( EFI_HANDLE handle ) { EFI_STATUS efirc; int rc; + /* Uninstall Linux initrd fixed device path file */ + efi_file_path_install ( &efi_file_initrd_path.vendor.Header, NULL ); + /* Close our own disk I/O protocol */ bs->CloseProtocol ( handle, &efi_disk_io_protocol_guid, efi_image_handle, handle ); -- cgit v1.1