[v4] libio: Fix ungetwc operating on byte stream [BZ #33998]

Message ID 20260418101742.3355742-1-marocketbd@gmail.com (mailing list archive)
State Superseded
Headers
Series [v4] libio: Fix ungetwc operating on byte stream [BZ #33998] |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 success Build passed
redhat-pt-bot/TryBot-32bit success Build for i686
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 success Test passed
linaro-tcwg-bot/tcwg_glibc_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_glibc_check--master-arm success Test passed

Commit Message

Rocket Ma April 18, 2026, 10:17 a.m. UTC
  * libio/wgenops.c: When _IO_wdefault_pbackfail attempts to push back one
character, it accidently compare the wchar to push back with the last
char from byte stream, instead of wide stream. Under specific coding,
attacker may exploit this to leak information. This commit fix bug
33998, or CVE-2026-5928.

Signed-off-by: Rocket Ma <marocketbd@gmail.com>
---
Removed redundant macro from previous patch.
---
 libio/Makefile              |  1 +
 libio/bug-wgenops-bz33998.c | 44 +++++++++++++++++++++++++++++++++++++
 libio/wgenops.c             |  4 ++--
 3 files changed, 47 insertions(+), 2 deletions(-)
 create mode 100644 libio/bug-wgenops-bz33998.c
  

Comments

Florian Weimer April 30, 2026, 4:11 p.m. UTC | #1
* Rocket Ma:

> diff --git a/libio/bug-wgenops-bz33998.c b/libio/bug-wgenops-bz33998.c
> new file mode 100644
> index 0000000000..b3f750a753
> --- /dev/null
> +++ b/libio/bug-wgenops-bz33998.c
> @@ -0,0 +1,44 @@
> +/* Regression test for ungetwc operating on byte stream (BZ #33998)
> +   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 <unistd.h>
> +#include <sys/mman.h>
> +#include <stdio.h>
> +#include <wchar.h>
> +#include <support/check.h>
> +
> +static int
> +do_test (void)
> +{
> +  int fd = memfd_create ("test", MFD_CLOEXEC);
> +  TEST_VERIFY (fd != -1);

This may fail on older kernels, so please use create_temp_file from
<support/temp_file.h>.

> +  TEST_COMPARE (write (fd, (unsigned char[]){ 'A', 0, 0, 0 }, 4), 4);
> +  TEST_COMPARE (lseek (fd, 0, SEEK_SET), 0);
> +  FILE *fp = fdopen (fd, "r+");

You could use xwrite, xlseek, xfopen.

> +  TEST_VERIFY (fp != NULL);
> +  TEST_COMPARE (getwc (fp), L'A');
> +
> +  /* if the bug is fixed, then ungetwc should not touch byte stream. */
> +  char *old_read_ptr = fp->_IO_read_ptr;
> +  TEST_COMPARE (ungetwc (0, fp), L'\0');

Can you please use 0 or L'\0' in both places?

> +  TEST_VERIFY (fp->_IO_read_ptr == old_read_ptr);

You could check that the null character can be read back with fgetwc.
And call xfclose at the end.

> diff --git a/libio/wgenops.c b/libio/wgenops.c
> index 6829477e0c..5f36bc49a1 100644
> --- a/libio/wgenops.c
> +++ b/libio/wgenops.c
> @@ -110,8 +110,8 @@ _IO_wdefault_pbackfail (FILE *fp, wint_t c)
>  {
>    if (fp->_wide_data->_IO_read_ptr > fp->_wide_data->_IO_read_base
>        && !_IO_in_backup (fp)
> -      && (wint_t) fp->_IO_read_ptr[-1] == c)
> -    --fp->_IO_read_ptr;
> +      && (wint_t) fp->_wide_data->_IO_read_ptr[-1] == c)
> +    --fp->_wide_data->_IO_read_ptr;
>    else
>      {
>        /* Need to handle a filebuf in write mode (switch to read mode). FIXME!*/

The fix itself looks good to me.

Thanks,
Florian
  
Rocket Ma May 1, 2026, 5:24 p.m. UTC | #2
> > +  TEST_VERIFY (fp->_IO_read_ptr == old_read_ptr);
>
> You could check that the null character can be read back with fgetwc.
> And call xfclose at the end.

In my regression test, the buffer in FILE (byte stream) is "A\0\0\0",
and the buffer in wide FILE (wide stream) is L"A\0\0\0\0", in this
case we can reproduce the error easily, instead of crafting a
complicated test case.[1] In this case, read_ptr of wide stream is 1
out of 4 (A | \0\0\0), leading to next fgetwc returns L'\0' (no new
buffer allocated, read_ptr in byte stream is decreased by 1, no change
on read_ptr in wide stream). If the fix is applied, read_ptr will be
set to the buffer allocated by pbackfail, so L'\0' is returned. The
value returned by fgetwc is always L'\0', so we can not distinguish if
the fix is applied.

[1]: If the buffer in byte stream is "A" instead, then read_ptr[-1] is
'A', like wide stream. Then we can not verify if the bug exists any
more as the bug code path is not entered. Or we set up a locale and
find a character that could be verified if the bug still exists, but
that's a bit hard.

So I still think verifying `fp->_IO_read_ptr == old_read_ptr` is suitable.

Rocket
  

Patch

diff --git a/libio/Makefile b/libio/Makefile
index 93656466df..6e0627bb88 100644
--- a/libio/Makefile
+++ b/libio/Makefile
@@ -84,6 +84,7 @@  tests = \
   bug-ungetwc1 \
   bug-ungetwc2 \
   bug-wfflush \
+  bug-wgenops-bz33998 \
   bug-wmemstream1 \
   bug-wsetpos \
   test-fmemopen \
diff --git a/libio/bug-wgenops-bz33998.c b/libio/bug-wgenops-bz33998.c
new file mode 100644
index 0000000000..b3f750a753
--- /dev/null
+++ b/libio/bug-wgenops-bz33998.c
@@ -0,0 +1,44 @@ 
+/* Regression test for ungetwc operating on byte stream (BZ #33998)
+   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 <unistd.h>
+#include <sys/mman.h>
+#include <stdio.h>
+#include <wchar.h>
+#include <support/check.h>
+
+static int
+do_test (void)
+{
+  int fd = memfd_create ("test", MFD_CLOEXEC);
+  TEST_VERIFY (fd != -1);
+  TEST_COMPARE (write (fd, (unsigned char[]){ 'A', 0, 0, 0 }, 4), 4);
+  TEST_COMPARE (lseek (fd, 0, SEEK_SET), 0);
+  FILE *fp = fdopen (fd, "r+");
+  TEST_VERIFY (fp != NULL);
+  TEST_COMPARE (getwc (fp), L'A');
+
+  /* if the bug is fixed, then ungetwc should not touch byte stream. */
+  char *old_read_ptr = fp->_IO_read_ptr;
+  TEST_COMPARE (ungetwc (0, fp), L'\0');
+  TEST_VERIFY (fp->_IO_read_ptr == old_read_ptr);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/libio/wgenops.c b/libio/wgenops.c
index 6829477e0c..5f36bc49a1 100644
--- a/libio/wgenops.c
+++ b/libio/wgenops.c
@@ -110,8 +110,8 @@  _IO_wdefault_pbackfail (FILE *fp, wint_t c)
 {
   if (fp->_wide_data->_IO_read_ptr > fp->_wide_data->_IO_read_base
       && !_IO_in_backup (fp)
-      && (wint_t) fp->_IO_read_ptr[-1] == c)
-    --fp->_IO_read_ptr;
+      && (wint_t) fp->_wide_data->_IO_read_ptr[-1] == c)
+    --fp->_wide_data->_IO_read_ptr;
   else
     {
       /* Need to handle a filebuf in write mode (switch to read mode). FIXME!*/