[PATCH v2] libio: Fix crash in fputws [BZ #20632]
Peter Ammon
corydoras@ridiculousfish.com
Mon Sep 30 00:14:30 GMT 2024
> On Sep 8, 2024, at 8:34 PM, Peter Ammon <corydoras@ridiculousfish.com> wrote:
>
> This fixes a buffer overflow in wide character string output, reproducing
> when output fails, such as if the output fd is closed or is redirected
> to a full device.
>
> Wide character output data attempts to maintain the invariant that
> `_IO_buf_base <= _IO_write_base <= _IO_write_end <= _IO_buf_end` (that is,
> that the write region is a sub-region of `_IO_buf`). Prior to this commit,
> this invariant is violated by the `_IO_wfile_overflow` function as so:
>
> 1. `_IO_wsetg` is called, assigning `_IO_write_base` to `_IO_buf_base`
> 2. `_IO_doallocbuf` is called, which jumps to `_IO_wfile_doallocate` via
> the _IO_wfile_jumps vtable. This function then assigns the wide data
> `_IO_buf_base` and `_IO_buf_end` to a malloc'd buffer.
>
> Thus the invariant is violated. The fix is simply to reverse the order:
> malloc the `_IO_buf` first and then assign `_IO_write_base` to it.
>
> We also take this opportunity to defensively guard the initialization of
> the number of unwritten characters via pointer arithmetic. We now check
> that the buffer end is not before the buffer beginning; this matches a
> similar defensive check in the narrow analogue `fileops.c`.
>
> Add a test which fails without the fix.
>
> Signed-off-by: Peter Ammon <corydoras@ridiculousfish.com>
> ---
> libio/Makefile | 2 +
> libio/test-fputs-unbuffered-full.c | 78 +++++++++++++++++++++++++++++
> libio/test-fputws-unbuffered-full.c | 21 ++++++++
> libio/wfileops.c | 10 ++--
> 4 files changed, 107 insertions(+), 4 deletions(-)
> create mode 100644 libio/test-fputs-unbuffered-full.c
> create mode 100644 libio/test-fputws-unbuffered-full.c
>
> diff --git a/libio/Makefile b/libio/Makefile
> index 59f3ee0b7c..f8adfb8951 100644
> --- a/libio/Makefile
> +++ b/libio/Makefile
> @@ -86,6 +86,8 @@ tests = \
> bug-wmemstream1 \
> bug-wsetpos \
> test-fmemopen \
> + test-fputs-unbuffered-full \
> + test-fputws-unbuffered-full \
> tst-atime \
> tst-bz22415 \
> tst-bz24051 \
> diff --git a/libio/test-fputs-unbuffered-full.c b/libio/test-fputs-unbuffered-full.c
> new file mode 100644
> index 0000000000..8fb762f701
> --- /dev/null
> +++ b/libio/test-fputs-unbuffered-full.c
> @@ -0,0 +1,78 @@
> +/* Regression test for 20632.
> + Copyright (C) 2024 Free Software Foundation, Inc.
> + Copyright The GNU Toolchain Authors.
> + This file is part of the GNU C Library.
> +
> + The GNU C Library is free software; you can redistribute it and/or
> + modify it under the terms of the GNU Lesser General Public
> + License as published by the Free Software Foundation; either
> + version 2.1 of the License, or (at your option) any later version.
> +
> + The GNU C Library is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + Lesser General Public License for more details.
> +
> + You should have received a copy of the GNU Lesser General Public
> + License along with the GNU C Library; if not, see
> + <https://www.gnu.org/licenses/>. */
> +
> +#include <errno.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <support/check.h>
> +#include <support/xunistd.h>
> +#include <unistd.h>
> +
> +#ifndef WIDE
> +# define TEST_NAME "fputs-unbuffered-full"
> +# define CHAR char
> +# define FPUTS fputs
> +# define TEXT "0123456789ABCDEF"
> +#else
> +# include <wchar.h>
> +# define TEST_NAME "fputws-unbuffered-full"
> +# define CHAR wchar_t
> +# define FPUTS fputws
> +# define TEXT L"0123456789ABCDEF"
> +#endif /* WIDE */
> +
> +
> +static int
> +do_test (void)
> +{
> + /* Open an unbuffered stream to /dev/full. */
> + FILE *fp = fopen ("/dev/full", "w");
> + TEST_VERIFY_EXIT (fp != NULL);
> + int ret = setvbuf (fp, NULL, _IONBF, 0);
> + TEST_VERIFY_EXIT (ret == 0);
> +
> + /* Output a long string. */
> + const int sz = 4096;
> + CHAR *buff = calloc (sz+1, sizeof *buff);
> + for (int i=0; i < sz; i++)
> + buff[i] = (CHAR) 'x';
> + buff[sz] = (CHAR) '\0';
> + errno = 0;
> + ret = FPUTS (buff, fp);
> + TEST_VERIFY (ret == EOF);
> + TEST_VERIFY (errno == ENOSPC);
> + free (buff);
> +
> + /* Output shorter strings. */
> + for (int i=0; i < 1024; i++)
> + {
> + errno = 0;
> + ret = FPUTS (TEXT, fp);
> + TEST_VERIFY (ret == EOF);
> + TEST_VERIFY (errno == ENOSPC);
> +
> + /* Call malloc, triggering a crash if its
> + function pointers have been overwritten. */
> + void *volatile ptr = malloc (1);
> + free (ptr);
> + }
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/libio/test-fputws-unbuffered-full.c b/libio/test-fputws-unbuffered-full.c
> new file mode 100644
> index 0000000000..f3d79326e4
> --- /dev/null
> +++ b/libio/test-fputws-unbuffered-full.c
> @@ -0,0 +1,21 @@
> +/* Regression test for 20632.
> + Copyright (C) 2024 Free Software Foundation, Inc.
> + Copyright The GNU Toolchain Authors.
> + This file is part of the GNU C Library.
> +
> + The GNU C Library is free software; you can redistribute it and/or
> + modify it under the terms of the GNU Lesser General Public
> + License as published by the Free Software Foundation; either
> + version 2.1 of the License, or (at your option) any later version.
> +
> + The GNU C Library is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + Lesser General Public License for more details.
> +
> + You should have received a copy of the GNU Lesser General Public
> + License along with the GNU C Library; if not, see
> + <https://www.gnu.org/licenses/>. */
> +
> +#define WIDE 1
> +#include "./test-fputs-unbuffered-full.c"
> diff --git a/libio/wfileops.c b/libio/wfileops.c
> index 6de5968358..8a1912cc0e 100644
> --- a/libio/wfileops.c
> +++ b/libio/wfileops.c
> @@ -420,14 +420,14 @@ _IO_wfile_overflow (FILE *f, wint_t wch)
> {
> _IO_wdoallocbuf (f);
> _IO_free_wbackup_area (f);
> - _IO_wsetg (f, f->_wide_data->_IO_buf_base,
> - f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);
>
> if (f->_IO_write_base == NULL)
> {
> _IO_doallocbuf (f);
> _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
> }
> + _IO_wsetg (f, f->_wide_data->_IO_buf_base,
> + f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);
> }
> else
> {
> @@ -958,7 +958,7 @@ _IO_wfile_xsputn (FILE *f, const void *data, size_t n)
> const wchar_t *s = (const wchar_t *) data;
> size_t to_do = n;
> int must_flush = 0;
> - size_t count;
> + size_t count = 0;
>
> if (n <= 0)
> return 0;
> @@ -967,7 +967,6 @@ _IO_wfile_xsputn (FILE *f, const void *data, size_t n)
> (or the filebuf is unbuffered), use sys_write directly. */
>
> /* First figure out how much space is available in the buffer. */
> - count = f->_wide_data->_IO_write_end - f->_wide_data->_IO_write_ptr;
> if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
> {
> count = f->_wide_data->_IO_buf_end - f->_wide_data->_IO_write_ptr;
> @@ -985,6 +984,9 @@ _IO_wfile_xsputn (FILE *f, const void *data, size_t n)
> }
> }
> }
> + else if (f->_wide_data->_IO_write_end > f->_wide_data->_IO_write_ptr)
> + count = f->_wide_data->_IO_write_end - f->_wide_data->_IO_write_ptr; /* Space available. */
> +
> /* Then fill the buffer. */
> if (count > 0)
> {
> --
> 2.46.0
Ping, thank you!
More information about the Libc-alpha
mailing list