Blame SOURCES/0477-kern-misc-Add-function-to-check-printf-format-agains.patch

80913e
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
80913e
From: Thomas Frauendorfer | Miray Software <tf@miray.de>
80913e
Date: Thu, 4 Feb 2021 19:02:33 +0100
80913e
Subject: [PATCH] kern/misc: Add function to check printf() format against
80913e
 expected format
80913e
80913e
The grub_printf_fmt_check() function parses the arguments of an untrusted
80913e
printf() format and an expected printf() format and then compares the
80913e
arguments counts and arguments types. The arguments count in the untrusted
80913e
format string must be less or equal to the arguments count in the expected
80913e
format string and both arguments types must match.
80913e
80913e
To do this the parse_printf_arg_fmt() helper function is extended in the
80913e
following way:
80913e
80913e
  1. Add a return value to report errors to the grub_printf_fmt_check().
80913e
80913e
  2. Add the fmt_check argument to enable stricter format verification:
80913e
     - the function expects that arguments definitions are always
80913e
       terminated by a supported conversion specifier.
80913e
     - positional parameters, "$", are not allowed, as they cannot be
80913e
       validated correctly with the current implementation. For example
80913e
       "%s%1$d" would assign the first args entry twice while leaving the
80913e
       second one unchanged.
80913e
     - Return an error if preallocated space in args is too small and
80913e
       allocation fails for the needed size. The grub_printf_fmt_check()
80913e
       should verify all arguments. So, if validation is not possible for
80913e
       any reason it should return an error.
80913e
     This also adds a case entry to handle "%%", which is the escape
80913e
     sequence to print "%" character.
80913e
80913e
  3. Add the max_args argument to check for the maximum allowed arguments
80913e
     count in a printf() string. This should be set to the arguments count
80913e
     of the expected format. Then the parse_printf_arg_fmt() function will
80913e
     return an error if the arguments count is exceeded.
80913e
80913e
The two additional arguments allow us to use parse_printf_arg_fmt() in
80913e
printf() and grub_printf_fmt_check() calls.
80913e
80913e
When parse_printf_arg_fmt() is used by grub_printf_fmt_check() the
80913e
function parse user provided untrusted format string too. So, in
80913e
that case it is better to be too strict than too lenient.
80913e
80913e
Signed-off-by: Thomas Frauendorfer | Miray Software <tf@miray.de>
80913e
Reviewed-by: Daniel Kiper <daniel.kiper@oracle.com>
80913e
---
80913e
 grub-core/kern/misc.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++++---
80913e
 include/grub/misc.h   | 16 ++++++++++
80913e
 2 files changed, 94 insertions(+), 4 deletions(-)
80913e
80913e
diff --git a/grub-core/kern/misc.c b/grub-core/kern/misc.c
b32e65
index 07456faa2..859d71659 100644
80913e
--- a/grub-core/kern/misc.c
80913e
+++ b/grub-core/kern/misc.c
80913e
@@ -711,8 +711,26 @@ grub_lltoa (char *str, int c, unsigned long long n)
80913e
   return p;
80913e
 }
80913e
 
80913e
-static void
80913e
-parse_printf_arg_fmt (const char *fmt0, struct printf_args *args)
80913e
+/*
80913e
+ * Parse printf() fmt0 string into args arguments.
80913e
+ *
80913e
+ * The parsed arguments are either used by a printf() function to format the fmt0
80913e
+ * string or they are used to compare a format string from an untrusted source
80913e
+ * against a format string with expected arguments.
80913e
+ *
80913e
+ * When the fmt_check is set to !0, e.g. 1, then this function is executed in
80913e
+ * printf() format check mode. This enforces stricter rules for parsing the
80913e
+ * fmt0 to limit exposure to possible errors in printf() handling. It also
80913e
+ * disables positional parameters, "$", because some formats, e.g "%s%1$d",
80913e
+ * cannot be validated with the current implementation.
80913e
+ *
80913e
+ * The max_args allows to set a maximum number of accepted arguments. If the fmt0
80913e
+ * string defines more arguments than the max_args then the parse_printf_arg_fmt()
80913e
+ * function returns an error. This is currently used for format check only.
80913e
+ */
80913e
+static grub_err_t
80913e
+parse_printf_arg_fmt (const char *fmt0, struct printf_args *args,
80913e
+		      int fmt_check, grub_size_t max_args)
80913e
 {
80913e
   const char *fmt;
80913e
   char c;
80913e
@@ -739,7 +757,12 @@ parse_printf_arg_fmt (const char *fmt0, struct printf_args *args)
80913e
 	fmt++;
80913e
 
80913e
       if (*fmt == '$')
80913e
-	fmt++;
80913e
+	{
80913e
+	  if (fmt_check)
80913e
+	    return grub_error (GRUB_ERR_BAD_ARGUMENT,
80913e
+			       "positional arguments are not supported");
80913e
+	  fmt++;
80913e
+	}
80913e
 
80913e
       if (*fmt =='-')
80913e
 	fmt++;
80913e
@@ -771,9 +794,19 @@ parse_printf_arg_fmt (const char *fmt0, struct printf_args *args)
80913e
 	case 's':
80913e
 	  args->count++;
80913e
 	  break;
80913e
+	case '%':
80913e
+	  /* "%%" is the escape sequence to output "%". */
80913e
+	  break;
80913e
+	default:
80913e
+	  if (fmt_check)
80913e
+	    return grub_error (GRUB_ERR_BAD_ARGUMENT, "unexpected format");
80913e
+	  break;
80913e
 	}
