[v4] libio: Fix CVE-2026-18374 heap buffer overflow in ccs= handling
Checks
Commit Message
When fopen() is called with a ,ccs= parameter whose value becomes empty
after strip(), the code must reject it with EINVAL instead of attempting
to use it. The original upstr() fallback could read past the ',' delimiter
and cause a heap buffer overflow.
The fix checks if the charset specification is empty after strip() and
returns EINVAL immediately, preventing the overflow and following the
approach described in BZ #34574.
A regression test is added using /dev/null with both trivially empty
(,ccs=) and effectively-empty-after-strip (,ccs=/) cases, verifying that
EINVAL is returned.
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>
---
libio/fileops.c | 14 ++++++++++++--
libio/tst-fopenloc.c | 39 +++++++++++++++++++++++++++++++++++++++
2 files changed, 51 insertions(+), 2 deletions(-)
Comments
* 손동균:
> When fopen() is called with a ,ccs= parameter whose value becomes empty
> after strip(), the code must reject it with EINVAL instead of attempting
> to use it. The original upstr() fallback could read past the ',' delimiter
> and cause a heap buffer overflow.
>
> The fix checks if the charset specification is empty after strip() and
> returns EINVAL immediately, preventing the overflow and following the
> approach described in BZ #34574.
>
> A regression test is added using /dev/null with both trivially empty
> (,ccs=) and effectively-empty-after-strip (,ccs=/) cases, verifying that
> EINVAL is returned.
>
> 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>
> ---
> libio/fileops.c | 14 ++++++++++++--
> libio/tst-fopenloc.c | 39 +++++++++++++++++++++++++++++++++++++++
> 2 files changed, 51 insertions(+), 2 deletions(-)
>
> diff --git a/libio/fileops.c b/libio/fileops.c
> index 9348d7c3a1..80ea5e90ad 100644
> --- a/libio/fileops.c
> +++ b/libio/fileops.c
> @@ -355,8 +355,18 @@ _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)
> + /* After stripping, ccs[2] == '\0' means the charset name is empty.
> + This is not a valid charset and would cause problems downstream.
> + Reject it with EINVAL (BZ #34574, CVE-2026-18374). */
> + if (ccs[2] == '\0')
> + {
> + (void) _IO_file_close_it (fp);
> + free (ccs);
> + __set_errno (EINVAL);
> + return NULL;
> + }
> +
> + 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
Please adjust the condition of the following if statement, consdering
that the statement bodies are the same.
If your policies do not allow you to include a proper test case, please
drop the test and I'll submit a test separately.
Thanks,
Florian
@@ -355,8 +355,18 @@ _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)
+ /* After stripping, ccs[2] == '\0' means the charset name is empty.
+ This is not a valid charset and would cause problems downstream.
+ Reject it with EINVAL (BZ #34574, CVE-2026-18374). */
+ if (ccs[2] == '\0')
+ {
+ (void) _IO_file_close_it (fp);
+ free (ccs);
+ __set_errno (EINVAL);
+ return NULL;
+ }
+
+ 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,44 @@ do_bz18906 (void)
return EXIT_SUCCESS;
}
+static int
+do_cve_2026_18374 (void)
+{
+ /* CVE-2026-18374 (BZ #34574): Test that fopen() rejects an effectively
+ empty ccs= specification. The ,ccs= parameter that is not empty before
+ strip() but becomes empty after strip() should fail with EINVAL. */
+
+ FILE *fp = fopen ("/dev/null", "r,ccs=/");
+ if (fp != NULL)
+ {
+ printf ("fopen with empty-after-strip ccs= should have failed\n");
+ fclose (fp);
+ return 1;
+ }
+ if (errno != EINVAL)
+ {
+ printf ("expected EINVAL, got %d\n", errno);
+ return 1;
+ }
+
+ /* Also check the trivially empty ,ccs= case. */
+ errno = 0;
+ fp = fopen ("/dev/null", "r,ccs=");
+ if (fp != NULL)
+ {
+ printf ("fopen with empty ccs= should have failed\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 +148,7 @@ do_test (void)
TEST_COMPARE (do_bz17916 (), 0);
TEST_COMPARE (do_bz18906 (), 0);
+ TEST_COMPARE (do_cve_2026_18374 (), 0);
return EXIT_SUCCESS;
}