1*4882a593SmuzhiyunFrom 83603bea6ce8fdff5ab3fbc4c9e592a8c71a8706 Mon Sep 17 00:00:00 2001
2*4882a593SmuzhiyunFrom: Thomas Frauendorfer | Miray Software <tf@miray.de>
3*4882a593SmuzhiyunDate: Thu, 4 Feb 2021 19:02:33 +0100
4*4882a593SmuzhiyunSubject: [PATCH] kern/misc: Add function to check printf() format against
5*4882a593Smuzhiyun expected format
6*4882a593Smuzhiyun
7*4882a593SmuzhiyunThe grub_printf_fmt_check() function parses the arguments of an untrusted
8*4882a593Smuzhiyunprintf() format and an expected printf() format and then compares the
9*4882a593Smuzhiyunarguments counts and arguments types. The arguments count in the untrusted
10*4882a593Smuzhiyunformat string must be less or equal to the arguments count in the expected
11*4882a593Smuzhiyunformat string and both arguments types must match.
12*4882a593Smuzhiyun
13*4882a593SmuzhiyunTo do this the parse_printf_arg_fmt() helper function is extended in the
14*4882a593Smuzhiyunfollowing way:
15*4882a593Smuzhiyun
16*4882a593Smuzhiyun  1. Add a return value to report errors to the grub_printf_fmt_check().
17*4882a593Smuzhiyun
18*4882a593Smuzhiyun  2. Add the fmt_check argument to enable stricter format verification:
19*4882a593Smuzhiyun     - the function expects that arguments definitions are always
20*4882a593Smuzhiyun       terminated by a supported conversion specifier.
21*4882a593Smuzhiyun     - positional parameters, "$", are not allowed, as they cannot be
22*4882a593Smuzhiyun       validated correctly with the current implementation. For example
23*4882a593Smuzhiyun       "%s%1$d" would assign the first args entry twice while leaving the
24*4882a593Smuzhiyun       second one unchanged.
25*4882a593Smuzhiyun     - Return an error if preallocated space in args is too small and
26*4882a593Smuzhiyun       allocation fails for the needed size. The grub_printf_fmt_check()
27*4882a593Smuzhiyun       should verify all arguments. So, if validation is not possible for
28*4882a593Smuzhiyun       any reason it should return an error.
29*4882a593Smuzhiyun     This also adds a case entry to handle "%%", which is the escape
30*4882a593Smuzhiyun     sequence to print "%" character.
31*4882a593Smuzhiyun
32*4882a593Smuzhiyun  3. Add the max_args argument to check for the maximum allowed arguments
33*4882a593Smuzhiyun     count in a printf() string. This should be set to the arguments count
34*4882a593Smuzhiyun     of the expected format. Then the parse_printf_arg_fmt() function will
35*4882a593Smuzhiyun     return an error if the arguments count is exceeded.
36*4882a593Smuzhiyun
37*4882a593SmuzhiyunThe two additional arguments allow us to use parse_printf_arg_fmt() in
38*4882a593Smuzhiyunprintf() and grub_printf_fmt_check() calls.
39*4882a593Smuzhiyun
40*4882a593SmuzhiyunWhen parse_printf_arg_fmt() is used by grub_printf_fmt_check() the
41*4882a593Smuzhiyunfunction parse user provided untrusted format string too. So, in
42*4882a593Smuzhiyunthat case it is better to be too strict than too lenient.
43*4882a593Smuzhiyun
44*4882a593SmuzhiyunSigned-off-by: Thomas Frauendorfer | Miray Software <tf@miray.de>
45*4882a593SmuzhiyunReviewed-by: Daniel Kiper <daniel.kiper@oracle.com>
46*4882a593SmuzhiyunSigned-off-by: Stefan Sørensen <stefan.sorensen@spectralink.com>
47*4882a593Smuzhiyun---
48*4882a593Smuzhiyun grub-core/kern/misc.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++++---
49*4882a593Smuzhiyun include/grub/misc.h   | 16 ++++++++++
50*4882a593Smuzhiyun 2 files changed, 94 insertions(+), 4 deletions(-)
51*4882a593Smuzhiyun
52*4882a593Smuzhiyundiff --git a/grub-core/kern/misc.c b/grub-core/kern/misc.c
53*4882a593Smuzhiyunindex 22417f7..90317b6 100644
54*4882a593Smuzhiyun--- a/grub-core/kern/misc.c
55*4882a593Smuzhiyun+++ b/grub-core/kern/misc.c
56*4882a593Smuzhiyun@@ -632,8 +632,26 @@ grub_lltoa (char *str, int c, unsigned long long n)
57*4882a593Smuzhiyun   return p;
58*4882a593Smuzhiyun }
59*4882a593Smuzhiyun
60*4882a593Smuzhiyun-static void
61*4882a593Smuzhiyun-parse_printf_arg_fmt (const char *fmt0, struct printf_args *args)
62*4882a593Smuzhiyun+/*
63*4882a593Smuzhiyun+ * Parse printf() fmt0 string into args arguments.
64*4882a593Smuzhiyun+ *
65*4882a593Smuzhiyun+ * The parsed arguments are either used by a printf() function to format the fmt0
66*4882a593Smuzhiyun+ * string or they are used to compare a format string from an untrusted source
67*4882a593Smuzhiyun+ * against a format string with expected arguments.
68*4882a593Smuzhiyun+ *
69*4882a593Smuzhiyun+ * When the fmt_check is set to !0, e.g. 1, then this function is executed in
70*4882a593Smuzhiyun+ * printf() format check mode. This enforces stricter rules for parsing the
71*4882a593Smuzhiyun+ * fmt0 to limit exposure to possible errors in printf() handling. It also
72*4882a593Smuzhiyun+ * disables positional parameters, "$", because some formats, e.g "%s%1$d",
73*4882a593Smuzhiyun+ * cannot be validated with the current implementation.
74*4882a593Smuzhiyun+ *
75*4882a593Smuzhiyun+ * The max_args allows to set a maximum number of accepted arguments. If the fmt0
76*4882a593Smuzhiyun+ * string defines more arguments than the max_args then the parse_printf_arg_fmt()
77*4882a593Smuzhiyun+ * function returns an error. This is currently used for format check only.
78*4882a593Smuzhiyun+ */
79*4882a593Smuzhiyun+static grub_err_t
80*4882a593Smuzhiyun+parse_printf_arg_fmt (const char *fmt0, struct printf_args *args,
81*4882a593Smuzhiyun+		      int fmt_check, grub_size_t max_args)
82*4882a593Smuzhiyun {
83*4882a593Smuzhiyun   const char *fmt;
84*4882a593Smuzhiyun   char c;
85*4882a593Smuzhiyun@@ -660,7 +678,12 @@ parse_printf_arg_fmt (const char *fmt0, struct printf_args *args)
86*4882a593Smuzhiyun 	fmt++;
87*4882a593Smuzhiyun
88*4882a593Smuzhiyun       if (*fmt == '$')
89*4882a593Smuzhiyun-	fmt++;
90*4882a593Smuzhiyun+	{
91*4882a593Smuzhiyun+	  if (fmt_check)
92*4882a593Smuzhiyun+	    return grub_error (GRUB_ERR_BAD_ARGUMENT,
93*4882a593Smuzhiyun+			       "positional arguments are not supported");
94*4882a593Smuzhiyun+	  fmt++;
95*4882a593Smuzhiyun+	}
96*4882a593Smuzhiyun
97*4882a593Smuzhiyun       if (*fmt =='-')
98*4882a593Smuzhiyun 	fmt++;
99*4882a593Smuzhiyun@@ -691,9 +714,19 @@ parse_printf_arg_fmt (const char *fmt0, struct printf_args *args)
100*4882a593Smuzhiyun 	case 's':
101*4882a593Smuzhiyun 	  args->count++;
102*4882a593Smuzhiyun 	  break;
103*4882a593Smuzhiyun+	case '%':
104*4882a593Smuzhiyun+	  /* "%%" is the escape sequence to output "%". */
105*4882a593Smuzhiyun+	  break;
106*4882a593Smuzhiyun+	default:
107*4882a593Smuzhiyun+	  if (fmt_check)
108*4882a593Smuzhiyun+	    return grub_error (GRUB_ERR_BAD_ARGUMENT, "unexpected format");
109*4882a593Smuzhiyun+	  break;
110*4882a593Smuzhiyun 	}
111*4882a593Smuzhiyun     }
112*4882a593Smuzhiyun
113*4882a593Smuzhiyun+  if (fmt_check && args->count > max_args)
114*4882a593Smuzhiyun+    return grub_error (GRUB_ERR_BAD_ARGUMENT, "too many arguments");
115*4882a593Smuzhiyun+
116*4882a593Smuzhiyun   if (args->count <= ARRAY_SIZE (args->prealloc))
117*4882a593Smuzhiyun     args->ptr = args->prealloc;
118*4882a593Smuzhiyun   else
119*4882a593Smuzhiyun@@ -701,6 +734,9 @@ parse_printf_arg_fmt (const char *fmt0, struct printf_args *args)
120*4882a593Smuzhiyun       args->ptr = grub_calloc (args->count, sizeof (args->ptr[0]));
121*4882a593Smuzhiyun       if (!args->ptr)
122*4882a593Smuzhiyun 	{
123*4882a593Smuzhiyun+	  if (fmt_check)
124*4882a593Smuzhiyun+	    return grub_errno;
125*4882a593Smuzhiyun+
126*4882a593Smuzhiyun 	  grub_errno = GRUB_ERR_NONE;
127*4882a593Smuzhiyun 	  args->ptr = args->prealloc;
128*4882a593Smuzhiyun 	  args->count = ARRAY_SIZE (args->prealloc);
129*4882a593Smuzhiyun@@ -791,6 +827,8 @@ parse_printf_arg_fmt (const char *fmt0, struct printf_args *args)
130*4882a593Smuzhiyun 	  break;
131*4882a593Smuzhiyun 	}
132*4882a593Smuzhiyun     }
133*4882a593Smuzhiyun+
134*4882a593Smuzhiyun+  return GRUB_ERR_NONE;
135*4882a593Smuzhiyun }
136*4882a593Smuzhiyun
137*4882a593Smuzhiyun static void
138*4882a593Smuzhiyun@@ -798,7 +836,7 @@ parse_printf_args (const char *fmt0, struct printf_args *args, va_list args_in)
139*4882a593Smuzhiyun {
140*4882a593Smuzhiyun   grub_size_t n;
141*4882a593Smuzhiyun
142*4882a593Smuzhiyun-  parse_printf_arg_fmt (fmt0, args);
143*4882a593Smuzhiyun+  parse_printf_arg_fmt (fmt0, args, 0, 0);
144*4882a593Smuzhiyun
145*4882a593Smuzhiyun   for (n = 0; n < args->count; n++)
146*4882a593Smuzhiyun     switch (args->ptr[n].type)
147*4882a593Smuzhiyun@@ -1105,6 +1143,42 @@ grub_xasprintf (const char *fmt, ...)
148*4882a593Smuzhiyun   return ret;
149*4882a593Smuzhiyun }
150*4882a593Smuzhiyun
151*4882a593Smuzhiyun+grub_err_t
152*4882a593Smuzhiyun+grub_printf_fmt_check (const char *fmt, const char *fmt_expected)
153*4882a593Smuzhiyun+{
154*4882a593Smuzhiyun+  struct printf_args args_expected, args_fmt;
155*4882a593Smuzhiyun+  grub_err_t ret;
156*4882a593Smuzhiyun+  grub_size_t n;
157*4882a593Smuzhiyun+
158*4882a593Smuzhiyun+  if (fmt == NULL || fmt_expected == NULL)
159*4882a593Smuzhiyun+    return grub_error (GRUB_ERR_BAD_ARGUMENT, "invalid format");
160*4882a593Smuzhiyun+
161*4882a593Smuzhiyun+  ret = parse_printf_arg_fmt (fmt_expected, &args_expected, 1, GRUB_SIZE_MAX);
162*4882a593Smuzhiyun+  if (ret != GRUB_ERR_NONE)
163*4882a593Smuzhiyun+    return ret;
164*4882a593Smuzhiyun+
165*4882a593Smuzhiyun+  /* Limit parsing to the number of expected arguments. */
166*4882a593Smuzhiyun+  ret = parse_printf_arg_fmt (fmt, &args_fmt, 1, args_expected.count);
167*4882a593Smuzhiyun+  if (ret != GRUB_ERR_NONE)
168*4882a593Smuzhiyun+    {
169*4882a593Smuzhiyun+      free_printf_args (&args_expected);
170*4882a593Smuzhiyun+      return ret;
171*4882a593Smuzhiyun+    }
172*4882a593Smuzhiyun+
173*4882a593Smuzhiyun+  for (n = 0; n < args_fmt.count; n++)
174*4882a593Smuzhiyun+    if (args_fmt.ptr[n].type != args_expected.ptr[n].type)
175*4882a593Smuzhiyun+     {
176*4882a593Smuzhiyun+	ret = grub_error (GRUB_ERR_BAD_ARGUMENT, "arguments types do not match");
177*4882a593Smuzhiyun+	break;
178*4882a593Smuzhiyun+     }
179*4882a593Smuzhiyun+
180*4882a593Smuzhiyun+  free_printf_args (&args_expected);
181*4882a593Smuzhiyun+  free_printf_args (&args_fmt);
182*4882a593Smuzhiyun+
183*4882a593Smuzhiyun+  return ret;
184*4882a593Smuzhiyun+}
185*4882a593Smuzhiyun+
186*4882a593Smuzhiyun+
187*4882a593Smuzhiyun /* Abort GRUB. This function does not return.  */
188*4882a593Smuzhiyun static void __attribute__ ((noreturn))
189*4882a593Smuzhiyun grub_abort (void)
190*4882a593Smuzhiyundiff --git a/include/grub/misc.h b/include/grub/misc.h
191*4882a593Smuzhiyunindex ee48eb7..d1c5709 100644
192*4882a593Smuzhiyun--- a/include/grub/misc.h
193*4882a593Smuzhiyun+++ b/include/grub/misc.h
194*4882a593Smuzhiyun@@ -440,6 +440,22 @@ grub_error_load (const struct grub_error_saved *save)
195*4882a593Smuzhiyun   grub_errno = save->grub_errno;
196*4882a593Smuzhiyun }
197*4882a593Smuzhiyun
198*4882a593Smuzhiyun+/*
199*4882a593Smuzhiyun+ * grub_printf_fmt_checks() a fmt string for printf() against an expected
200*4882a593Smuzhiyun+ * format. It is intended for cases where the fmt string could come from
201*4882a593Smuzhiyun+ * an outside source and cannot be trusted.
202*4882a593Smuzhiyun+ *
203*4882a593Smuzhiyun+ * While expected fmt accepts a printf() format string it should be kept
204*4882a593Smuzhiyun+ * as simple as possible. The printf() format strings with positional
205*4882a593Smuzhiyun+ * parameters are NOT accepted, neither for fmt nor for fmt_expected.
206*4882a593Smuzhiyun+ *
207*4882a593Smuzhiyun+ * The fmt is accepted if it has equal or less arguments than fmt_expected
208*4882a593Smuzhiyun+ * and if the type of all arguments match.
209*4882a593Smuzhiyun+ *
210*4882a593Smuzhiyun+ * Returns GRUB_ERR_NONE if fmt is acceptable.
211*4882a593Smuzhiyun+ */
212*4882a593Smuzhiyun+grub_err_t EXPORT_FUNC (grub_printf_fmt_check) (const char *fmt, const char *fmt_expected);
213*4882a593Smuzhiyun+
214*4882a593Smuzhiyun #if BOOT_TIME_STATS
215*4882a593Smuzhiyun struct grub_boot_time
216*4882a593Smuzhiyun {
217*4882a593Smuzhiyun--
218*4882a593Smuzhiyun2.14.2
219*4882a593Smuzhiyun
220