diff options
author | Michael Brown <mcb30@ipxe.org> | 2023-02-15 15:48:31 +0000 |
---|---|---|
committer | Michael Brown <mcb30@ipxe.org> | 2023-02-15 17:36:47 +0000 |
commit | 6a004be0cceab5d669eedb5a2e6ee2feab31d5bd (patch) | |
tree | 433b4f9bb316d977b23d1e6bc7cec8467435c1d5 /src | |
parent | cf9ad00afcd6d8873fbefbbaf1f0813948a796ba (diff) | |
download | ipxe-6a004be0cceab5d669eedb5a2e6ee2feab31d5bd.zip ipxe-6a004be0cceab5d669eedb5a2e6ee2feab31d5bd.tar.gz ipxe-6a004be0cceab5d669eedb5a2e6ee2feab31d5bd.tar.bz2 |
[efi] Support the initrd autodetection mechanism in newer Linux kernelsefiinitrd
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 <mcb30@ipxe.org>
Diffstat (limited to 'src')
-rw-r--r-- | src/interface/efi/efi_file.c | 190 |
1 files changed, 190 insertions, 0 deletions
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 <ipxe/efi/Protocol/SimpleFileSystem.h> #include <ipxe/efi/Protocol/BlockIo.h> #include <ipxe/efi/Protocol/DiskIo.h> +#include <ipxe/efi/Protocol/LoadFile2.h> #include <ipxe/efi/Guid/FileInfo.h> #include <ipxe/efi/Guid/FileSystemInfo.h> #include <ipxe/efi/efi_strings.h> +#include <ipxe/efi/efi_path.h> #include <ipxe/efi/efi_file.h> /** 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 ); |