root/drivers/firmware/efi/libstub/file.c
// SPDX-License-Identifier: GPL-2.0
/*
 * Helper functions used by the EFI stub on multiple
 * architectures. This should be #included by the EFI stub
 * implementation files.
 *
 * Copyright 2011 Intel Corporation; author Matt Fleming
 */

#include <linux/efi.h>
#include <asm/efi.h>

#include "efistub.h"

#define MAX_FILENAME_SIZE       256

/*
 * Some firmware implementations have problems reading files in one go.
 * A read chunk size of 1MB seems to work for most platforms.
 *
 * Unfortunately, reading files in chunks triggers *other* bugs on some
 * platforms, so we provide a way to disable this workaround, which can
 * be done by passing "efi=nochunk" on the EFI boot stub command line.
 *
 * If you experience issues with initrd images being corrupt it's worth
 * trying efi=nochunk, but chunking is enabled by default on x86 because
 * there are far more machines that require the workaround than those that
 * break with it enabled.
 */
#define EFI_READ_CHUNK_SIZE     SZ_1M

struct finfo {
        efi_file_info_t info;
        efi_char16_t    filename[MAX_FILENAME_SIZE];
};

static efi_status_t efi_open_file(efi_file_protocol_t *volume,
                                  struct finfo *fi,
                                  efi_file_protocol_t **handle,
                                  unsigned long *file_size)
{
        efi_guid_t info_guid = EFI_FILE_INFO_ID;
        efi_file_protocol_t *fh;
        unsigned long info_sz;
        efi_status_t status;
        efi_char16_t *c;

        /* Replace UNIX dir separators with EFI standard ones */
        for (c = fi->filename; *c != L'\0'; c++) {
                if (*c == L'/')
                        *c = L'\\';
        }

        status = efi_call_proto(volume, open, &fh, fi->filename,
                                EFI_FILE_MODE_READ, 0);
        if (status != EFI_SUCCESS) {
                efi_err("Failed to open file: %ls\n", fi->filename);
                return status;
        }

        info_sz = sizeof(struct finfo);
        status = efi_call_proto(fh, get_info, &info_guid, &info_sz, fi);
        if (status != EFI_SUCCESS) {
                efi_err("Failed to get file info\n");
                efi_call_proto(fh, close);
                return status;
        }

        *handle = fh;
        *file_size = fi->info.file_size;
        return EFI_SUCCESS;
}

static efi_status_t efi_open_volume(efi_loaded_image_t *image,
                                    efi_file_protocol_t **fh)
{
        efi_guid_t fs_proto = EFI_FILE_SYSTEM_GUID;
        efi_simple_file_system_protocol_t *io;
        efi_status_t status;

        status = efi_bs_call(handle_protocol, efi_table_attr(image, device_handle),
                             &fs_proto, (void **)&io);
        if (status != EFI_SUCCESS) {
                efi_err("Failed to handle fs_proto\n");
                return status;
        }

        status = efi_call_proto(io, open_volume, fh);
        if (status != EFI_SUCCESS)
                efi_err("Failed to open volume\n");

        return status;
}

static int find_file_option(const efi_char16_t *cmdline, int cmdline_len,
                            const efi_char16_t *prefix, int prefix_size,
                            efi_char16_t *result, int result_len)
{
        int prefix_len = prefix_size / 2;
        bool found = false;
        int i;

        for (i = prefix_len; i < cmdline_len; i++) {
                if (!memcmp(&cmdline[i - prefix_len], prefix, prefix_size)) {
                        found = true;
                        break;
                }
        }

        if (!found)
                return 0;

        /* Skip any leading slashes */
        while (i < cmdline_len && (cmdline[i] == L'/' || cmdline[i] == L'\\'))
                i++;

        while (--result_len > 0 && i < cmdline_len) {
                efi_char16_t c = cmdline[i++];

                if (c == L'\0' || c == L'\n' || c == L' ')
                        break;
                *result++ = c;
        }
        *result = L'\0';
        return i;
}

static efi_status_t efi_open_device_path(efi_file_protocol_t **volume,
                                         struct finfo *fi)
{
        efi_guid_t text_to_dp_guid = EFI_DEVICE_PATH_FROM_TEXT_PROTOCOL_GUID;
        static efi_device_path_from_text_protocol_t *text_to_dp = NULL;
        efi_guid_t fs_proto = EFI_FILE_SYSTEM_GUID;
        efi_device_path_protocol_t *initrd_dp;
        efi_simple_file_system_protocol_t *io;
        struct efi_file_path_dev_path *fpath;
        efi_handle_t handle;
        efi_status_t status;

        /* See if the text to device path protocol exists */
        if (!text_to_dp &&
            efi_bs_call(locate_protocol, &text_to_dp_guid, NULL,
                        (void **)&text_to_dp) != EFI_SUCCESS)
                return EFI_UNSUPPORTED;


        /* Convert the filename wide string into a device path */
        initrd_dp = efi_fn_call(text_to_dp, convert_text_to_device_path,
                                fi->filename);

        /* Check whether the device path in question implements simple FS */
        if ((efi_bs_call(locate_device_path, &fs_proto, &initrd_dp, &handle) ?:
             efi_bs_call(handle_protocol, handle, &fs_proto, (void **)&io))
            != EFI_SUCCESS)
                return EFI_NOT_FOUND;

        /* Check whether the remaining device path is a file device path */
        if (initrd_dp->type != EFI_DEV_MEDIA ||
            initrd_dp->sub_type != EFI_DEV_MEDIA_FILE) {
                efi_warn("Unexpected device path node type: (%x, %x)\n",
                         initrd_dp->type, initrd_dp->sub_type);
                return EFI_LOAD_ERROR;
        }

