[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