[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