Skip to content

Commit 7fcaa82

Browse files
author
MarcoFalke
committed
Merge #18009: tests: Add fuzzing harness for strprintf(…)
cc668d0 tests: Add fuzzing harness for strprintf(...) (practicalswift) ccc3c76 tests: Add fuzzer strprintf to FUZZERS_MISSING_CORPORA (temporarily) (practicalswift) 6ef0491 tests: Update FuzzedDataProvider.h from upstream (LLVM) (practicalswift) Pull request description: Add fuzzing harness for `strprintf(…)`. Update `FuzzedDataProvider.h`. Avoid hitting some issues in tinyformat (reported upstreams in c42f/tinyformat#70). --- Found issues in tinyformat: **Issue 1.** The following causes a signed integer overflow followed by an allocation of 9 GB of RAM (or an OOM in memory constrained environments): ``` strprintf("%.777777700000000$", 1.0); ``` **Issue 2.** The following causes a stack overflow: ``` strprintf("%987654321000000:", 1); ``` **Issue 3.** The following causes a stack overflow: ``` strprintf("%1$*1$*", -11111111); ``` **Issue 4.** The following causes a `NULL` pointer dereference: ``` strprintf("%.1s", (char *)nullptr); ``` **Issue 5.** The following causes a float cast overflow: ``` strprintf("%c", -1000.0); ``` **Issue 6.** The following causes a float cast overflow followed by an invalid integer negation: ``` strprintf("%*", std::numeric_limits<double>::lowest()); ``` Top commit has no ACKs. Tree-SHA512: 9b765559281470f4983eb5aeca94bab1b15ec9837c0ee01a20f4348e9335e4ee4e4fecbd7a1a5a8ac96aabe0f9eeb597b8fc9a2c8faf1bab386e8225d5cdbc18
2 parents 0130abb + cc668d0 commit 7fcaa82

File tree

5 files changed

+228
-13
lines changed

5 files changed

+228
-13
lines changed

src/Makefile.test.include

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ FUZZ_TARGETS = \
5454
test/fuzz/script_flags \
5555
test/fuzz/service_deserialize \
5656
test/fuzz/spanparsing \
57+
test/fuzz/strprintf \
5758
test/fuzz/sub_net_deserialize \
5859
test/fuzz/transaction \
5960
test/fuzz/tx_in \
@@ -536,6 +537,12 @@ test_fuzz_spanparsing_LDADD = $(FUZZ_SUITE_LD_COMMON)
536537
test_fuzz_spanparsing_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS)
537538
test_fuzz_spanparsing_SOURCES = $(FUZZ_SUITE) test/fuzz/spanparsing.cpp
538539

540+
test_fuzz_strprintf_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES)
541+
test_fuzz_strprintf_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
542+
test_fuzz_strprintf_LDADD = $(FUZZ_SUITE_LD_COMMON)
543+
test_fuzz_strprintf_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS)
544+
test_fuzz_strprintf_SOURCES = $(FUZZ_SUITE) test/fuzz/strprintf.cpp
545+
539546
test_fuzz_sub_net_deserialize_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) -DSUB_NET_DESERIALIZE=1
540547
test_fuzz_sub_net_deserialize_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
541548
test_fuzz_sub_net_deserialize_LDADD = $(FUZZ_SUITE_LD_COMMON)

src/test/fuzz/FuzzedDataProvider.h

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,21 @@
1313
#ifndef LLVM_FUZZER_FUZZED_DATA_PROVIDER_H_
1414
#define LLVM_FUZZER_FUZZED_DATA_PROVIDER_H_
1515

16-
#include <limits.h>
17-
#include <stddef.h>
18-
#include <stdint.h>
19-
2016
#include <algorithm>
17+
#include <climits>
18+
#include <cstddef>
19+
#include <cstdint>
2120
#include <cstring>
2221
#include <initializer_list>
2322
#include <string>
2423
#include <type_traits>
2524
#include <utility>
2625
#include <vector>
2726

