[PATCH v3] libstdc++: Fix SIGSEGV in std::print with a setvbuf'd FILE

Jonathan Wakely jwakely@redhat.com
Wed Aug 19 14:17:31 GMT 2026


On Mon, 17 Aug 2026 at 09:58 +0200, Tomasz Kamiński wrote:
>From: Anlai Lu <agicy@qq.com>
>
>_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.

Please replace the paragraph above with the first two paragraphs
from the original [PATCH 0/1] email, and a shorter form of the third
paragraph:

   This is a regression introduced by the P3107R5 implementation in
   r16-4350-g8bd872f1ea7414. Before that formatting to a FILE* wrote to
   _Str_sink and used fwrite, which handles this legal stream state.


>Calls __overflow when _IO_write_ptr is null in _File_sink constructor.

Let's say:

   This fixes it by calling __overflow from the _File_sink constructor
   when _IO_write_ptr is null.

>
>libstdc++-v3/ChangeLog:
>
>	* include/bits/print.h (_File_sink::_File::_File)
>	[_GLIBCXX_USE_STDIO_LOCKING && _GLIBCXX_USE_GLIBC_STDIO_EXT]:
>	Move __overflow call to separte member function.
>	(_File_sink::_File::_M_init_write_buf)
>	[_GLIBCXX_USE_STDIO_LOCKING && _GLIBCXX_USE_GLIBC_STDIO_EXT]:
>	Extracted from constructor, call __overflow for null
>	_M_file->_IO_write_ptr.
>	(_File_sink::_File_sink)
>	[_GLIBCXX_USE_STDIO_LOCKING && _GLIBCXX_USE_GLIBC_STDIO_EXT]:
>	Call _M_init_write_buf instead of _M_writebuf.
>	* testsuite/27_io/print/1.cc: Add test_print_setvbuf.
>
>Co-authored-by: Tomasz Kamiński <tkaminsk@redhat.com>
>Signed-off-by: Anlai Lu <agicy@qq.com>
>Signed-off-by: Tomasz Kamiński <tkaminsk@redhat.com>
>---
>v3 introduced _M_init_write_buf that I suggested on review.
>
>Tested on x86_64-linux. OK for trunk and 16?
>
> libstdc++-v3/include/bits/print.h       | 28 ++++++++++++++++---------
> libstdc++-v3/testsuite/27_io/print/1.cc | 17 +++++++++++++++
> 2 files changed, 35 insertions(+), 10 deletions(-)
>
>diff --git a/libstdc++-v3/include/bits/print.h b/libstdc++-v3/include/bits/print.h
>index 20724844726..d63267c8ecf 100644
>--- a/libstdc++-v3/include/bits/print.h
>+++ b/libstdc++-v3/include/bits/print.h
>@@ -80,20 +80,28 @@ namespace __format
> 	    ::funlockfile(__f);
> 	    __throw_system_error(EACCES);
> 	  }
>-	// Allocate buffer if needed:
>-	if (_M_write_buf().empty())
>-	  if (::__overflow(__f, EOF) == EOF)
>-	    {
>-	      const int __err = errno;
>-	      ::funlockfile(__f);
>-	      __throw_system_error(__err);
>-	    }
>       }
>
>       ~_File() { ::funlockfile(_M_file); }
>
>       _File(_File&&) = delete;
>
>+      // Allocate FILE's output buffer if needed, and returns span

s/returns span/return a span/

>+      // viewing unused portion of it.
>+      std::span<char>
>+      _M_init_write_buf()
>+      {
>+        // After setvbuf glibc pre-allocates the buffer but _IO_write_ptr
>+	// remains null until the first write.

The two comment lines above are indented inconsistently, the first one
uses spaces and the second uses a tab.

>+	if (!_M_file->_IO_write_ptr || _M_write_buf().empty())
>+	  if (::__overflow(_M_file, EOF) == EOF)
>+	    {
>+	      const int __err = errno;
>+	      __throw_system_error(__err);

There's no need to store errno before throwing (because we're not
calling funlockfile here) so it can be just:

     __throw_system_error(errno);

OK with those changes, thanks.


>+	    }
>+	return _M_write_buf();
>+      }
>+
>       // A span viewing the unused portion of the stream's output buffer.
>       std::span<char>
>       _M_write_buf() noexcept
>@@ -158,8 +166,8 @@ namespace __format
>     : _M_file(__f), _M_add_newline(__add_newline)
>     {
>       if (!_M_file._M_unbuffered())
>-	// Write directly to the FILE's output buffer.
>-	this->_M_reset(_M_file._M_write_buf());
>+	// Allocate FILE's output buffer if needed, and write directly to it.
>+	this->_M_reset(_M_file._M_init_write_buf());
>     }
>
>     // This calls I/O functions which are cancellation points, so they
>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.55.0
>
>



More information about the Libstdc++ mailing list