[PATCH v2 1/1] libstdc++: Fix SIGSEGV in std::print with a setvbuf'd FILE
Tomasz Kaminski
tkaminsk@redhat.com
Mon Aug 17 06:52:39 GMT 2026
On Sun, Aug 16, 2026 at 6:04 AM Anlai Lu <agicy@qq.com> wrote:
> _File_sink::_M_write_buf() returns a span with a null data pointer when
> the FILE buffer was pre-allocated by setvbuf but never written (glibc
> keeps _IO_write_ptr as nullptr until the first write). The empty()
> check in the constructor then fails to force buffer initialization and
> the format engine memcpys into nullptr.
>
> Return an empty span when the write pointer is null.
>
> libstdc++-v3/ChangeLog:
>
> * include/bits/print.h (_File_sink::_File::_M_write_buf): Return
> an empty span when _IO_write_ptr is null.
> * testsuite/27_io/print/1.cc: Add test_print_setvbuf.
>
> Signed-off-by: Anlai Lu <agicy@qq.com>
> ---
> Changes in v2: removed [[unlikely]]; VERIFY the setvbuf return value in
> the test.
>
> libstdc++-v3/include/bits/print.h | 5 +++++
> libstdc++-v3/testsuite/27_io/print/1.cc | 17 +++++++++++++++++
> 2 files changed, 22 insertions(+)
>
> diff --git a/libstdc++-v3/include/bits/print.h
> b/libstdc++-v3/include/bits/print.h
> index 20724844726..7e023c79c55 100644
> --- a/libstdc++-v3/include/bits/print.h
> +++ b/libstdc++-v3/include/bits/print.h
> @@ -95,9 +95,14 @@ namespace __format
> _File(_File&&) = delete;
>
> // A span viewing the unused portion of the stream's output buffer.
> + // Empty when the buffer is not yet initialized: after setvbuf
> + // glibc pre-allocates the buffer but keeps _IO_write_ptr as
> + // nullptr until the first write.
> std::span<char>
> _M_write_buf() noexcept
>
This function is called in the constructor, and then only after flush, but
the above
check is only needed for the constructor case.
I would preffer if we extract the overflow call, to separate
_M_init_write_buf function,
something like:
std::span<char>
_M_init_write_but()
{
// After setvbuf glibc pre-allocates the buffer but _IO_write_ptr
// remains null until the first write.
if (!_M_file->_IO_write_ptr || _M_write_buf().empty())
if (::__overflow(__f, EOF) == EOF)
{
const int __err = errno;
__throw_system_error(__err);
}
return _M_write_buf();
}
And then call int in File_sinki cosntructor:
if (!_M_file._M_unbuffered())
// Allocate FILE's output buffer if needed, and write directly to
it.
this->_M_reset(_M_file._M_init_write_buf());
This has another benefits:
* it clearly calls overflow only for buffered streams output, and
* we do not need to call funlock file, as File was already constructed, and
destructort will call it.
> {
> + if (_M_file->_IO_write_ptr == nullptr)
> + return {};
> return {_M_file->_IO_write_ptr,
> size_t(_M_file->_IO_buf_end - _M_file->_IO_write_ptr)};
> }
> diff --git a/libstdc++-v3/testsuite/27_io/print/1.cc
> b/libstdc++-v3/testsuite/27_io/print/1.cc
> index 4fd7f5dc925..641a97c1f9c 100644
> --- a/libstdc++-v3/testsuite/27_io/print/1.cc
> +++ b/libstdc++-v3/testsuite/27_io/print/1.cc
> @@ -67,6 +67,22 @@ test_print_raw()
> VERIFY( txt == "\xa3" );
> }
>
> +void
> +test_print_setvbuf()
> +{
> + __gnu_test::scoped_file f;
> + FILE* strm = std::fopen(f.path.string().c_str(), "w");
> + VERIFY( strm );
> + VERIFY( std::setvbuf(strm, nullptr, _IOFBF, 4096) == 0 );
> + std::string str{"Hello, World!"};
> + std::print(strm, "{}", str);
> + std::fclose(strm);
> +
> + std::ifstream in(f.path);
> + std::string txt(std::istreambuf_iterator<char>(in), {});
> + VERIFY( txt == "Hello, World!" );
> +}
> +
> void
> test_vprint_nonunicode()
> {
> @@ -142,6 +158,7 @@ int main()
> test_print_file();
> test_println_file();
> test_print_raw();
> + test_print_setvbuf();
> test_vprint_nonunicode();
> #ifdef __cpp_exceptions
> test_errors();
> --
> 2.34.1
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://gcc.gnu.org/pipermail/libstdc++/attachments/20260817/e2941c3e/attachment-0001.htm>
More information about the Libstdc++
mailing list