Skip to content

Commit bc34bc2

Browse files
committed
fuzz: limit the number of nested wrappers in descriptors
The script building logic performs a quadratic number of copies in the number of nested wrappers in the miniscript. Limit the number of nested wrappers to avoid fuzz timeouts. Thanks to Marco Falke for reporting the fuzz timeouts and providing a minimal input to reproduce.
1 parent 8d73401 commit bc34bc2

File tree

3 files changed

+47
-0
lines changed

3 files changed

+47
-0
lines changed

src/test/fuzz/descriptor_parse.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ FUZZ_TARGET(mocked_descriptor_parse, .init = initialize_mocked_descriptor_parse)
7676
// may perform quadratic operations on them. Limit the number of sub-fragments per fragment.
7777
if (HasTooManySubFrag(buffer)) return;
7878

79+
// The script building logic performs quadratic copies in the number of nested wrappers. Limit
80+
// the number of nested wrappers per fragment.
81+
if (HasTooManyWrappers(buffer)) return;
82+
7983
const std::string mocked_descriptor{buffer.begin(), buffer.end()};
8084
if (const auto descriptor = MOCKED_DESC_CONVERTER.GetDescriptor(mocked_descriptor)) {
8185
FlatSigningProvider signing_provider;
@@ -90,6 +94,7 @@ FUZZ_TARGET(descriptor_parse, .init = initialize_descriptor_parse)
9094
// See comments above for rationales.
9195
if (HasDeepDerivPath(buffer)) return;
9296
if (HasTooManySubFrag(buffer)) return;
97+
if (HasTooManyWrappers(buffer)) return;
9398

9499
const std::string descriptor(buffer.begin(), buffer.end());
95100
FlatSigningProvider signing_provider;

src/test/fuzz/util/descriptor.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <test/fuzz/util/descriptor.h>
66

7+
#include <ranges>
78
#include <stack>
89

910
void MockedDescriptorConverter::Init() {
@@ -110,3 +111,35 @@ bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs, const siz
110111
}
111112
return false;
112113
}
114+
115+
bool HasTooManyWrappers(const FuzzBufferType& buff, const int max_wrappers)
116+
{
117+
// The number of nested wrappers. Nested wrappers are always characters which follow each other so we don't have to
118+
// use a stack as we do above when counting the number of sub-fragments.
119+
std::optional<int> count;
120+
121+
// We want to detect nested wrappers. A wrapper is a character prepended to a fragment, separated by a colon. There
122+
// may be more than one wrapper, in which case the colon is not repeated. For instance `jjjjj:pk()`. To count
123+
// wrappers we iterate in reverse and use the colon to detect the end of a wrapper expression and count how many
124+
// characters there are since the beginning of the expression. We stop counting when we encounter a character
125+
// indicating the beginning of a new expression.
126+
for (const auto ch: buff | std::views::reverse) {
127+
// A colon, start counting.
128+
if (ch == ':') {
129+
// The colon itself is not a wrapper so we start at 0.
130+
count = 0;
131+
} else if (count) {
132+
// If we are counting wrappers, stop when we crossed the beginning of the wrapper expression. Otherwise keep
133+
// counting and bail if we reached the limit.
134+
// A wrapper may only ever occur as the first sub of a descriptor/miniscript expression ('('), as the
135+
// first Taproot leaf in a pair ('{') or as the nth sub in each case (',').
136+
if (ch == ',' || ch == '(' || ch == '{') {
137+
count.reset();
138+
} else if (++*count > max_wrappers) {
139+
return true;
140+
}
141+
}
142+
}
143+
144+
return false;
145+
}

src/test/fuzz/util/descriptor.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,13 @@ constexpr size_t MAX_NESTED_SUBS{10'000};
6767
bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs = MAX_SUBS,
6868
const size_t max_nested_subs = MAX_NESTED_SUBS);
6969

70+
//! Default maximum number of wrappers per fragment.
71+
constexpr int MAX_WRAPPERS{100};
72+
73+
/**
74+
* Whether the buffer, if it represents a valid descriptor, contains a fragment with more
75+
* wrappers than the given maximum.
76+
*/
77+
bool HasTooManyWrappers(const FuzzBufferType& buff, const int max_wrappers = MAX_WRAPPERS);
78+
7079
#endif // BITCOIN_TEST_FUZZ_UTIL_DESCRIPTOR_H

0 commit comments

Comments
 (0)