[v4,2/2] stdio-common: Fix buffer overflow in scanf %mc [BZ #34008]

Message ID 20260405181821.475180-3-marocketbd@gmail.com (mailing list archive)
State Superseded
Headers
Series stdio-common: Fix heap overflow in scanf %mc pattern [BZ #34008] |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 success Test passed
linaro-tcwg-bot/tcwg_glibc_build--master-arm success Build passed
redhat-pt-bot/TryBot-32bit success Build for i686
linaro-tcwg-bot/tcwg_glibc_check--master-arm success Test passed

Commit Message

Rocket Ma April 5, 2026, 6:18 p.m. UTC
  * stdio-common/vfscanf-internal.c: When enlarging allocated buffer with
format %mc or %mC, glibc allocates one byte less, leading to
user-controlled one byte overflow. This commit fixes BZ #34008, or
CVE-2026-5450. Unify newsize calculation of allocated buffer.

Signed-off-by: Rocket Ma <marocketbd@gmail.com>
---
 stdio-common/vfscanf-internal.c | 74 ++++++++++++++++++++-------------
 1 file changed, 46 insertions(+), 28 deletions(-)
  

Comments

Florian Weimer April 6, 2026, 9 a.m. UTC | #1
* Rocket Ma:

> * stdio-common/vfscanf-internal.c: When enlarging allocated buffer with
> format %mc or %mC, glibc allocates one byte less, leading to
> user-controlled one byte overflow. This commit fixes BZ #34008, or
> CVE-2026-5450. Unify newsize calculation of allocated buffer.
>
> Signed-off-by: Rocket Ma <marocketbd@gmail.com>
> ---
>  stdio-common/vfscanf-internal.c | 74 ++++++++++++++++++++-------------
>  1 file changed, 46 insertions(+), 28 deletions(-)
>
> diff --git a/stdio-common/vfscanf-internal.c b/stdio-common/vfscanf-internal.c
> index 59fc8208aa..6bf2a55876 100644
> --- a/stdio-common/vfscanf-internal.c
> +++ b/stdio-common/vfscanf-internal.c
> @@ -265,6 +265,19 @@ char_buffer_add (struct char_buffer *buffer, CHAR_T ch)
>      *buffer->current++ = ch;
>  }
>  
> +/* Calculate the result size of expanded char array in %ms, %mS,
> +   %m[, %lm[, %mc or %mC. */
> +static __always_inline size_t
> +grow_to_fit (size_t oldsize, int need, int extra)
> +{
> +  /* extra = 0 if %m[cC], %m[cC] always have positive width */
> +  if ((extra && need < 0) || oldsize < need)
> +    return oldsize * 2;
> +  /* oldsize >= need:
> +     grow requested capacity and `extra' byte for `\0' */
> +  return oldsize + need + extra;
> +}

Thanks for working on this.

The last (extra) argument is constant.  I'd suggest two functions with
descriptive names instead (maybe grow_to_fit_for_fixed for the %c
family, and grow_to_fit_with_null for the %s/%[] family that perform
null termination).  The new functions probably shouldn't be inline, so
that the compiler can apply its heuristics.

As written, the interaction with the extra argument and a negative
need argument is not quite obvious from the function alone (code and
comments).  The function seems correct because need (called width in
the caller) can only be -1 for the %s/%[] case, where extra is 1.
  
Rocket Ma April 6, 2026, 4:17 p.m. UTC | #2
Florian Weimer <fw@deneb.enyo.de> 于2026年4月6日周一 17:00写道:

> The last (extra) argument is constant.  I'd suggest two functions with
> descriptive names instead (maybe grow_to_fit_for_fixed for the %c
> family, and grow_to_fit_with_null for the %s/%[] family that perform
> null termination).  The new functions probably shouldn't be inline, so
> that the compiler can apply its heuristics.

If the function need to be separated, then the old behavior, "size_t
newsize = strsize + (strsize >= width ? width : strsize)", is not
worth a new function. And the function only expands to several
instructions, less than 10, observed via Compiler Explorer. I think
it's OK to inline the function.

> As written, the interaction with the extra argument and a negative
> need argument is not quite obvious from the function alone (code and
> comments).  The function seems correct because need (called width in
> the caller) can only be -1 for the %s/%[] case, where extra is 1.

Since the behavior mentioned has become some sort of convention, the
function should be OK? Readers has the constant to distinguish between
%ms and %mc, and they can understand the code via enough comments.
Personally I think it's worth to put these two actions together to do
one thing: calculate the size of expanded array.
  
Rocket Ma April 8, 2026, 4:13 p.m. UTC | #3
> If the function need to be separated, then the old behavior, "size_t
> newsize = strsize + (strsize >= width ? width : strsize)", is not
> worth a new function. And the function only expands to several
> instructions, less than 10, observed via Compiler Explorer. I think
> it's OK to inline the function.
>
> Since the behavior mentioned has become some sort of convention, the
> function should be OK? Readers has the constant to distinguish between
> %ms and %mc, and they can understand the code via enough comments.
> Personally I think it's worth to put these two actions together to do
> one thing: calculate the size of expanded array.

Any idea?
  
Carlos O'Donell April 13, 2026, 1:46 p.m. UTC | #4
On 4/8/26 12:13 PM, Rocket Ma wrote:
>> If the function need to be separated, then the old behavior, "size_t
>> newsize = strsize + (strsize >= width ? width : strsize)", is not
>> worth a new function. And the function only expands to several
>> instructions, less than 10, observed via Compiler Explorer. I think
>> it's OK to inline the function.
>>
>> Since the behavior mentioned has become some sort of convention, the
>> function should be OK? Readers has the constant to distinguish between
>> %ms and %mc, and they can understand the code via enough comments.
>> Personally I think it's worth to put these two actions together to do
>> one thing: calculate the size of expanded array.
> 
> Any idea?
> 

The best way forward is to simplify and just fix the %mc case for bug
34008 and the CVE.

Then we can have a follow-on changes that refactors?
  

Patch

diff --git a/stdio-common/vfscanf-internal.c b/stdio-common/vfscanf-internal.c
index 59fc8208aa..6bf2a55876 100644
--- a/stdio-common/vfscanf-internal.c
+++ b/stdio-common/vfscanf-internal.c
@@ -265,6 +265,19 @@  char_buffer_add (struct char_buffer *buffer, CHAR_T ch)
     *buffer->current++ = ch;
 }
 
