[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 <mcb30@ipxe.org>
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 @@
 #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 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 @@
 	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 @@
 	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 @@
 		.SetInfo = efi_file_set_info,
 		.Flush = efi_file_flush,
 	},
+	.load = {
+		.LoadFile = efi_file_load,
+	},
 	.image = NULL,
 	.name = "",
 };
@@ -720,11 +772,34 @@
 		.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 @@
 };
 
 /**
+ * (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 @@
 	}
 	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 @@
 	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 );