diff --git a/0300-Add-Xen-parameter-parsing.patch b/0300-Add-Xen-parameter-parsing.patch new file mode 100644 index 0000000..a297da2 --- /dev/null +++ b/0300-Add-Xen-parameter-parsing.patch @@ -0,0 +1,565 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aaron Rainbolt +Date: Thu, 4 Sep 2025 09:27:00 -0500 +Subject: [PATCH v5] kern/xen: Add Xen command line parsing + +The purpose of this patch is to allow the Xen hypervisor to pass extra +data to GRUB in the form of a kernel command line, allowing the host to +customize the boot process of the guest. The command line from Xen is +parsed, and any variables within that start with the string +`xen_grub_env_` are exposed as environment variables. The grub.cfg +script can then use those environment variables as it sees fit. + +The main reason for doing this is to allow implementing boot modes in +Qubes OS while also using in-VM kernels. For more context on Qubes boot +modes, see [1]. In order for this to work with in-VM kernels, it is +necessary for dom0 to pass kernel parameters to the guest without +modifying the guest's grub.cfg manually. This patch allows this to be +done, by allowing dom0 to pass kernel parameters to GRUB, which then +provides them to grub.cfg as an environment variable. The grub.cfg +script within the VM can then append those parameters to the kernel +command line. + +All of the changes from version 4 of the patch are simply refinements +after review from Daniel Kiper. The patch's functionality is unchanged. + +Since there are substantial changes since version 4, I've re-run the +entire battery of tests that were used against the v4 patch. All tests +pass. As previously, the tests were done by booting a patched GRUB in +Xen with various different parameters passed to it via the Xen-provided +kernel command line. The effects of these parameters on the +bootloader's environment were then tested, and then an Arch Linux image +was booted to ensure that boot still worked. + +[1] https://github.com/QubesOS/qubes-linux-pvgrub2/pull/16 + +Signed-off-by: Aaron Rainbolt +--- + docs/grub.texi | 52 +++++ + grub-core/Makefile.core.def | 2 + + grub-core/kern/i386/xen/pvh.c | 4 + + grub-core/kern/xen/cmdline.c | 361 ++++++++++++++++++++++++++++++++++ + grub-core/kern/xen/init.c | 2 + + include/grub/xen.h | 2 + + include/xen/xen.h | 9 +- + 7 files changed, 430 insertions(+), 2 deletions(-) + create mode 100644 grub-core/kern/xen/cmdline.c + +diff --git a/docs/grub.texi b/docs/grub.texi +index 30ca89e..fabb623 100644 +--- a/docs/grub.texi ++++ b/docs/grub.texi +@@ -3260,6 +3260,7 @@ GRUB. Others may be used freely in GRUB configuration files. + @menu + * Special environment variables:: + * Environment block:: ++* Passing environment variables through Xen:: + @end menu + + +@@ -3873,6 +3874,57 @@ using BIOS or EFI functions (no ATA, USB or IEEE1275). + @command{grub-mkconfig} uses this facility to implement + @samp{GRUB_SAVEDEFAULT} (@pxref{Simple configuration}). + ++@node Passing environment variables through Xen ++@section Passing environment variables through Xen ++ ++If you are using a GRUB image as the kernel for a PV or PVH Xen virtual ++machine, you can pass environment variables from Xen's dom0 to the VM through ++the Xen-provided kernel command line. When combined with a properly configured ++guest, this can be used to customize the guest's behavior on bootup via the ++VM's Xen configuration file. ++ ++GRUB will parse the kernel command line passed to it by Xen during bootup. ++The command line will be split into space-delimited words. Single and ++double quotes may be used to quote words or portions of words that contain ++spaces. Single quotes will be considered part of a word if inside double ++quotes, and vice versa. Arbitrary characters may be backslash-escaped to make ++them a literal component of a word rather than being parsed as quotes or word ++separators. The command line must consist entirely of printable 7-bit ASCII ++characters and spaces. If a non-printing ASCII character is found anywhere in ++the command line, the entire command line will be ignored by GRUB. (This ++splitter algorithm is meant to behave somewhat like Bash's word splitting.) ++ ++Each word should be a variable assignment in the format ``variable'' or ++``variable=value''. Variable names must contain only the characters A-Z, a-z, ++and underscore (``_''). Variable names must begin with the string ++``xen_grub_env_''. Variable values can contain arbitrary printable 7-bit ++ASCII characters and space. If any variable contains an illegal name, that ++variable will be ignored. ++ ++If a variable name and value are both specified, the variable will be set to ++the specified value. If only a variable name is specified, the variable's ++value will be set to ``1''. ++ ++The following is a simple example of how to use this functionality to append ++arbitrary variables to a guest's kernel command line: ++ ++@example ++# In the Xen configuration file for the guest ++name = "linux_vm" ++type = "pvh" ++kernel = "/path/to/grub-i386-xen_pvh.bin" ++extra = "xen_grub_env_linux_append='loglevel=3'" ++memory = 1024 ++disk = [ "file:/srv/vms/linux_vm.img,sda,w" ] ++ ++# In the guest's GRUB configuration file ++menuentry "Linux VM with dom0-specified kernel parameters" @{ ++ search --set=root --label linux_vm --hint hd0,msdos1 ++ linux /boot/vmlinuz root=LABEL=linux_vm $@{xen_grub_env_linux_append@} ++ initrd /boot/initrd.img ++@} ++@end example ++ + @node Modules + @chapter Modules + +diff --git a/grub-core/Makefile.core.def b/grub-core/Makefile.core.def +index 95fd18d..3a50f3c 100644 +--- a/grub-core/Makefile.core.def ++++ b/grub-core/Makefile.core.def +@@ -254,6 +254,7 @@ kernel = { + xen = term/xen/console.c; + xen = disk/xen/xendisk.c; + xen = commands/boot.c; ++ xen = kern/xen/cmdline.c; + + i386_xen_pvh = commands/boot.c; + i386_xen_pvh = disk/xen/xendisk.c; +@@ -261,6 +262,7 @@ kernel = { + i386_xen_pvh = kern/i386/xen/tsc.c; + i386_xen_pvh = kern/i386/xen/pvh.c; + i386_xen_pvh = kern/xen/init.c; ++ i386_xen_pvh = kern/xen/cmdline.c; + i386_xen_pvh = term/xen/console.c; + + ia64_efi = kern/ia64/efi/startup.S; +diff --git a/grub-core/kern/i386/xen/pvh.c b/grub-core/kern/i386/xen/pvh.c +index 91fbca8..52020b7 100644 +--- a/grub-core/kern/i386/xen/pvh.c ++++ b/grub-core/kern/i386/xen/pvh.c +@@ -352,6 +352,10 @@ grub_xen_setup_pvh (void) + grub_xen_mm_init_regions (); + + grub_rsdp_addr = pvh_start_info->rsdp_paddr; ++ ++ grub_strncpy ((char *) grub_xen_start_page_addr->cmd_line, ++ (const char *) pvh_start_info->cmdline_paddr, ++ GRUB_XEN_MAX_GUEST_CMDLINE); + } + + grub_err_t +diff --git a/grub-core/kern/xen/cmdline.c b/grub-core/kern/xen/cmdline.c +new file mode 100644 +index 0000000..8021bf8 +--- /dev/null ++++ b/grub-core/kern/xen/cmdline.c +@@ -0,0 +1,361 @@ ++/* ++ * GRUB -- GRand Unified Bootloader ++ * Copyright (C) 2025 Free Software Foundation, Inc. ++ * ++ * GRUB is free software: you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation, either version 3 of the License, or ++ * (at your option) any later version. ++ * ++ * GRUB is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with GRUB. If not, see . ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++ ++enum splitter_state ++{ ++ SPLITTER_NORMAL = 0x0, ++ SPLITTER_HIT_BACKSLASH = 0x1, ++ SPLITTER_IN_SINGLE_QUOTES = 0x2, ++ SPLITTER_IN_DOUBLE_QUOTES = 0x4, ++}; ++typedef enum splitter_state splitter_state_t; ++ ++/* ++ * The initial size of the current_word buffer. The buffer may be resized as ++ * needed. ++ */ ++#define PARSER_BASE_WORD_SIZE 32 ++ ++struct parser_state ++{ ++ char **words; ++ grub_size_t words_count; ++ char *current_word; ++ grub_size_t current_word_len; ++ grub_size_t current_word_pos; ++}; ++typedef struct parser_state parser_state_t; ++ ++static grub_err_t ++append_char_to_word (parser_state_t *ps, char c, bool allow_null) ++{ ++ /* ++ * We ban any chars that are not in the ASCII printable range. If ++ * allow_null == true, we make an exception for NUL. (This is needed so that ++ * append_word_to_list can add a NUL terminator to the word). ++ */ ++ if (!grub_isprint (c) && allow_null == false) ++ return GRUB_ERR_BAD_ARGUMENT; ++ else if (allow_null == true && c != '\0') ++ return GRUB_ERR_BAD_ARGUMENT; ++ ++ if (ps->current_word_pos == ps->current_word_len) ++ { ++ ps->current_word = grub_realloc (ps->current_word, ps->current_word_len *= 2); ++ if (ps->current_word == NULL) ++ { ++ ps->current_word_len /= 2; ++ return grub_errno; ++ } ++ } ++ ++ ps->current_word[ps->current_word_pos++] = c; ++ return GRUB_ERR_NONE; ++} ++ ++static grub_err_t ++append_word_to_list (parser_state_t *ps) ++{ ++ /* No-op on empty words. */ ++ if (ps->current_word_pos == 0) ++ return GRUB_ERR_NONE; ++ ++ if (append_char_to_word (ps, '\0', true) != GRUB_ERR_NONE) ++ grub_fatal ("couldn't append NUL terminator to word during Xen cmdline parsing"); ++ ++ ps->current_word_len = grub_strlen (ps->current_word) + 1; ++ ps->current_word = grub_realloc (ps->current_word, ps->current_word_len); ++ if (ps->current_word == NULL) ++ return grub_errno; ++ ps->words = grub_realloc (ps->words, ++ps->words_count * sizeof (char *)); ++ if (ps->words == NULL) ++ return grub_errno; ++ ps->words[ps->words_count - 1] = ps->current_word; ++ ++ ps->current_word_len = PARSER_BASE_WORD_SIZE; ++ ps->current_word_pos = 0; ++ ps->current_word = grub_malloc (ps->current_word_len); ++ if (ps->current_word == NULL) ++ return grub_errno; ++ ++ return GRUB_ERR_NONE; ++} ++ ++static bool ++is_key_safe (char *key, grub_size_t len) ++{ ++ grub_size_t i; ++ ++ for (i = 0; i < len; i++) ++ if (!grub_isalpha (key[i]) && key[i] != '_') ++ return false; ++ ++ return true; ++} ++ ++void ++grub_parse_xen_cmdline (void) ++{ ++ parser_state_t ps = {0}; ++ splitter_state_t ss = SPLITTER_NORMAL; ++ ++ const char *cmdline = (const char *) grub_xen_start_page_addr->cmd_line; ++ grub_size_t cmdline_len; ++ bool cmdline_valid = false; ++ char **param_keys = NULL; ++ char **param_vals = NULL; ++ grub_size_t param_dict_len = 0; ++ grub_size_t param_dict_pos = 0; ++ char current_char = '\0'; ++ grub_size_t i = 0; ++ ++ /* ++ * The following algorithm is used to parse the Xen command line: ++ * ++ * - The command line is split into space-separated words. ++ * - Single and double quotes may be used to suppress the splitting ++ * behavior of spaces. ++ * - Double quotes are appended to the current word verbatim if they ++ * appear within a single-quoted string portion, and vice versa. ++ * - Backslashes may be used to cause the next character to be ++ * appended to the current word verbatim. This is only useful when ++ * used to escape quotes, spaces, and backslashes, but for simplicity ++ * we allow backslash-escaping anything. ++ * - After splitting the command line into words, each word is checked to ++ * see if it contains an equals sign. ++ * - If it does, it is split on the equals sign into a key-value pair. The ++ * key is then treated as an variable name, and the value is treated as ++ * the variable's value. ++ * - If it does not, the entire word is treated as a variable name. The ++ * variable's value is implicitly considered to be `1`. ++ * - All variables detected on the command line are checked to see if their ++ * names begin with the string `xen_grub_env_`. Variables that do not pass ++ * this check are discarded, variables that do pass this check are ++ * exported so they are available to the GRUB configuration. ++ * ++ * This behavior is intended to somewhat mimic the splitter behavior in Bash ++ * and in GRUB's config file parser. ++ */ ++ ++ ps.current_word_len = PARSER_BASE_WORD_SIZE; ++ ps.current_word = grub_malloc (ps.current_word_len); ++ if (ps.current_word == NULL) ++ goto cleanup_main; ++ ++ for (i = 0; i < GRUB_XEN_MAX_GUEST_CMDLINE; i++) ++ { ++ if (cmdline[i] == '\0') ++ { ++ cmdline_valid = true; ++ break; ++ } ++ } ++ ++ if (cmdline_valid == false) ++ { ++ grub_error (GRUB_ERR_BAD_ARGUMENT, ++ "command line from Xen is not NUL-terminated"); ++ grub_print_error (); ++ goto cleanup_main; ++ } ++ ++ cmdline_len = grub_strlen (cmdline); ++ for (i = 0; i < cmdline_len; i++) ++ { ++ current_char = cmdline[i]; ++ ++ /* ++ * If the previous character was a backslash, append the current ++ * character to the word verbatim ++ */ ++ if (ss & SPLITTER_HIT_BACKSLASH) ++ { ++ ss &= ~SPLITTER_HIT_BACKSLASH; ++ if (append_char_to_word (&ps, current_char, false) != GRUB_ERR_NONE) ++ goto cleanup_main; ++ continue; ++ } ++ ++ switch (current_char) ++ { ++ case '\\': ++ /* Backslashes escape arbitrary characters. */ ++ ss |= SPLITTER_HIT_BACKSLASH; ++ break; ++ ++ case '\'': ++ /* ++ * Single quotes suppress word splitting and double quoting until ++ * the next single quote is encountered. ++ */ ++ if (ss & SPLITTER_IN_DOUBLE_QUOTES) ++ { ++ if (append_char_to_word (&ps, current_char, false) != GRUB_ERR_NONE) ++ goto cleanup_main; ++ break; ++ } ++ ++ ss ^= SPLITTER_IN_SINGLE_QUOTES; ++ break; ++ ++ case '"': ++ /* ++ * Double quotes suppress word splitting and single quoting until ++ * the next double quote is encountered. ++ */ ++ if (ss & SPLITTER_IN_SINGLE_QUOTES) ++ { ++ if (append_char_to_word (&ps, current_char, false) != GRUB_ERR_NONE) ++ goto cleanup_main; ++ break; ++ } ++ ++ ss ^= SPLITTER_IN_DOUBLE_QUOTES; ++ break; ++ ++ case ' ': ++ /* Spaces separate words in the command line from each other. */ ++ if (ss & SPLITTER_IN_SINGLE_QUOTES || ++ ss & SPLITTER_IN_DOUBLE_QUOTES) ++ { ++ if (append_char_to_word (&ps, current_char, false) != GRUB_ERR_NONE) ++ goto cleanup_main; ++ break; ++ } ++ ++ if (append_word_to_list (&ps) != GRUB_ERR_NONE) ++ goto cleanup_main; ++ break; ++ ++ default: ++ if (append_char_to_word (&ps, current_char, false) != GRUB_ERR_NONE) ++ goto cleanup_main; ++ } ++ } ++ ++ if (append_word_to_list (&ps) != GRUB_ERR_NONE) ++ goto cleanup_main; ++ ++ param_keys = grub_malloc (ps.words_count * sizeof (char *)); ++ if (param_keys == NULL) ++ goto cleanup_main; ++ param_vals = grub_malloc (ps.words_count * sizeof (char *)); ++ if (param_vals == NULL) ++ goto cleanup_main; ++ ++ for (i = 0; i < ps.words_count; i++) ++ { ++ char *eq_pos; ++ ++ ps.current_word = ps.words[i]; ++ ps.current_word_len = grub_strlen (ps.current_word) + 1; ++ eq_pos = grub_strchr (ps.current_word, '='); ++ ++ if (eq_pos != NULL) ++ { ++ /* ++ * Both pre_eq_len and post_eq_len represent substring lengths ++ * without a NUL terminator. ++ */ ++ grub_size_t pre_eq_len = (grub_size_t) (eq_pos - ps.current_word); ++ /* ++ * ps.current_word_len includes the NUL terminator, so we subtract ++ * one to get rid of the terminator, and one more to get rid of the ++ * equals sign. ++ */ ++ grub_size_t post_eq_len = (ps.current_word_len - 2) - pre_eq_len; ++ ++ if (is_key_safe (ps.current_word, pre_eq_len) == true) ++ { ++ param_dict_pos = param_dict_len++; ++ param_keys[param_dict_pos] = grub_malloc (pre_eq_len + 1); ++ if (param_keys == NULL) ++ goto cleanup_main; ++ param_vals[param_dict_pos] = grub_malloc (post_eq_len + 1); ++ if (param_vals == NULL) ++ goto cleanup_main; ++ ++ grub_strncpy (param_keys[param_dict_pos], ps.current_word, pre_eq_len); ++ grub_strncpy (param_vals[param_dict_pos], ++ ps.current_word + pre_eq_len + 1, post_eq_len); ++ param_keys[param_dict_pos][pre_eq_len] = '\0'; ++ param_vals[param_dict_pos][post_eq_len] = '\0'; ++ } ++ } ++ else if (is_key_safe (ps.current_word, ps.current_word_len - 1) == true) ++ { ++ param_dict_pos = param_dict_len++; ++ param_keys[param_dict_pos] = grub_malloc (ps.current_word_len); ++ if (param_keys == NULL) ++ goto cleanup_main; ++ param_vals[param_dict_pos] = grub_malloc (2); ++ if (param_vals == NULL) ++ goto cleanup_main; ++ ++ grub_strncpy (param_keys[param_dict_pos], ps.current_word, ++ ps.current_word_len); ++ if (param_keys[param_dict_pos][ps.current_word_len - 1] != '\0' ) ++ grub_fatal ("NUL terminator missing from key during Xen cmdline parsing"); ++ grub_strcpy (param_vals[param_dict_pos], "1"); ++ } ++ ++ } ++ ++ for (i = 0; i < param_dict_len; i++) ++ { ++ /* ++ * Find keys that start with "xen_grub_env_" and export them ++ * as environment variables. ++ */ ++ if (grub_strncmp (param_keys[i], ++ "xen_grub_env_", ++ sizeof ("xen_grub_env_") - 1) != 0) ++ continue; ++ ++ if (grub_env_set (param_keys[i], param_vals[i]) != GRUB_ERR_NONE) ++ { ++ grub_printf ("warning: could not set environment variable `%s' to value `%s'\n", ++ param_keys[i], param_vals[i]); ++ continue; ++ } ++ ++ if (grub_env_export (param_keys[i]) != GRUB_ERR_NONE) ++ grub_printf ("warning: could not export environment variable `%s'", ++ param_keys[i]); ++ } ++ ++ cleanup_main: ++ for (i = 0; i < ps.words_count; i++) ++ grub_free (ps.words[i]); ++ ++ for (i = 0; i < param_dict_len; i++) ++ { ++ grub_free (param_keys[i]); ++ grub_free (param_vals[i]); ++ } ++ ++ grub_free (param_keys); ++ grub_free (param_vals); ++ grub_free (ps.words); ++} +diff --git a/grub-core/kern/xen/init.c b/grub-core/kern/xen/init.c +index 708b060..4b95c41 100644 +--- a/grub-core/kern/xen/init.c ++++ b/grub-core/kern/xen/init.c +@@ -581,6 +581,8 @@ grub_machine_init (void) + grub_xendisk_init (); + + grub_boot_init (); ++ ++ grub_parse_xen_cmdline (); + } + + void +diff --git a/include/grub/xen.h b/include/grub/xen.h +index 91cb7cf..7f9efee 100644 +--- a/include/grub/xen.h ++++ b/include/grub/xen.h +@@ -89,6 +89,8 @@ void grub_console_init (void); + void grub_xendisk_fini (void); + void grub_xendisk_init (void); + ++void grub_parse_xen_cmdline (void); ++ + #ifdef __x86_64__ + typedef grub_uint64_t grub_xen_mfn_t; + #else +diff --git a/include/xen/xen.h b/include/xen/xen.h +index 692f97a..16f3fd7 100644 +--- a/include/xen/xen.h ++++ b/include/xen/xen.h +@@ -823,8 +823,13 @@ struct start_info { + /* (PFN of pre-loaded module if */ + /* SIF_MOD_START_PFN set in flags). */ + unsigned long mod_len; /* Size (bytes) of pre-loaded module. */ +-#define MAX_GUEST_CMDLINE 1024 +- int8_t cmd_line[MAX_GUEST_CMDLINE]; ++ /* ++ * cmd_line will contain a NUL-termianted string if it contains valid ++ * data, but it MAY be invalid and not contain a NUL byte at all. Code ++ * that accesses cmd_line MUST NOT assume it is NUL-terminated. ++ */ ++#define GRUB_XEN_MAX_GUEST_CMDLINE 1024 ++ int8_t cmd_line[GRUB_XEN_MAX_GUEST_CMDLINE]; + /* The pfn range here covers both page table and p->m table frames. */ + unsigned long first_p2m_pfn;/* 1st pfn forming initial P->M table. */ + unsigned long nr_p2m_frames;/* # of pfns forming initial P->M table. */ diff --git a/grub2-xen.spec b/grub2-xen.spec index f0a29a7..f1398c3 100644 --- a/grub2-xen.spec +++ b/grub2-xen.spec @@ -300,6 +300,7 @@ Patch0281: 0281-loader-efi-Fix-RISC-V-build.patch Patch0282: 0282-kern-riscv-efi-init-Use-time-register-in-grub_efi_ge.patch Patch0283: 0283-Use-medany-instead-of-large-model-for-RISCV.patch Patch0284: 0284-fs-xfs-Fix-large-extent-counters-incompat-feature-su.patch +Patch0300: 0300-Add-Xen-parameter-parsing.patch BuildRequires: autoconf