From c24b9507e35fa43ddb40211a50fae9d58a0381bc Mon Sep 17 00:00:00 2001 From: Alex Burmashev Date: Mon, 27 Nov 2023 11:47:40 +0000 Subject: [PATCH 6/7] Backport additional functions for SquidMath This includes some cherry-picks from b308d7e2ad02ae6622f380d94d2303446f5831a9 and later commits. This is needed for CVE-2023-5824 fix Signed-off-by: Alex Burmashev --- src/SquidMath.h | 164 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/src/SquidMath.h b/src/SquidMath.h index c70acd1..e5b6e58 100644 --- a/src/SquidMath.h +++ b/src/SquidMath.h @@ -8,7 +8,11 @@ #ifndef _SQUID_SRC_SQUIDMATH_H #define _SQUID_SRC_SQUIDMATH_H +#include "base/forward.h" +#include "base/TypeTraits.h" +#include +#include /* Math functions we define locally for Squid */ namespace Math { @@ -21,5 +25,165 @@ double doubleAverage(const double, const double, int, const int); } // namespace Math +// If Sum() performance becomes important, consider using GCC and clang +// built-ins like __builtin_add_overflow() instead of manual overflow checks. + +/// detects a pair of unsigned types +/// reduces code duplication in declarations further below +template +using AllUnsigned = typename std::conditional< + std::is_unsigned::value && std::is_unsigned::value, + std::true_type, + std::false_type + >::type; + +// TODO: Replace with std::cmp_less() after migrating to C++20. +/// whether integer a is less than integer b, with correct overflow handling +template +constexpr bool +Less(const A a, const B b) { + // The casts below make standard C++ integer conversions explicit. They + // quell compiler warnings about signed/unsigned comparison. The first two + // lines exclude different-sign a and b, making the casts/comparison safe. + using AB = typename std::common_type::type; + return + (a >= 0 && b < 0) ? false : + (a < 0 && b >= 0) ? true : + /* (a >= 0) == (b >= 0) */ static_cast(a) < static_cast(b); +} + +/// ensure that T is supported by NaturalSum() and friends +template +constexpr void +AssertNaturalType() +{ + static_assert(std::numeric_limits::is_bounded, "std::numeric_limits::max() is meaningful"); + static_assert(std::numeric_limits::is_exact, "no silent loss of precision"); + static_assert(!std::is_enum::value, "no silent creation of non-enumerated values"); +} + +// TODO: Investigate whether this optimization can be expanded to [signed] types +// A and B when std::numeric_limits::is_modulo is true. +/// This IncreaseSumInternal() overload is optimized for speed. +/// \returns a non-overflowing sum of the two unsigned arguments (or nothing) +/// \prec both argument types are unsigned +template ::value, int> = 0> +std::optional +IncreaseSumInternal(const A a, const B b) { + // paranoid: AllUnsigned precondition established that already + static_assert(std::is_unsigned::value, "AllUnsigned dispatch worked for A"); + static_assert(std::is_unsigned::value, "AllUnsigned dispatch worked for B"); + + AssertNaturalType(); + AssertNaturalType(); + AssertNaturalType(); + + // we should only be called by IncreaseSum(); it forces integer promotion + static_assert(std::is_same::value, "a will not be promoted"); + static_assert(std::is_same::value, "b will not be promoted"); + // and without integer promotions, a sum of unsigned integers is unsigned + static_assert(std::is_unsigned::value, "a+b is unsigned"); + + // with integer promotions ruled out, a or b can only undergo integer + // conversion to the higher rank type (A or B, we do not know which) + using AB = typename std::common_type::type; + static_assert(std::is_same::value || std::is_same::value, "no unexpected conversions"); + static_assert(std::is_same::value, "lossless assignment"); + const AB sum = a + b; + + static_assert(std::numeric_limits::is_modulo, "we can detect overflows"); + // 1. modulo math: overflowed sum is smaller than any of its operands + // 2. the sum may overflow S (i.e. the return base type) + // We do not need Less() here because we compare promoted unsigned types. + return (sum >= a && sum <= std::numeric_limits::max()) ? + std::optional(sum) : std::optional(); +} + +/// This IncreaseSumInternal() overload supports a larger variety of types. +/// \returns a non-overflowing sum of the two arguments (or nothing) +/// \returns nothing if at least one of the arguments is negative +/// \prec at least one of the argument types is signed +template ::value, int> = 0> +std::optional constexpr +IncreaseSumInternal(const A a, const B b) { + AssertNaturalType(); + AssertNaturalType(); + AssertNaturalType(); + + // we should only be called by IncreaseSum() that does integer promotion + static_assert(std::is_same::value, "a will not be promoted"); + static_assert(std::is_same::value, "b will not be promoted"); + + return + // We could support a non-under/overflowing sum of negative numbers, but + // our callers use negative values specially (e.g., for do-not-use or + // do-not-limit settings) and are not supposed to do math with them. + (a < 0 || b < 0) ? std::optional() : + // To avoid undefined behavior of signed overflow, we must not compute + // the raw a+b sum if it may overflow. When A is not B, a or b undergoes + // (safe for non-negatives) integer conversion in these expressions, so + // we do not know the resulting a+b type AB and its maximum. We must + // also detect subsequent casting-to-S overflows. + // Overflow condition: (a + b > maxAB) or (a + b > maxS). + // A is an integer promotion of S, so maxS <= maxA <= maxAB. + // Since maxS <= maxAB, it is sufficient to just check: a + b > maxS, + // which is the same as the overflow-safe condition here: maxS - a < b. + // Finally, (maxS - a) cannot overflow because a is not negative and + // cannot underflow because a is a promotion of s: 0 <= a <= maxS. + Less(std::numeric_limits::max() - a, b) ? std::optional() : + std::optional(a + b); +} + +/// argument pack expansion termination for IncreaseSum() +template +std::optional +IncreaseSum(const S s, const T t) +{ + // Force (always safe) integer promotions now, to give std::enable_if_t<> + // promoted types instead of entering IncreaseSumInternal(s,t) + // but getting a _signed_ promoted value of s or t in s + t. + return IncreaseSumInternal(+s, +t); +} + +/// \returns a non-overflowing sum of the arguments (or nothing) +template +std::optional +IncreaseSum(const S sum, const T t, const Args... args) { + if (const auto head = IncreaseSum(sum, t)) { + return IncreaseSum(head.value(), args...); + } else { + // std::optional() triggers bogus -Wmaybe-uninitialized warnings in GCC v10.3 + return std::nullopt; + } +} + +/// \returns an exact, non-overflowing sum of the arguments (or nothing) +template +std::optional +NaturalSum(const Args... args) { + return IncreaseSum(0, args...); +} + +/// Safely resets the given variable to NaturalSum() of the given arguments. +/// If the sum overflows, resets to variable's maximum possible value. +/// \returns the new variable value (like an assignment operator would) +template +S +SetToNaturalSumOrMax(S &var, const Args... args) +{ + var = NaturalSum(args...).value_or(std::numeric_limits::max()); + return var; +} + +/// converts a given non-negative integer into an integer of a given type +/// without loss of information or undefined behavior +template +Result +NaturalCast(const Source s) +{ + return NaturalSum(s).value(); +} + + #endif /* _SQUID_SRC_SQUIDMATH_H */ -- 2.39.3