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

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