diff options
author | 2021-05-25 12:52:24 -0700 | |
---|---|---|
committer | 2021-06-04 13:38:17 -0700 | |
commit | 9f5e5dbcb31032fc729501805ed548d80abad115 (patch) | |
tree | 865155d757a9187df0e39359a98cc1d38c15cb47 | |
parent | 776941460afa5d714a7f2c29866c35cdd75efb7c (diff) |
FTL: Add cast safety checker
Add a function to check cast safety, to be used by a saturating cast
that (among other uses) will replace ui::Size::clamp, which invokes
the undefined behavior of signed overflow when casting INT32_MAX to
float and back.
Bug: 185536303
Test: ftl_test
Change-Id: I412a1d72de7235b73ea3dd3f194d0da178ce5eb2
-rw-r--r-- | include/ftl/cast.h | 84 | ||||
-rw-r--r-- | include/ftl/details/cast.h | 57 | ||||
-rw-r--r-- | libs/ftl/Android.bp | 1 | ||||
-rw-r--r-- | libs/ftl/cast_test.cpp | 200 |
4 files changed, 342 insertions, 0 deletions
diff --git a/include/ftl/cast.h b/include/ftl/cast.h new file mode 100644 index 0000000000..ff1b58ad56 --- /dev/null +++ b/include/ftl/cast.h @@ -0,0 +1,84 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <limits> +#include <type_traits> + +#include <ftl/details/cast.h> + +namespace android::ftl { + +enum class CastSafety { kSafe, kUnderflow, kOverflow }; + +// Returns whether static_cast<R>(v) is safe, or would result in underflow or overflow. +// +// static_assert(ftl::cast_safety<uint8_t>(-1) == ftl::CastSafety::kUnderflow); +// static_assert(ftl::cast_safety<int8_t>(128u) == ftl::CastSafety::kOverflow); +// +// static_assert(ftl::cast_safety<uint32_t>(-.1f) == ftl::CastSafety::kUnderflow); +// static_assert(ftl::cast_safety<int32_t>(static_cast<float>(INT32_MAX)) == +// ftl::CastSafety::kOverflow); +// +// static_assert(ftl::cast_safety<float>(-DBL_MAX) == ftl::CastSafety::kUnderflow); +// +template <typename R, typename T> +constexpr CastSafety cast_safety(T v) { + static_assert(std::is_arithmetic_v<T>); + static_assert(std::is_arithmetic_v<R>); + + constexpr bool kFromSigned = std::is_signed_v<T>; + constexpr bool kToSigned = std::is_signed_v<R>; + + using details::max_exponent; + + // If the R range contains the T range, then casting is always safe. + if constexpr ((kFromSigned == kToSigned && max_exponent<R> >= max_exponent<T>) || + (!kFromSigned && kToSigned && max_exponent<R> > max_exponent<T>)) { + return CastSafety::kSafe; + } + + using C = std::common_type_t<R, T>; + + if constexpr (kFromSigned) { + using L = details::safe_limits<R, T>; + + if constexpr (kToSigned) { + // Signed to signed. + if (v < L::lowest()) return CastSafety::kUnderflow; + return v <= L::max() ? CastSafety::kSafe : CastSafety::kOverflow; + } else { + // Signed to unsigned. + if (v < 0) return CastSafety::kUnderflow; + return static_cast<C>(v) <= static_cast<C>(L::max()) ? CastSafety::kSafe + : CastSafety::kOverflow; + } + } else { + using L = std::numeric_limits<R>; + + if constexpr (kToSigned) { + // Unsigned to signed. + return static_cast<C>(v) <= static_cast<C>(L::max()) ? CastSafety::kSafe + : CastSafety::kOverflow; + } else { + // Unsigned to unsigned. + return v <= L::max() ? CastSafety::kSafe : CastSafety::kOverflow; + } + } +} + +} // namespace android::ftl diff --git a/include/ftl/details/cast.h b/include/ftl/details/cast.h new file mode 100644 index 0000000000..87b9f1e20a --- /dev/null +++ b/include/ftl/details/cast.h @@ -0,0 +1,57 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <limits> +#include <type_traits> + +namespace android::ftl::details { + +// Exponent whose power of 2 is the (exclusive) upper bound of T. +template <typename T, typename L = std::numeric_limits<T>> +constexpr int max_exponent = std::is_floating_point_v<T> ? L::max_exponent : L::digits; + +// Extension of std::numeric_limits<T> that reduces the maximum for integral types T such that it +// has an exact representation for floating-point types F. For example, the maximum int32_t value +// is 2'147'483'647, but casting it to float commonly rounds up to 2'147'483'650.f, which cannot +// be safely converted back lest the signed overflow invokes undefined behavior. This pitfall is +// avoided by clearing the lower (31 - 24 =) 7 bits of precision to 2'147'483'520. Note that the +// minimum is representable. +template <typename T, typename F> +struct safe_limits : std::numeric_limits<T> { + static constexpr T max() { + using Base = std::numeric_limits<T>; + + if constexpr (std::is_integral_v<T> && std::is_floating_point_v<F>) { + // Assume the mantissa is 24 bits for float, or 53 bits for double. + using Float = std::numeric_limits<F>; + static_assert(Float::is_iec559); + + // If the integer is wider than the mantissa, clear the excess bits of precision. + constexpr int kShift = Base::digits - Float::digits; + if constexpr (kShift > 0) { + using U = std::make_unsigned_t<T>; + constexpr U kOne = static_cast<U>(1); + return static_cast<U>(Base::max()) & ~((kOne << kShift) - kOne); + } + } + + return Base::max(); + } +}; + +} // namespace android::ftl::details diff --git a/libs/ftl/Android.bp b/libs/ftl/Android.bp index 97626bec7d..22267912b9 100644 --- a/libs/ftl/Android.bp +++ b/libs/ftl/Android.bp @@ -14,6 +14,7 @@ cc_test { address: true, }, srcs: [ + "cast_test.cpp", "future_test.cpp", "small_map_test.cpp", "small_vector_test.cpp", diff --git a/libs/ftl/cast_test.cpp b/libs/ftl/cast_test.cpp new file mode 100644 index 0000000000..2abcb8fe66 --- /dev/null +++ b/libs/ftl/cast_test.cpp @@ -0,0 +1,200 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <ftl/cast.h> +#include <gtest/gtest.h> + +#include <cfloat> +#include <cmath> +#include <limits> + +namespace android::test { + +using ftl::cast_safety; +using ftl::CastSafety; + +template <typename T> +constexpr T min = std::numeric_limits<T>::lowest(); + +template <typename T> +constexpr T max = std::numeric_limits<T>::max(); + +template <typename T> +constexpr T inf = std::numeric_limits<T>::infinity(); + +template <typename T> +constexpr T NaN = std::numeric_limits<T>::quiet_NaN(); + +// Keep in sync with example usage in header file. + +static_assert(cast_safety<uint8_t>(-1) == CastSafety::kUnderflow); +static_assert(cast_safety<int8_t>(128u) == CastSafety::kOverflow); + +static_assert(cast_safety<uint32_t>(-.1f) == CastSafety::kUnderflow); +static_assert(cast_safety<int32_t>(static_cast<float>(INT32_MAX)) == CastSafety::kOverflow); + +static_assert(cast_safety<float>(-DBL_MAX) == CastSafety::kUnderflow); + +// Unsigned to unsigned. + +static_assert(cast_safety<uint8_t>(0u) == CastSafety::kSafe); +static_assert(cast_safety<uint16_t>(max<uint8_t>) == CastSafety::kSafe); +static_assert(cast_safety<uint8_t>(static_cast<uint32_t>(max<uint8_t>)) == CastSafety::kSafe); + +static_assert(cast_safety<uint32_t>(max<uint64_t>) == CastSafety::kOverflow); +static_assert(cast_safety<uint8_t>(static_cast<uint32_t>(max<uint8_t>) + 1) == + CastSafety::kOverflow); + +// Unsigned to signed. + +static_assert(cast_safety<int16_t>(0u) == CastSafety::kSafe); +static_assert(cast_safety<int16_t>(max<uint8_t>) == CastSafety::kSafe); +static_assert(cast_safety<int16_t>(max<uint16_t>) == CastSafety::kOverflow); + +static_assert(cast_safety<int64_t>(static_cast<uint64_t>(max<int64_t>) - 1) == CastSafety::kSafe); +static_assert(cast_safety<int64_t>(static_cast<uint64_t>(max<int64_t>)) == CastSafety::kSafe); +static_assert(cast_safety<int64_t>(static_cast<uint64_t>(max<int64_t>) + 1) == + CastSafety::kOverflow); + +// Signed to unsigned. + +static_assert(cast_safety<uint16_t>(0) == CastSafety::kSafe); +static_assert(cast_safety<uint16_t>(max<int8_t>) == CastSafety::kSafe); +static_assert(cast_safety<uint16_t>(max<int16_t>) == CastSafety::kSafe); + +static_assert(cast_safety<uint32_t>(-1) == CastSafety::kUnderflow); +static_assert(cast_safety<uint32_t>(max<int64_t>) == CastSafety::kOverflow); + +static_assert(cast_safety<uint32_t>(static_cast<int64_t>(max<uint32_t>) - 1) == CastSafety::kSafe); +static_assert(cast_safety<uint32_t>(static_cast<int64_t>(max<uint32_t>)) == CastSafety::kSafe); +static_assert(cast_safety<uint32_t>(static_cast<int64_t>(max<uint32_t>) + 1) == + CastSafety::kOverflow); + +// Signed to signed. + +static_assert(cast_safety<int8_t>(-129) == CastSafety::kUnderflow); +static_assert(cast_safety<int8_t>(-128) == CastSafety::kSafe); +static_assert(cast_safety<int8_t>(127) == CastSafety::kSafe); +static_assert(cast_safety<int8_t>(128) == CastSafety::kOverflow); + +static_assert(cast_safety<int32_t>(static_cast<int64_t>(min<int32_t>)) == CastSafety::kSafe); +static_assert(cast_safety<int32_t>(static_cast<int64_t>(max<int32_t>)) == CastSafety::kSafe); + +static_assert(cast_safety<int16_t>(min<int32_t>) == CastSafety::kUnderflow); +static_assert(cast_safety<int32_t>(max<int64_t>) == CastSafety::kOverflow); + +// Float to float. + +static_assert(cast_safety<double>(max<float>) == CastSafety::kSafe); +static_assert(cast_safety<double>(min<float>) == CastSafety::kSafe); + +static_assert(cast_safety<float>(min<double>) == CastSafety::kUnderflow); +static_assert(cast_safety<float>(max<double>) == CastSafety::kOverflow); + +TEST(CastSafety, FloatToFloat) { + EXPECT_EQ(cast_safety<float>(std::nexttoward(static_cast<double>(min<float>), min<double>)), + CastSafety::kUnderflow); + EXPECT_EQ(cast_safety<float>(std::nexttoward(static_cast<double>(max<float>), max<double>)), + CastSafety::kOverflow); +} + +// Unsigned to float. + +static_assert(cast_safety<float>(0u) == CastSafety::kSafe); +static_assert(cast_safety<float>(max<uint64_t>) == CastSafety::kSafe); + +static_assert(cast_safety<double>(0u) == CastSafety::kSafe); +static_assert(cast_safety<double>(max<uint64_t>) == CastSafety::kSafe); + +// Signed to float. + +static_assert(cast_safety<float>(min<int64_t>) == CastSafety::kSafe); +static_assert(cast_safety<float>(max<int64_t>) == CastSafety::kSafe); + +static_assert(cast_safety<double>(min<int64_t>) == CastSafety::kSafe); +static_assert(cast_safety<double>(max<int64_t>) == CastSafety::kSafe); + +// Float to unsigned. + +static_assert(cast_safety<uint32_t>(0.f) == CastSafety::kSafe); +static_assert(cast_safety<uint32_t>(min<float>) == CastSafety::kUnderflow); +static_assert(cast_safety<uint32_t>(max<float>) == CastSafety::kOverflow); +static_assert(cast_safety<uint32_t>(-.1f) == CastSafety::kUnderflow); + +static_assert(cast_safety<uint16_t>(-inf<float>) == CastSafety::kUnderflow); +static_assert(cast_safety<uint32_t>(inf<float>) == CastSafety::kOverflow); +static_assert(cast_safety<uint64_t>(NaN<float>) == CastSafety::kOverflow); + +static_assert(cast_safety<uint32_t>(static_cast<float>(max<int32_t>)) == CastSafety::kSafe); +static_assert(cast_safety<uint32_t>(static_cast<float>(max<uint32_t>)) == CastSafety::kOverflow); +static_assert(cast_safety<uint32_t>(static_cast<double>(max<int32_t>)) == CastSafety::kSafe); +static_assert(cast_safety<uint32_t>(static_cast<double>(max<uint32_t>)) == CastSafety::kSafe); + +static_assert(cast_safety<uint64_t>(0.0) == CastSafety::kSafe); +static_assert(cast_safety<uint64_t>(min<double>) == CastSafety::kUnderflow); +static_assert(cast_safety<uint64_t>(max<double>) == CastSafety::kOverflow); +static_assert(cast_safety<uint64_t>(-.1) == CastSafety::kUnderflow); + +static_assert(cast_safety<uint64_t>(static_cast<float>(max<int64_t>)) == CastSafety::kSafe); +static_assert(cast_safety<uint64_t>(static_cast<float>(max<uint64_t>)) == CastSafety::kOverflow); +static_assert(cast_safety<uint64_t>(static_cast<double>(max<int64_t>)) == CastSafety::kSafe); +static_assert(cast_safety<uint64_t>(static_cast<double>(max<uint64_t>)) == CastSafety::kOverflow); + +// Float to signed. + +static_assert(cast_safety<int32_t>(0.f) == CastSafety::kSafe); +static_assert(cast_safety<int32_t>(min<float>) == CastSafety::kUnderflow); +static_assert(cast_safety<int32_t>(max<float>) == CastSafety::kOverflow); + +static_assert(cast_safety<int16_t>(-inf<double>) == CastSafety::kUnderflow); +static_assert(cast_safety<int32_t>(inf<double>) == CastSafety::kOverflow); +static_assert(cast_safety<int64_t>(NaN<double>) == CastSafety::kOverflow); + +static_assert(cast_safety<int32_t>(static_cast<float>(min<int32_t>)) == CastSafety::kSafe); +static_assert(cast_safety<int32_t>(static_cast<float>(max<int32_t>)) == CastSafety::kOverflow); +static_assert(cast_safety<int32_t>(static_cast<double>(min<int32_t>)) == CastSafety::kSafe); +static_assert(cast_safety<int32_t>(static_cast<double>(max<int32_t>)) == CastSafety::kSafe); + +static_assert(cast_safety<int64_t>(0.0) == CastSafety::kSafe); +static_assert(cast_safety<int64_t>(min<double>) == CastSafety::kUnderflow); +static_assert(cast_safety<int64_t>(max<double>) == CastSafety::kOverflow); + +static_assert(cast_safety<int64_t>(static_cast<float>(min<int64_t>)) == CastSafety::kSafe); +static_assert(cast_safety<int64_t>(static_cast<float>(max<int64_t>)) == CastSafety::kOverflow); +static_assert(cast_safety<int64_t>(static_cast<double>(min<int64_t>)) == CastSafety::kSafe); +static_assert(cast_safety<int64_t>(static_cast<double>(max<int64_t>)) == CastSafety::kOverflow); + +TEST(CastSafety, FloatToSigned) { + constexpr int32_t kMax = ftl::details::safe_limits<int32_t, float>::max(); + static_assert(kMax == 2'147'483'520); + EXPECT_EQ(kMax, static_cast<int32_t>(std::nexttowardf(max<int32_t>, 0))); + + EXPECT_EQ(cast_safety<int32_t>(std::nexttowardf(min<int32_t>, 0)), CastSafety::kSafe); + EXPECT_EQ(cast_safety<int32_t>(std::nexttowardf(max<int32_t>, 0)), CastSafety::kSafe); + EXPECT_EQ(cast_safety<int64_t>(std::nexttoward(min<int64_t>, 0)), CastSafety::kSafe); + EXPECT_EQ(cast_safety<int64_t>(std::nexttoward(max<int64_t>, 0)), CastSafety::kSafe); + + EXPECT_EQ(cast_safety<int32_t>(std::nexttowardf(min<int32_t>, min<float>)), + CastSafety::kUnderflow); + EXPECT_EQ(cast_safety<int32_t>(std::nexttowardf(max<int32_t>, max<float>)), + CastSafety::kOverflow); + EXPECT_EQ(cast_safety<int64_t>(std::nexttoward(min<int64_t>, min<double>)), + CastSafety::kUnderflow); + EXPECT_EQ(cast_safety<int64_t>(std::nexttoward(max<int64_t>, max<double>)), + CastSafety::kOverflow); +} + +} // namespace android::test |