The unbuffered path in basic_filebuf::overflow calls
_M_convert_to_external(&__conv, 1) to output a single character. But if
a partial codecvt conversion happens in _M_convert_to_external then it
calculates the number of unconverted characters in the filebuf's put
area by calling pptr(). In the unbuffered case pptr() returns a null
pointer and so the arithmetic on it is undefined.

Although GCC doesn't diagnose this, Clang's UBSan does:
.../include/c++/16/bits/fstream.tcc:655:19: runtime error: applying non-zero 
offset to non-null pointer 0x7ffd0d20eca7 produced null pointer

_M_convert_to_external should only read from the buffer passed to it,
not assume that the streambuf's put area exists or is equivalent to the
buffer that was passed.

This probably isn't a problem in real code, because the null pptr() in
_M_convert_to_external was only reachable for a non-conforming codecvt
facet that lies about max_length() or for some reason performs a partial
conversion on a single character even when sufficient output space is
available (as in the new testcases).  But it is more robust to not
access pptr() directly when we are called with a pointer to the sequence
that should be converted. This also enables a further correctness fix to
basic_filebuf::overflow in a subsequent commit.

The retry code can be further simplified because not only is using
pptr() unsafe, but we can just use [__iend, __ibuf + __ilen) as the
input range, instead of [__iend, __iend + (pptr() - __iend)). That was
just unnecessarily complicated.

The retry_forcing_codecvt in the new test was generated by an LLM,
although greatly simplified by me so that the bug is detected with a
simple VERIFY rather than examining the characters output by the facet.

Assisted-by: LLM

libstdc++-v3/ChangeLog:

        PR libstdc++/118099
        * include/bits/fstream.tcc
        (basic_filebuf::_M_convert_to_external): Use __ibuf + __ilen
        instead of this->pptr().
        * testsuite/27_io/basic_filebuf/overflow/char/118099-unbuffered.cc:
        New test.
        * testsuite/27_io/basic_filebuf/overflow/wchar_t/118099-unbuffered.cc:
        New test.
---

Tested x86_64-linux.

 libstdc++-v3/include/bits/fstream.tcc         |  9 +--
 .../overflow/char/118099-unbuffered.cc        | 72 +++++++++++++++++++
 .../overflow/wchar_t/118099-unbuffered.cc     | 72 +++++++++++++++++++
 3 files changed, 147 insertions(+), 6 deletions(-)
 create mode 100644 
libstdc++-v3/testsuite/27_io/basic_filebuf/overflow/char/118099-unbuffered.cc
 create mode 100644 
libstdc++-v3/testsuite/27_io/basic_filebuf/overflow/wchar_t/118099-unbuffered.cc

