[v2] The following functions invoke signed integer overflow (UB) for some extreme values of days and months [1]:

Message ID 20231118000653.6924-1-cassio.neri@gmail.com
State Superseded
Headers
Series [v2] The following functions invoke signed integer overflow (UB) for some extreme values of days and months [1]: |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gcc_build--master-arm success Testing passed
linaro-tcwg-bot/tcwg_gcc_build--master-aarch64 success Testing passed

Commit Message

Cassio Neri Nov. 18, 2023, 12:06 a.m. UTC
  weekday operator+(const weekday& x, const days& y); // #1
  month operator+(const month& x, const months& y);   // #2

For #1 the problem is that in libstdc++ days::rep is int64_t. Other
implementations use int32_t and cast operands to int64_t. Hence then perform
arithmetic operations without fear of overflowing. For instance, #1 evaluates:

  modulo(static_cast<long long>(unsigned{x}._M_wd) + __y.count(), 7);

For x86-64, long long is int64 so the cast is useless.  For #2, casting to a
larger type could help but all implementations follow the Standard's "Returns
clause" and evaluate:

   modulo(static_cast<long long>(unsigned{__x}) + (__y.count() - 1), 12);

Hence, overflow occurs when __y.count() is the minimum value of its type.  When
long long is larger than months::rep, this is a fix:

   modulo(static_cast<long long>(unsigned{__x}) + 11 + __y.count(), 12);

Again, this is not possible for libstdc++.  The fix uses this new function:

  template <unsigned __d, typename _T>
  unsigned __add_modulo(unsigned __x, _T __y);

which returns the remainder of Euclidean division of __x +__y by __d without
overflowing. This function replaces

  constexpr unsigned __modulo(long long __n, unsigned __d);

In addition to solve the UB issues, __add_modulo allows shorter branchless code
on x86-64 and ARM [2].

[1] https://godbolt.org/z/WqvosbrvG
[2] https://godbolt.org/z/o63794GEE

libstdc++-v3/ChangeLog:

	* include/std/chrono: Fix operator+ for months and weekdays.
	* testsuite/std/time/month/1.cc: Add constexpr tests against overflow.
	* testsuite/std/time/month/2.cc: New test for extreme values.
	* testsuite/std/time/weekday/1.cc: Add constexpr tests against overflow.
	* testsuite/std/time/weekday/2.cc: New test for extreme values.
---
 libstdc++-v3/include/std/chrono              | 61 ++++++++++++--------
 libstdc++-v3/testsuite/std/time/month/1.cc   |  9 +++
 libstdc++-v3/testsuite/std/time/weekday/1.cc |  8 +++
 3 files changed, 54 insertions(+), 24 deletions(-)

 Changes with respect to previous versions:
 v2: Replaced _T with _Tp and _U with _Up. Removed copyright+license from test.

--
2.41.0
  

Patch

