https://gcc.gnu.org/bugzilla/show_bug.cgi?id=125505

            Bug ID: 125505
           Summary: libstdc++: num_put crashes (SIGSEGV) on
                    std::fixed/std::scientific with a near-INT_MAX stream
                    precision — unchecked negative  __convert_from_v
                    return in _M_insert_float
           Product: gcc
           Version: 15.1.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: libstdc++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: liweifriends at gmail dot com
  Target Milestone: ---

Overview
  --------
  std::num_put<>::_M_insert_float passes ios_base::precision() through to the C
  library's vsnprintf as the "%.*f" precision field. With std::fixed (or
  std::scientific) and a precision at/near INT_MAX, the program crashes with
  SIGSEGV instead of producing output or failing cleanly.

  precision(streamsize) accepts any streamsize, so os.precision(INT_MAX) is
  well-formed standard usage ([ios.base.fmtflags]/[basic.ios], no upper bound).

  Version / environment
  ---------------------
  g++ (Homebrew GCC) 15.1.0, libstdc++.6, target arm64-apple-darwin.
  The libstdc++ code path is platform-independent; the only platform-dependent
  precondition is that the C library's vsnprintf returns a negative value
(errno
  EOVERFLOW) when the would-be output exceeds INT_MAX, which holds on glibc as
  well as on the Apple libc used here.

  Minimal reproducer
  ------------------
      #include <ios>
      #include <limits>
      #include <sstream>
      int main() {
          std::ostringstream os;
          os << std::fixed;
          os.precision(std::numeric_limits<int>::max());   // INT_MAX, a legal
streamsize
          os << 1.5;                                        // SIGSEGV
      }

      $ g++ -std=c++20 -O0 -g repro.cpp -o repro && ./repro
      Segmentation fault

  Note: the default float format uses "%.*g" (trailing zeros stripped), so the
  output stays short and the bug is masked — it only manifests under
  std::fixed / std::scientific where precision counts fractional digits.

  Expected behaviour
  ------------------
  num_put must format using str.precision() ([facet.num.put.virtuals]). For
  precision INT_MAX of a fixed float this is a ~2 GB string; a conforming
  implementation must either produce it or fail cleanly (e.g. set badbit, or
  throw on resource exhaustion). A crash is not conforming.

  Actual behaviour
  ----------------
  SIGSEGV inside libstdc++. lldb/gdb shows the fault in memmove, called from
  std::num_put<char, ...>::_M_pad, with the memmove size argument equal to the
  reused negative length interpreted as size_t (observed 0xffffffffff805f7f).

  Root cause
  ----------
  In libstdc++-v3/include/bits/locale_facets.tcc, _M_insert_float (line numbers
  from the GCC 15.1.0 install):

    1009  const streamsize __prec = __io.precision() < 0 ? 6 :
__io.precision();
          // clamps the negative side only; never bounds > INT_MAX
    1015  int __len;
    1027  int   __cs_size = __max_digits * 3;          // small (~51 for
double)
    1028  char* __cs = (char*)__builtin_alloca(__cs_size);
    1030  __len = std::__convert_from_v(_S_get_c_locale(), __cs, __cs_size,
                                        __fbuf, __prec, __v);   // -> vsnprintf
    1037  if (__len >= __cs_size) { ... }              // THE ONLY GUARD
    1071  _CharT* __ws = (_CharT*)__builtin_alloca(sizeof(_CharT) * __len);
    1073  __ctype.widen(__cs, __cs + __len, __ws);

  1. "%.*f" with precision INT_MAX would emit ~2.1e9 fractional digits; total
     length exceeds INT_MAX. vsnprintf (int return type) cannot represent that,
     so it returns -1 and sets errno=EOVERFLOW.
  2. libstdc++ stores -1 in `int __len`. The only check, `__len >= __cs_size`
     (line 1037), does not catch a negative value: -1 >= 51 is false, so the -1
     is treated as a successful conversion of length -1.
  3. -1 is then reused as a length: alloca(sizeof(_CharT) * __len) (1071),
     widen(__cs, __cs + __len, __ws) (1073), and later _M_pad. As size_t, -1
     becomes ~SIZE_MAX, driving an out-of-bounds memmove -> SIGSEGV.

  A second, related crash mode: a precision somewhat below INT_MAX (e.g.
  INT_MAX-100) makes vsnprintf return a valid ~2.1e9, so `__len >= __cs_size`
is
  true and the retry does `__cs = alloca(__len + 1)` ~2 GB -> stack overflow.

  Direct precondition check (same machine), showing the negative return:

      snprintf(buf, n, "%.*f", INT_MAX,     1.5) -> -1,         errno=EOVERFLOW
      snprintf(buf, n, "%.*f", INT_MAX-100, 1.5) -> 2147483549, errno=0

  Suggested fix
  -------------
  Check the conversion result for a negative value before reusing it as a
length,
  e.g. after line 1030/1046:

      if (__len < 0)
        {
          __io.setstate(ios_base::badbit);   // or throw, per convention
          return __s;
        }

  A stronger fix bounds __prec before the conversion (any precision large
enough
  to push total output past INT_MAX cannot yield a representable result
anyway).
  • [Bug libstdc++/125505] New: lib... liweifriends at gmail dot com via Gcc-bugs

Reply via email to