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).