On Wed, May 21, 2025 at 12:51:26PM +0000, Alec Brown wrote:
> A Unified Kernel Image is a single UEFI PE file that combines a UEFI boot
> stub,
> a Linux kernel image, an initrd, and further resources. The uki command will
> locate where the UKI file is and create a GRUB menu entry to load it.
>
> The Unified Kernel Image Specification:
> https://uapi-group.org/specifications/specs/unified_kernel_image/
>
> Signed-off-by: Alec Brown <[email protected]>
> ---
> docs/grub.texi | 33 +++
> grub-core/commands/blsuki.c | 463 +++++++++++++++++++++++++++++++++---
> include/grub/menu.h | 2 +
> 3 files changed, 463 insertions(+), 35 deletions(-)
>
> diff --git a/docs/grub.texi b/docs/grub.texi
> index adab93668..9a63129c7 100644
> --- a/docs/grub.texi
> +++ b/docs/grub.texi
> @@ -6491,6 +6491,7 @@ you forget a command, you can run the command
> @command{help}
> * tpm2_key_protector_clear:: Clear the TPM2 key protector
> * true:: Do nothing, successfully
> * trust:: Add public key to list of trusted keys
> +* uki:: Load Unified Kernel Image menu entries
> * unset:: Unset an environment variable
> @comment * vbeinfo:: List available video modes
> * verify_detached:: Verify detached digital signature
> @@ -8184,6 +8185,38 @@ Unset the environment variable @var{envvar}.
> @end deffn
>
>
> +@node uki
> +@subsection uki
> +
> +@deffn Command uki [@option{-p|--path} dir] [@option{-f|--enable-fallback}]
> [@option{-d|--show-default}] [@option{-n|--show-non-default}]
> [@option{-e|--entry} file]
> +Load Unified Kernel Image (UKI) entries into the GRUB menu. Boot entries
> +generated from @command{uki} won't interfere with entries from
> @file{grub.cfg} appearing in the
> +GRUB menu. Also, entries generated from @command{uki} only generate in
> memory and don't
s/only generate/exist only/
> +update @file{grub.cfg}.
> +
> +The @option{--path} option overrides the default path to the directory
> containing
> +the UKI entries. If this option isn't used, the default location is
> +/EFI/Linux in the EFI system partition. If no UKI entries are found, the
> +@option{--enable-fallback} option can be used to check for entries in the
> default
> +directory.
What is "the default directory"?
> +The @option{--show-default} option allows the default boot entry to be added
> to the
> +GRUB menu from the UKI entries.
> +
> +The @option{--show-non-default} option allows non-default boot entries to be
> added to
> +the GRUB menu from the UKI entries.
> +
> +The @option{--entry} option allows specific boot entries to be added to the
> GRUB menu
> +from the UKI entries.
> +
> +The @option{--entry}, @option{--show-default}, and
> @option{--show-non-default} options
> +are used to filter which UKI entries are added to the GRUB menu. If none are
> +used, all entries in the default location or the location specified by
> @option{--path}
> +will be added to the GRUB menu.
> +
> +References:
> @uref{https://uapi-group.org/specifications/specs/unified_kernel_image/, The
> Unified Kernel Image Specification}
> +@end deffn
> +
> @ignore
> @node vbeinfo
> @subsection vbeinfo
> diff --git a/grub-core/commands/blsuki.c b/grub-core/commands/blsuki.c
> index bf2edc5ac..3f067281d 100644
> --- a/grub-core/commands/blsuki.c
> +++ b/grub-core/commands/blsuki.c
> @@ -32,6 +32,12 @@
> #include <grub/vercmp.h>
> #include <grub/lib/envblk.h>
>
> +#ifdef GRUB_MACHINE_EFI
> +#include <grub/efi/efi.h>
> +#include <grub/efi/disk.h>
> +#include <grub/efi/pe32.h>
> +#endif
> +
> #ifdef GRUB_MACHINE_EMU
> #include <grub/emu/misc.h>
> #define GRUB_BOOT_DEVICE "/boot"
> @@ -42,6 +48,13 @@
> GRUB_MOD_LICENSE ("GPLv3+");
>
> #define GRUB_BLS_CONFIG_PATH "/loader/entries/"
> +#define GRUB_UKI_CONFIG_PATH "/EFI/Linux"
> +
> +enum
> + {
> + BLSUKI_BLS_CMD,
> + BLSUKI_UKI_CMD,
> + };
>
> static const struct grub_arg_option bls_opt[] =
> {
> @@ -53,6 +66,18 @@ static const struct grub_arg_option bls_opt[] =
> {0, 0, 0, 0, 0, 0}
> };
>
> +#ifdef GRUB_MACHINE_EFI
> +static const struct grub_arg_option uki_opt[] =
> + {
> + {"path", 'p', 0, N_("Specify path to find UKI entries."), N_("DIR"),
> ARG_TYPE_PATHNAME},
> + {"enable-fallback", 'f', 0, "Fallback to the default BLS path if --path
> fails to find UKI entries.", 0, ARG_TYPE_NONE},
> + {"show-default", 'd', 0, N_("Allow the default UKI entry to be added to
> the GRUB menu."), 0, ARG_TYPE_NONE},
> + {"show-non-default", 'n', 0, N_("Allow the non-default UKI entries to be
> added to the GRUB menu."), 0, ARG_TYPE_NONE},
> + {"entry", 'e', 0, N_("Allow specificUKII entries to be added to the GRUB
> menu."), N_("FILE"), ARG_TYPE_FILE},
> + {0, 0, 0, 0, 0, 0}
> + };
> +#endif
> +
> struct keyval
> {
> const char *key;
> @@ -162,7 +187,7 @@ blsuki_add_keyval (grub_blsuki_entry_t *entry, char *key,
> char *val)
> * Find the value of the key named by keyname. If there are allowed to be
> * more than one, pass a pointer to an int set to -1 the first time, and pass
> * the same pointer through each time after, and it'll return them in sorted
> - * order as defined in the BLS fragment file.
> + * order.
> */
> static char *
> blsuki_get_val (grub_blsuki_entry_t *entry, const char *keyname, int *last)
> @@ -304,27 +329,243 @@ bls_parse_keyvals (grub_file_t f, grub_blsuki_entry_t
> *entry)
> return err;
> }
>
> +/*
> + * This function searches for the .cmdline, .osrel, and .linux sections of a
> + * UKI. We only need to store the data for the .cmdline and .osrel sections,
> + * but we also need to verify that the .linux section exists.
> + */
> +#ifdef GRUB_MACHINE_EFI
> +static grub_err_t
> +uki_parse_keyvals (grub_file_t f, grub_blsuki_entry_t *entry)
> +{
> + struct grub_msdos_image_header *dos = NULL;
> + struct grub_pe_image_header *pe = NULL;
> + grub_off_t section_offset = 0;
> + struct grub_pe32_section_table *section = NULL;
> + struct grub_pe32_coff_header *coff_header = NULL;
> + char *val = NULL;
> + char *key = NULL;
> + const char *target[] = {".cmdline", ".osrel", ".linux", NULL};
> + bool has_linux = false;
> + grub_err_t err = GRUB_ERR_NONE;
> +
> + dos = grub_zalloc (sizeof (*dos));
> + if (dos == NULL)
> + return grub_errno;
> + if (grub_file_read (f, dos, sizeof (*dos)) < (grub_ssize_t) sizeof (*dos))
> + {
> + err = grub_error (GRUB_ERR_FILE_READ_ERROR, "failed to read UKI image
> header");
> + goto finish;
> + }
> + if (dos->msdos_magic != GRUB_PE32_MAGIC)
> + {
> + err = grub_error (GRUB_ERR_BAD_FILE_TYPE, N_("plain image kernel not
> supported"));
I would drop N_() from here. And s/kernel not/kernel is not/...
> + goto finish;
> + }
> +
> + grub_dprintf ("blsuki", "PE/COFF header @ %08x\n",
> dos->pe_image_header_offset);
> + pe = grub_zalloc (sizeof (*pe));
> + if (pe == NULL)
> + {
> + err = grub_errno;
> + goto finish;
> + }
> + if (grub_file_seek (f, dos->pe_image_header_offset) == (grub_off_t) -1 ||
> + grub_file_read (f, pe, sizeof (*pe)) != sizeof (*pe))
> + {
> + err = grub_error (GRUB_ERR_FILE_READ_ERROR, "failed to read COFF image
> header");
> + goto finish;
> + }
> + if (pe->optional_header.magic != GRUB_PE32_NATIVE_MAGIC)
> + {
> + err = grub_error (GRUB_ERR_BAD_FILE_TYPE, "non-native image not
> supported");
> + goto finish;
> + }
> +
> + coff_header = &(pe->coff_header);
> + section_offset = dos->pe_image_header_offset + sizeof (*pe);
> +
> + for (int i = 0; i < coff_header->num_sections; i++)
> + {
> + key = NULL;
> + val = NULL;
> + section = grub_zalloc (sizeof (*section));
> + if (section == NULL)
> + {
> + err = grub_errno;
> + goto finish;
> + }
> +
> + if (grub_file_seek (f, section_offset) == (grub_off_t) -1 ||
> + grub_file_read (f, section, sizeof (*section)) != sizeof
> (*section))
> + {
> + err = grub_error (GRUB_ERR_FILE_READ_ERROR, "failed to read section
> header");
> + goto finish;
> + }
> +
> + key = grub_strndup (section->name, 8);
> + if (key == NULL)
> + {
> + err = grub_errno;
> + goto finish;
> + }
> +
> + for (int j = 0; target[j] != NULL; j++)
> + {
> + if (grub_strcmp (key, target[j]) == 0)
> + {
> + /*
> + * We don't need to read the contents of the .linux PE section,
> but we
> + * should verify that the section exists.
> + */
> + if (grub_strcmp (key, ".linux") == 0)
> + {
> + has_linux = true;
> + break;
> + }
> +
> + val = grub_zalloc (section->raw_data_size);
You blindly assume it is valid section size. Probably we should enforce
sensible limits here...
> + if (val == NULL)
> + {
> + err = grub_errno;
> + goto finish;
> + }
> +
> + if (grub_file_seek (f, section->raw_data_offset) == (grub_off_t)
> -1 ||
> + grub_file_read (f, val, section->raw_data_size) !=
> (grub_ssize_t) section->raw_data_size)
> + {
> + err = grub_error (GRUB_ERR_FILE_READ_ERROR, "failed to read
> section");
> + goto finish;
> + }
> +
> + err = blsuki_add_keyval (entry, key, val);
> + if (err != GRUB_ERR_NONE)
> + goto finish;
> +
> + break;
> + }
> + }
> +
> + section_offset += sizeof (*section);
> + grub_free (section);
> + grub_free (val);
> + grub_free (key);
> + section = NULL;
> + val = NULL;
> + key = NULL;
> + }
> +
> + if (has_linux == false)
> + err = grub_error (GRUB_ERR_NO_KERNEL, "UKI is missing the '.linux'
> section");
> +
> + finish:
> + grub_free (dos);
> + grub_free (pe);
> + grub_free (section);
> + grub_free (val);
> + grub_free (key);
> + return err;
> +}
> +#endif
> +
> +/*
> + * This function obtains the keyval pairs when the .osrel data is input into
> + * the content parameter and returns the full line that it obtained the
> keyval
> + * pair from.
> + */
> +static char *
> +uki_read_osrel (char *content, grub_off_t *pos, char **key_ret, char
> **val_ret)
> +{
> + char *line;
> + char *value;
> + grub_size_t linelen;
> +
> + for (;;)
> + {
> + line = content + *pos;
> + if (*line == '\0')
> + return NULL;
> +
> + linelen = 0;
> + while (line[linelen] != '\0' && line[linelen] != '\n' && line[linelen]
> != '\r')
> + linelen++;
> +
> + /* Move pos to the next line */
> + *pos += linelen;
> + if (content[*pos] != '\0')
> + (*pos)++;
> +
> + /* Skip empty line */
> + if (linelen == 0)
> + continue;
> +
> + line[linelen] = '\0';
> +
> + /* Remove leading white space */
> + while (linelen > 0 && (*line == ' ' || *line == '\t'))
> + {
> + line++;
> + linelen--;
> + }
> +
> + /* Remove trailing whitespace */
> + while (linelen > 0 && (line[linelen - 1] == ' ' || line[linelen - 1]
> == '\t'))
> + linelen--;
> + line[linelen] = '\0';
> +
> + if (*line == '#')
> + continue;
> +
> + /* Split key/value */
grub_strtok() here and there?
> + value = line;
> + while (*value != '\0' && *value != '=')
> + value++;
> + if (*value == '\0')
> + continue;
> + *value = '\0';
> + value++;
> + while (*value != '\0' && *value == '=')
> + value++;
> +
> + /* Remove quotes from value */
> + if (*value == '\"' && line[linelen - 1] == '\"')
> + {
> + value++;
> + line[linelen - 1] = '\0';
> + }
> +
> + *key_ret = line;
> + *val_ret = value;
> + break;
> + }
> +
> + return line;
> +}
> +
> struct read_entry_info
> {
> const char *devid;
> const char *dirname;
> + int cmd_type;
It should be an enum not an int. And if you put this together at the
beginning of the file you would realize that earlier...
> grub_file_t file;
> };
Daniel
_______________________________________________
Grub-devel mailing list
[email protected]
https://lists.gnu.org/mailman/listinfo/grub-devel