diff --git a/libstdc++-v3/include/std/chrono b/libstdc++-v3/include/std/chrono
index 10bdd1c4ede..691bb106bb9 100644
--- a/libstdc++-v3/include/std/chrono
+++ b/libstdc++-v3/include/std/chrono
@@ -497,18 +497,38 @@  _GLIBCXX_BEGIN_NAMESPACE_VERSION

     namespace __detail
     {
-      // Compute the remainder of the Euclidean division of __n divided by __d.
-      // Euclidean division truncates toward negative infinity and always
-      // produces a remainder in the range of [0,__d-1] (whereas standard
-      // division truncates toward zero and yields a nonpositive remainder
-      // for negative __n).
+      // Compute the remainder of the Euclidean division of __x + __y divided by
+      // __d without overflowing.  Typically, __x <= 255 + d - 1 is sum of
+      // weekday/month and an offset in [0, d - 1] and __y is a duration count.
+      // For instance, [time.cal.month.nonmembers] says that given month x and
+      // months y, to get x + y one must calculate:
+      //
+      // modulo(static_cast<long long>(unsigned{x}) + (y.count() - 1), 12) + 1.
+      //
+      // Since y.count() is a 64-bits signed value the subtraction y.count() - 1
+      // or the addition of this value with static_cast<long long>(unsigned{x})
+      // might overflow.  This function can be used to avoid this problem:
+      //     __add_modulo<12>(unsigned{x} + 11, y.count()) + 1;
+      // (More details in the implementation of operator+(month, months).)
+      template <unsigned __d, typename _Tp>
       constexpr unsigned
-      __modulo(long long __n, unsigned __d)
-      {
-	if (__n >= 0)
-	  return __n % __d;
-	else
-	  return (__d + (__n % __d)) % __d;
+      __add_modulo(unsigned __x, _Tp __y)
+      {
+	using _Up = make_unsigned_t<_Tp>;
+	// For __y >= 0, _Up(__y) has the same mathematical value as __y and
+	// this function simply returns (__x + _Up(__y)) % d.  Typically, this
+	// doesn't overflow since the range of _Up contains many more positive
+	// values than _Tp's.  For __y < 0, _Up(__y) has a mathematical value in
+	// the upper-half range of _Up so that adding a positive value to it
+	// might overflow.  Moreover, most likely, _Up(__y) != __y mod d.  To
+	// fix both issues we from _Up(__y)"subtract"  an __offset >=
+	// 255 + d - 1 to make room for the addition to __x and shift the modulo
+	// to the correct value.
+	auto constexpr __a = _Up(-1) - _Up(255 + __d - 2);
+	auto constexpr __b = _Up(__d * (__a / __d) - 1);
+	// Notice: b <= a - 1 <= _Up(-1) - _Up(255 + d - 1) and b % d = d - 1.
+	auto const __offset = __y >= 0 ? _Up(0) : __b - _Up(-1);
+	return (__x + _Up(__y) + __offset) % __d;
       }

       inline constexpr unsigned __days_per_month[12]
@@ -700,8 +720,10 @@  _GLIBCXX_BEGIN_NAMESPACE_VERSION
       friend constexpr month
       operator+(const month& __x, const months& __y) noexcept
       {
-	auto __n = static_cast<long long>(unsigned{__x}) + (__y.count() - 1);
-	return month{__detail::__modulo(__n, 12) + 1};
+	// modulo(x + (y - 1), 12) = modulo(x + (y - 1) + 12, 12)
+	//                         = modulo((x + 11) - y, 12)
+	return month{1 + __detail::__add_modulo<12>(
+	  unsigned{__x} + 11, __y.count())};
       }

       friend constexpr month
@@ -930,15 +952,7 @@  _GLIBCXX_BEGIN_NAMESPACE_VERSION
       static constexpr weekday
       _S_from_days(const days& __d)
       {
-	using _Rep = days::rep;
-	using _URep = make_unsigned_t<_Rep>;
-	const auto __n = __d.count();
-	const auto __m = static_cast<_URep>(__n);
-
-	// 1970-01-01 (__n =  0, __m = 0        ) -> Thursday (4)
-	// 1969-31-12 (__n = -1, __m = _URep(-1)) -> Wednesday (3)
-	const auto __offset = __n >= 0 ? _URep(4) : 3 - _URep(-1) % 7 - 7;
-	return weekday((__m + __offset) % 7);
+	return weekday{__detail::__add_modulo<7>(4, __d.count())};
       }

     public:
@@ -1028,8 +1042,7 @@  _GLIBCXX_BEGIN_NAMESPACE_VERSION
       friend constexpr weekday
       operator+(const weekday& __x, const days& __y) noexcept
       {
-	auto __n = static_cast<long long>(__x._M_wd) + __y.count();
-	return weekday{__detail::__modulo(__n, 7)};
+	return weekday{__detail::__add_modulo<7>(__x._M_wd, __y.count())};
       }

       friend constexpr weekday
diff --git a/libstdc++-v3/testsuite/std/time/month/1.cc b/libstdc++-v3/testsuite/std/time/month/1.cc
index 773f772bf07..f72a4ab9a5d 100644
--- a/libstdc++-v3/testsuite/std/time/month/1.cc
+++ b/libstdc++-v3/testsuite/std/time/month/1.cc
@@ -20,6 +20,7 @@ 
 // Class template day [time.cal.month]

 #include <chrono>
+#include <limits>

 constexpr void
 constexpr_month()
@@ -71,4 +72,12 @@  constexpr_month()
   static_assert(month{0} <=> month{1} == std::strong_ordering::less);
   static_assert(month{3} <=> month{3} == std::strong_ordering::equal);
   static_assert(month{5} <=> month{2} == std::strong_ordering::greater);
+
+  auto constexpr months_min = months{std::numeric_limits<months::rep>::min()};
+  auto constexpr m0_min     = month{ 0 } + months_min;
+  auto constexpr m255_min   = month{255} + months_min;
+
+  auto constexpr months_max = months{std::numeric_limits<months::rep>::max()};
+  auto constexpr m0_max     = month{ 0 } + months_max;
+  auto constexpr m255_max   = month{255} + months_max;
 }
diff --git a/libstdc++-v3/testsuite/std/time/weekday/1.cc b/libstdc++-v3/testsuite/std/time/weekday/1.cc
index e89fca47d4b..cc8b0db78f8 100644
--- a/libstdc++-v3/testsuite/std/time/weekday/1.cc
+++ b/libstdc++-v3/testsuite/std/time/weekday/1.cc
@@ -107,4 +107,12 @@  constexpr_weekday()
   static_assert(weekday{7} == weekday{0});
   static_assert(!(weekday{0} == weekday{1}));
   static_assert( (weekday{0} != weekday{2}));
+
+  auto constexpr days_min  = days{std::numeric_limits<days::rep>::min()};
+  auto constexpr wd0_min   = weekday{ 0 } + days_min;
+  auto constexpr wd255_min = weekday{255} + days_min;
+
+  auto constexpr days_max  = days{std::numeric_limits<days::rep>::max()};
+  auto constexpr m0_max    = weekday{ 0 } + days_max;
+  auto constexpr m255_max  = weekday{255} + days_max;
 }