From d111fc301c58b6b1ba8311c142e9087e7e35b10e Mon Sep 17 00:00:00 2001 From: Charlie Vigue Date: Wed, 22 Feb 2023 19:58:08 +0000 Subject: [PATCH] Add numeric limiting headers and tests This commit adds two useful numeric limiting functions in two headers plus a third supporting header and unit tests. The unit tests cover all code paths and many conditions but may not be 100% complete from a viewpoint of covering all edge cases. Signed-off-by: Charlie Vigue --- openvpn/common/clamp_typerange.hpp | 70 ++++++++++++++++ openvpn/common/numeric_cast.hpp | 91 ++++++++++++++++++++ openvpn/common/numeric_util.hpp | 63 ++++++++++++++ test/unittests/CMakeLists.txt | 2 + test/unittests/test_clamp_typerange.cpp | 106 ++++++++++++++++++++++++ test/unittests/test_numeric_cast.cpp | 104 +++++++++++++++++++++++ 6 files changed, 436 insertions(+) create mode 100644 openvpn/common/clamp_typerange.hpp create mode 100644 openvpn/common/numeric_cast.hpp create mode 100644 openvpn/common/numeric_util.hpp create mode 100644 test/unittests/test_clamp_typerange.cpp create mode 100644 test/unittests/test_numeric_cast.cpp diff --git a/openvpn/common/clamp_typerange.hpp b/openvpn/common/clamp_typerange.hpp new file mode 100644 index 00000000..ced13703 --- /dev/null +++ b/openvpn/common/clamp_typerange.hpp @@ -0,0 +1,70 @@ +// OpenVPN -- An application to securely tunnel IP networks +// over a single port, with support for SSL/TLS-based +// session authentication and key exchange, +// packet encryption, packet authentication, and +// packet compression. +// +// Copyright (C) 2023 OpenVPN Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License Version 3 +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program in the COPYING file. +// If not, see . + + +#pragma once + +#include +#include + +#include "numeric_util.hpp" + +namespace openvpn::numeric_util { + +/* ============================================================================================================= */ +// clamp_to_typerange +/* ============================================================================================================= */ + +/** + * @brief Clamps the input value to the legal range for the output type + * + * @tparam OutT Output type + * @tparam InT Input type + * @param inVal Input value + * @return OutT safely converted from InT, range limited to fit. + */ +template +OutT clamp_to_typerange(InT inVal) +{ + if constexpr (numeric_util::is_int_rangesafe()) + { + return static_cast(inVal); + } + else if constexpr (numeric_util::is_int_u2s()) + { + auto unsignedInVal = static_cast(inVal); + return static_cast(std::min(static_cast(std::numeric_limits::max()), unsignedInVal)); + } + else if constexpr (numeric_util::is_int_s2u()) + { + auto lowerVal = static_cast(std::max(inVal, 0)); + auto upperLimit = static_cast(std::numeric_limits::max()); + return static_cast(std::min(lowerVal, upperLimit)); + } + else + { + auto outMin = static_cast(std::numeric_limits::min()); + auto outMax = static_cast(std::numeric_limits::max()); + return static_cast(std::clamp(inVal, outMin, outMax)); + } +} + +} // namespace openvpn::numeric_util diff --git a/openvpn/common/numeric_cast.hpp b/openvpn/common/numeric_cast.hpp new file mode 100644 index 00000000..00e42dba --- /dev/null +++ b/openvpn/common/numeric_cast.hpp @@ -0,0 +1,91 @@ +// OpenVPN -- An application to securely tunnel IP networks +// over a single port, with support for SSL/TLS-based +// session authentication and key exchange, +// packet encryption, packet authentication, and +// packet compression. +// +// Copyright (C) 2023 OpenVPN Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License Version 3 +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program in the COPYING file. +// If not, see . + + +#pragma once + +#include "numeric_util.hpp" + +#include + +#include // For OPENVPN_EXCEPTION_INHERIT + +/*** + * @brief Exception type for numeric conversion failures + */ +OPENVPN_EXCEPTION_INHERIT(std::range_error, numeric_out_of_range); + +namespace openvpn::numeric_util { + +/* ============================================================================================================= */ +// numeric_cast +/* ============================================================================================================= */ + +/** + * @brief Tests attempted casts to ensure the input value does not exceed the capacity of the output type + * + * If the types are the same, or the range of the output type equals or exceeds the range of the input type + * we just cast and return the value which should ideally optimize away completely. Otherwise we do appropriate + * range checks and if those succeed we cast, otherwise the failure exception openvpn::numeric_out_of_range + * is thrown. + * + * Example: + * + * int64_t s64 = std::numeric_limits::max(); + * EXPECT_THROW(numeric_cast(s64), numeric_out_of_range); + * + * @param inVal The value to be converted. + * @return The safely converted inVal. + * @tparam InT Source (input) type, inferred from 'inVal' + * @tparam OutT Desired result type + */ +template +OutT numeric_cast(InT inVal) +{ + if constexpr (!numeric_util::is_int_rangesafe() && numeric_util::is_int_u2s()) + { + // Conversion to uintmax_t should be safe for ::max() in all integral cases + if (static_cast(inVal) > static_cast(std::numeric_limits::max())) + { + throw numeric_out_of_range("Range exceeded for unsigned --> signed integer conversion"); + } + } + else if constexpr (!numeric_util::is_int_rangesafe() && numeric_util::is_int_s2u()) + { + // Cast to uintmax_t only applied if inVal is positive ... + if (inVal < 0 || static_cast(inVal) > static_cast(std::numeric_limits::max())) + { + throw numeric_out_of_range("Range exceeded for signed --> unsigned integer conversion"); + } + } + else if constexpr (!numeric_util::is_int_rangesafe()) + { + // We already know the in and out are sign compatible + if (std::numeric_limits::min() > inVal || std::numeric_limits::max() < inVal) + { + throw numeric_out_of_range("Range exceeded for integer conversion"); + } + } + + return static_cast(inVal); +} + +} // namespace openvpn::numeric_util \ No newline at end of file diff --git a/openvpn/common/numeric_util.hpp b/openvpn/common/numeric_util.hpp new file mode 100644 index 00000000..658ee5ac --- /dev/null +++ b/openvpn/common/numeric_util.hpp @@ -0,0 +1,63 @@ +// OpenVPN -- An application to securely tunnel IP networks +// over a single port, with support for SSL/TLS-based +// session authentication and key exchange, +// packet encryption, packet authentication, and +// packet compression. +// +// Copyright (C) 2023 OpenVPN Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License Version 3 +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program in the COPYING file. +// If not, see . + + +#pragma once + +#include +#include +#include + + +namespace openvpn::numeric_util { + +// Evaluates true if both template args are integral. +template +constexpr bool is_int_conversion() +{ + return std::is_integral_v && std::is_integral_v; +} + +// Returns true if the in param is an unsigned integral type and out param is a signed integral type. +template +constexpr bool is_int_u2s() +{ + return is_int_conversion() && std::is_unsigned_v && std::is_signed_v; +} + +// Returns true if the in param is a signed integral type and out param is an unsigned integral type. +template +constexpr bool is_int_s2u() +{ + return is_int_conversion() && std::is_signed_v && std::is_unsigned_v; +} + +// Returns true if both args are integral and the range of OutT can contain the range of InT +template +constexpr bool is_int_rangesafe() +{ + constexpr auto out_digits = std::numeric_limits::digits; + constexpr auto in_digits = std::numeric_limits::digits; + + return is_int_conversion() && !is_int_s2u() && out_digits >= in_digits; +} + +} // namespace openvpn::numeric_util \ No newline at end of file diff --git a/test/unittests/CMakeLists.txt b/test/unittests/CMakeLists.txt index e0e9eeb0..9ae6bb2e 100644 --- a/test/unittests/CMakeLists.txt +++ b/test/unittests/CMakeLists.txt @@ -55,12 +55,14 @@ add_executable(coreUnitTests test_continuation.cpp test_crypto.cpp test_optfilt.cpp + test_clamp_typerange.cpp test_pktstream.cpp test_remotelist.cpp test_relack.cpp test_http_proxy.cpp test_peer_fingerprint.cpp test_safestr.cpp + test_numeric_cast.cpp test_dns.cpp test_header_deps.cpp test_capture.cpp diff --git a/test/unittests/test_clamp_typerange.cpp b/test/unittests/test_clamp_typerange.cpp new file mode 100644 index 00000000..6e4d4d85 --- /dev/null +++ b/test/unittests/test_clamp_typerange.cpp @@ -0,0 +1,106 @@ +// OpenVPN -- An application to securely tunnel IP networks +// over a single port, with support for SSL/TLS-based +// session authentication and key exchange, +// packet encryption, packet authentication, and +// packet compression. +// +// Copyright (C) 2023 OpenVPN Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License Version 3 +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program in the COPYING file. +// If not, see . + + +#include "test_common.h" + +#include + +#include + +using namespace openvpn::numeric_util; + +TEST(clamp_to_typerange, same_type_nocast1) +{ + int32_t i32 = -1; + auto result = clamp_to_typerange(i32); + EXPECT_EQ(result, i32); +} + +TEST(clamp_to_typerange, sign_mismatch_32_1) +{ + int32_t i32 = -1; + auto result = clamp_to_typerange(i32); + EXPECT_EQ(result, 0); +} + +TEST(clamp_to_typerange, sign_mismatch_32_2) +{ + uint32_t u32 = std::numeric_limits::max(); + auto result = clamp_to_typerange(u32); + EXPECT_EQ(result, std::numeric_limits::max()); +} + +TEST(clamp_to_typerange, sign_mismatch_32_3) +{ + uint32_t u32 = 0; + auto result = clamp_to_typerange(u32); + EXPECT_EQ(result, 0); +} + +TEST(clamp_to_typerange, sign_mismatch_32_4) +{ + uint32_t u32 = 42; + auto result = clamp_to_typerange(u32); + EXPECT_EQ(result, 42); +} + +TEST(clamp_to_typerange, sign_mismatch_32_5) +{ + uint32_t u32 = uint32_t(std::numeric_limits::max()); + auto result = clamp_to_typerange(u32); + EXPECT_EQ(result, std::numeric_limits::max()); +} + +TEST(clamp_to_typerange, sign_mismatch_32_6) +{ + int32_t s32 = std::numeric_limits::max(); + auto result = clamp_to_typerange(s32); + EXPECT_EQ(result, std::numeric_limits::max()); +} + +TEST(clamp_to_typerange, sign_mismatch_32_7) +{ + int32_t s32 = 42; + auto result = clamp_to_typerange(s32); + EXPECT_EQ(result, 42); +} + +TEST(clamp_to_typerange, s_range_mismatch_16_64_1) +{ + int64_t s64 = std::numeric_limits::max(); + auto result = clamp_to_typerange(s64); + EXPECT_EQ(result, std::numeric_limits::max()); +} + +TEST(clamp_to_typerange, s_range_match_16_64_1) +{ + int64_t s64 = 0; + auto result = clamp_to_typerange(s64); + EXPECT_EQ(result, 0); +} + +TEST(clamp_to_typerange, u_range_mismatch_16_64_1) +{ + uint64_t u64 = std::numeric_limits::max(); + auto result = clamp_to_typerange(u64); + EXPECT_EQ(result, std::numeric_limits::max()); +} diff --git a/test/unittests/test_numeric_cast.cpp b/test/unittests/test_numeric_cast.cpp new file mode 100644 index 00000000..913eb333 --- /dev/null +++ b/test/unittests/test_numeric_cast.cpp @@ -0,0 +1,104 @@ +// OpenVPN -- An application to securely tunnel IP networks +// over a single port, with support for SSL/TLS-based +// session authentication and key exchange, +// packet encryption, packet authentication, and +// packet compression. +// +// Copyright (C) 2023 OpenVPN Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License Version 3 +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program in the COPYING file. +// If not, see . + + +#include "test_common.h" + +#include +#include + +#include + + +using namespace openvpn::numeric_util; + + +TEST(numeric_cast, same_type_nocast1) +{ + int32_t i32 = -1; + auto result = numeric_cast(i32); + EXPECT_EQ(result, i32); +} + +TEST(numeric_cast, sign_mismatch_32_1) +{ + int32_t i32 = -1; + EXPECT_THROW(numeric_cast(i32), numeric_out_of_range); +} + +TEST(numeric_cast, sign_mismatch_32_2) +{ + uint32_t u32 = std::numeric_limits::max(); + EXPECT_THROW(numeric_cast(u32), numeric_out_of_range); +} + +TEST(numeric_cast, sign_mismatch_32_3) +{ + uint32_t u32 = 0; + auto result = numeric_cast(u32); + EXPECT_EQ(result, 0); +} + +TEST(numeric_cast, sign_mismatch_32_4) +{ + uint32_t u32 = 42; + auto result = numeric_cast(u32); + EXPECT_EQ(result, 42); +} + +TEST(numeric_cast, sign_mismatch_32_5) +{ + uint32_t u32 = uint32_t(std::numeric_limits::max()); + auto result = numeric_cast(u32); + EXPECT_EQ(result, std::numeric_limits::max()); +} + +TEST(numeric_cast, sign_mismatch_32_6) +{ + int32_t s32 = std::numeric_limits::max(); + EXPECT_THROW(numeric_cast(s32), numeric_out_of_range); +} + +TEST(numeric_cast, sign_mismatch_32_7) +{ + int32_t s32 = 42; + auto result = numeric_cast(s32); + EXPECT_EQ(result, 42); +} + +TEST(numeric_cast, s_range_mismatch_16_64_1) +{ + int64_t s64 = std::numeric_limits::max(); + EXPECT_THROW(numeric_cast(s64), numeric_out_of_range); +} + +TEST(numeric_cast, s_range_match_16_64_1) +{ + int64_t s64 = 0; + auto result = numeric_cast(s64); + EXPECT_EQ(result, 0); +} + +TEST(numeric_cast, u_range_mismatch_16_64_1) +{ + uint64_t u64 = std::numeric_limits::max(); + EXPECT_THROW(numeric_cast(u64), numeric_out_of_range); +}