From 4ea7bae51f97e49c84dc67ea30b466ca8633b9f6 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Thu, 7 Jan 2021 19:21:03 +0000 Subject: kern/parser: Fix a stack buffer overflow grub_parser_split_cmdline() expands variable names present in the supplied command line in to their corresponding variable contents and uses a 1 kiB stack buffer for temporary storage without sufficient bounds checking. If the function is called with a command line that references a variable with a sufficiently large payload, it is possible to overflow the stack buffer via tab completion, corrupt the stack frame and potentially control execution. Fixes: CVE-2020-27749 Reported-by: Chris Coulson Signed-off-by: Chris Coulson Signed-off-by: Darren Kenny Reviewed-by: Daniel Kiper Upstream-Status: Backport [https://git.savannah.gnu.org/cgit/grub.git/commit/?h=grub-2.06&id=c6c426e5ab6ea715153b72584de6bd8c82f698ec && https://git.savannah.gnu.org/cgit/grub.git/commit/?h=grub-2.06&id=b1c9e9e889e4273fb15712051c887e6078511448 && https://git.savannah.gnu.org/cgit/grub.git/commit/?h=grub-2.06&id=3d157bbd06506b170fde5ec23980c4bf9f7660e2 && https://git.savannah.gnu.org/cgit/grub.git/commit/?h=grub-2.06&id=8bc817014ce3d7a498db44eae33c8b90e2430926 && https://git.savannah.gnu.org/cgit/grub.git/commit/?h=grub-2.06&id=030fb6c4fa354cdbd6a8d6903dfed5d36eaf3cb2 && https://git.savannah.gnu.org/cgit/grub.git/commit/?h=grub-2.06&id=4ea7bae51f97e49c84dc67ea30b466ca8633b9f6] CVE: CVE-2020-27749 Signed-off-by: Hitendra Prajapati --- grub-core/Makefile.core.def | 1 + grub-core/kern/buffer.c | 117 +++++++++++++++++++++ grub-core/kern/parser.c | 204 +++++++++++++++++++++++------------- include/grub/buffer.h | 144 +++++++++++++++++++++++++ 4 files changed, 395 insertions(+), 71 deletions(-) create mode 100644 grub-core/kern/buffer.c create mode 100644 include/grub/buffer.h diff --git a/grub-core/Makefile.core.def b/grub-core/Makefile.core.def index 651ea2a..823cd57 100644 --- a/grub-core/Makefile.core.def +++ b/grub-core/Makefile.core.def @@ -123,6 +123,7 @@ kernel = { riscv32_efi_startup = kern/riscv/efi/startup.S; riscv64_efi_startup = kern/riscv/efi/startup.S; + common = kern/buffer.c; common = kern/command.c; common = kern/corecmd.c; common = kern/device.c; diff --git a/grub-core/kern/buffer.c b/grub-core/kern/buffer.c new file mode 100644 index 0000000..9f5f8b8 --- /dev/null +++ b/grub-core/kern/buffer.c @@ -0,0 +1,117 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2021 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 +#include + +grub_buffer_t +grub_buffer_new (grub_size_t sz) +{ + struct grub_buffer *ret; + + ret = (struct grub_buffer *) grub_malloc (sizeof (*ret)); + if (ret == NULL) + return NULL; + + ret->data = (grub_uint8_t *) grub_malloc (sz); + if (ret->data == NULL) + { + grub_free (ret); + return NULL; + } + + ret->sz = sz; + ret->pos = 0; + ret->used = 0; + + return ret; +} + +void +grub_buffer_free (grub_buffer_t buf) +{ + grub_free (buf->data); + grub_free (buf); +} + +grub_err_t +grub_buffer_ensure_space (grub_buffer_t buf, grub_size_t req) +{ + grub_uint8_t *d; + grub_size_t newsz = 1; + + /* Is the current buffer size adequate? */ + if (buf->sz >= req) + return GRUB_ERR_NONE; + + /* Find the smallest power-of-2 size that satisfies the request. */ + while (newsz < req) + { + if (newsz == 0) + return grub_error (GRUB_ERR_OUT_OF_RANGE, + N_("requested buffer size is too large")); + newsz <<= 1; + } + + d = (grub_uint8_t *) grub_realloc (buf->data, newsz); + if (d == NULL) + return grub_errno; + + buf->data = d; + buf->sz = newsz; + + return GRUB_ERR_NONE; +} + +void * +grub_buffer_take_data (grub_buffer_t buf) +{ + void *data = buf->data; + + buf->data = NULL; + buf->sz = buf->pos = buf->used = 0; + + return data; +} + +void +grub_buffer_reset (grub_buffer_t buf) +{ + buf->pos = buf->used = 0; +} + +grub_err_t +grub_buffer_advance_read_pos (grub_buffer_t buf, grub_size_t n) +{ + grub_size_t newpos; + + if (grub_add (buf->pos, n, &newpos)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, N_("overflow is detected")); + + if (newpos > buf->used) + return grub_error (GRUB_ERR_OUT_OF_RANGE, + N_("new read is position beyond the end of the written data")); + + buf->pos = newpos; + + return GRUB_ERR_NONE; +} diff --git a/grub-core/kern/parser.c b/grub-core/kern/parser.c index d1cf061..6ab7aa4 100644 --- a/grub-core/kern/parser.c +++ b/grub-core/kern/parser.c @@ -1,7 +1,7 @@ /* parser.c - the part of the parser that can return partial tokens */ /* * GRUB -- GRand Unified Bootloader - * Copyright (C) 2005,2007,2009 Free Software Foundation, Inc. + * Copyright (C) 2005,2007,2009,2021 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 @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -107,8 +108,8 @@ check_varstate (grub_parser_state_t s) } -static void -add_var (char *varname, char **bp, char **vp, +static grub_err_t +add_var (grub_buffer_t varname, grub_buffer_t buf, grub_parser_state_t state, grub_parser_state_t newstate) { const char *val; @@ -116,17 +117,74 @@ add_var (char *varname, char **bp, char **vp, /* Check if a variable was being read in and the end of the name was reached. */ if (!(check_varstate (state) && !check_varstate (newstate))) - return; + return GRUB_ERR_NONE; + + if (grub_buffer_append_char (varname, '\0') != GRUB_ERR_NONE) + return grub_errno; - *((*vp)++) = '\0'; - val = grub_env_get (varname); - *vp = varname; + val = grub_env_get ((const char *) grub_buffer_peek_data (varname)); + grub_buffer_reset (varname); if (!val) - return; + return GRUB_ERR_NONE; /* Insert the contents of the variable in the buffer. */ - for (; *val; val++) - *((*bp)++) = *val; + return grub_buffer_append_data (buf, val, grub_strlen (val)); +} + +static grub_err_t +terminate_arg (grub_buffer_t buffer, int *argc) +{ + grub_size_t unread = grub_buffer_get_unread_bytes (buffer); + + if (unread == 0) + return GRUB_ERR_NONE; + + if (*(const char *) grub_buffer_peek_data_at (buffer, unread - 1) == '\0') + return GRUB_ERR_NONE; + + if (grub_buffer_append_char (buffer, '\0') != GRUB_ERR_NONE) + return grub_errno; + + (*argc)++; + + return GRUB_ERR_NONE; +} + +static grub_err_t +process_char (char c, grub_buffer_t buffer, grub_buffer_t varname, + grub_parser_state_t state, int *argc, + grub_parser_state_t *newstate) +{ + char use; + + *newstate = grub_parser_cmdline_state (state, c, &use); + + /* + * If a variable was being processed and this character does + * not describe the variable anymore, write the variable to + * the buffer. + */ + if (add_var (varname, buffer, state, *newstate) != GRUB_ERR_NONE) + return grub_errno; + + if (check_varstate (*newstate)) + { + if (use) + return grub_buffer_append_char (varname, use); + } + else if (*newstate == GRUB_PARSER_STATE_TEXT && + state != GRUB_PARSER_STATE_ESC && grub_isspace (use)) + { + /* + * Don't add more than one argument if multiple + * spaces are used. + */ + return terminate_arg (buffer, argc); + } + else if (use) + return grub_buffer_append_char (buffer, use); + + return GRUB_ERR_NONE; } grub_err_t @@ -135,24 +193,36 @@ grub_parser_split_cmdline (const char *cmdline, int *argc, char ***argv) { grub_parser_state_t state = GRUB_PARSER_STATE_TEXT; - /* XXX: Fixed size buffer, perhaps this buffer should be dynamically - allocated. */ - char buffer[1024]; - char *bp = buffer; + grub_buffer_t buffer, varname; char *rd = (char *) cmdline; - char varname[200]; - char *vp = varname; - char *args; + char *rp = rd; int i; *argc = 0; *argv = NULL; + + buffer = grub_buffer_new (1024); + if (buffer == NULL) + return grub_errno; + + varname = grub_buffer_new (200); + if (varname == NULL) + goto fail; + do { - if (!rd || !*rd) + if (rp == NULL || *rp == '\0') { + if (rd != cmdline) + { + grub_free (rd); + rd = rp = NULL; + } if (getline) - getline (&rd, 1, getline_data); + { + getline (&rd, 1, getline_data); + rp = rd; + } else break; } @@ -160,39 +230,14 @@ grub_parser_split_cmdline (const char *cmdline, if (!rd) break; - for (; *rd; rd++) + for (; *rp != '\0'; rp++) { grub_parser_state_t newstate; - char use; - newstate = grub_parser_cmdline_state (state, *rd, &use); + if (process_char (*rp, buffer, varname, state, argc, + &newstate) != GRUB_ERR_NONE) + goto fail; - /* If a variable was being processed and this character does - not describe the variable anymore, write the variable to - the buffer. */ - add_var (varname, &bp, &vp, state, newstate); - - if (check_varstate (newstate)) - { - if (use) - *(vp++) = use; - } - else - { - if (newstate == GRUB_PARSER_STATE_TEXT - && state != GRUB_PARSER_STATE_ESC && grub_isspace (use)) - { - /* Don't add more than one argument if multiple - spaces are used. */ - if (bp != buffer && *(bp - 1)) - { - *(bp++) = '\0'; - (*argc)++; - } - } - else if (use) - *(bp++) = use; - } state = newstate; } } @@ -200,43 +245,60 @@ grub_parser_split_cmdline (const char *cmdline, /* A special case for when the last character was part of a variable. */ - add_var (varname, &bp, &vp, state, GRUB_PARSER_STATE_TEXT); + if (add_var (varname, buffer, state, GRUB_PARSER_STATE_TEXT) != GRUB_ERR_NONE) + goto fail; - if (bp != buffer && *(bp - 1)) - { - *(bp++) = '\0'; - (*argc)++; - } + /* Ensure that the last argument is terminated. */ + if (terminate_arg (buffer, argc) != GRUB_ERR_NONE) + goto fail; /* If there are no args, then we're done. */ if (!*argc) - return 0; - - /* Reserve memory for the return values. */ - args = grub_malloc (bp - buffer); - if (!args) - return grub_errno; - grub_memcpy (args, buffer, bp - buffer); + { + grub_errno = GRUB_ERR_NONE; + goto out; + } *argv = grub_calloc (*argc + 1, sizeof (char *)); if (!*argv) - { - grub_free (args); - return grub_errno; - } + goto fail; /* The arguments are separated with 0's, setup argv so it points to the right values. */ - bp = args; for (i = 0; i < *argc; i++) { - (*argv)[i] = bp; - while (*bp) - bp++; - bp++; + char *arg; + + if (i > 0) + { + if (grub_buffer_advance_read_pos (buffer, 1) != GRUB_ERR_NONE) + goto fail; + } + + arg = (char *) grub_buffer_peek_data (buffer); + if (arg == NULL || + grub_buffer_advance_read_pos (buffer, grub_strlen (arg)) != GRUB_ERR_NONE) + goto fail; + + (*argv)[i] = arg; } - return 0; + /* Keep memory for the return values. */ + grub_buffer_take_data (buffer); + + grub_errno = GRUB_ERR_NONE; + + out: + if (rd != cmdline) + grub_free (rd); + grub_buffer_free (buffer); + grub_buffer_free (varname); + + return grub_errno; + + fail: + grub_free (*argv); + goto out; } /* Helper for grub_parser_execute. */ diff --git a/include/grub/buffer.h b/include/grub/buffer.h new file mode 100644 index 0000000..f4b10cf --- /dev/null +++ b/include/grub/buffer.h @@ -0,0 +1,144 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2021 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 . + */ + +#ifndef GRUB_BUFFER_H +#define GRUB_BUFFER_H 1 + +#include +#include +#include +#include +#include + +struct grub_buffer +{ + grub_uint8_t *data; + grub_size_t sz; + grub_size_t pos; + grub_size_t used; +}; + +/* + * grub_buffer_t represents a simple variable sized byte buffer with + * read and write cursors. It currently only implements + * functionality required by the only user in GRUB (append byte[s], + * peeking data at a specified position and updating the read cursor. + * Some things that this doesn't do yet are: + * - Reading a portion of the buffer by copying data from the current + * read position in to a caller supplied destination buffer and then + * automatically updating the read cursor. + * - Dropping the read part at the start of the buffer when an append + * requires more space. + */ +typedef struct grub_buffer *grub_buffer_t; + +/* Allocate a new buffer with the specified initial size. */ +extern grub_buffer_t grub_buffer_new (grub_size_t sz); + +/* Free the buffer and its resources. */ +extern void grub_buffer_free (grub_buffer_t buf); + +/* Return the number of unread bytes in this buffer. */ +static inline grub_size_t +grub_buffer_get_unread_bytes (grub_buffer_t buf) +{ + return buf->used - buf->pos; +} + +/* + * Ensure that the buffer size is at least the requested + * number of bytes. + */ +extern grub_err_t grub_buffer_ensure_space (grub_buffer_t buf, grub_size_t req); + +/* + * Append the specified number of bytes from the supplied + * data to the buffer. + */ +static inline grub_err_t +grub_buffer_append_data (grub_buffer_t buf, const void *data, grub_size_t len) +{ + grub_size_t req; + + if (grub_add (buf->used, len, &req)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, N_("overflow is detected")); + + if (grub_buffer_ensure_space (buf, req) != GRUB_ERR_NONE) + return grub_errno; + + grub_memcpy (&buf->data[buf->used], data, len); + buf->used = req; + + return GRUB_ERR_NONE; +} + +/* Append the supplied character to the buffer. */ +static inline grub_err_t +grub_buffer_append_char (grub_buffer_t buf, char c) +{ + return grub_buffer_append_data (buf, &c, 1); +} + +/* + * Forget and return the underlying data buffer. The caller + * becomes the owner of this buffer, and must free it when it + * is no longer required. + */ +extern void *grub_buffer_take_data (grub_buffer_t buf); + +/* Reset this buffer. Note that this does not deallocate any resources. */ +void grub_buffer_reset (grub_buffer_t buf); + +/* + * Return a pointer to the underlying data buffer at the specified + * offset from the current read position. Note that this pointer may + * become invalid if the buffer is mutated further. + */ +static inline void * +grub_buffer_peek_data_at (grub_buffer_t buf, grub_size_t off) +{ + if (grub_add (buf->pos, off, &off)) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, N_("overflow is detected.")); + return NULL; + } + + if (off >= buf->used) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, N_("peek out of range")); + return NULL; + } + + return &buf->data[off]; +} + +/* + * Return a pointer to the underlying data buffer at the current + * read position. Note that this pointer may become invalid if the + * buffer is mutated further. + */ +static inline void * +grub_buffer_peek_data (grub_buffer_t buf) +{ + return grub_buffer_peek_data_at (buf, 0); +} + +/* Advance the read position by the specified number of bytes. */ +extern grub_err_t grub_buffer_advance_read_pos (grub_buffer_t buf, grub_size_t n); + +#endif /* GRUB_BUFFER_H */ -- 2.25.1