On Mon, Aug 04, 2025 at 11:50:09PM -0500, Aaron Rainbolt wrote: > Xen traditionally allows customizing guest behavior by passing arguments > to the VM kernel via the kernel command line. This is no longer possible > when using GRUB with Xen, as the kernel command line is decided by the > GRUB configuration file within the guest, not data passed to the guest > by Xen. > > To work around this limitation, enable GRUB to parse a command line > passed to it by Xen, and expose data from the command line to the GRUB > configuration as environment variables. These variables can be used in > the GRUB configuration for any desired purpose, such as extending the > kernel command line passed to the guest. The command line format is > inspired by the Linux kernel's command line format. > > To reduce the risk of misuse, abuse, or accidents in production, the > command line will only be parsed if it consists entirely of 7-bit ASCII > characters, only alphabetical characters and underscores are permitted > in variable names, and all variable names must start with the string > "xen_grub_env_". This also allows room for expanding the command line > arguments accepted by GRUB in the future, should other arguments end up > becoming desirable in the future. > > Signed-off-by: Aaron Rainbolt <arraybo...@gmail.com> > --- > docs/grub.texi | 51 +++++ > grub-core/Makefile.core.def | 2 + > grub-core/kern/i386/xen/pvh.c | 23 +++ > grub-core/kern/xen/cmdline.c | 376 ++++++++++++++++++++++++++++++++++ > grub-core/kern/xen/init.c | 2 + > include/grub/xen.h | 2 + > 6 files changed, 456 insertions(+) > create mode 100644 grub-core/kern/xen/cmdline.c > > diff --git a/docs/grub.texi b/docs/grub.texi > index 34b3484..b58cf98 100644 > --- a/docs/grub.texi > +++ b/docs/grub.texi > @@ -3271,6 +3271,7 @@ GRUB. Others may be used freely in GRUB configuration > files. > @menu > * Special environment variables:: > * Environment block:: > +* Passing environment variables through Xen:: > @end menu > > > @@ -3871,6 +3872,56 @@ 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. > + > +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 b3f7119..df0f266 100644 > --- a/grub-core/Makefile.core.def > +++ b/grub-core/Makefile.core.def > @@ -248,6 +248,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; > @@ -255,6 +256,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..a8988d2 100644 > --- a/grub-core/kern/i386/xen/pvh.c > +++ b/grub-core/kern/i386/xen/pvh.c > @@ -321,6 +321,8 @@ void > grub_xen_setup_pvh (void) > { > grub_addr_t par; > + const char *xen_cmdline; > + int i; > > grub_xen_cpuid_base (); > grub_xen_setup_hypercall_page (); > @@ -352,6 +354,27 @@ grub_xen_setup_pvh (void) > grub_xen_mm_init_regions (); > > grub_rsdp_addr = pvh_start_info->rsdp_paddr; > + > + xen_cmdline = (const char *) pvh_start_info->cmdline_paddr; > + for (i = 0; i < GRUB_XEN_MAX_GUEST_CMDLINE; i++) > + { > + if (xen_cmdline[i] == '\0')
This code still does not make a lot of sense for me. You have NUL check in grub_parse_xen_cmdline(). So, you duplicate the check here... I would just fire grub_strncpy() here and forget... > + { > + grub_strncpy ((char *) grub_xen_start_page_addr->cmd_line, > + (char *) pvh_start_info->cmdline_paddr, > + GRUB_XEN_MAX_GUEST_CMDLINE); > + > + if (grub_xen_start_page_addr->cmd_line[GRUB_XEN_MAX_GUEST_CMDLINE > - 1] != '\0') If you convince me this code is still needed then I am afraid that this is not what you meant here... > + { > + grub_error (GRUB_ERR_BUG, > + "Xen command line is not NUL-terminated"); > + grub_print_error (); > + grub_exit (); grub_fatal() and you are done... > + } > + > + break; > + } > + } > } > > 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..46a9998 > --- /dev/null > +++ b/grub-core/kern/xen/cmdline.c > @@ -0,0 +1,376 @@ > +/* > + * 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 <http://www.gnu.org/licenses/>. > + */ > + > +#include <grub/env.h> > +#include <grub/misc.h> > +#include <grub/mm.h> > +#include <grub/xen.h> > + > +enum splitter_state > +{ > + 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 > +{ > + grub_size_t word_list_len; s/word_list_len/words_count/ And I would put it behind word_list... > + char **word_list; s/word_list/words/ > + grub_size_t current_word_len; > + grub_size_t current_word_pos; > + char *current_word; > +}; > +typedef struct parser_state parser_state_t; > + > +static bool s/bool/grub_err_t/ > +append_char_to_word (parser_state_t *s, 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) == false && allow_null == false) grub_isprint() et consortes return int instead of bool. So, it should be "!grub_isprint(c)" here... > + return false; > + else if (allow_null == true && c != '\0') > + return false; > + > + if (s->current_word_pos == s->current_word_len) > + { > + s->current_word = grub_realloc (s->current_word, s->current_word_len > *= 2); > + if (s->current_word == NULL) > + { > + s->current_word_len /= 2; > + return false; > + } > + } > + > + s->current_word[s->current_word_pos++] = c; > + return true; > +} > + > +static bool s/bool/grub_err_t/ > +append_word_to_list (parser_state_t *s) > +{ > + /* No-op on empty words. */ > + if (s->current_word_pos == 0) > + return true; > + > + if (append_char_to_word (s, '\0', true) == false) > + { > + grub_error (GRUB_ERR_BUG, > + "couldn't append NUL terminator to word during Xen cmdline > parsing"); > + grub_print_error (); > + grub_exit (); grub_fatal() > + } > + > + s->current_word_len = grub_strlen (s->current_word) + 1; > + s->current_word = grub_realloc (s->current_word, s->current_word_len); > + if (s->current_word == NULL) > + return false; return grub_errno; > + s->word_list = grub_realloc (s->word_list, ++s->word_list_len * sizeof > (char *)); > + if (s->word_list == NULL) > + return false; return grub_errno; ... I think many (related) functions in this code returning bool should really return grub_err_t... > + s->word_list[s->word_list_len - 1] = s->current_word; > + > + s->current_word_len = PARSER_BASE_WORD_SIZE; > + s->current_word_pos = 0; > + s->current_word = grub_malloc (s->current_word_len); > + if (s->current_word == NULL) > + return false; > + > + return true; > +} > + > +static bool But this bool makes sense... > +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] == '_')) Please drop space after "!"... > + return false; > + } You can drop curly braces from here... > + > + return true; > +} > + > +void > +grub_parse_xen_cmdline (void) > +{ > + parser_state_t *s = NULL; parser_state_t ps = {0}; ... and you do not need grub_malloc(s) and stuff any longer below... And I would put it next to splitter_state... > + 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; > + splitter_state_t splitter_state = 0; You nicely define an enum and then assign plain number. Sigh... I think you should define SPLITTER_NORMAL or something similar as well... And s/splitter_state/ss/... > + char current_char = '\0'; > + grub_size_t i = 0; > + > + s = grub_malloc (sizeof (parser_state_t)); > + if (s == NULL) > + goto cleanup_final; > + > + /* > + * 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. > + */ > + > + s->current_word_len = PARSER_BASE_WORD_SIZE; > + s->current_word = grub_malloc (s->current_word_len); > + if (s->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 (); grub_fatal()? > + 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 (splitter_state & SPLITTER_HIT_BACKSLASH) > + { > + splitter_state &= ~SPLITTER_HIT_BACKSLASH; > + if (append_char_to_word (s, current_char, false) == false) > + goto cleanup_main; > + continue; > + } > + > + switch (current_char) > + { > + case '\\': > + /* Backslashes escape arbitrary characters. */ > + splitter_state |= SPLITTER_HIT_BACKSLASH; > + continue; > + > + case '\'': > + /* > + * Single quotes suppress word splitting and double quoting until > + * the next single quote is encountered. > + */ > + if (splitter_state & SPLITTER_IN_DOUBLE_QUOTES) > + { > + if (append_char_to_word (s, current_char, false) == false) > + goto cleanup_main; > + continue; > + } > + > + splitter_state ^= SPLITTER_IN_SINGLE_QUOTES; > + continue; > + > + case '"': > + /* > + * Double quotes suppress word splitting and single quoting until > + * the next double quote is encountered. > + */ > + if (splitter_state & SPLITTER_IN_SINGLE_QUOTES) > + { > + if (append_char_to_word (s, current_char, false) == false) > + goto cleanup_main; > + continue; > + } > + > + splitter_state ^= SPLITTER_IN_DOUBLE_QUOTES; > + continue; > + > + case ' ': > + /* Spaces separate words in the command line from each other. */ > + if (splitter_state & SPLITTER_IN_SINGLE_QUOTES || > + splitter_state & SPLITTER_IN_DOUBLE_QUOTES) > + { > + if (append_char_to_word (s, current_char, false) == false) > + goto cleanup_main; > + continue; > + } > + > + if (append_word_to_list (s) == false) > + goto cleanup_main; I think this is not fully correct. You should not run append_word_to_list() until the closing quote. So, here you should have "else" for the first "if", i.e., "if (splitter_state & ..." and call append_word_to_list() for closing \" and \' above. > + continue; > + } > + > + if (append_char_to_word (s, current_char, false) == false) > + goto cleanup_main; This should be part of "default:" for the switch above... Even if it works now... Then many "continue" should be converted to more natural "break"... > + } > + > + if (append_word_to_list (s) == false) > + goto cleanup_main; > + > + param_keys = grub_malloc (s->word_list_len * sizeof (char *)); > + if (param_keys == NULL) > + goto cleanup_main; > + param_vals = grub_malloc (s->word_list_len * sizeof (char *)); > + if (param_vals == NULL) > + goto cleanup_main; > + > + for (i = 0; i < s->word_list_len; i++) > + { > + char *current_word_eq_ptr; s/current_word_eq_ptr/eq_pos/ > + s->current_word = s->word_list[i]; > + s->current_word_len = grub_strlen (s->current_word) + 1; > + current_word_eq_ptr = grub_strchr (s->current_word, '='); > + > + if (current_word_eq_ptr != 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) (current_word_eq_ptr - > s->current_word); > + /* > + * s->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 = (s->current_word_len - 2) - pre_eq_len; > + > + if (is_key_safe (s->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], s->current_word, > pre_eq_len); > + grub_strncpy (param_vals[param_dict_pos], > + s->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 else if (is_key_safe (s->current_word, s->current_word_len - 1) == true) ... and you can drop an extra indention... > + { > + if (is_key_safe (s->current_word, s->current_word_len - 1) == true) > + { > + param_dict_pos = param_dict_len++; > + param_keys[param_dict_pos] = grub_malloc (s->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], s->current_word, > + s->current_word_len); > + if (param_keys[param_dict_pos][s->current_word_len - 1] != > '\0' ) > + { > + grub_error (GRUB_ERR_BUG, > + "NUL terminator missing from key during Xen > cmdline parsing"); > + grub_print_error (); > + grub_exit (); grub_fatal() > + } > + grub_strcpy (param_vals[param_dict_pos], "1"); > + } > + } > + } Daniel _______________________________________________ Grub-devel mailing list Grub-devel@gnu.org https://lists.gnu.org/mailman/listinfo/grub-devel