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

Adhemerval Zanella via Glibc-cvs <[email protected]> Thu, 25 Jun 2026 14:14:46 +0000 (GMT)
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=21103aef1263beae17dea86153d594a428ed6a7f

commit 21103aef1263beae17dea86153d594a428ed6a7f
Author: Xiang Gao <[email protected]>
Date:   Thu Jun 25 16:29:57 2026 +0800

    stdio-common: Fix scanf nan/inf pushback [BZ #34208]
    
    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 <[email protected]>
    Reviewed-by: Adhemerval Zanella  <[email protected]>

Diff:
---
 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(-)

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);