[PATCH v2] stdio-common: Fix scanf nan/inf pushback [BZ #34208]
Gao Xiang
gaoxiang@kylinos.cn
Thu Jun 25 08:29:57 GMT 2026
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>
---
Changes in v2:
- Removed _GNU_SOURCE. It was left over from an early debugging version
which is not needed.
- Simplified the typed test data and iteration code as suggested.
- Sorted the includes order alphabetically.
- Retested the cases listed above.
stdio-common/Makefile | 2 +
stdio-common/tst-scanf-bz34208.c | 103 ++++++++++++++++++++++++++++++
stdio-common/tst-wscanf-bz34208.c | 20 ++++++
stdio-common/vfscanf-internal.c | 73 ++++++++++++++-------
4 files changed, 174 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 \
diff --git a/stdio-common/tst-scanf-bz34208.c b/stdio-common/tst-scanf-bz34208.c
new file mode 100644
index 0000000000..f8232dacc8
--- /dev/null
+++ b/stdio-common/tst-scanf-bz34208.c
@@ -0,0 +1,103 @@
+/* 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/>. */
+
+#include <array_length.h>
+#include <stdio.h>
+#include <support/check.h>
+#include <wchar.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 L_(Str) L ## Str
+#else
+# define CHAR_T char
+# define WINT_T int
+# define FSCANF fscanf
+# define FPUTC fputc
+# define FGETC fgetc
+# define EOF_VALUE EOF
+# define L_(Str) Str
+#endif
+
+#define SCAN_FORMAT L_("%e")
+
+static const float sentinel = -123.0f;
+
+struct test
+{
+ const CHAR_T *input;
+ long int expected_offset;
+ const CHAR_T *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. */
+ { L_("+NA[..z"), 3, L_("[..z") },
+
+ /* Mismatch while matching "nan(...)", "inf" and "infinity" must be
+ pushed back. */
+ { L_("nan(@X"), 4, L_("@X") },
+ { L_("iX"), 1, L_("X") },
+ { L_("infiX"), 4, L_("X") },
+ };
+
+static void
+do_one_test (const struct test *test)
+{
+ FILE *fp = tmpfile ();
+ TEST_VERIFY_EXIT (fp != NULL);
+
+ for (const CHAR_T *p = test->input; *p != '\0'; p++)
+ TEST_COMPARE (FPUTC (*p, fp), (WINT_T) *p);
+
+ 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_T *p = test->expected_rest; *p != '\0'; p++)
+ TEST_COMPARE (FGETC (fp), (WINT_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 < array_length (tests); i++)
+ {
+ 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);
--
2.53.0
More information about the Libc-alpha
mailing list