27+
// In addition to the comments below, the API is also briefly documented at
28+
// https://github.com/google/fuzzing/blob/master/docs/split-inputs.md#fuzzed-data-provider
2829
class FuzzedDataProvider {
29-
public:
30+
public:
3031
// |data| is an array of length |size| that the FuzzedDataProvider wraps to
3132
// provide more granular access. |data| must outlive the FuzzedDataProvider.
3233
FuzzedDataProvider(const uint8_t *data, size_t size)
@@ -143,9 +144,9 @@ class FuzzedDataProvider {
143144
return ConsumeBytes<T>(remaining_bytes_);
144145
}
145146

147+
// Returns a std::string containing all remaining bytes of the input data.
146148
// Prefer using |ConsumeRemainingBytes| unless you actually need a std::string
147149
// object.
148-
// Returns a std::vector containing all remaining bytes of the input data.
149150
std::string ConsumeRemainingBytesAsString() {
150151
return ConsumeBytesAsString(remaining_bytes_);
151152
}
@@ -161,7 +162,7 @@ class FuzzedDataProvider {
161162
// Reads one byte and returns a bool, or false when no data remains.
162163
bool ConsumeBool() { return 1 & ConsumeIntegral<uint8_t>(); }
163164

164-
// Returns a copy of a value selected from a fixed-size |array|.
165+
// Returns a copy of the value selected from the given fixed-size |array|.
165166
template <typename T, size_t size>
166167
T PickValueInArray(const T (&array)[size]) {
167168
static_assert(size > 0, "The array must be non empty.");
@@ -170,11 +171,14 @@ class FuzzedDataProvider {
170171

171172
template <typename T>
172173
T PickValueInArray(std::initializer_list<const T> list) {
173-
// static_assert(list.size() > 0, "The array must be non empty.");
174+
// TODO(Dor1s): switch to static_assert once C++14 is allowed.
175+
if (!list.size())
176+
abort();
177+
174178
return *(list.begin() + ConsumeIntegralInRange<size_t>(0, list.size() - 1));
175179
}
176180

177-
// Return an enum value. The enum must start at 0 and be contiguous. It must
181+
// Returns an enum value. The enum must start at 0 and be contiguous. It must
178182
// also contain |kMaxValue| aliased to its largest (inclusive) value. Such as:
179183
// enum class Foo { SomeValue, OtherValue, kMaxValue = OtherValue };
180184
template <typename T> T ConsumeEnum() {
@@ -183,10 +187,60 @@ class FuzzedDataProvider {
183187
0, static_cast<uint32_t>(T::kMaxValue)));
184188
}
185189

190+
// Returns a floating point number in the range [0.0, 1.0]. If there's no
191+
// input data left, always returns 0.
192+
template <typename T> T ConsumeProbability() {
193+
static_assert(std::is_floating_point<T>::value,
194+
"A floating point type is required.");
195+
196+
// Use different integral types for different floating point types in order
197+
// to provide better density of the resulting values.
198+
using IntegralType =
199+
typename std::conditional<(sizeof(T) <= sizeof(uint32_t)), uint32_t,
200+
uint64_t>::type;
201+
202+
T result = static_cast<T>(ConsumeIntegral<IntegralType>());
203+
result /= static_cast<T>(std::numeric_limits<IntegralType>::max());
204+
return result;
205+
}
206+
207+
// Returns a floating point value in the range [Type's lowest, Type's max] by
208+
// consuming bytes from the input data. If there's no input data left, always
209+
// returns approximately 0.
210+
template <typename T> T ConsumeFloatingPoint() {
211+
return ConsumeFloatingPointInRange<T>(std::numeric_limits<T>::lowest(),
212+
std::numeric_limits<T>::max());
213+
}
214+
215+
// Returns a floating point value in the given range by consuming bytes from
216+
// the input data. If there's no input data left, returns |min|. Note that
217+
// |min| must be less than or equal to |max|.
218+
template <typename T> T ConsumeFloatingPointInRange(T min, T max) {
219+
if (min > max)
220+
abort();
221+
222+
T range = .0;
223+
T result = min;
224+
constexpr T zero(.0);
225+
if (max > zero && min < zero && max > min + std::numeric_limits<T>::max()) {
226+
// The diff |max - min| would overflow the given floating point type. Use
227+
// the half of the diff as the range and consume a bool to decide whether
228+
// the result is in the first of the second part of the diff.
229+
range = (max / 2.0) - (min / 2.0);
230+
if (ConsumeBool()) {
231+
result += range;
232+
}
233+
} else {
234+
range = max - min;
235+
}
236+
237+
return result + range * ConsumeProbability<T>();
238+
}
239+
186240
// Reports the remaining bytes available for fuzzed input.
187241
size_t remaining_bytes() { return remaining_bytes_; }
188242

189-
private:
243+
private:
190244
FuzzedDataProvider(const FuzzedDataProvider &) = delete;
191245
FuzzedDataProvider &operator=(const FuzzedDataProvider &) = delete;
192246

@@ -209,6 +263,12 @@ class FuzzedDataProvider {
209263
// which seems to be a natural choice for other implementations as well.
210264
// To increase the odds even more, we also call |shrink_to_fit| below.
211265
std::vector<T> result(size);
266+
if (size == 0) {
267+
if (num_bytes_to_consume != 0)
268+
abort();
269+
return result;
270+
}
271+
212272
std::memcpy(result.data(), data_ptr_, num_bytes_to_consume);
213273
Advance(num_bytes_to_consume);
214274

@@ -230,9 +290,9 @@ class FuzzedDataProvider {
230290

231291
// Avoid using implementation-defined unsigned to signer conversions.
232292
// To learn more, see https://stackoverflow.com/questions/13150449.
233-
if (value <= std::numeric_limits<TS>::max())
293+
if (value <= std::numeric_limits<TS>::max()) {
234294
return static_cast<TS>(value);
235-
else {
295+
} else {
236296
constexpr auto TS_min = std::numeric_limits<TS>::min();
237297
return TS_min + static_cast<char>(value - TS_min);
238298
}

src/test/fuzz/strprintf.cpp

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright (c) 2020 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <test/fuzz/FuzzedDataProvider.h>
6+
#include <test/fuzz/fuzz.h>
7+
#include <tinyformat.h>
8+
#include <util/strencodings.h>
9+
10+
#include <algorithm>
11+
#include <cassert>
12+
#include <cstdint>
13+
#include <string>
14+
#include <vector>
15+
16+
void test_one_input(const std::vector<uint8_t>& buffer)
17+
{
18+
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
19+
const std::string format_string = fuzzed_data_provider.ConsumeRandomLengthString(64);
20+
21+
const int digits_in_format_specifier = std::count_if(format_string.begin(), format_string.end(), IsDigit);
22+
23+
// Avoid triggering the following crash bug:
24+
// * strprintf("%987654321000000:", 1);
25+
//
26+
// Avoid triggering the following OOM bug:
27+
// * strprintf("%.222222200000000$", 1.1);
28+
//
29+
// Upstream bug report: https://github.com/c42f/tinyformat/issues/70
30+
if (format_string.find("%") != std::string::npos && digits_in_format_specifier >= 7) {
31+
return;
32+
}
33+
34+
// Avoid triggering the following crash bug:
35+
// * strprintf("%1$*1$*", -11111111);
36+
//
37+
// Upstream bug report: https://github.com/c42f/tinyformat/issues/70
38+
if (format_string.find("%") != std::string::npos && format_string.find("$") != std::string::npos && format_string.find("*") != std::string::npos && digits_in_format_specifier > 0) {
39+
return;
40+
}
41+
42+
// Avoid triggering the following crash bug:
43+
// * strprintf("%.1s", (char*)nullptr);
44+
//
45+
// (void)strprintf(format_string, (char*)nullptr);
46+
//
47+
// Upstream bug report: https://github.com/c42f/tinyformat/issues/70
48+
49+
try {
50+
(void)strprintf(format_string, (signed char*)nullptr);
51+
} catch (const tinyformat::format_error&) {
52+
}
53+
try {
54+
(void)strprintf(format_string, (unsigned char*)nullptr);
55+
} catch (const tinyformat::format_error&) {
56+
}
57+
try {
58+
(void)strprintf(format_string, (void*)nullptr);
59+
} catch (const tinyformat::format_error&) {
60+
}
61+
try {
62+
(void)strprintf(format_string, (bool*)nullptr);
63+
} catch (const tinyformat::format_error&) {
64+
}
65+
try {
66+
(void)strprintf(format_string, (float*)nullptr);
67+
} catch (const tinyformat::format_error&) {
68+
}
69+
try {
70+
(void)strprintf(format_string, (double*)nullptr);
71+
} catch (const tinyformat::format_error&) {
72+
}
73+
try {
74+
(void)strprintf(format_string, (int16_t*)nullptr);
75+
} catch (const tinyformat::format_error&) {
76+
}
77+
try {
78+
(void)strprintf(format_string, (uint16_t*)nullptr);
79+
} catch (const tinyformat::format_error&) {
80+
}
81+
try {
82+
(void)strprintf(format_string, (int32_t*)nullptr);
83+
} catch (const tinyformat::format_error&) {
84+
}
85+
try {
86+
(void)strprintf(format_string, (uint32_t*)nullptr);
87+
} catch (const tinyformat::format_error&) {
88+
}
89+
try {
90+
(void)strprintf(format_string, (int64_t*)nullptr);
91+
} catch (const tinyformat::format_error&) {
92+
}
93+
try {
94+
(void)strprintf(format_string, (uint64_t*)nullptr);
95+
} catch (const tinyformat::format_error&) {
96+
}
97+
98+
try {
99+
switch (fuzzed_data_provider.ConsumeIntegralInRange(0, 13)) {
100+
case 0:
101+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeRandomLengthString(32));
102+
break;
103+
case 1:
104+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeRandomLengthString(32).c_str());
105+
break;
106+
case 2:
107+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<signed char>());
108+
break;
109+
case 3:
110+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<unsigned char>());
111+
break;
112+
case 4:
113+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<char>());
114+
break;
115+
case 5:
116+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeBool());
117+
break;
118+
case 6:
119+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeFloatingPoint<float>());
120+
break;
121+
case 7:
122+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeFloatingPoint<double>());
123+
break;
124+
case 8:
125+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<int16_t>());
126+
break;
127+
case 9:
128+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<uint16_t>());
129+
break;
130+
case 10:
131+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<int32_t>());
132+
break;
133+
case 11:
134+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<uint32_t>());
135+
break;
136+
case 12:
137+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<int64_t>());
138+
break;
139+
case 13:
140+
(void)strprintf(format_string, fuzzed_data_provider.ConsumeIntegral<uint64_t>());
141+
break;
142+
default:
143+
assert(false);
144+
}
145+
} catch (const tinyformat::format_error&) {
146+
}
147+
}