80913e
     }
80913e
 
80913e
+  if (fmt_check && args->count > max_args)
80913e
+    return grub_error (GRUB_ERR_BAD_ARGUMENT, "too many arguments");
80913e
+
80913e
   if (args->count <= ARRAY_SIZE (args->prealloc))
80913e
     args->ptr = args->prealloc;
80913e
   else
80913e
@@ -781,6 +814,9 @@ parse_printf_arg_fmt (const char *fmt0, struct printf_args *args)
80913e
       args->ptr = grub_calloc (args->count, sizeof (args->ptr[0]));
80913e
       if (!args->ptr)
80913e
 	{
80913e
+	  if (fmt_check)
80913e
+	    return grub_errno;
80913e
+
80913e
 	  grub_errno = GRUB_ERR_NONE;
80913e
 	  args->ptr = args->prealloc;
80913e
 	  args->count = ARRAY_SIZE (args->prealloc);
80913e
@@ -872,6 +908,8 @@ parse_printf_arg_fmt (const char *fmt0, struct printf_args *args)
80913e
 	  break;
80913e
 	}
80913e
     }
80913e
+
80913e
+  return GRUB_ERR_NONE;
80913e
 }
80913e
 
80913e
 static void
80913e
@@ -879,7 +917,7 @@ parse_printf_args (const char *fmt0, struct printf_args *args, va_list args_in)
80913e
 {
80913e
   grub_size_t n;
80913e
 
80913e
-  parse_printf_arg_fmt (fmt0, args);
80913e
+  parse_printf_arg_fmt (fmt0, args, 0, 0);
80913e
 
80913e
   for (n = 0; n < args->count; n++)
80913e
     switch (args->ptr[n].type)
80913e
@@ -1187,6 +1225,42 @@ grub_xasprintf (const char *fmt, ...)
80913e
   return ret;
80913e
 }
80913e
 
80913e
+grub_err_t
80913e
+grub_printf_fmt_check (const char *fmt, const char *fmt_expected)
80913e
+{
80913e
+  struct printf_args args_expected, args_fmt;
80913e
+  grub_err_t ret;
80913e
+  grub_size_t n;
80913e
+
80913e
+  if (fmt == NULL || fmt_expected == NULL)
80913e
+    return grub_error (GRUB_ERR_BAD_ARGUMENT, "invalid format");
80913e
+
80913e
+  ret = parse_printf_arg_fmt (fmt_expected, &args_expected, 1, GRUB_SIZE_MAX);
80913e
+  if (ret != GRUB_ERR_NONE)
80913e
+    return ret;
80913e
+
80913e
+  /* Limit parsing to the number of expected arguments. */
80913e
+  ret = parse_printf_arg_fmt (fmt, &args_fmt, 1, args_expected.count);
80913e
+  if (ret != GRUB_ERR_NONE)
80913e
+    {
80913e
+      free_printf_args (&args_expected);
80913e
+      return ret;
80913e
+    }
80913e
+
80913e
+  for (n = 0; n < args_fmt.count; n++)
80913e
+    if (args_fmt.ptr[n].type != args_expected.ptr[n].type)
80913e
+     {
80913e
+	ret = grub_error (GRUB_ERR_BAD_ARGUMENT, "arguments types do not match");
80913e
+	break;
80913e
+     }
80913e
+
80913e
+  free_printf_args (&args_expected);
80913e
+  free_printf_args (&args_fmt);
80913e
+
80913e
+  return ret;
80913e
+}
80913e
+
80913e
+
80913e
 /* Abort GRUB. This function does not return.  */
80913e
 static inline void __attribute__ ((noreturn))
80913e
 grub_abort (void)
80913e
diff --git a/include/grub/misc.h b/include/grub/misc.h
b32e65
index 6ca03c4d6..6be6a88f6 100644
80913e
--- a/include/grub/misc.h
80913e
+++ b/include/grub/misc.h
80913e
@@ -488,6 +488,22 @@ grub_error_load (const struct grub_error_saved *save)
80913e
   grub_errno = save->grub_errno;
80913e
 }
80913e
 
80913e
+/*
80913e
+ * grub_printf_fmt_checks() a fmt string for printf() against an expected
80913e
+ * format. It is intended for cases where the fmt string could come from
80913e
+ * an outside source and cannot be trusted.
80913e
+ *
80913e
+ * While expected fmt accepts a printf() format string it should be kept
80913e
+ * as simple as possible. The printf() format strings with positional
80913e
+ * parameters are NOT accepted, neither for fmt nor for fmt_expected.
80913e
+ *
80913e
+ * The fmt is accepted if it has equal or less arguments than fmt_expected
80913e
+ * and if the type of all arguments match.
80913e
+ *
80913e
+ * Returns GRUB_ERR_NONE if fmt is acceptable.
80913e
+ */
80913e
+grub_err_t EXPORT_FUNC (grub_printf_fmt_check) (const char *fmt, const char *fmt_expected);
80913e
+
80913e
 #if BOOT_TIME_STATS
80913e
 struct grub_boot_time
80913e
 {