diff --git a/.arclint b/.arclint --- a/.arclint +++ b/.arclint @@ -41,7 +41,8 @@ "type": "lint-format-strings", "include": "(^src/.*\\.(h|c|cpp)$)", "exclude": [ - "(^src/(secp256k1|univalue|leveldb)/)" + "(^src/(secp256k1|univalue|leveldb)/)", + "(^src/test/fuzz/strprintf.cpp$)" ] }, "check-doc": { diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt --- a/src/test/fuzz/CMakeLists.txt +++ b/src/test/fuzz/CMakeLists.txt @@ -57,6 +57,7 @@ script script_flags spanparsing + strprintf timedata transaction ) diff --git a/src/test/fuzz/FuzzedDataProvider.h b/src/test/fuzz/FuzzedDataProvider.h --- a/src/test/fuzz/FuzzedDataProvider.h +++ b/src/test/fuzz/FuzzedDataProvider.h @@ -13,11 +13,10 @@ #ifndef LLVM_FUZZER_FUZZED_DATA_PROVIDER_H_ #define LLVM_FUZZER_FUZZED_DATA_PROVIDER_H_ +#include #include #include #include - -#include #include #include #include @@ -25,6 +24,8 @@ #include #include +// In addition to the comments below, the API is also briefly documented at +// https://github.com/google/fuzzing/blob/master/docs/split-inputs.md#fuzzed-data-provider class FuzzedDataProvider { public: // |data| is an array of length |size| that the FuzzedDataProvider wraps to @@ -84,7 +85,9 @@ static_assert(sizeof(T) <= sizeof(uint64_t), "Unsupported integral type."); - if (min > max) abort(); + if (min > max) { + abort(); + } // Use the biggest type possible to hold the range and the result. uint64_t range = static_cast(max) - min; @@ -105,8 +108,9 @@ } // Avoid division by 0, in case |range + 1| results in overflow. - if (range != std::numeric_limits::max()) + if (range != std::numeric_limits::max()) { result = result % (range + 1); + } return static_cast(min + result); } @@ -132,7 +136,9 @@ if (next == '\\' && remaining_bytes_ != 0) { next = ConvertUnsignedToSigned(data_ptr_[0]); Advance(1); - if (next != '\\') break; + if (next != '\\') { + break; + } } result += next; } @@ -146,6 +152,7 @@ return ConsumeBytes(remaining_bytes_); } + // Returns a std::string containing all remaining bytes of the input data. // Prefer using |ConsumeRemainingBytes| unless you actually need a // std::string object. Returns a std::vector containing all remaining bytes // of the input data. @@ -164,7 +171,7 @@ // Reads one byte and returns a bool, or false when no data remains. bool ConsumeBool() { return 1 & ConsumeIntegral(); } - // Returns a copy of a value selected from a fixed-size |array|. + // Returns a copy of the value selected from the given fixed-size |array|. template T PickValueInArray(const T (&array)[size]) { static_assert(size > 0, "The array must be non empty."); @@ -173,20 +180,79 @@ template T PickValueInArray(std::initializer_list list) { - // static_assert(list.size() > 0, "The array must be non empty."); + // Static assert won't work here: + // https://stackoverflow.com/questions/5438671/static-assert-on-initializer-listsize + if (!list.size()) { + abort(); + } + return *(list.begin() + ConsumeIntegralInRange(0, list.size() - 1)); } - // Return an enum value. The enum must start at 0 and be contiguous. It must - // also contain |kMaxValue| aliased to its largest (inclusive) value. Such - // as: enum class Foo { SomeValue, OtherValue, kMaxValue = OtherValue }; + // Returns an enum value. The enum must start at 0 and be contiguous. It + // must also contain |kMaxValue| aliased to its largest (inclusive) value. + // Such as: + // enum class Foo { SomeValue, OtherValue, kMaxValue = OtherValue }; template T ConsumeEnum() { static_assert(std::is_enum::value, "|T| must be an enum type."); return static_cast(ConsumeIntegralInRange( 0, static_cast(T::kMaxValue))); } + // Returns a floating point number in the range [0.0, 1.0]. If there's no + // input data left, always returns 0. + template T ConsumeProbability() { + static_assert(std::is_floating_point::value, + "A floating point type is required."); + + // Use different integral types for different floating point types in + // order to provide better density of the resulting values. + using IntegralType = + typename std::conditional<(sizeof(T) <= sizeof(uint32_t)), uint32_t, + uint64_t>::type; + + T result = static_cast(ConsumeIntegral()); + result /= static_cast(std::numeric_limits::max()); + return result; + } + + // Returns a floating point value in the range [Type's lowest, Type's max] + // by consuming bytes from the input data. If there's no input data left, + // always returns approximately 0. + template T ConsumeFloatingPoint() { + return ConsumeFloatingPointInRange(std::numeric_limits::lowest(), + std::numeric_limits::max()); + } + + // Returns a floating point value in the given range by consuming bytes from + // the input data. If there's no input data left, returns |min|. Note that + // |min| must be less than or equal to |max|. + template T ConsumeFloatingPointInRange(T min, T max) { + if (min > max) { + abort(); + } + + T range = .0; + T result = min; + constexpr T zero(.0); + if (max > zero && min < zero && + max > min + std::numeric_limits::max()) { + // The diff |max - min| would overflow the given floating point + // type. Use the half of the diff as the range and consume a bool to + // decide whether the result is in the first of the second part of + // the diff. + range = (max / 2.0) - (min / 2.0); + if (ConsumeBool()) { + result += range; + } + } else { + range = max - min; + } + + return result + range * ConsumeProbability(); + } + // Reports the remaining bytes available for fuzzed input. size_t remaining_bytes() { return remaining_bytes_; } @@ -195,7 +261,9 @@ FuzzedDataProvider &operator=(const FuzzedDataProvider &) = delete; void Advance(size_t num_bytes) { - if (num_bytes > remaining_bytes_) abort(); + if (num_bytes > remaining_bytes_) { + abort(); + } data_ptr_ += num_bytes; remaining_bytes_ -= num_bytes; @@ -213,6 +281,13 @@ // implementations as well. To increase the odds even more, we also call // |shrink_to_fit| below. std::vector result(size); + if (size == 0) { + if (num_bytes_to_consume != 0) { + abort(); + } + return result; + } + std::memcpy(result.data(), data_ptr_, num_bytes_to_consume); Advance(num_bytes_to_consume); @@ -230,13 +305,15 @@ "Source type must be unsigned."); // TODO(Dor1s): change to `if constexpr` once C++17 becomes mainstream. - if (std::numeric_limits::is_modulo) return static_cast(value); + if (std::numeric_limits::is_modulo) { + return static_cast(value); + } // Avoid using implementation-defined unsigned to signer conversions. // To learn more, see https://stackoverflow.com/questions/13150449. - if (value <= std::numeric_limits::max()) + if (value <= std::numeric_limits::max()) { return static_cast(value); - else { + } else { constexpr auto TS_min = std::numeric_limits::min(); return TS_min + static_cast(value - TS_min); } diff --git a/src/test/fuzz/strprintf.cpp b/src/test/fuzz/strprintf.cpp new file mode 100644 --- /dev/null +++ b/src/test/fuzz/strprintf.cpp @@ -0,0 +1,180 @@ +// Copyright (c) 2020 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +void test_one_input(const std::vector &buffer) { + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + const std::string format_string = + fuzzed_data_provider.ConsumeRandomLengthString(64); + + const int digits_in_format_specifier = + std::count_if(format_string.begin(), format_string.end(), IsDigit); + + // Avoid triggering the following crash bug: + // * strprintf("%987654321000000:", 1); + // + // Avoid triggering the following OOM bug: + // * strprintf("%.222222200000000$", 1.1); + // + // Upstream bug report: https://github.com/c42f/tinyformat/issues/70 + if (format_string.find("%") != std::string::npos && + digits_in_format_specifier >= 7) { + return; + } + + // Avoid triggering the following crash bug: + // * strprintf("%1$*1$*", -11111111); + // + // Upstream bug report: https://github.com/c42f/tinyformat/issues/70 + if (format_string.find("%") != std::string::npos && + format_string.find("$") != std::string::npos && + format_string.find("*") != std::string::npos && + digits_in_format_specifier > 0) { + return; + } + + // Avoid triggering the following crash bug: + // * strprintf("%.1s", (char*)nullptr); + // + // ()strprintf(format_string, (char*)nullptr); + // + // Upstream bug report: https://github.com/c42f/tinyformat/issues/70 + + try { + (void)strprintf(format_string, (signed char *)nullptr); + } catch (const tinyformat::format_error &) { + } + try { + (void)strprintf(format_string, (uint8_t *)nullptr); + } catch (const tinyformat::format_error &) { + } + try { + (void)strprintf(format_string, (void *)nullptr); + } catch (const tinyformat::format_error &) { + } + try { + (void)strprintf(format_string, (bool *)nullptr); + } catch (const tinyformat::format_error &) { + } + try { + (void)strprintf(format_string, (float *)nullptr); + } catch (const tinyformat::format_error &) { + } + try { + (void)strprintf(format_string, (double *)nullptr); + } catch (const tinyformat::format_error &) { + } + try { + (void)strprintf(format_string, (int16_t *)nullptr); + } catch (const tinyformat::format_error &) { + } + try { + (void)strprintf(format_string, (uint16_t *)nullptr); + } catch (const tinyformat::format_error &) { + } + try { + (void)strprintf(format_string, (int32_t *)nullptr); + } catch (const tinyformat::format_error &) { + } + try { + (void)strprintf(format_string, (uint32_t *)nullptr); + } catch (const tinyformat::format_error &) { + } + try { + (void)strprintf(format_string, (int64_t *)nullptr); + } catch (const tinyformat::format_error &) { + } + try { + (void)strprintf(format_string, (uint64_t *)nullptr); + } catch (const tinyformat::format_error &) { + } + + try { + switch (fuzzed_data_provider.ConsumeIntegralInRange(0, 13)) { + case 0: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeRandomLengthString(32)); + break; + case 1: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeRandomLengthString(32).c_str()); + break; + case 2: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeIntegral()); + break; + case 3: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeIntegral()); + break; + case 4: + (void)strprintf(format_string, + fuzzed_data_provider.ConsumeIntegral()); + break; + case 5: + (void)strprintf(format_string, + fuzzed_data_provider.ConsumeBool()); + break; + case 6: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeFloatingPoint()); + break; + case 7: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeFloatingPoint()); + break; + case 8: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeIntegral()); + break; + case 9: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeIntegral()); + break; + case 10: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeIntegral()); + break; + case 11: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeIntegral()); + break; + case 12: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeIntegral()); + break; + case 13: + (void)strprintf( + format_string, + fuzzed_data_provider.ConsumeIntegral()); + break; + default: + assert(false); + } + } catch (const tinyformat::format_error &) { + } +} diff --git a/test/fuzz/test_runner.py b/test/fuzz/test_runner.py --- a/test/fuzz/test_runner.py +++ b/test/fuzz/test_runner.py @@ -14,7 +14,9 @@ # Fuzzers known to lack a seed corpus in # https://github.com/Bitcoin-ABC/qa-assets/tree/master/fuzz_seed_corpus -FUZZERS_MISSING_CORPORA = [] +FUZZERS_MISSING_CORPORA = [ + "strprintf", +] def main():