[glibc] libio: Fix wide stream backup buffer leak on fclose [BZ #33999]

Adhemerval Zanella via Glibc-cvs <[email protected]>
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=cd1b1c1035171c9281922acc9670a7a50415f71e

commit cd1b1c1035171c9281922acc9670a7a50415f71e
Author: Xiang Gao <[email protected]>
Date:   Thu Apr 2 14:25:44 2026 +0800

    libio: Fix wide stream backup buffer leak on fclose [BZ #33999]
    
    This patch fixes a memory leak when ungetwc is used on a wide oriented stream.
    The backup buffer was never freed on fclose, causing a memory leak per
    ungetwc/fclose call.
    
    The leak has two causes:
    
    In iofclose.c, for wide streams (fp->mode > 0), _IO_new_fclose never calls
    _IO_free_wbackup_area. Fixed by adding the missing call.
    
    In wgenops.c, _IO_wdefault_finish checks fp->_IO_save_base (the narrow field,
    always NULL for wide streams) instead of fp->_wide_data->_IO_save_base,
    and uses a bare free() that leaves _IO_save_end and _IO_backup_base dangling.
    Replace the hand-rolled cleanup with _IO_have_wbackup/_IO_free_wbackup_area,
    which handles backup-mode switching and clears all three pointers.
    
    This was independently reported by Rocket Ma [1], whose patch corrects the condition
    but still uses the manual free path.
    
    Apply the same _IO_have_backup condition in genops.c for consistency.
    
    Tested by:
    make test t=libio/tst-wbackup-leak
    
    [1] https://patchwork.sourceware.org/project/glibc/patch/[email protected]/
    
    Signed-off-by: Xiang Gao <[email protected]>

Diff:
---
 libio/Makefile           |  1 +
 libio/genops.c           |  7 ++-----
 libio/iofclose.c         |  2 ++
 libio/tst-wbackup-leak.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++++
 libio/wgenops.c          |  7 ++-----
 5 files changed, 58 insertions(+), 10 deletions(-)

diff --git a/libio/Makefile b/libio/Makefile
index 08e1e0ec25..93656466df 100644
--- a/libio/Makefile
+++ b/libio/Makefile
@@ -135,6 +135,7 @@ tests = \
   tst-swscanf \
   tst-ungetwc1 \
   tst-ungetwc2 \
+  tst-wbackup-leak \
   tst-wfile-sync \
   tst-wfiledoallocate-static \
   tst-widetext \
diff --git a/libio/genops.c b/libio/genops.c
index cc1684e00a..90e08e6571 100644
--- a/libio/genops.c
+++ b/libio/genops.c
@@ -636,11 +636,8 @@ _IO_default_finish (FILE *fp, int dummy)
   for (mark = fp->_markers; mark != NULL; mark = mark->_next)
     mark->_sbuf = NULL;
 
-  if (fp->_IO_save_base)
-    {
-      _IO_free_backup_buf (fp, fp->_IO_save_base);
-      fp->_IO_save_base = NULL;
-    }
+  if (_IO_have_backup (fp))
+    _IO_free_backup_area (fp);
 
   _IO_un_link ((struct _IO_FILE_plus *) fp);
 
diff --git a/libio/iofclose.c b/libio/iofclose.c
index 89782e99d7..0ba85955bf 100644
--- a/libio/iofclose.c
+++ b/libio/iofclose.c
@@ -67,6 +67,8 @@ _IO_new_fclose (FILE *fp)
   _IO_FINISH (fp);
   if (fp->_mode > 0)
     {
+      if (_IO_have_wbackup (fp))
+	_IO_free_wbackup_area (fp);
       /* This stream has a wide orientation.  This means we have to free
 	 the conversion functions.  */
       struct _IO_codecvt *cc = fp->_codecvt;
diff --git a/libio/tst-wbackup-leak.c b/libio/tst-wbackup-leak.c
new file mode 100644
index 0000000000..0b28f5df37
--- /dev/null
+++ b/libio/tst-wbackup-leak.c
@@ -0,0 +1,51 @@
+/* Test _IO_wdefault_finish frees wide backup buffer [BZ #33999].  */
+
+#include <malloc.h>
+#include <stdio.h>
+#include <wchar.h>
+#include <support/check.h>
+
+static void
+one_round (void)
+{
+  wchar_t *buf = NULL;
+  size_t size = 0;
+
+  FILE *fp = open_wmemstream (&buf, &size);
+  TEST_VERIFY_EXIT (fp != NULL);
+  fputwc (L'A', fp);
+  fflush (fp);
+  /* Push back without prior read. read_ptr == read_base, so
+     _IO_wdefault_pbackfail skips the buggy narrow read_ptr access
+     (BZ #33998) and goes straight to allocating a wide backup
+     buffer at fp->_wide_data->_IO_save_base.
+
+     Note: this testcase relies on the fact that open_wmemstream
+     does not set _IO_NO_READS on the stream. If that implementation
+     is changed, this test would need a different stream type to verify
+     the leak.  */
+  rewind (fp);
+  ungetwc (L'Z', fp);
+  fclose (fp);
+  free (buf);
+}
+
+static int
+do_test (void)
+{
+  /* Warm up to stabilize allocator state.  */
+  one_round ();
+
+  struct mallinfo2 before = mallinfo2 ();
+  for (int i = 0; i < 1000; i++)
+    one_round ();
+  struct mallinfo2 after = mallinfo2 ();
+
+  /* Each leak is 128 * sizeof(wchar_t) = 512 bytes.
+   * 1000 iterations would leak ~512 KB. Allow 4 KB noise. */
+  TEST_VERIFY (after.uordblks - before.uordblks < 4096);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/libio/wgenops.c b/libio/wgenops.c
index 064d71266d..6829477e0c 100644
--- a/libio/wgenops.c
+++ b/libio/wgenops.c
@@ -181,11 +181,8 @@ _IO_wdefault_finish (FILE *fp, int dummy)
   for (mark = fp->_markers; mark != NULL; mark = mark->_next)
     mark->_sbuf = NULL;
 
-  if (fp->_IO_save_base)
-    {
-      free (fp->_wide_data->_IO_save_base);
-      fp->_IO_save_base = NULL;
-    }
+  if (_IO_have_wbackup (fp))
+    _IO_free_wbackup_area (fp);
 
 #ifdef _IO_MTSAFE_IO
   if (fp->_lock != NULL)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.