[efi] Omit EFI_LOAD_FILE2_PROTOCOL for a zero-length initrd

When the Linux kernel is being used with no initrd, iPXE will still
provide a zero-length initrd.magic file within the virtual filesystem.
As of commit 6a004be ("[efi] Support the initrd autodetection
mechanism in newer Linux kernels"), this zero-length file will also be
exposed via an EFI_LOAD_FILE2_PROTOCOL instance on a handle with a
fixed device path.

The correct handling of zero-length files via EFI_LOAD_FILE2_PROTOCOL
is unfortunately not well defined.

Linux expects the first call to LoadFile() to always fail with
EFI_BUFFER_TOO_SMALL.  When the initrd is genuinely zero-length, iPXE
will return success since the buffer is not too small to hold the
(zero-length) file.  This causes Linux to immediately report a
spurious EFI_LOAD_ERROR boot failure.

We could change the logic in iPXE's efi_file_load() to always return
EFI_BUFFER_TOO_SMALL if Buffer is NULL on entry.  Since the correct
behaviour of LoadFile() in the corner case of a zero-length file is
left undefined by the UEFI specification, this would be permissible.

Unfortunately this approach would not fix the problem.  If we return
EFI_BUFFER_TOO_SMALL and set the file length to zero, then Linux will
call the boot services AllocatePages() method with a zero length.  In
at least the EDK2 implementation, this combination of parameters will
cause AllocatePages() to return EFI_OUT_OF_RESOURCES, and Linux will
again report a boot failure.

Another approach would be to install the initrd device path handle
only if we have a non-empty initrd to offer.  Unfortunately this would
lead to a failure in yet another corner case: if a previous bootloader
has installed an initrd device path handle (e.g. to pass a boot script
to iPXE) then we must not leave that initrd in place, since then our
loaded kernel would end up seeing the wrong initrd content.

The cleanest fix seems to be to ensure that the initrd device path
handle is installed with the EFI_DEVICE_PATH_PROTOCOL instance present
but with the EFI_LOAD_FILE2_PROTOCOL instance absent (and forcibly
uninstalled if necessary), matching the state in which we leave the
handle after uninstalling our virtual filesystem.  Linux will then not
find any handle that supports EFI_LOAD_FILE2_PROTOCOL within the fixed
device path, and so will fall through to trying other mechanisms to
locate the initrd.

Reported-by: Chris Bradshaw <cwbshaw@gmail.com>
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 e8debbb..7bcf8d5 100644
--- a/src/interface/efi/efi_file.c
+++ b/src/interface/efi/efi_file.c
@@ -1020,6 +1020,7 @@
  */
 int efi_file_install ( EFI_HANDLE handle ) {
 	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
+	EFI_LOAD_FILE2_PROTOCOL *load;
 	union {
 		EFI_DISK_IO_PROTOCOL *diskio;
 		void *interface;
@@ -1082,9 +1083,17 @@
 	}
 	assert ( diskio.diskio == &efi_disk_io_protocol );
 
-	/* Install Linux initrd fixed device path file */
+	/* Install Linux initrd fixed device path file
+	 *
+	 * Install the device path handle unconditionally, since we
+	 * are definitively the bootloader providing the initrd, if
+	 * any, to the booted image.  Install the load file protocol
+	 * instance only if the initrd is non-empty, since Linux does
+	 * not gracefully handle a zero-length initrd.
+	 */
+	load = ( list_is_singular ( &images ) ? NULL : &efi_file_initrd.load );
 	if ( ( rc = efi_file_path_install ( &efi_file_initrd_path.vendor.Header,
-					    &efi_file_initrd.load ) ) != 0 ) {
+					    load ) ) != 0 ) {
 		goto err_initrd;
 	}