aboutsummaryrefslogtreecommitdiff
path: root/src/test/fuzz
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/fuzz')
-rw-r--r--src/test/fuzz/crypto_chacha20poly1305.cpp200
-rw-r--r--src/test/fuzz/descriptor_parse.cpp12
-rw-r--r--src/test/fuzz/mini_miner.cpp6
-rw-r--r--src/test/fuzz/process_message.cpp2
-rw-r--r--src/test/fuzz/process_messages.cpp2
-rw-r--r--src/test/fuzz/rpc.cpp2
-rw-r--r--src/test/fuzz/util/descriptor.cpp59
-rw-r--r--src/test/fuzz/util/descriptor.h21
-rw-r--r--src/test/fuzz/utxo_total_supply.cpp2
9 files changed, 298 insertions, 8 deletions
diff --git a/src/test/fuzz/crypto_chacha20poly1305.cpp b/src/test/fuzz/crypto_chacha20poly1305.cpp
new file mode 100644
index 0000000000..2b39a06094
--- /dev/null
+++ b/src/test/fuzz/crypto_chacha20poly1305.cpp
@@ -0,0 +1,200 @@
+// Copyright (c) 2020-2021 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 <crypto/chacha20poly1305.h>
+#include <random.h>
+#include <span.h>
+#include <test/fuzz/FuzzedDataProvider.h>
+#include <test/fuzz/fuzz.h>
+#include <test/fuzz/util.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <vector>
+
+constexpr static inline void crypt_till_rekey(FSChaCha20Poly1305& aead, int rekey_interval, bool encrypt)
+{
+ for (int i = 0; i < rekey_interval; ++i) {
+ std::byte dummy_tag[FSChaCha20Poly1305::EXPANSION] = {{}};
+ if (encrypt) {
+ aead.Encrypt(Span{dummy_tag}.first(0), Span{dummy_tag}.first(0), dummy_tag);
+ } else {
+ aead.Decrypt(dummy_tag, Span{dummy_tag}.first(0), Span{dummy_tag}.first(0));
+ }
+ }
+}
+
+FUZZ_TARGET(crypto_aeadchacha20poly1305)
+{
+ FuzzedDataProvider provider{buffer.data(), buffer.size()};
+
+ auto key = provider.ConsumeBytes<std::byte>(32);
+ key.resize(32);
+ AEADChaCha20Poly1305 aead(key);
+
+ // Initialize RNG deterministically, to generate contents and AAD. We assume that there are no
+ // (potentially buggy) edge cases triggered by specific values of contents/AAD, so we can avoid
+ // reading the actual data for those from the fuzzer input (which would need large amounts of
+ // data).
+ InsecureRandomContext rng(provider.ConsumeIntegral<uint64_t>());
+
+ LIMITED_WHILE(provider.ConsumeBool(), 10000)
+ {
+ // Mode:
+ // - Bit 0: whether to use single-plain Encrypt/Decrypt; otherwise use a split at prefix.
+ // - Bit 2: whether this ciphertext will be corrupted (making it the last sent one)
+ // - Bit 3-4: controls the maximum aad length (max 511 bytes)
+ // - Bit 5-7: controls the maximum content length (max 16383 bytes, for performance reasons)
+ unsigned mode = provider.ConsumeIntegral<uint8_t>();
+ bool use_splits = mode & 1;
+ bool damage = mode & 4;
+ unsigned aad_length_bits = 3 * ((mode >> 3) & 3);
+ unsigned aad_length = provider.ConsumeIntegralInRange<unsigned>(0, (1 << aad_length_bits) - 1);
+ unsigned length_bits = 2 * ((mode >> 5) & 7);
+ unsigned length = provider.ConsumeIntegralInRange<unsigned>(0, (1 << length_bits) - 1);
+ // Generate aad and content.
+ auto aad = rng.randbytes<std::byte>(aad_length);
+ auto plain = rng.randbytes<std::byte>(length);
+ std::vector<std::byte> cipher(length + AEADChaCha20Poly1305::EXPANSION);
+ // Generate nonce
+ AEADChaCha20Poly1305::Nonce96 nonce = {(uint32_t)rng(), rng()};
+
+ if (use_splits && length > 0) {
+ size_t split_index = provider.ConsumeIntegralInRange<size_t>(1, length);
+ aead.Encrypt(Span{plain}.first(split_index), Span{plain}.subspan(split_index), aad, nonce, cipher);
+ } else {
+ aead.Encrypt(plain, aad, nonce, cipher);
+ }
+
+ // Test Keystream output
+ std::vector<std::byte> keystream(length);
+ aead.Keystream(nonce, keystream);
+ for (size_t i = 0; i < length; ++i) {
+ assert((plain[i] ^ keystream[i]) == cipher[i]);
+ }
+
+ std::vector<std::byte> decrypted_contents(length);
+ bool ok{false};
+
+ // damage the key
+ unsigned key_position = provider.ConsumeIntegralInRange<unsigned>(0, 31);
+ std::byte damage_val{(uint8_t)(1U << (key_position & 7))};
+ std::vector<std::byte> bad_key = key;
+ bad_key[key_position] ^= damage_val;
+
+ AEADChaCha20Poly1305 bad_aead(bad_key);
+ ok = bad_aead.Decrypt(cipher, aad, nonce, decrypted_contents);
+ assert(!ok);
+
+ // Optionally damage 1 bit in either the cipher (corresponding to a change in transit)
+ // or the aad (to make sure that decryption will fail if the AAD mismatches).
+ if (damage) {
+ unsigned damage_bit = provider.ConsumeIntegralInRange<unsigned>(0, (cipher.size() + aad.size()) * 8U - 1U);
+ unsigned damage_pos = damage_bit >> 3;
+ std::byte damage_val{(uint8_t)(1U << (damage_bit & 7))};
+ if (damage_pos >= cipher.size()) {
+ aad[damage_pos - cipher.size()] ^= damage_val;
+ } else {
+ cipher[damage_pos] ^= damage_val;
+ }
+ }
+
+ if (use_splits && length > 0) {
+ size_t split_index = provider.ConsumeIntegralInRange<size_t>(1, length);
+ ok = aead.Decrypt(cipher, aad, nonce, Span{decrypted_contents}.first(split_index), Span{decrypted_contents}.subspan(split_index));
+ } else {
+ ok = aead.Decrypt(cipher, aad, nonce, decrypted_contents);
+ }
+
+ // Decryption *must* fail if the packet was damaged, and succeed if it wasn't.
+ assert(!ok == damage);
+ if (!ok) break;
+ assert(decrypted_contents == plain);
+ }
+}
+
+FUZZ_TARGET(crypto_fschacha20poly1305)
+{
+ FuzzedDataProvider provider{buffer.data(), buffer.size()};
+
+ uint32_t rekey_interval = provider.ConsumeIntegralInRange<size_t>(32, 512);
+ auto key = provider.ConsumeBytes<std::byte>(32);
+ key.resize(32);
+ FSChaCha20Poly1305 enc_aead(key, rekey_interval);
+ FSChaCha20Poly1305 dec_aead(key, rekey_interval);
+
+ // Initialize RNG deterministically, to generate contents and AAD. We assume that there are no
+ // (potentially buggy) edge cases triggered by specific values of contents/AAD, so we can avoid
+ // reading the actual data for those from the fuzzer input (which would need large amounts of
+ // data).
+ InsecureRandomContext rng(provider.ConsumeIntegral<uint64_t>());
+
+ LIMITED_WHILE(provider.ConsumeBool(), 10000)
+ {
+ // Mode:
+ // - Bit 0: whether to use single-plain Encrypt/Decrypt; otherwise use a split at prefix.
+ // - Bit 2: whether this ciphertext will be corrupted (making it the last sent one)
+ // - Bit 3-4: controls the maximum aad length (max 511 bytes)
+ // - Bit 5-7: controls the maximum content length (max 16383 bytes, for performance reasons)
+ unsigned mode = provider.ConsumeIntegral<uint8_t>();
+ bool use_splits = mode & 1;
+ bool damage = mode & 4;
+ unsigned aad_length_bits = 3 * ((mode >> 3) & 3);
+ unsigned aad_length = provider.ConsumeIntegralInRange<unsigned>(0, (1 << aad_length_bits) - 1);
+ unsigned length_bits = 2 * ((mode >> 5) & 7);
+ unsigned length = provider.ConsumeIntegralInRange<unsigned>(0, (1 << length_bits) - 1);
+ // Generate aad and content.
+ auto aad = rng.randbytes<std::byte>(aad_length);
+ auto plain = rng.randbytes<std::byte>(length);
+ std::vector<std::byte> cipher(length + FSChaCha20Poly1305::EXPANSION);
+
+ crypt_till_rekey(enc_aead, rekey_interval, true);
+ if (use_splits && length > 0) {
+ size_t split_index = provider.ConsumeIntegralInRange<size_t>(1, length);
+ enc_aead.Encrypt(Span{plain}.first(split_index), Span{plain}.subspan(split_index), aad, cipher);
+ } else {
+ enc_aead.Encrypt(plain, aad, cipher);
+ }
+
+ std::vector<std::byte> decrypted_contents(length);
+ bool ok{false};
+
+ // damage the key
+ unsigned key_position = provider.ConsumeIntegralInRange<unsigned>(0, 31);
+ std::byte damage_val{(uint8_t)(1U << (key_position & 7))};
+ std::vector<std::byte> bad_key = key;
+ bad_key[key_position] ^= damage_val;
+
+ FSChaCha20Poly1305 bad_fs_aead(bad_key, rekey_interval);
+ crypt_till_rekey(bad_fs_aead, rekey_interval, false);
+ ok = bad_fs_aead.Decrypt(cipher, aad, decrypted_contents);
+ assert(!ok);
+
+ // Optionally damage 1 bit in either the cipher (corresponding to a change in transit)
+ // or the aad (to make sure that decryption will fail if the AAD mismatches).
+ if (damage) {
+ unsigned damage_bit = provider.ConsumeIntegralInRange<unsigned>(0, (cipher.size() + aad.size()) * 8U - 1U);
+ unsigned damage_pos = damage_bit >> 3;
+ std::byte damage_val{(uint8_t)(1U << (damage_bit & 7))};
+ if (damage_pos >= cipher.size()) {
+ aad[damage_pos - cipher.size()] ^= damage_val;
+ } else {
+ cipher[damage_pos] ^= damage_val;
+ }
+ }
+
+ crypt_till_rekey(dec_aead, rekey_interval, false);
+ if (use_splits && length > 0) {
+ size_t split_index = provider.ConsumeIntegralInRange<size_t>(1, length);
+ ok = dec_aead.Decrypt(cipher, aad, Span{decrypted_contents}.first(split_index), Span{decrypted_contents}.subspan(split_index));
+ } else {
+ ok = dec_aead.Decrypt(cipher, aad, decrypted_contents);
+ }
+
+ // Decryption *must* fail if the packet was damaged, and succeed if it wasn't.
+ assert(!ok == damage);
+ if (!ok) break;
+ assert(decrypted_contents == plain);
+ }
+}
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/mini_miner.cpp b/src/test/fuzz/mini_miner.cpp
index 3a1663364f..51de4d0166 100644
--- a/src/test/fuzz/mini_miner.cpp
+++ b/src/test/fuzz/mini_miner.cpp
@@ -188,9 +188,9 @@ FUZZ_TARGET(mini_miner_selection, .init = initialize_miner)
auto mock_template_txids = mini_miner.GetMockTemplateTxids();
// MiniMiner doesn't add a coinbase tx.
assert(mock_template_txids.count(blocktemplate->block.vtx[0]->GetHash()) == 0);
- mock_template_txids.emplace(blocktemplate->block.vtx[0]->GetHash());
- assert(mock_template_txids.size() <= blocktemplate->block.vtx.size());
- assert(mock_template_txids.size() >= blocktemplate->block.vtx.size());
+ auto [iter, new_entry] = mock_template_txids.emplace(blocktemplate->block.vtx[0]->GetHash());
+ assert(new_entry);
+
assert(mock_template_txids.size() == blocktemplate->block.vtx.size());
for (const auto& tx : blocktemplate->block.vtx) {
assert(mock_template_txids.count(tx->GetHash()));
diff --git a/src/test/fuzz/process_message.cpp b/src/test/fuzz/process_message.cpp
index d10d9dafe8..6373eac1c3 100644
--- a/src/test/fuzz/process_message.cpp
+++ b/src/test/fuzz/process_message.cpp
@@ -42,7 +42,7 @@ void initialize_process_message()
static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>(
/*chain_type=*/ChainType::REGTEST,
- /*extra_args=*/{"-txreconciliation"});
+ {.extra_args = {"-txreconciliation"}});
g_setup = testing_setup.get();
for (int i = 0; i < 2 * COINBASE_MATURITY; i++) {
MineBlock(g_setup->m_node, CScript() << OP_TRUE);
diff --git a/src/test/fuzz/process_messages.cpp b/src/test/fuzz/process_messages.cpp
index 38acd432fa..62f38967a3 100644
--- a/src/test/fuzz/process_messages.cpp
+++ b/src/test/fuzz/process_messages.cpp
@@ -32,7 +32,7 @@ void initialize_process_messages()
{
static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>(
/*chain_type=*/ChainType::REGTEST,
- /*extra_args=*/{"-txreconciliation"});
+ {.extra_args = {"-txreconciliation"}});
g_setup = testing_setup.get();
for (int i = 0; i < 2 * COINBASE_MATURITY; i++) {
MineBlock(g_setup->m_node, CScript() << OP_TRUE);
diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp
index 4e52c1c091..9122617e46 100644
--- a/src/test/fuzz/rpc.cpp
+++ b/src/test/fuzz/rpc.cpp
@@ -41,7 +41,7 @@ using util::ToString;
namespace {
struct RPCFuzzTestingSetup : public TestingSetup {
- RPCFuzzTestingSetup(const ChainType chain_type, const std::vector<const char*>& extra_args) : TestingSetup{chain_type, extra_args}
+ RPCFuzzTestingSetup(const ChainType chain_type, TestOpts opts) : TestingSetup{chain_type, opts}
{
}
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
diff --git a/src/test/fuzz/utxo_total_supply.cpp b/src/test/fuzz/utxo_total_supply.cpp
index 48ed266abe..b0f1a1251a 100644
--- a/src/test/fuzz/utxo_total_supply.cpp
+++ b/src/test/fuzz/utxo_total_supply.cpp
@@ -23,7 +23,7 @@ FUZZ_TARGET(utxo_total_supply)
ChainTestingSetup test_setup{
ChainType::REGTEST,
{
- "-testactivationheight=bip34@2",
+ .extra_args = {"-testactivationheight=bip34@2"},
},
};
// Create chainstate