On Sun, Aug 03, 2025 at 10:57:03AM -0500, Aaron Rainbolt wrote: > On Fri, 1 Aug 2025 14:55:36 +0200 Daniel Kiper <dki...@net-space.pl> wrote: > > On Fri, Jul 25, 2025 at 03:31:12PM -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 | 50 +++++ > > > grub-core/Makefile.core.def | 2 + > > > grub-core/kern/i386/xen/pvh.c | 14 ++ > > > grub-core/kern/main.c | 12 ++ > > > grub-core/kern/xen/cmdline.c | 343 > > > ++++++++++++++++++++++++++++++++++ include/grub/xen.h | > > > 2 + 6 files changed, 423 insertions(+) > > > create mode 100644 grub-core/kern/xen/cmdline.c > > > > > > diff --git a/docs/grub.texi b/docs/grub.texi > > > index 34b3484..ce82483 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,55 @@ 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. 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_kernelappend='loglevel=3'" > > > > s/kernelappend/kernel_append/ > > > > or maybe even > > > > s/kernelappend/linux_append/ > > > > to make it clear it is appended to the "linux" command line... > > Will do. > > > > +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_kernelappend@} > > > > Ditto... > > Will do. > > > > + 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..12df2d8 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,18 @@ 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 < MAX_GUEST_CMDLINE; i++) > > > > Oh, MAX_GUEST_CMDLINE is a too generic. May I ask you to rename it to > > GRUB_XEN_MAX_GUEST_CMDLINE? This should be done in separate patch > > preceding this one. > > Sure. > > > > + { > > > + if (xen_cmdline[i] == '\0') > > > > I cannot see much sense in this check. Please look below... > > > > > + { > > > + grub_strncpy ((char *) > > > grub_xen_start_page_addr->cmd_line, > > > + (char *) pvh_start_info->cmdline_paddr, > > > > s/char */const char */ > > > > Is it always guaranteed that pvh_start_info->cmdline_paddr have > > MAX_GUEST_CMDLINE size? If yes then... > > It is not guaranteed. I tested this under Qubes OS, and it is > unfortunately very possible to pass a guest command line longer than > MAX_GUEST_CMDLINE via pvh_start_info->cmdline_paddr. I do not know of > any documentation specifying what the "real" maximum length is in this > instance (if any), but in any event the string ultimately has to be > crammed into an array that is only MAX_GUEST_CMDLINE long. Originally I > wrote this to truncate the command line in this situation, but later > decided that discarding the command line would be safer than truncation.
I think you are confusing source, grub_xen_start_page_addr->cmd_line, and destination, pvh_start_info->cmdline_paddr. I am asking about the destination... > (When booting a VM in PV mode, Xen does guarantee the command line will > not be longer than MAX_GUEST_CMDLINE and will simply return a truncated > command line. There is no way to detect this condition to my awareness, > so in PV mode, we simply live with the truncation since we aren't given > another option.) OK... > > grub_memset ((void *) pvh_start_info->cmdline_paddr, 0, > > MAX_GUEST_CMDLINE); grub_strncpy ((char *) > > grub_xen_start_page_addr->cmd_line, (const char *) > > pvh_start_info->cmdline_paddr, MAX_GUEST_CMDLINE - 1); > > > > ... and you are done... > > This would truncate the command line rather than discarding it. If the > goal is consistency with Xen's PV mode, then I can switch to this, but > I don't like the idea of booting a guest with corrupted data inserted > randomly into the grub.cfg file. (Like mentioned above, there isn't any > other option when using PV mode, but just because PV mode does things > dangerously doesn't mean we have to when running in PVH mode.) OK, but please remember you have to ensure the string is NUL-terminated after grub_strncpy(). Potentially you could also use grub_strlen() to make a check and add NUL in proper place... > > > + MAX_GUEST_CMDLINE); > > > + break; > > > + } > > > + } > > > } > > > > > > grub_err_t > > > diff --git a/grub-core/kern/main.c b/grub-core/kern/main.c > > > index 143a232..b4377d3 100644 > > > --- a/grub-core/kern/main.c > > > +++ b/grub-core/kern/main.c > > > @@ -37,6 +37,10 @@ > > > #include <grub/machine/memory.h> > > > #endif > > > > > > +#if defined (GRUB_MACHINE_XEN) || defined (GRUB_MACHINE_XEN_PVH) > > > +#include <grub/xen.h> > > > +#endif > > > + > > > static bool cli_disabled = false; > > > static bool cli_need_auth = false; > > > > > > @@ -351,6 +355,14 @@ grub_main (void) > > > grub_env_export ("root"); > > > grub_env_export ("prefix"); > > > > > > + /* > > > + * Parse command line parameters from Xen and export them as > > > environment > > > + * variables > > > + */ > > > +#if defined (GRUB_MACHINE_XEN) || defined (GRUB_MACHINE_XEN_PVH) > > > + grub_parse_xen_cmdline (); > > > > Please move this call to the > > grub-core/kern/xen/init.c:grub_machine_init(). > > Will do. > > > > +#endif > > > + > > > /* Reclaim space used for modules. */ > > > reclaim_module_space (); > > > > > > diff --git a/grub-core/kern/xen/cmdline.c > > > b/grub-core/kern/xen/cmdline.c new file mode 100644 > > > index 0000000..03dd88f > > > --- /dev/null > > > +++ b/grub-core/kern/xen/cmdline.c > > > @@ -0,0 +1,343 @@ > > > +/* > > > + * 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 parse_flags > > > +{ > > > + PARSER_HIT_BACKSLASH = 0x1, > > > + PARSER_IN_SINGLE_QUOTES = 0x2, > > > + PARSER_IN_DOUBLE_QUOTES = 0x4, > > > +}; > > typedef parse_flags parse_flags_t; > > > > ... and use parse_flags_t instead below... > > Will do. > > > And probably this should be a local variable... > > See below for comments on globals in the parser. > > > > + > > > +#define PARSER_BASE_WORD_LEN 16 > > > > This constant begs for a few words of comment... > > Will add. This is just the initial length of the dynamically allocated > buffer for storing each "word" of the command line, but it is confusing > as written. Perhaps it should be renamed to PARSER_WORD_BUF_START_LEN > or similar, to make it clear this isn't a string length, but a buffer > length? s/length/size/ then... However, then why 16 not, e.g., 32? > > > +static grub_size_t word_list_len; > > > +static char **word_list; > > > +static grub_size_t current_word_len; > > > +static grub_size_t current_word_pos; > > > +static char *current_word; > > > +static char current_char; > > > > I have a feeling that most if not all of this variables should be > > local in a given function... > > I made them globals because making them local would have required an > awful lot of parameter "shuttling" that I believed made the code less > readable. For instance, the `append_word_to_list` function right now > has no parameters and can be called very simply by the parser whenever > it is needed. If all variables used were local, the function would have > to have a signature akin to: > > static bool append_word_to_list(grub_size_t *current_word_pos_ref, > grub_size_t *current_word_len_ref, char *current_word, > grub_size_t *word_list_len_ref, char **word_list) > > IMO this is difficult to read and frustrating to call (possibly even > error-prone). Ultimately the variables are "local" to the parser, and > the functions are just there to split up the parser's algorithm to > avoid repetition. They all ultimately operate on the same state, so > having the state variables static but global within the file fits the > current parser design well. First of all, think about the design. If you are sure it is optimal then please use struct and a pointer to pass as many argument as you need... > > > +static bool > > > +append_char_to_word (bool allow_null) > > > +{ > > > + /* > > > + * Fail if the current char is outside of the range 0x20 to > > > 0x7E. If > > > + * allow_null is true, make an exception for 0x00. This is a > > > safeguard > > > > Could you elaborate here when allow_null == true is useful? > > Usually when appending characters to a word, you aren't going to be > appending null characters, thus `append_char_to_word` usually rejects s/null/NUL/... > those so as to make it harder to mess things up. However, the string > does need to be null-terminated, and thus once a command line word is Ditto... In general, even it is correct, I prefer to use NUL instead of null/NULL to avoid confusion... > fully loaded into current_word, it is necessary to add the null > terminator on the end. In this instance the parser sets `current_char` > to '\0' and then calls the `append_char_to_word` function with > `allow_null` set to true, so that the function knows that the addition > of a null is intentional here. > > (In retrospect, if allow_null is set to true, the only thing > `append_char_to_word` should append is a null character, it should > reject anything else in that instance. Otherwise someone could > accidentally append something other than a null when they meant to > append a null.) Is not it simpler to open code this instead of complicating this function? > > > + * against likely-invalid data. > > > + */ > > > + if ( (!(current_char >= 0x20) || !(current_char <= 0x7E) ) > > > > grub_isprint()? > > That would work, I wasn't aware that existed. Maybe a mention of some > valuable GRUB internal API functions that are likely to remain stable > could be added to the GRUB development manual? (Or just a reference to > header files where useful things like this are defined.) Certainly it is a good idea. In general you can assume the GRUB has a lot of POSIX compatible functions which names are prefixed with "grub_". > > > + && (current_char != '\0' || allow_null == false)) > > > > I would drop redundant spaces between "(" and ")" and move "&&" to the > > end of first line of "if". > > Will do, though I'm likely to rewrite this conditional anyway so that > anything other than '\0' will be rejected when `allow_null == true`. > > > > + return false; > > > + > > > + if (current_word_pos == current_word_len) > > > + { > > > + current_word_len *= 2; > > > > You can do this in the argument of the function below... > > I mean, yes, technically I can, but that's a lot less readable to me. I > can't find anywhere else in GRUB that uses that style (using > `grep -ri '\*='` to search), and can find a few spots that use a style > similar to the one I've used here: > > * `util/grub-install.c` in function `device_map_check_duplicates` > * `grub-core/osdep/windows/platform.c` in function `get_efi_variable` > * `grub-core/osdep/unix/platform.c` in function `get_ofpathname` > > > > + current_word = grub_realloc (current_word, current_word_len); > > > + if (current_word == NULL) > > > + { > > > + current_word_len /= 2; > > > + return false; > > > + } > > > + } > > > + > > > + current_word[current_word_pos] = current_char; > > > + current_word_pos++; > > > > Again, you can do this operation in the "[]" above... > > Will do. (This does seem to be a much more common way to write things > in GRUB than what I've done.) > > > > + return true; > > > +} > > > + > > > +static bool > > > +append_word_to_list (void) > > > +{ > > > + /* No-op on empty words. */ > > > + if (current_word_pos == 0) > > > > This should be probably an argument for the function... > > See above for parser state and global variables rationale. > > > > + return true; > > > + > > > + current_char = '\0'; > > > + if (append_char_to_word (true) == false) > > > + { > > > + grub_error (GRUB_ERR_BUG, > > > + "couldn't append null terminator to word during > > > Xen cmdline parsing"); > > > + grub_print_error (); > > > + grub_exit (); > > > + } > > > + > > > + current_word_len = grub_strlen (current_word) + 1; > > > + current_word = grub_realloc (current_word, current_word_len); > > > + if (current_word == NULL) > > > + return false; > > > + word_list_len++; > > > > Again this, OK ++word_list_len to be precise, can be done in a > > function argument below... > > `grep -ri 'alloc.*++'` doesn't show me any instance of this style > anywhere else in the GRUB codebase, and I find it much more difficult > to read since now I have to think about incrementing `word_list_len`'s > value and returning it and multiplying the returned value all at the > same time. `grub-core/osdep/unix/platform.c` uses the style I've used > here. > > > > + word_list = grub_realloc (word_list, word_list_len * sizeof > > > (char *)); word_list = grub_realloc (word_list, ++word_list_len * sizeof (char *a)); Is it really difficult to read? I do not think so... [...] > > > +void > > > +grub_parse_xen_cmdline (void) > > > +{ > > > + 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; > > > + enum parse_flags parse_flags = 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. > > > > I think not all from this valuable comment landed in the > > documentation. Please fix it... > > Comparing the two side-by-side: > > +----------------------------------+----------------------------------+ > | Documentation | Comment | > +==================================+==================================+ > | The command line will be split | The command line is split into | > | into space-delimited words. | space-separated words. Single | > | Single and double quotes may be | and double quotes may be used to | > | used to quote words or portions | suppress the splitting behavior | > | of words that contain spaces. | of spaces. | > +----------------------------------+----------------------------------+ > | (Self-evident, any other | Double quotes are appended to | > | behavior involving quotes would | the current word verbatim if | > | be extremely unexpected.) | they appear within a single- | > | | quoted string, and vice versa. | > +----------------------------------+----------------------------------+ > | Arbitrary characters may be | Backslashes may be used to cause | > | backslash-escaped to make them a | the next character to be | > | literal component of a word | appended to the current word | > | rather than being parsed as | verbatim. This is only useful | > | quotes or word separators. | when used to escape quotes, | > | | spaces, and backslashes, but for | > | | simplicity we allow backslash- | > | | escaping anything. | > +----------------------------------+----------------------------------+ > | Each word should be a variable | After splitting the command line | > | assignment in the format | into words, each word is checked | > | ``variable'' or | to see if it contains an equals | > | ``variable=value''. | sign. | > +----------------------------------+----------------------------------+ > | If a variable name and value are | If it does, it is split on the | > | both specified, the variable | equals sign into a key-value | > | will be set to the specified | pair. The key is then treated as | > | value. | a variable name, and the value | > | | treated as the variable's value. | > +----------------------------------+----------------------------------+ > | If only a variable name is | If it does not, the entire word | > | specified, the variable's value | is treated as a variable name. | > | will be set to ``1''. | The variable's value is | > | | implicitly considered to be '1'. | > +----------------------------------+----------------------------------+ > | Variable names must begin with | All variables detected on the | > | the string ``xen_grub_env_''... | command line are checked to see | > | If any variable contains an | if their names begin with the | > | illegal name, that variable will | string `xen_grub_env_`. | > | be ignored. | Variables that do not pass this | > | | check are discarded, | > +----------------------------------+----------------------------------+ > | ...you can pass environment | variables that do pass this | > | variables from Xen's dom0 to the | check are exported so they are | > | VM through the Xen-provided | available to the GRUB | > | kernel command line. | configuration. | > +----------------------------------+----------------------------------+ > > The only missing detail I see is that single quotes are legal in > double-quoted strings and vice versa, though I guess I can make it more > explicit that the variables passed on the command line are exported > before getting to the example that shows this. Is there anything else > you see that's missing? Exactly, I was thing about single/double quotes here. If other stuff is in the documentation then OK... > > > + */ > > > + > > > + current_word_len = PARSER_BASE_WORD_LEN; > > > + current_word = grub_malloc (current_word_len); > > > + if (current_word == NULL) > > > + goto cleanup; > > > + > > > + for (i = 0; i < 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 errors usually start with lowercase... > > Will fix. > > > > + grub_print_error (); > > > + goto cleanup; > > > + } > > > + > > > + 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 (parse_flags & PARSER_HIT_BACKSLASH) > > > > s/parse_flags/parser_state/ > > Hmm, I don't like 'parser_state' as a name because the parser's "state" > is the global variables in `grub-core/kern/xen/cmdline.`. Maybe > 'splitter_state' instead, since this is just the bit of state that the > word splitter is concerned with? I am OK with both... Daniel _______________________________________________ Grub-devel mailing list Grub-devel@gnu.org https://lists.gnu.org/mailman/listinfo/grub-devel