https://gcc.gnu.org/bugzilla/show_bug.cgi?id=125554
Bug ID: 125554
Summary: libstdc++: std::money_get accepts malformed grouping
when a single group has 256*k+g digits
(inter-separator digit count truncated to char in
money_get::_M_extract)
Product: gcc
Version: 15.2.1
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::money_get<charT>::do_get (via the internal helper money_get::_M_extract)
records the number of digits seen between two thousands-separators by
appending
that count, cast to char, to the temporary grouping string:
__grouping_tmp += static_cast<char>(__n); // per separator
...
__grouping_tmp += static_cast<char>(__testdecfound ? __last_pos : __n);
// last group
Because the digit count __n is an int but is stored in a char, a group of
(256*k + g) digits (k >= 1) is recorded as g. The resulting value is then
checked by std::__verify_grouping against numpunct/moneypunct grouping().
Consequently a grossly malformed group whose length is congruent to a valid
group size modulo 256 passes verification: failbit is NOT set, even though
the
input violates the grouping rule. For example, with grouping() == "\3"
(groups
of 3), a group of 259 digits (= 256 + 3) is mis-recorded as 3 and accepted.
This is a conformance/correctness defect only. The truncated count is solely
compared inside __verify_grouping; it is never used as an index or size, so
there is no out-of-bounds access and no memory-safety issue. The extracted
numeric digit string is also unaffected (it contains all digits); only the
grouping-validity verdict (failbit) is wrong. Triggering it requires a single
group of >= 256 consecutive digits, which does not occur in real monetary
input.
Reproducer
----------
// money_group_bug.cpp
#include <ios>
#include <iostream>
#include <iterator>
#include <locale>
#include <sstream>
#include <string>
template <bool Intl>
class groups_of_three : public std::moneypunct<char, Intl>
{
protected:
char do_decimal_point() const override { return '.'; }
char do_thousands_sep() const override { return ','; }
std::string do_grouping() const override { return "\3"; } // every
group == 3
std::string do_curr_symbol() const override { return ""; }
std::string do_positive_sign() const override { return ""; }
std::string do_negative_sign() const override { return "-"; }
int do_frac_digits() const override { return 0; }
std::money_base::pattern do_pos_format() const override
{ return {{ std::money_base::symbol, std::money_base::sign,
std::money_base::value, std::money_base::none }}; }
std::money_base::pattern do_neg_format() const override
{ return do_pos_format(); }
};
int main()
{
std::locale loc(std::locale::classic(), new groups_of_three<false>());
const auto& mg = std::use_facet<std::money_get<char>>(loc);
auto parse = [&](const std::string& label, const std::string& input)
{
std::istringstream ss(input);
ss.imbue(loc);
std::ios_base::iostate err = std::ios_base::goodbit;
std::string digits;
std::istreambuf_iterator<char> it(ss), end;
mg.get(it, end, /*intl=*/false, ss, err, digits);
const bool failed = (err & std::ios_base::failbit) != 0;
std::cout << label << '\n'
<< " failbit = " << (failed ? "1 (rejected)" : "0
(ACCEPTED)")
<< " digits parsed = " << digits.size() << " chars\n\n";
};
parse("A) \"12,345,678\" (valid: groups 2,3,3)",
"12,345,678");
parse("B) \"12,3456,789\" (malformed: middle group = 4)",
"12,3456,789");
const std::string big(259, '5');
parse("C) \"12,<259 digits>,789\" (malformed: middle group = 259)",
"12," + big + ",789");
return 0;
}
Build / run
-----------
g++ -std=c++20 -O2 money_group_bug.cpp -o money_group_bug &&
./money_group_bug
Expected output (a correct grouping check)
------------------------------------------
A) accepted (failbit = 0)
B) rejected (failbit = 1)
C) rejected (failbit = 1) // 259 digits in one group is malformed
Actual output
-------------
A) "12,345,678" failbit = 0 (ACCEPTED) digits parsed = 8 chars
B) "12,3456,789" failbit = 1 (rejected) digits parsed = 9 chars
C) "12,<259 digits>,789" failbit = 0 (ACCEPTED) digits parsed = 264 chars
A and B behave correctly; the only difference between B and C is the length
of
the middle group (4 vs 259). 259 = 256 + 3, static_cast<char>(259) == 3, so
the
malformed 259-digit group is accepted as a valid group of 3.
Version
-------
Reproduced on g++ (GCC) 15.2.1 20260123 (Red Hat 15.2.1-7),
x86_64-pc-linux-gnu, with -std=c++20.
The relevant code is in money_get::_M_extract
(libstdc++-v3/include/bits/locale_facets_nonio.tcc), which appends each
inter-separator digit count to the temporary grouping string via
static_cast<char>(__n). I have not checked trunk; please verify whether it
still reproduces there.