[PATCH] stdio-common: Fix scanf nan/inf pushback [BZ #34208]

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Tue Jun 23 17:06:27 GMT 2026



On 21/06/26 04:41, Gao Xiang wrote:
> From: Xiang Gao <gaoxiang@kylinos.cn>
> 
> When scanf parses special floating-point inputs like "nan", "inf", it
> may read one extra character to check whether the input still matches
> the expected word.
> 
> If that extra character does not match, the conversion should fail, but
> the character has already been read from the stream.  Some error paths
> did not push it back before reporting the conversion error, so later input
> started at the wrong position.
> 
> Fix this by keeping EOF and width errors separate from character mismatch
> errors.  Push the character back before calling conv_error for mismatch
> errors.
> 
> Add narrow and wide scanf tests using the same test body.  The test cases
> focus on the paths changed by this patch: mismatches while matching "nan",
> "nan(...)", "inf", and "infinity".
> 
> Tested on x86_64 Fedora 42.
> 
> The following tests passed without regressions:
>   make test t=stdio-common/tst-scanf-bz34208
>   make test t=stdio-common/tst-wscanf-bz34208
>   make subdirs="stdio-common libio" check
> 
> Signed-off-by: Xiang Gao <gaoxiang@kylinos.cn>

The patch looks good, thanks.  Some comments for the testcases.

> ---
>  stdio-common/Makefile             |   2 +
>  stdio-common/tst-scanf-bz34208.c  | 102 ++++++++++++++++++++++++++++++
>  stdio-common/tst-wscanf-bz34208.c |  20 ++++++
>  stdio-common/vfscanf-internal.c   |  73 ++++++++++++++-------
>  4 files changed, 173 insertions(+), 24 deletions(-)
>  create mode 100644 stdio-common/tst-scanf-bz34208.c
>  create mode 100644 stdio-common/tst-wscanf-bz34208.c
> 
> diff --git a/stdio-common/Makefile b/stdio-common/Makefile
> index 0c0085e607..a948bfde8f 100644
> --- a/stdio-common/Makefile
> +++ b/stdio-common/Makefile
> @@ -327,6 +327,7 @@ tests := \
>    tst-scanf-binary-gnu11 \
>    tst-scanf-binary-gnu89 \
>    tst-scanf-bz27650 \
> +  tst-scanf-bz34208 \
>    tst-scanf-intn \
>    tst-scanf-nan \
>    tst-scanf-round \
> @@ -351,6 +352,7 @@ tests := \
>    tst-vfprintf-width-prec-alloc \
>    tst-vfscanf-bz34008 \
>    tst-wc-printf \
> +  tst-wscanf-bz34208 \
>    tstdiomisc \
>    tstgetln \
>    tstscanf \

Ok.

> diff --git a/stdio-common/tst-scanf-bz34208.c b/stdio-common/tst-scanf-bz34208.c
> new file mode 100644
> index 0000000000..00c87eb98e
> --- /dev/null
> +++ b/stdio-common/tst-scanf-bz34208.c
> @@ -0,0 +1,102 @@
> +/* Test scanf pushback for incomplete nan/inf inputs (BZ #34208).
> +   Copyright (C) 2026 The GNU Toolchain Authors.
> +   This file is part of the GNU C Library.
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library 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
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +#define _GNU_SOURCE 1

I think there is no need to define it, but it is also not an issue.

> +
> +#include <wchar.h>
> +#include <stdio.h>
> +#include <support/check.h>
> +
> +#ifdef TEST_WCHAR
> +# define CHAR_T wchar_t
> +# define WINT_T wint_t
> +# define FSCANF fwscanf
> +# define FPUTC fputwc
> +# define FGETC fgetwc
> +# define EOF_VALUE WEOF
> +# define SCAN_FORMAT L"%e"
> +#else
> +# define CHAR_T char
> +# define WINT_T int
> +# define FSCANF fscanf
> +# define FPUTC fputc
> +# define FGETC fgetc
> +# define EOF_VALUE EOF
> +# define SCAN_FORMAT "%e"
> +#endif
> +
> +static const float sentinel = -123.0f;
> +
> +struct test
> +{
> +  const char *input;
> +  long int expected_offset;
> +  const char *expected_rest;
> +};
> +
> +static const struct test tests[] =
> +  {
> +    /* Original reproducer.  The "[" is read while looking for the
> +       second "n" in "nan", so it must be pushed back.  */
> +    { "+NA[..z", 3, "[..z" },
> +
> +    /* Mismatch while matching "nan(...)", "inf" and "infinity" must be
> +       pushed back.  */
> +    { "nan(@X", 4, "@X" },
> +    { "iX", 1, "X" },
> +    { "infiX", 4, "X" },
> +  };
> +
> +static void
> +do_one_test (const struct test *test)
> +{
> +  FILE *fp = tmpfile ();
> +  TEST_VERIFY_EXIT (fp != NULL);
> +
> +  for (const char *p = test->input; *p != '\0'; p++)
> +    TEST_COMPARE (FPUTC ((CHAR_T) *p, fp), (WINT_T) (CHAR_T) *p);

I think you can define a macro:

#ifdef TEST_WCHAR
[...]
# define L_(C) L ## C
#else
[...]
# define L_(C) C
#endif

And then you can define the 'struct test' by using the wide types (CHAR_T)
and get rid of the casts here.

> +
> +  TEST_COMPARE (fseek (fp, 0, SEEK_SET), 0);
> +
> +  float value = sentinel;
> +  TEST_COMPARE (FSCANF (fp, SCAN_FORMAT, &value), 0);
> +  TEST_COMPARE (ftell (fp), test->expected_offset);
> +  TEST_VERIFY (value == sentinel);
> +
> +  for (const char *p = test->expected_rest; *p != '\0'; p++)
> +    TEST_COMPARE (FGETC (fp), (WINT_T) (CHAR_T) *p);
> +
> +  TEST_COMPARE (FGETC (fp), EOF_VALUE);
> +  TEST_VERIFY (feof (fp));
> +  TEST_VERIFY (! ferror (fp));
> +
> +  TEST_COMPARE (fclose (fp), 0);
> +}
> +
> +static int
> +do_test (void)
> +{
> +  for (size_t i = 0; i < sizeof (tests) / sizeof (tests[0]); i++)

Use array_length(...) here.

> +    {
> +      printf ("info: case %zu\n", i);
> +      do_one_test (&tests[i]);
> +    }
> +
> +  return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/stdio-common/tst-wscanf-bz34208.c b/stdio-common/tst-wscanf-bz34208.c
> new file mode 100644
> index 0000000000..6d7a96c8aa
> --- /dev/null
> +++ b/stdio-common/tst-wscanf-bz34208.c
> @@ -0,0 +1,20 @@
> +/* Test wscanf pushback for incomplete nan/inf inputs (BZ #34208).
> +   Copyright (C) 2026 The GNU Toolchain Authors.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library 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
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +#define TEST_WCHAR
> +#include "tst-scanf-bz34208.c"
> diff --git a/stdio-common/vfscanf-internal.c b/stdio-common/vfscanf-internal.c
> index 9871cff6a4..5f548f709f 100644
> --- a/stdio-common/vfscanf-internal.c
> +++ b/stdio-common/vfscanf-internal.c
> @@ -2048,17 +2048,23 @@ digits_extended_fail:
>  	    {
>  	      /* Maybe "nan".  */
>  	      char_buffer_add (&charbuf, c);
> -	      if (__builtin_expect (width == 0
> -				    || inchar () == EOF
> -				    || TOLOWER (c) != L_('a'), 0))
> +	      if (__glibc_unlikely (width == 0 || inchar () == EOF))
>  		conv_error ();
> +	      if (__glibc_unlikely (TOLOWER (c) != L_('a')))
> +		{
> +		  ungetc (c, s);
> +		  conv_error ();
> +		}
>  	      if (width > 0)
>  		--width;
>  	      char_buffer_add (&charbuf, c);
> -	      if (__builtin_expect (width == 0
> -				    || inchar () == EOF
> -				    || TOLOWER (c) != L_('n'), 0))
> +	      if (__glibc_unlikely (width == 0 || inchar () == EOF))
>  		conv_error ();
> +	      if (__glibc_unlikely (TOLOWER (c) != L_('n')))
> +		{
> +		  ungetc (c, s);
> +		  conv_error ();
> +		}
>  	      if (width > 0)
>  		--width;
>  	      char_buffer_add (&charbuf, c);
> @@ -2092,6 +2098,7 @@ digits_extended_fail:
>  			    {
>  			      /* Invalid character was observed.  Only valid
>  				 characters are [a-zA-Z0-9_] and ')'.  */
> +			      ungetc (c, s);
>  			      conv_error ();
>  			      break;
>  			    }
> @@ -2113,17 +2120,23 @@ digits_extended_fail:
>  	    {
>  	      /* Maybe "inf" or "infinity".  */
>  	      char_buffer_add (&charbuf, c);
> -	      if (__builtin_expect (width == 0
> -				    || inchar () == EOF
> -				    || TOLOWER (c) != L_('n'), 0))
> +	      if (__glibc_unlikely (width == 0 || inchar () == EOF))
>  		conv_error ();
> +	      if (__glibc_unlikely (TOLOWER (c) != L_('n')))
> +		{
> +		  ungetc (c, s);
> +		  conv_error ();
> +		}
>  	      if (width > 0)
>  		--width;
>  	      char_buffer_add (&charbuf, c);
> -	      if (__builtin_expect (width == 0
> -				    || inchar () == EOF
> -				    || TOLOWER (c) != L_('f'), 0))
> +	      if (__glibc_unlikely (width == 0 || inchar () == EOF))
>  		conv_error ();
> +	      if (__glibc_unlikely (TOLOWER (c) != L_('f')))
> +		{
> +		  ungetc (c, s);
> +		  conv_error ();
> +		}
>  	      if (width > 0)
>  		--width;
>  	      char_buffer_add (&charbuf, c);
> @@ -2136,31 +2149,43 @@ digits_extended_fail:
>  			--width;
>  		      /* Now we have to read the rest as well.  */
>  		      char_buffer_add (&charbuf, c);
> -		      if (__builtin_expect (width == 0
> -					    || inchar () == EOF
> -					    || TOLOWER (c) != L_('n'), 0))
> +		      if (__glibc_unlikely (width == 0 || inchar () == EOF))
>  			conv_error ();
> +		      if (__glibc_unlikely (TOLOWER (c) != L_('n')))
> +			{
> +			  ungetc (c, s);
> +			  conv_error ();
> +			}
>  		      if (width > 0)
>  			--width;
>  		      char_buffer_add (&charbuf, c);
> -		      if (__builtin_expect (width == 0
> -					    || inchar () == EOF
> -					    || TOLOWER (c) != L_('i'), 0))
> +		      if (__glibc_unlikely (width == 0 || inchar () == EOF))
>  			conv_error ();
> +		      if (__glibc_unlikely (TOLOWER (c) != L_('i')))
> +			{
> +			  ungetc (c, s);
> +			  conv_error ();
> +			}
>  		      if (width > 0)
>  			--width;
>  		      char_buffer_add (&charbuf, c);
> -		      if (__builtin_expect (width == 0
> -					    || inchar () == EOF
> -					    || TOLOWER (c) != L_('t'), 0))
> +		      if (__glibc_unlikely (width == 0 || inchar () == EOF))
>  			conv_error ();
> +		      if (__glibc_unlikely (TOLOWER (c) != L_('t')))
> +			{
> +			  ungetc (c, s);
> +			  conv_error ();
> +			}
>  		      if (width > 0)
>  			--width;
>  		      char_buffer_add (&charbuf, c);
> -		      if (__builtin_expect (width == 0
> -					    || inchar () == EOF
> -					    || TOLOWER (c) != L_('y'), 0))
> +		      if (__glibc_unlikely (width == 0 || inchar () == EOF))
>  			conv_error ();
> +		      if (__glibc_unlikely (TOLOWER (c) != L_('y')))
> +			{
> +			  ungetc (c, s);
> +			  conv_error ();
> +			}
>  		      if (width > 0)
>  			--width;
>  		      char_buffer_add (&charbuf, c);



More information about the Libc-alpha mailing list