+/* Calculate the result size of expanded char array in %ms, %mS,
+   %m[, %lm[, %mc or %mC. */
+static __always_inline size_t
+grow_to_fit (size_t oldsize, int need, int extra)
+{
+  /* extra = 0 if %m[cC], %m[cC] always have positive width */
+  if ((extra && need < 0) || oldsize < need)
+    return oldsize * 2;
+  /* oldsize >= need:
+     grow requested capacity and `extra' byte for `\0' */
+  return oldsize + need + extra;
+}
+
 /* Read formatted input from S according to the format string
    FORMAT, using the argument list in ARG.
    Return the number of assignments made, or -1 for an input error.  */
@@ -804,7 +817,8 @@  __vfscanf_internal (FILE *s, const char *format, va_list argptr,
 		      && *strptr + strsize - str <= MB_LEN_MAX)
 		    {
 		      /* We have to enlarge the buffer if the `m' flag
-			 was given.  */
+			 was given. And we may not expand str by width
+			 as the wcrtomb may return various bytes */
 		      size_t strleng = str - *strptr;
 		      char *newstr;
 
@@ -854,9 +868,7 @@  __vfscanf_internal (FILE *s, const char *format, va_list argptr,
 			  && (char *) str == *strptr + strsize)
 			{
 			  /* Enlarge the buffer.  */
-			  size_t newsize
-			    = strsize
-			      + (strsize >= width ? width - 1 : strsize);
+			  size_t newsize = grow_to_fit (strsize, width, 0);
 
 			  str = (char *) realloc (*strptr, newsize);
 			  if (str == NULL)
@@ -928,8 +940,7 @@  __vfscanf_internal (FILE *s, const char *format, va_list argptr,
 		  if ((flags & MALLOC)
 		      && wstr == (wchar_t *) *strptr + strsize)
 		    {
-		      size_t newsize
-			= strsize + (strsize > width ? width - 1 : strsize);
+		      size_t newsize = grow_to_fit (strsize, width, 0);
 		      /* Enlarge the buffer.  */
 		      wstr = (wchar_t *) realloc (*strptr,
 						  newsize * sizeof (wchar_t));
@@ -983,8 +994,7 @@  __vfscanf_internal (FILE *s, const char *format, va_list argptr,
 		if (!(flags & SUPPRESS) && (flags & MALLOC)
 		    && wstr == (wchar_t *) *strptr + strsize)
 		  {
-		    size_t newsize
-		      = strsize + (strsize > width ? width - 1 : strsize);
+		    size_t newsize = grow_to_fit (strsize, width, 0);
 		    /* Enlarge the buffer.  */
 		    wstr = (wchar_t *) realloc (*strptr,
 						newsize * sizeof (wchar_t));
@@ -1099,7 +1109,8 @@  __vfscanf_internal (FILE *s, const char *format, va_list argptr,
 			&& *strptr + strsize - str <= MB_LEN_MAX)
 		      {
 			/* We have to enlarge the buffer if the `a' or `m'
-			   flag was given.  */
+			   flag was given. And we may not expand str by
+			   width as the wcrtomb may return various bytes */
 			size_t strleng = str - *strptr;
 			char *newstr;
 
@@ -1157,7 +1168,8 @@  __vfscanf_internal (FILE *s, const char *format, va_list argptr,
 			  && (char *) str == *strptr + strsize)
 			{
 			  /* Enlarge the buffer.  */
-			  str = (char *) realloc (*strptr, 2 * strsize);
+			  size_t newsize = grow_to_fit (strsize, width, 1);
+			  str = (char *) realloc (*strptr, newsize);
 			  if (str == NULL)
 			    {
 			      /* Can't allocate that much.  Last-ditch
@@ -1189,7 +1201,7 @@  __vfscanf_internal (FILE *s, const char *format, va_list argptr,
 			    {
 			      *strptr = (char *) str;
 			      str += strsize;
-			      strsize *= 2;
+			      strsize = newsize;
 			    }
 			}
 		    }
@@ -1287,9 +1299,10 @@  __vfscanf_internal (FILE *s, const char *format, va_list argptr,
 			&& wstr == (wchar_t *) *strptr + strsize)
 		      {
 			/* Enlarge the buffer.  */
-			wstr = (wchar_t *) realloc (*strptr,
-						    (2 * strsize)
-						    * sizeof (wchar_t));
+			size_t newsize = grow_to_fit (strsize, width, 1);
+
+			wstr = (wchar_t *) realloc (
+			    *strptr, newsize * sizeof (wchar_t));
 			if (wstr == NULL)
 			  {
 			    /* Can't allocate that much.  Last-ditch
@@ -1323,7 +1336,7 @@  __vfscanf_internal (FILE *s, const char *format, va_list argptr,
 			  {
 			    *strptr = (char *) wstr;
 			    wstr += strsize;
-			    strsize *= 2;
+			    strsize = newsize;
 			  }
 		      }
 		  }
@@ -1363,9 +1376,10 @@  __vfscanf_internal (FILE *s, const char *format, va_list argptr,
 		      && wstr == (wchar_t *) *strptr + strsize)
 		    {
 		      /* Enlarge the buffer.  */
+		      size_t newsize = grow_to_fit (strsize, width, 1);
+
 		      wstr = (wchar_t *) realloc (*strptr,
-						  (2 * strsize
-						   * sizeof (wchar_t)));
+						  newsize * sizeof (wchar_t));
 		      if (wstr == NULL)
 			{
 			  /* Can't allocate that much.  Last-ditch effort.  */
@@ -1398,7 +1412,7 @@  __vfscanf_internal (FILE *s, const char *format, va_list argptr,
 			{
 			  *strptr = (char *) wstr;
 			  wstr += strsize;
-			  strsize *= 2;
+			  strsize = newsize;
 			}
 		    }
 		}
@@ -2755,9 +2769,10 @@  digits_extended_fail:
 			  && wstr == (wchar_t *) *strptr + strsize)
 			{
 			  /* Enlarge the buffer.  */
-			  wstr = (wchar_t *) realloc (*strptr,
-						      (2 * strsize)
-						      * sizeof (wchar_t));
+			  size_t newsize = grow_to_fit (strsize, width, 1);
+
+			  wstr = (wchar_t *) realloc (
+			      *strptr, newsize * sizeof (wchar_t));
 			  if (wstr == NULL)
 			    {
 			      /* Can't allocate that much.  Last-ditch
@@ -2791,7 +2806,7 @@  digits_extended_fail:
 			    {
 			      *strptr = (char *) wstr;
 			      wstr += strsize;
-			      strsize *= 2;
+			      strsize = newsize;
 			    }
 			}
 		    }
@@ -2840,9 +2855,10 @@  digits_extended_fail:
 			  && wstr == (wchar_t *) *strptr + strsize)
 			{
 			  /* Enlarge the buffer.  */
-			  wstr = (wchar_t *) realloc (*strptr,
-						      (2 * strsize
-						       * sizeof (wchar_t)));
+			  size_t newsize = grow_to_fit (strsize, width, 1);
+
+			  wstr = (wchar_t *) realloc (
+			      *strptr, newsize * sizeof (wchar_t));
 			  if (wstr == NULL)
 			    {
 			      /* Can't allocate that much.  Last-ditch
@@ -2876,7 +2892,7 @@  digits_extended_fail:
 			    {
 			      *strptr = (char *) wstr;
 			      wstr += strsize;
-			      strsize *= 2;
+			      strsize = newsize;
 			    }
 			}
 		    }
@@ -2984,7 +3000,9 @@  digits_extended_fail:
 		      if ((flags & MALLOC)
 			  && *strptr + strsize - str <= MB_LEN_MAX)
 			{
-			  /* Enlarge the buffer.  */
+			  /* Enlarge the buffer. And we may not
+			   expand str by width as the wcrtomb may
+			   return various bytes */
 			  size_t strleng = str - *strptr;
 			  char *newstr;
 
@@ -3052,7 +3070,7 @@  digits_extended_fail:
 			  && (char *) str == *strptr + strsize)
 			{
 			  /* Enlarge the buffer.  */
-			  size_t newsize = 2 * strsize;
+			  size_t newsize = grow_to_fit (strsize, width, 1);
 
 			allocagain:
 			  str = (char *) realloc (*strptr, newsize);