        /* Copy the remaining file path into the fi structure */
        fpath = (struct efi_file_path_dev_path *)initrd_dp;
        memcpy(fi->filename, fpath->filename,
               min(sizeof(fi->filename),
                   fpath->header.length - sizeof(fpath->header)));

        status = efi_call_proto(io, open_volume, volume);
        if (status != EFI_SUCCESS)
                efi_err("Failed to open volume\n");

        return status;
}

#ifndef CONFIG_CMDLINE
#define CONFIG_CMDLINE
#endif

static const efi_char16_t builtin_cmdline[] = L"" CONFIG_CMDLINE;

/*
 * Check the cmdline for a LILO-style file= arguments.
 *
 * We only support loading a file from the same filesystem as
 * the kernel image.
 */
efi_status_t handle_cmdline_files(efi_loaded_image_t *image,
                                  const efi_char16_t *optstr,
                                  int optstr_size,
                                  unsigned long soft_limit,
                                  unsigned long hard_limit,
                                  unsigned long *load_addr,
                                  unsigned long *load_size)
{
        const bool ignore_load_options = IS_ENABLED(CONFIG_CMDLINE_OVERRIDE) ||
                                         IS_ENABLED(CONFIG_CMDLINE_FORCE);
        const efi_char16_t *cmdline = efi_table_attr(image, load_options);
        u32 cmdline_len = efi_table_attr(image, load_options_size);
        unsigned long efi_chunk_size = ULONG_MAX;
        efi_file_protocol_t *volume = NULL;
        efi_file_protocol_t *file;
        unsigned long alloc_addr;
        unsigned long alloc_size;
        efi_status_t status;
        bool twopass;
        int offset;

        if (!load_addr || !load_size)
                return EFI_INVALID_PARAMETER;

        efi_apply_loadoptions_quirk((const void **)&cmdline, &cmdline_len);
        cmdline_len /= sizeof(*cmdline);

        if (IS_ENABLED(CONFIG_X86) && !efi_nochunk)
                efi_chunk_size = EFI_READ_CHUNK_SIZE;

        alloc_addr = alloc_size = 0;

        if (!ignore_load_options && cmdline_len > 0) {
                twopass = IS_ENABLED(CONFIG_CMDLINE_BOOL) ||
                          IS_ENABLED(CONFIG_CMDLINE_EXTEND);
        } else {
do_builtin:     cmdline     = builtin_cmdline;
                cmdline_len = ARRAY_SIZE(builtin_cmdline) - 1;
                twopass     = false;
        }

        do {
                struct finfo fi;
                unsigned long size;
                void *addr;

                offset = find_file_option(cmdline, cmdline_len,
                                          optstr, optstr_size,
                                          fi.filename, ARRAY_SIZE(fi.filename));

                if (!offset)
                        break;

                cmdline += offset;
                cmdline_len -= offset;

                status = efi_open_device_path(&volume, &fi);
                if (status == EFI_UNSUPPORTED || status == EFI_NOT_FOUND)
                        /* try the volume that holds the kernel itself */
                        status = efi_open_volume(image, &volume);

                if (status != EFI_SUCCESS)
                        goto err_free_alloc;

                status = efi_open_file(volume, &fi, &file, &size);
                if (status != EFI_SUCCESS)
                        goto err_close_volume;

                /*
                 * Check whether the existing allocation can contain the next
                 * file. This condition will also trigger naturally during the
                 * first (and typically only) iteration of the loop, given that
                 * alloc_size == 0 in that case.
                 */
                if (round_up(alloc_size + size, EFI_ALLOC_ALIGN) >
                    round_up(alloc_size, EFI_ALLOC_ALIGN)) {
                        unsigned long old_addr = alloc_addr;

                        status = EFI_OUT_OF_RESOURCES;
                        if (soft_limit < hard_limit)
                                status = efi_allocate_pages(alloc_size + size,
                                                            &alloc_addr,
                                                            soft_limit);
                        if (status == EFI_OUT_OF_RESOURCES)
                                status = efi_allocate_pages(alloc_size + size,
                                                            &alloc_addr,
                                                            hard_limit);
                        if (status != EFI_SUCCESS) {
                                efi_err("Failed to allocate memory for files\n");
                                goto err_close_file;
                        }

                        if (old_addr != 0) {
                                /*
                                 * This is not the first time we've gone
                                 * around this loop, and so we are loading
                                 * multiple files that need to be concatenated
                                 * and returned in a single buffer.
                                 */
                                memcpy((void *)alloc_addr, (void *)old_addr, alloc_size);
                                efi_free(alloc_size, old_addr);
                        }
                }

                addr = (void *)alloc_addr + alloc_size;
                alloc_size += size;

                while (size) {
                        unsigned long chunksize = min(size, efi_chunk_size);

                        status = efi_call_proto(file, read, &chunksize, addr);
                        if (status != EFI_SUCCESS) {
                                efi_err("Failed to read file\n");
                                goto err_close_file;
                        }
                        addr += chunksize;
                        size -= chunksize;
                }
                efi_call_proto(file, close);
                efi_call_proto(volume, close);
        } while (offset > 0);

        if (twopass)
                goto do_builtin;

        *load_addr = alloc_addr;
        *load_size = alloc_size;

        if (*load_size == 0)
                return EFI_NOT_READY;
        return EFI_SUCCESS;

err_close_file:
        efi_call_proto(file, close);

err_close_volume:
        efi_call_proto(volume, close);

err_free_alloc:
        efi_free(alloc_size, alloc_addr);
        return status;
}