diff --git a/libstdc++-v3/include/bits/fstream.tcc 
b/libstdc++-v3/include/bits/fstream.tcc
index 2a1257c3e341..5d13633b6841 100644
--- a/libstdc++-v3/include/bits/fstream.tcc
+++ b/libstdc++-v3/include/bits/fstream.tcc
@@ -651,14 +651,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
          // Try once more for partial conversions.
          if (__r == codecvt_base::partial && __elen == __plen)
            {
-             const char_type* __iresume = __iend;
-             streamsize __rlen = this->pptr() - __iend;
-             __r = _M_codecvt->out(_M_state_cur, __iresume,
-                                   __iresume + __rlen, __iend, __buf,
-                                   __buf + __blen, __bend);
+             __r = _M_codecvt->out(_M_state_cur, __iend, __ibuf + __ilen,
+                                   __iend, __buf, __buf + __blen, __bend);
              if (__r != codecvt_base::error)
                {
-                 __rlen = __bend - __buf;
+                 const streamsize __rlen = __bend - __buf;
                  __elen = _M_file.xsputn(__buf, __rlen);
                  __plen = __rlen;
                }
diff --git 
a/libstdc++-v3/testsuite/27_io/basic_filebuf/overflow/char/118099-unbuffered.cc 
b/libstdc++-v3/testsuite/27_io/basic_filebuf/overflow/char/118099-unbuffered.cc
new file mode 100644
index 000000000000..a77a635feb73
--- /dev/null
+++ 
b/libstdc++-v3/testsuite/27_io/basic_filebuf/overflow/char/118099-unbuffered.cc
@@ -0,0 +1,72 @@
+// { dg-do run }
+// { dg-require-fileio "" }
+
+// PR libstdc++/118099
+// basic_filebuf::overflow leaves streambuf in inconsistent state on I/O error
+//
+// Companion test of 118099.cc testing the unbuffered put path, which calls
+// _M_convert_to_external(&__conv, 1).  When a codecvt forces the
+// "partial conversion" retry, that retry must compute the amount of input
+// left to convert from the buffer it was handed (__ibuf + __ilen), not from
+// pptr().  In unbuffered mode pptr() is null, so the old code called
+// codecvt::out with from_end == __iresume + (pptr() - __iend) == pptr(),
+// i.e. a null `from_end` for a non-null `from`.
+
+#include <fstream>
+#include <locale>
+#include <testsuite_hooks.h>
+
+typedef char C;
+
+// Emits one external char per do_out() call without consuming input, and
+// under-reports max_length(), so that _M_convert_to_external is forced to
+// take its partial-conversion retry branch.  The retry is where the bogus
+// (null) from_end is passed.
+// This is probably not a conforming codecvt facet and is intended only to
+// test the libstdc++ basic_filebuf::_M_convert_to_external behaviour.
+struct retry_forcing_codecvt : std::codecvt<C, char, std::mbstate_t>
+{
+protected:
+  result
+  do_out(state_type&, const intern_type* from, const intern_type* from_end,
+        const intern_type*& from_next, extern_type* to, extern_type* to_end,
+        extern_type*& to_next) const
+  {
+    // The _M_convert_to_external bug passed non-null from and null from_end.
+    if (from != 0)
+      VERIFY( from_end != 0 );
+
+    from_next = from;
+    to_next = to;
+    if (from < from_end && to < to_end)
+      {
+       *to_next++ = *from; // emit a character; do not advance from_next
+       return partial;
+      }
+    return from == from_end ? ok : partial;
+  }
+
+  bool do_always_noconv() const throw() { return false; }
+  int  do_max_length()    const throw() { return 1; }
+};
+
+static void
+test_pr118099_unbuffered()
+{
+  std::basic_filebuf<C> fb;
+  fb.pubimbue(std::locale(std::locale::classic(), new retry_forcing_codecvt));
+  fb.pubsetbuf(0, 0); // unbuffered: pptr() == epptr() == null
+  fb.open("tmp_118099_unbuffered", std::ios_base::out | std::ios_base::trunc);
+  VERIFY( fb.is_open() );
+
+  // Each put goes straight to _M_convert_to_external(&__conv, 1); the codecvt
+  // above asserts it is never called with the bogus null from_end.
+  fb.sputc('a');
+  fb.sputc('b');
+  fb.sputc('c');
+}
+
+int main()
+{
+  test_pr118099_unbuffered();
+}
diff --git 
a/libstdc++-v3/testsuite/27_io/basic_filebuf/overflow/wchar_t/118099-unbuffered.cc
 
b/libstdc++-v3/testsuite/27_io/basic_filebuf/overflow/wchar_t/118099-unbuffered.cc
new file mode 100644
index 000000000000..b2c8ae3ec8c0
--- /dev/null
+++ 
b/libstdc++-v3/testsuite/27_io/basic_filebuf/overflow/wchar_t/118099-unbuffered.cc
@@ -0,0 +1,72 @@
+// { dg-do run }
+// { dg-require-fileio "" }
+
+// PR libstdc++/118099
+// basic_filebuf::overflow leaves streambuf in inconsistent state on I/O error
+//
+// Companion test of 118099.cc testing the unbuffered put path, which calls
+// _M_convert_to_external(&__conv, 1).  When a codecvt forces the
+// "partial conversion" retry, that retry must compute the amount of input
+// left to convert from the buffer it was handed (__ibuf + __ilen), not from
+// pptr().  In unbuffered mode pptr() is null, so the old code called
+// codecvt::out with from_end == __iresume + (pptr() - __iend) == pptr(),
+// i.e. a null `from_end` for a non-null `from`.
+
+#include <fstream>
+#include <locale>
+#include <testsuite_hooks.h>
+
+typedef wchar_t C;
+
+// Emits one external char per do_out() call without consuming input, and
+// under-reports max_length(), so that _M_convert_to_external is forced to
+// take its partial-conversion retry branch.  The retry is where the bogus
+// (null) from_end is passed.
+// This is probably not a conforming codecvt facet and is intended only to
+// test the libstdc++ basic_filebuf::_M_convert_to_external behaviour.
+struct retry_forcing_codecvt : std::codecvt<C, char, std::mbstate_t>
+{
+protected:
+  result
+  do_out(state_type&, const intern_type* from, const intern_type* from_end,
+        const intern_type*& from_next, extern_type* to, extern_type* to_end,
+        extern_type*& to_next) const
+  {
+    // The _M_convert_to_external bug passed non-null from and null from_end.
+    if (from != 0)
+      VERIFY( from_end != 0 );
+
+    from_next = from;
+    to_next = to;
+    if (from < from_end && to < to_end)
+      {
+       *to_next++ = *from; // emit a character; do not advance from_next
+       return partial;
+      }
+    return from == from_end ? ok : partial;
+  }
+
+  bool do_always_noconv() const throw() { return false; }
+  int  do_max_length()    const throw() { return 1; }
+};
+
+static void
+test_pr118099_unbuffered()
+{
+  std::basic_filebuf<C> fb;
+  fb.pubimbue(std::locale(std::locale::classic(), new retry_forcing_codecvt));
+  fb.pubsetbuf(0, 0); // unbuffered: pptr() == epptr() == null
+  fb.open("tmp_118099_unbuffered", std::ios_base::out | std::ios_base::trunc);
+  VERIFY( fb.is_open() );
+
+  // Each put goes straight to _M_convert_to_external(&__conv, 1); the codecvt
+  // above asserts it is never called with the bogus null from_end.
+  fb.sputc(L'a');
+  fb.sputc(L'b');
+  fb.sputc(L'c');
+}
+
+int main()
+{
+  test_pr118099_unbuffered();
+}
-- 
2.56.0

Reply via email to