Skip to content

Commit

Permalink
fuzz: limit the number of nested wrappers in descriptors
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
darosior committed Jul 11, 2024
1 parent 0d961c1 commit 63f62b5
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/test/fuzz/descriptor_parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ FUZZ_TARGET(mocked_descriptor_parse, .init = initialize_mocked_descriptor_parse)
// may perform quadratic operations on them. Limit the number of sub-fragments per fragment.
if (HasTooManySubFrag(buffer)) return;

// The script building logic performs quadratic copies in the number of nested wrappers. Limit
// the number of nested wrappers per fragment.
if (HasTooManyWrappers(buffer)) return;

const std::string mocked_descriptor{buffer.begin(), buffer.end()};
if (const auto descriptor = MOCKED_DESC_CONVERTER.GetDescriptor(mocked_descriptor)) {
FlatSigningProvider signing_provider;
Expand All @@ -90,6 +94,7 @@ FUZZ_TARGET(descriptor_parse, .init = initialize_descriptor_parse)
// See comments above for rationales.
if (HasDeepDerivPath(buffer)) return;
if (HasTooManySubFrag(buffer)) return;
if (HasTooManyWrappers(buffer)) return;

const std::string descriptor(buffer.begin(), buffer.end());
FlatSigningProvider signing_provider;
Expand Down
34 changes: 34 additions & 0 deletions src/test/fuzz/util/descriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

#include <ranges>
#include <stack>

void MockedDescriptorConverter::Init() {
Expand Down Expand Up @@ -113,3 +114,36 @@ bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs)
}
return false;
}

bool HasTooManyWrappers(const FuzzBufferType& buff, const int max_wrappers)
{
// The number of nested wrappers. Nested wrappers are always characters which follow each other so we don't have to
// use a stack as we do above when counting the number of sub-fragments.
std::optional<int> count;

// We want to detect nested wrappers. A wrapper is a character prepanded to a fragment, separated by a column. There
// may be more than one wrapper, in which case the colon is not repeated. For instance `jjjjj:pk()`. Since the
// miniscript parsing logic performs a number of copies quadratic in the number of wrappers, we want to limit too
// large such occurences. To do so we use the colon to detect the end of a wrapper expression and count how many
// characters there is since the beginning of the expression. We therefore iterate in reverse to start counting when
// we encounter a colon and stop once we encounter a character indicating the beginning of a new expression.
for (const auto ch: buff | std::views::reverse) {
// A colon, start counting.
if (ch == ':') {
// The colon itself is not a wrapper so we start at 0.
count = 0;
} else if (count) {
// If we are counting wrappers, stop when we crossed the beginning of the wrapper expression. Otherwise keep
// counting and bail if we reached the limit.
// A wrapper may only ever occur as the first sub of a descriptor/miniscript expression ('('), as the
// first Taproot leaf in a pair ('{') or as the nth sub in each case (',').
if (ch == ',' || ch == '(' || ch == '{') {
count.reset();
} else if (++*count > max_wrappers) {
return true;
}
}
}

return false;
}
9 changes: 9 additions & 0 deletions src/test/fuzz/util/descriptor.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,13 @@ constexpr int MAX_SUBS{1'000};
*/
bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs = MAX_SUBS);

//! Default maximum number of wrappers per fragment.
constexpr int MAX_WRAPPERS{100};

/**
* Whether the buffer, if it represents a valid descriptor, contains a fragment with more
* wrappers than the given maximum.
*/
bool HasTooManyWrappers(const FuzzBufferType& buff, const int max_wrappers = MAX_WRAPPERS);

#endif // BITCOIN_TEST_FUZZ_UTIL_DESCRIPTOR_H

0 comments on commit 63f62b5

Please sign in to comment.