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

Gao Xiang gaoxiang@kylinos.cn
Sun Jun 21 07:41:47 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>
---
 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 \
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
+
+#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);
+
+  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++)
+    {
+      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