[PATCH v3] libio: Fix CVE-2026-18374 heap buffer overflow in ccs= handling

손동균/Process & Infra Lab(SR)/삼성전자 <[email protected]>
Newsgroups gmane.comp.lib.glibc.alpha
Message-ID <[email protected]>
When fopen is called with a ,ccs= specification whose value is not empty
but becomes empty after strip() (for example "r,ccs=/,..."), the code took
the fallback branch

    __wcsmbs_named_conv (&fcts, ccs[2] == '\0' ? upstr (ccs, cs + 5) : ccs)

and upstr() copied from cs + 5 up to the terminating NUL of the whole mode
string -- past the ',' delimiter at endp -- into ccs, which is only
allocated for endp - (cs + 5) + 3 bytes.  This overflows the heap buffer.

Checking for an empty ccs= token before strip() (as an earlier attempt did)
does not help, because in the reproducer the token is only empty after
strip(), not before.

Bound the fallback copy to the [cs + 5, endp) charset token so it can no
longer read past the delimiter.  A specification that is empty after
strip() has no valid charset name and keeps failing with EINVAL from
__wcsmbs_named_conv.

A regression test that reproduces the overflow (and the trivial empty
,ccs= case) is added to libio/tst-fopenloc.c.

CVE-2026-18374 - CVSS 4.9 (AV:L/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L)
Reported-by: AISLE in partnership with Red Hat
Signed-off-by: Dongkyun Son <[email protected]>
---
Changes since v2 (thanks to Florian Weimer for the review):
 - v2 was wrong: it rejected a ccs= token that was empty *before*
   strip().  The actual overflow happens when the token is non-empty
   but becomes empty *after* strip(), which takes the
   upstr (ccs, cs + 5) fallback and copies past the ',' delimiter.
   v3 bounds that fallback to the [cs + 5, endp) token instead.

 libio/fileops.c      | 15 ++++++++++++--
 libio/tst-fopenloc.c | 49 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 2 deletions(-)

diff --git a/libio/fileops.c b/libio/fileops.c
index 9348d7c3a1..f42588a94f 100644
--- a/libio/fileops.c
+++ b/libio/fileops.c
@@ -355,8 +355,19 @@ _IO_new_file_fopen (FILE *fp, const char *filename, const char *mode,
 	  *((char *) __mempcpy (ccs, cs + 5, endp - (cs + 5))) = '\0';
 	  strip (ccs, ccs);
 
-	  if (__wcsmbs_named_conv (&fcts, ccs[2] == '\0'
-				   ? upstr (ccs, cs + 5) : ccs) != 0)
+	  /* If strip() reduced the specification to the empty string (leaving
+	     only the "//" that strip() always appends), fall back to the
+	     original charset name.  Bound the copy by ENDP so we do not read
+	     past the ',' delimiter and overflow CCS (CVE-2026-18374).  */
+	  if (ccs[2] == '\0')
+	    {
+	      char *wp = ccs;
+	      for (const char *rp = cs + 5; rp < endp; ++rp)
+		*wp++ = __toupper_l (*rp, _nl_C_locobj_ptr);
+	      *wp = '\0';
+	    }
+
+	  if (__wcsmbs_named_conv (&fcts, ccs) != 0)
 	    {
 	      /* Something went wrong, we cannot load the conversion modules.
 		 This means we cannot proceed since the user explicitly asked
diff --git a/libio/tst-fopenloc.c b/libio/tst-fopenloc.c
index ea3f7b5265..5ce3bc5abb 100644
--- a/libio/tst-fopenloc.c
+++ b/libio/tst-fopenloc.c
@@ -85,6 +85,54 @@ do_bz18906 (void)
   return EXIT_SUCCESS;
 }
 
+static int
+do_cve_2026_18374 (void)
+{
+  /* CVE-2026-18374 -- a ,ccs= specification that is not empty but becomes
+     empty after strip() must not make fopen read past the ',' delimiter and
+     overflow the heap buffer.  It has to fail cleanly with EINVAL.  */
+
+  const size_t sz = 8192;
+  char *ccs = xmalloc (sz);
+  strcpy (ccs, "r,ccs=/,");
+  memset (ccs + 8, 'A', sz - 8 - 1);
+  ccs[sz - 1] = '\0';
+
+  errno = 0;
+  FILE *fp = fopen (inputfile, ccs);
+  if (fp != NULL)
+    {
+      printf ("fopen with empty-after-strip ccs= unexpectedly succeeded\n");
+      free (ccs);
+      fclose (fp);
+      return 1;
+    }
+  if (errno != EINVAL)
+    {
+      printf ("expected EINVAL, got %d\n", errno);
+      free (ccs);
+      return 1;
+    }
+  free (ccs);
+
+  /* Also check the trivially empty ,ccs= case.  */
+  errno = 0;
+  fp = fopen (inputfile, "r,ccs=");
+  if (fp != NULL)
+    {
+      printf ("fopen with empty ccs= unexpectedly succeeded\n");
+      fclose (fp);
+      return 1;
+    }
+  if (errno != EINVAL)
+    {
+      printf ("expected EINVAL, got %d\n", errno);
+      return 1;
+    }
+
+  return 0;
+}
+
 static int
 do_test (void)
 {
@@ -110,6 +158,7 @@ do_test (void)
 
   TEST_COMPARE (do_bz17916 (), 0);
   TEST_COMPARE (do_bz18906 (), 0);
+  TEST_COMPARE (do_cve_2026_18374 (), 0);
 
   return EXIT_SUCCESS;
 }
-- 
2.43.0
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.