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.
  • [Bug libstdc++/125554] New: lib... liweifriends at gmail dot com via Gcc-bugs

Reply via email to