test/fuzz/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"psbt_output_deserialize",
4040
"pub_key_deserialize",
4141
"script_deserialize",
42+
"strprintf",
4243
"sub_net_deserialize",
4344
"tx_in",
4445
"tx_in_deserialize",

test/lint/lint-format-strings.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ if ! python3 -m doctest test/lint/lint-format-strings.py; then
3434
fi
3535
for S in "${FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS[@]}"; do
3636
IFS="," read -r FUNCTION_NAME SKIP_ARGUMENTS <<< "${S}"
37-
for MATCHING_FILE in $(git grep --full-name -l "${FUNCTION_NAME}" -- "*.c" "*.cpp" "*.h" | sort | grep -vE "^src/(leveldb|secp256k1|tinyformat|univalue)"); do
37+
for MATCHING_FILE in $(git grep --full-name -l "${FUNCTION_NAME}" -- "*.c" "*.cpp" "*.h" | sort | grep -vE "^src/(leveldb|secp256k1|tinyformat|univalue|test/fuzz/strprintf.cpp)"); do
3838
MATCHING_FILES+=("${MATCHING_FILE}")
3939
done
4040
if ! test/lint/lint-format-strings.py --skip-arguments "${SKIP_ARGUMENTS}" "${FUNCTION_NAME}" "${MATCHING_FILES[@]}"; then

0 commit comments

Comments
 (0)