/*
* 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_zalloc (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");
*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);
}