[v3] libio: Fix CVE-2026-18374 heap buffer overflow in ccs= handling
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-arm |
success
|
Build passed
|
| 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_check--master-arm |
success
|
Test passed
|
Commit Message
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 <dongkyun.s@samsung.com>
---
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(-)
Comments
* 손동균:
> 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 <dongkyun.s@samsung.com>
Please add a reference to bug 34574 to the commit message.
> 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';
> + }
I think the idea to return EINVAL in case the ccs= argument is
(effectively) empty was the right approach. No need to convert the
widespace to upper case.
> 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. */
I would recommend adding a new test, adapted from the reproducer on the
bug. It can open /dev/null for reading, and it should expect the EINVAL
failure.
If you don't want to write an actual heap overflow, I can add the new
test in a separate commit.
Thanks,
Florian
@@ -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
@@ -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;
}