aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/test/fuzz/descriptor_parse.cpp12
-rw-r--r--src/test/fuzz/util/descriptor.cpp59
-rw-r--r--src/test/fuzz/util/descriptor.h21
3 files changed, 91 insertions, 1 deletions
diff --git a/src/test/fuzz/descriptor_parse.cpp b/src/test/fuzz/descriptor_parse.cpp
index b9a5560ffb..6a3f4d6dfe 100644
--- a/src/test/fuzz/descriptor_parse.cpp
+++ b/src/test/fuzz/descriptor_parse.cpp
@@ -72,6 +72,14 @@ FUZZ_TARGET(mocked_descriptor_parse, .init = initialize_mocked_descriptor_parse)
// out strings which could correspond to a descriptor containing a too large derivation path.
if (HasDeepDerivPath(buffer)) return;
+ // Some fragments can take a virtually unlimited number of sub-fragments (thresh, multi_a) but
+ // 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;
@@ -83,8 +91,10 @@ FUZZ_TARGET(mocked_descriptor_parse, .init = initialize_mocked_descriptor_parse)
FUZZ_TARGET(descriptor_parse, .init = initialize_descriptor_parse)
{
- // See comment above for rationale.
+ // 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;
diff --git a/src/test/fuzz/util/descriptor.cpp b/src/test/fuzz/util/descriptor.cpp
index 0fed2bc5e1..9e52e990a2 100644
--- a/src/test/fuzz/util/descriptor.cpp
+++ b/src/test/fuzz/util/descriptor.cpp
@@ -4,6 +4,9 @@
#include <test/fuzz/util/descriptor.h>
+#include <ranges>
+#include <stack>
+
void MockedDescriptorConverter::Init() {
// The data to use as a private key or a seed for an xprv.
std::array<std::byte, 32> key_data{std::byte{1}};
@@ -84,3 +87,59 @@ bool HasDeepDerivPath(const FuzzBufferType& buff, const int max_depth)
}
return false;
}
+
+bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs, const size_t max_nested_subs)
+{
+ // We use a stack because there may be many nested sub-frags.
+ std::stack<int> counts;
+ for (const auto& ch: buff) {
+ // The fuzzer may generate an input with a ton of parentheses. Rule out pathological cases.
+ if (counts.size() > max_nested_subs) return true;
+
+ if (ch == '(') {
+ // A new fragment was opened, create a new sub-count for it and start as one since any fragment with
+ // parentheses has at least one sub.
+ counts.push(1);
+ } else if (ch == ',' && !counts.empty()) {
+ // When encountering a comma, account for an additional sub in the last opened fragment. If it exceeds the
+ // limit, bail.
+ if (++counts.top() > max_subs) return true;
+ } else if (ch == ')' && !counts.empty()) {
+ // Fragment closed! Drop its sub count and resume to counting the number of subs for its parent.
+ counts.pop();
+ }
+ }
+ 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 prepended to a fragment, separated by a colon. There
+ // may be more than one wrapper, in which case the colon is not repeated. For instance `jjjjj:pk()`. To count
+ // wrappers we iterate in reverse and use the colon to detect the end of a wrapper expression and count how many
+ // characters there are since the beginning of the expression. We stop counting when 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;
+}
diff --git a/src/test/fuzz/util/descriptor.h b/src/test/fuzz/util/descriptor.h
index cd41dbafa3..ea928c39f0 100644
--- a/src/test/fuzz/util/descriptor.h
+++ b/src/test/fuzz/util/descriptor.h
@@ -55,4 +55,25 @@ constexpr int MAX_DEPTH{2};
*/
bool HasDeepDerivPath(const FuzzBufferType& buff, const int max_depth = MAX_DEPTH);
+//! Default maximum number of sub-fragments.
+constexpr int MAX_SUBS{1'000};
+//! Maximum number of nested sub-fragments we'll allow in a descriptor.
+constexpr size_t MAX_NESTED_SUBS{10'000};
+
+/**
+ * Whether the buffer, if it represents a valid descriptor, contains a fragment with more
+ * sub-fragments than the given maximum.
+ */
+bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs = MAX_SUBS,
+ const size_t max_nested_subs = MAX_NESTED_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