Re: [PATCH v2 2/3] efi/libstub: Add support for loading the initrd from a device path

From: Laszlo Ersek
Date: Mon Feb 17 2020 - 05:27:01 EST


On 02/17/20 11:22, Ard Biesheuvel wrote:
> On Mon, 17 Feb 2020 at 10:23, Ard Biesheuvel <ardb@xxxxxxxxxx> wrote:
>>
>> On Mon, 17 Feb 2020 at 10:15, Laszlo Ersek <lersek@xxxxxxxxxx> wrote:
>>>
>>> On 02/16/20 15:11, Ard Biesheuvel wrote:
>>>> There are currently two ways to specify the initrd to be passed to the
>>>> Linux kernel when booting via the EFI stub:
>>>> - it can be passed as a initrd= command line option when doing a pure PE
>>>> boot (as opposed to the EFI handover protocol that exists for x86)
>>>> - otherwise, the bootloader or firmware can load the initrd into memory,
>>>> and pass the address and size via the bootparams struct (x86) or
>>>> device tree (ARM)
>>>>
>>>> In the first case, we are limited to loading from the same file system
>>>> that the kernel was loaded from, and it is also problematic in a trusted
>>>> boot context, given that we cannot easily protect the command line from
>>>> tampering without either adding complicated white/blacklisting of boot
>>>> arguments or locking down the command line altogether.
>>>>
>>>> In the second case, we force the bootloader to duplicate knowledge about
>>>> the boot protocol which is already encoded in the stub, and which may be
>>>> subject to change over time, e.g., bootparams struct definitions, memory
>>>> allocation/alignment requirements for the placement of the initrd etc etc.
>>>> In the ARM case, it also requires the bootloader to modify the hardware
>>>> description provided by the firmware, as it is passed in the same file.
>>>> On systems where the initrd is measured after loading, it creates a time
>>>> window where the initrd contents might be manipulated in memory before
>>>> handing over to the kernel.
>>>>
>>>> Address these concerns by adding support for loading the initrd into
>>>> memory by invoking the EFI LoadFile2 protocol installed on a vendor
>>>> GUIDed device path that specifically designates a Linux initrd.
>>>> This addresses the above concerns, by putting the EFI stub in charge of
>>>> placement in memory and of passing the base and size to the kernel proper
>>>> (via whatever means it desires) while still leaving it up to the firmware
>>>> or bootloader to obtain the file contents, potentially from other file
>>>> systems than the one the kernel itself was loaded from. On platforms that
>>>> implement measured boot, it permits the firmware to take the measurement
>>>> right before the kernel actually consumes the contents.
>>>>
>>>> Signed-off-by: Ard Biesheuvel <ardb@xxxxxxxxxx>
>>>> ---
>>>> drivers/firmware/efi/libstub/arm-stub.c | 15 +++-
>>>> drivers/firmware/efi/libstub/efi-stub-helper.c | 82 ++++++++++++++++++++
>>>> drivers/firmware/efi/libstub/efistub.h | 4 +
>>>> drivers/firmware/efi/libstub/x86-stub.c | 23 ++++++
>>>> include/linux/efi.h | 1 +
>>>> 5 files changed, 122 insertions(+), 3 deletions(-)
>>>>
>>>> diff --git a/drivers/firmware/efi/libstub/arm-stub.c b/drivers/firmware/efi/libstub/arm-stub.c
>>>> index 2edc673ea06c..4bae620b95b9 100644
>>>> --- a/drivers/firmware/efi/libstub/arm-stub.c
>>>> +++ b/drivers/firmware/efi/libstub/arm-stub.c
>>>> @@ -160,6 +160,7 @@ unsigned long efi_entry(void *handle, efi_system_table_t *sys_table_arg,
>>>> enum efi_secureboot_mode secure_boot;
>>>> struct screen_info *si;
>>>> efi_properties_table_t *prop_tbl;
>>>> + unsigned long max_addr;
>>>>
>>>> sys_table = sys_table_arg;
>>>>
>>>> @@ -258,10 +259,18 @@ unsigned long efi_entry(void *handle, efi_system_table_t *sys_table_arg,
>>>> if (!fdt_addr)
>>>> pr_efi("Generating empty DTB\n");
>>>>
>>>> - status = efi_load_initrd(image, &initrd_addr, &initrd_size, ULONG_MAX,
>>>> - efi_get_max_initrd_addr(dram_base, *image_addr));
>>>> + max_addr = efi_get_max_initrd_addr(dram_base, *image_addr);
>>>> + status = efi_load_initrd_dev_path(&initrd_addr, &initrd_size, max_addr);
>>>> + if (status == EFI_SUCCESS) {
>>>> + pr_efi("Loaded initrd from LINUX_EFI_INITRD_MEDIA_GUID device path\n");
>>>> + } else if (status == EFI_NOT_FOUND) {
>>>> + status = efi_load_initrd(image, &initrd_addr, &initrd_size,
>>>> + ULONG_MAX, max_addr);
>>>> + if (status == EFI_SUCCESS)
>>>> + pr_efi("Loaded initrd from command line option\n");
>>>> + }
>>>> if (status != EFI_SUCCESS)
>>>> - pr_efi_err("Failed initrd from command line!\n");
>>>> + pr_efi_err("Failed to load initrd!\n");
>>>>
>>>> efi_random_get_seed();
>>>>
>>>> diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
>>>> index 49008ac88b63..e37afe2c752e 100644
>>>> --- a/drivers/firmware/efi/libstub/efi-stub-helper.c
>>>> +++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
>>>> @@ -299,3 +299,85 @@ void efi_char16_printk(efi_char16_t *str)
>>>> efi_call_proto(efi_table_attr(efi_system_table(), con_out),
>>>> output_string, str);
>>>> }
>>>> +
>>>> +/*
>>>> + * The LINUX_EFI_INITRD_MEDIA_GUID vendor media device path below provides a way
>>>> + * for the firmware or bootloader to expose the initrd data directly to the stub
>>>> + * via the trivial LoadFile2 protocol, which is defined in the UEFI spec, and is
>>>> + * very easy to implement. It is a simple Linux initrd specific conduit between
>>>> + * kernel and firmware, allowing us to put the EFI stub (being part of the
>>>> + * kernel) in charge of where and when to load the initrd, while leaving it up
>>>> + * to the firmware to decide whether it needs to expose its filesystem hierarchy
>>>> + * via EFI protocols.
>>>> + */
>>>> +static const struct {
>>>> + struct efi_vendor_dev_path vendor;
>>>> + struct efi_generic_dev_path end;
>>>> +} __packed initrd_dev_path = {
>>>> + {
>>>> + EFI_DEV_MEDIA,
>>>> + EFI_DEV_MEDIA_VENDOR,
>>>> + sizeof(struct efi_vendor_dev_path),
>>>> + LINUX_EFI_INITRD_MEDIA_GUID
>>>> + }, {
>>>> + EFI_DEV_END_PATH,
>>>> + EFI_DEV_END_ENTIRE,
>>>> + sizeof(struct efi_generic_dev_path)
>>>> + }
>>>> +};
>>>> +
>>>> +/**
>>>> + * efi_load_initrd_dev_path - load the initrd from the Linux initrd device path
>>>> + * @load_addr: pointer to store the address where the initrd was loaded
>>>> + * @load_size: pointer to store the size of the loaded initrd
>>>> + * @max: upper limit for the initrd memory allocation
>>>> + * @return: %EFI_SUCCESS if the initrd was loaded successfully, in which case
>>>> + * @load_addr and @load_size are assigned accordingly
>>>> + * %EFI_NOT_FOUND if no LoadFile2 protocol exists on the initrd
>>>> + * device path
>>>> + * %EFI_LOAD_ERROR in all other cases
>>>
>>> [*]
>>>
>>>> + */
>>>> +efi_status_t efi_load_initrd_dev_path(unsigned long *load_addr,
>>>> + unsigned long *load_size,
>>>> + unsigned long max)
>>>> +{
>>>> + efi_guid_t lf2_proto_guid = EFI_LOAD_FILE2_PROTOCOL_GUID;
>>>> + efi_device_path_protocol_t *dp;
>>>> + efi_load_file2_protocol_t *lf2;
>>>> + unsigned long initrd_addr;
>>>> + unsigned long initrd_size;
>>>> + efi_handle_t handle;
>>>> + efi_status_t status;
>>>> +
>>>> + if (!load_addr || !load_size)
>>>> + return EFI_INVALID_PARAMETER;
>>>
>>> Doesn't return EFI_LOAD_ERROR.
>>>
>>>> +
>>>> + dp = (efi_device_path_protocol_t *)&initrd_dev_path;
>>>> + status = efi_bs_call(locate_device_path, &lf2_proto_guid, &dp, &handle);
>>>> + if (status != EFI_SUCCESS)
>>>> + return status;
>>>
>>> Seems safe (the only plausible error could be EFI_NOT_FOUND).
>>>
>>>> +
>>>> + status = efi_bs_call(handle_protocol, handle, &lf2_proto_guid,
>>>> + (void **)&lf2);
>>>> + if (status != EFI_SUCCESS)
>>>> + return status;
>>>
>>> Interesting case; this should never fail... but note, if it does, it
>>> returns EFI_UNSUPPORTED, not EFI_NOT_FOUND (if the protocol is missing
>>> from the handle).
>>>
>>>> +
>>>> + status = efi_call_proto(lf2, load_file, dp, false, &initrd_size, NULL);
>>>> + if (status != EFI_BUFFER_TOO_SMALL)
>>>> + return EFI_LOAD_ERROR;
>>>> +
>>>> + status = efi_allocate_pages(initrd_size, &initrd_addr, max);
>>>> + if (status != EFI_SUCCESS)
>>>> + return status;
>>>
>>> Not sure about the efi_allocate_pages() wrapper (?); the UEFI service
>>> could return EFI_OUT_OF_RESOURCES.
>>>
>>
>> Hmm, guess I was a bit sloppy with the return codes. The important
>> thing is that EFI_NOT_FOUND is only returned in the one specifically
>> defined case.
>>
>
> For the record [in case no respin+resend is needed for other reasons],
> I intend to update the comment block as below, and keep the code as
> is:
>
>
> * @load_addr: pointer to store the address where the initrd was loaded
> * @load_size: pointer to store the size of the loaded initrd
> * @max: upper limit for the initrd memory allocation
> - * @return: %EFI_SUCCESS if the initrd was loaded successfully, in
> which case
> - * @load_addr and @load_size are assigned accordingly
> - * %EFI_NOT_FOUND if no LoadFile2 protocol exists on the initrd
> - * device path
> + * @return: %EFI_SUCCESS if the initrd was loaded successfully, in which
> + * case @load_addr and @load_size are assigned accordingly
> + * %EFI_NOT_FOUND if no LoadFile2 protocol exists on the initrd
> + * device path
> + * %EFI_INVALID_PARAMETER if load_addr == NULL or load_size == NULL
> + * %EFI_OUT_OF_RESOURCES if memory allocation failed
> * %EFI_LOAD_ERROR in all other cases
>

Right, I agree that updating the comments suffices for consistency; the
code is not wrong, only the comments and the code don't match.

So with that

Acked-by: Laszlo Ersek <lersek@xxxxxxxxxx>


Thanks!
Laszlo