diff options
author | fanquake <fanquake@gmail.com> | 2023-02-28 16:58:56 +0000 |
---|---|---|
committer | fanquake <fanquake@gmail.com> | 2023-02-28 17:04:47 +0000 |
commit | cb40639bdf04bab31bcdceb3bf941e9bade8317a (patch) | |
tree | 65037ef547f4943573b6adcf0025e15cbdb6941b | |
parent | 4398cfb22b9c491125d65ac35c0c1c73d5d9dd11 (diff) | |
parent | 56e37e71a2538a240cc360678aeb752d17bd8f45 (diff) |
Merge bitcoin/bitcoin#27165: Make miniscript_{stable,smart} fuzzers avoid too large scripts
56e37e71a2538a240cc360678aeb752d17bd8f45 Make miniscript fuzzers avoid script size limit (Pieter Wuille)
bcec5ab4ff1039c0c309dbbb9953adbd0a4f3e88 Make miniscript fuzzers avoid ops limit (Pieter Wuille)
213fffa5138229eac2d4a9eda0f643fe90870378 Enforce type consistency in miniscript_stable fuzz test (Pieter Wuille)
e1f30414c6b9434048e089ccc3ec4f475f980c60 Simplify miniscript fuzzer NodeInfo struct (Pieter Wuille)
5abb0f5ac37e8a17072d5989a025227035fdc7e6 Do base type propagation in miniscript_stable fuzzer (Pieter Wuille)
Pull request description:
This adds a number of improvements to the miniscript fuzzers that all amount to rejecting invalid or overly big miniscripts early on:
* Base type propagation in the miniscript_stable fuzzers prevents constructing a large portion of miniscripts that would be illegal, with just a little bit of type logic in the fuzzer. The fuzzer input format is unchanged.
* Ops and script size tracking in GenNode means that too-large scripts (either due to script size limit or ops limit) will be detected on the fly during fuzz input processing, before actually constructing the scripts.
Closes #27147.
ACKs for top commit:
darosior:
re-ACK 56e37e71a2
dergoegge:
tACK 56e37e71a2538a240cc360678aeb752d17bd8f45
Tree-SHA512: 245584adf9a6644a35fe103bc81b619e5b4f5d467571a761b5809d08b1dec48f7ceaf4d8791ccd8208b45c6b309d2ccca23b3d1ec5399df76cd5bf88f2263280
-rw-r--r-- | src/script/miniscript.h | 3 | ||||
-rw-r--r-- | src/test/fuzz/miniscript.cpp | 230 |
2 files changed, 189 insertions, 44 deletions
diff --git a/src/script/miniscript.h b/src/script/miniscript.h index c42b530c4d..bb42bf3c92 100644 --- a/src/script/miniscript.h +++ b/src/script/miniscript.h @@ -1136,6 +1136,9 @@ public: //! Return the maximum number of ops needed to satisfy this script non-malleably. uint32_t GetOps() const { return ops.count + ops.sat.value; } + //! Return the number of ops in the script (not counting the dynamic ones that depend on execution). + uint32_t GetStaticOps() const { return ops.count; } + //! Check the ops limit of this script against the consensus limit. bool CheckOpsLimit() const { return GetOps() <= MAX_OPS_PER_SCRIPT; } diff --git a/src/test/fuzz/miniscript.cpp b/src/test/fuzz/miniscript.cpp index 73096cd5ca..f3a48daf2c 100644 --- a/src/test/fuzz/miniscript.cpp +++ b/src/test/fuzz/miniscript.cpp @@ -261,8 +261,6 @@ template<typename... Args> NodeRef MakeNodeRef(Args&&... args) { struct NodeInfo { //! The type of this node Fragment fragment; - //! Number of subs of this node - uint8_t n_subs; //! The timelock value for older() and after(), the threshold value for multi() and thresh() uint32_t k; //! Keys for this node, if it has some @@ -272,15 +270,13 @@ struct NodeInfo { //! The type requirements for the children of this node. std::vector<Type> subtypes; - NodeInfo(Fragment frag): fragment(frag), n_subs(0), k(0) {} - NodeInfo(Fragment frag, CPubKey key): fragment(frag), n_subs(0), k(0), keys({key}) {} - NodeInfo(Fragment frag, uint32_t _k): fragment(frag), n_subs(0), k(_k) {} - NodeInfo(Fragment frag, std::vector<unsigned char> h): fragment(frag), n_subs(0), k(0), hash(std::move(h)) {} - NodeInfo(uint8_t subs, Fragment frag): fragment(frag), n_subs(subs), k(0), subtypes(subs, ""_mst) {} - NodeInfo(uint8_t subs, Fragment frag, uint32_t _k): fragment(frag), n_subs(subs), k(_k), subtypes(subs, ""_mst) {} - NodeInfo(std::vector<Type> subt, Fragment frag): fragment(frag), n_subs(subt.size()), k(0), subtypes(std::move(subt)) {} - NodeInfo(std::vector<Type> subt, Fragment frag, uint32_t _k): fragment(frag), n_subs(subt.size()), k(_k), subtypes(std::move(subt)) {} - NodeInfo(Fragment frag, uint32_t _k, std::vector<CPubKey> _keys): fragment(frag), n_subs(0), k(_k), keys(std::move(_keys)) {} + NodeInfo(Fragment frag): fragment(frag), k(0) {} + NodeInfo(Fragment frag, CPubKey key): fragment(frag), k(0), keys({key}) {} + NodeInfo(Fragment frag, uint32_t _k): fragment(frag), k(_k) {} + NodeInfo(Fragment frag, std::vector<unsigned char> h): fragment(frag), k(0), hash(std::move(h)) {} + NodeInfo(std::vector<Type> subt, Fragment frag): fragment(frag), k(0), subtypes(std::move(subt)) {} + NodeInfo(std::vector<Type> subt, Fragment frag, uint32_t _k): fragment(frag), k(_k), subtypes(std::move(subt)) {} + NodeInfo(Fragment frag, uint32_t _k, std::vector<CPubKey> _keys): fragment(frag), k(_k), keys(std::move(_keys)) {} }; /** Pick an index in a collection from a single byte in the fuzzer's output. */ @@ -329,27 +325,51 @@ std::optional<uint32_t> ConsumeTimeLock(FuzzedDataProvider& provider) { * bytes as the number of keys define the index of each key in the test data. * - For thresh(), the next byte defines the threshold value and the following one the number of subs. */ -std::optional<NodeInfo> ConsumeNodeStable(FuzzedDataProvider& provider) { +std::optional<NodeInfo> ConsumeNodeStable(FuzzedDataProvider& provider, Type type_needed) { + bool allow_B = (type_needed == ""_mst) || (type_needed << "B"_mst); + bool allow_K = (type_needed == ""_mst) || (type_needed << "K"_mst); + bool allow_V = (type_needed == ""_mst) || (type_needed << "V"_mst); + bool allow_W = (type_needed == ""_mst) || (type_needed << "W"_mst); + switch (provider.ConsumeIntegral<uint8_t>()) { - case 0: return {{Fragment::JUST_0}}; - case 1: return {{Fragment::JUST_1}}; - case 2: return {{Fragment::PK_K, ConsumePubKey(provider)}}; - case 3: return {{Fragment::PK_H, ConsumePubKey(provider)}}; + case 0: + if (!allow_B) return {}; + return {{Fragment::JUST_0}}; + case 1: + if (!allow_B) return {}; + return {{Fragment::JUST_1}}; + case 2: + if (!allow_K) return {}; + return {{Fragment::PK_K, ConsumePubKey(provider)}}; + case 3: + if (!allow_K) return {}; + return {{Fragment::PK_H, ConsumePubKey(provider)}}; case 4: { + if (!allow_B) return {}; const auto k = ConsumeTimeLock(provider); if (!k) return {}; return {{Fragment::OLDER, *k}}; } case 5: { + if (!allow_B) return {}; const auto k = ConsumeTimeLock(provider); if (!k) return {}; return {{Fragment::AFTER, *k}}; } - case 6: return {{Fragment::SHA256, ConsumeSha256(provider)}}; - case 7: return {{Fragment::HASH256, ConsumeHash256(provider)}}; - case 8: return {{Fragment::RIPEMD160, ConsumeRipemd160(provider)}}; - case 9: return {{Fragment::HASH160, ConsumeHash160(provider)}}; + case 6: + if (!allow_B) return {}; + return {{Fragment::SHA256, ConsumeSha256(provider)}}; + case 7: + if (!allow_B) return {}; + return {{Fragment::HASH256, ConsumeHash256(provider)}}; + case 8: + if (!allow_B) return {}; + return {{Fragment::RIPEMD160, ConsumeRipemd160(provider)}}; + case 9: + if (!allow_B) return {}; + return {{Fragment::HASH160, ConsumeHash160(provider)}}; case 10: { + if (!allow_B) return {}; const auto k = provider.ConsumeIntegral<uint8_t>(); const auto n_keys = provider.ConsumeIntegral<uint8_t>(); if (n_keys > 20 || k == 0 || k > n_keys) return {}; @@ -357,26 +377,59 @@ std::optional<NodeInfo> ConsumeNodeStable(FuzzedDataProvider& provider) { for (auto& key: keys) key = ConsumePubKey(provider); return {{Fragment::MULTI, k, std::move(keys)}}; } - case 11: return {{3, Fragment::ANDOR}}; - case 12: return {{2, Fragment::AND_V}}; - case 13: return {{2, Fragment::AND_B}}; - case 15: return {{2, Fragment::OR_B}}; - case 16: return {{2, Fragment::OR_C}}; - case 17: return {{2, Fragment::OR_D}}; - case 18: return {{2, Fragment::OR_I}}; + case 11: + if (!(allow_B || allow_K || allow_V)) return {}; + return {{{"B"_mst, type_needed, type_needed}, Fragment::ANDOR}}; + case 12: + if (!(allow_B || allow_K || allow_V)) return {}; + return {{{"V"_mst, type_needed}, Fragment::AND_V}}; + case 13: + if (!allow_B) return {}; + return {{{"B"_mst, "W"_mst}, Fragment::AND_B}}; + case 15: + if (!allow_B) return {}; + return {{{"B"_mst, "W"_mst}, Fragment::OR_B}}; + case 16: + if (!allow_V) return {}; + return {{{"B"_mst, "V"_mst}, Fragment::OR_C}}; + case 17: + if (!allow_B) return {}; + return {{{"B"_mst, "B"_mst}, Fragment::OR_D}}; + case 18: + if (!(allow_B || allow_K || allow_V)) return {}; + return {{{type_needed, type_needed}, Fragment::OR_I}}; case 19: { + if (!allow_B) return {}; auto k = provider.ConsumeIntegral<uint8_t>(); auto n_subs = provider.ConsumeIntegral<uint8_t>(); if (k == 0 || k > n_subs) return {}; - return {{n_subs, Fragment::THRESH, k}}; + std::vector<Type> subtypes; + subtypes.reserve(n_subs); + subtypes.emplace_back("B"_mst); + for (size_t i = 1; i < n_subs; ++i) subtypes.emplace_back("W"_mst); + return {{std::move(subtypes), Fragment::THRESH, k}}; } - case 20: return {{1, Fragment::WRAP_A}}; - case 21: return {{1, Fragment::WRAP_S}}; - case 22: return {{1, Fragment::WRAP_C}}; - case 23: return {{1, Fragment::WRAP_D}}; - case 24: return {{1, Fragment::WRAP_V}}; - case 25: return {{1, Fragment::WRAP_J}}; - case 26: return {{1, Fragment::WRAP_N}}; + case 20: + if (!allow_W) return {}; + return {{{"B"_mst}, Fragment::WRAP_A}}; + case 21: + if (!allow_W) return {}; + return {{{"B"_mst}, Fragment::WRAP_S}}; + case 22: + if (!allow_B) return {}; + return {{{"K"_mst}, Fragment::WRAP_C}}; + case 23: + if (!allow_B) return {}; + return {{{"V"_mst}, Fragment::WRAP_D}}; + case 24: + if (!allow_V) return {}; + return {{{"B"_mst}, Fragment::WRAP_V}}; + case 25: + if (!allow_B) return {}; + return {{{"B"_mst}, Fragment::WRAP_J}}; + case 26: + if (!allow_B) return {}; + return {{{"B"_mst}, Fragment::WRAP_N}}; default: break; } @@ -709,11 +762,16 @@ std::optional<NodeInfo> ConsumeNodeSmart(FuzzedDataProvider& provider, Type type * a NodeRef whose Type() matches the type fed to ConsumeNode. */ template<typename F> -NodeRef GenNode(F ConsumeNode, Type root_type = ""_mst, bool strict_valid = false) { +NodeRef GenNode(F ConsumeNode, Type root_type, bool strict_valid = false) { /** A stack of miniscript Nodes being built up. */ std::vector<NodeRef> stack; /** The queue of instructions. */ std::vector<std::pair<Type, std::optional<NodeInfo>>> todo{{root_type, {}}}; + /** Predict the number of (static) script ops. */ + uint32_t ops{0}; + /** Predict the total script size (every unexplored subnode is counted as one, as every leaf is + * at least one script byte). */ + uint32_t scriptsize{1}; while (!todo.empty()) { // The expected type we have to construct. @@ -722,6 +780,78 @@ NodeRef GenNode(F ConsumeNode, Type root_type = ""_mst, bool strict_valid = fals // Fragment/children have not been decided yet. Decide them. auto node_info = ConsumeNode(type_needed); if (!node_info) return {}; + // Update predicted resource limits. Since every leaf Miniscript node is at least one + // byte long, we move one byte from each child to their parent. A similar technique is + // used in the miniscript::internal::Parse function to prevent runaway string parsing. + scriptsize += miniscript::internal::ComputeScriptLen(node_info->fragment, ""_mst, node_info->subtypes.size(), node_info->k, node_info->subtypes.size(), node_info->keys.size()) - 1; + if (scriptsize > MAX_STANDARD_P2WSH_SCRIPT_SIZE) return {}; + switch (node_info->fragment) { + case Fragment::JUST_0: + case Fragment::JUST_1: + break; + case Fragment::PK_K: + break; + case Fragment::PK_H: + ops += 3; + break; + case Fragment::OLDER: + case Fragment::AFTER: + ops += 1; + break; + case Fragment::RIPEMD160: + case Fragment::SHA256: + case Fragment::HASH160: + case Fragment::HASH256: + ops += 4; + break; + case Fragment::ANDOR: + ops += 3; + break; + case Fragment::AND_V: + break; + case Fragment::AND_B: + case Fragment::OR_B: + ops += 1; + break; + case Fragment::OR_C: + ops += 2; + break; + case Fragment::OR_D: + ops += 3; + break; + case Fragment::OR_I: + ops += 3; + break; + case Fragment::THRESH: + ops += node_info->subtypes.size(); + break; + case Fragment::MULTI: + ops += 1; + break; + case Fragment::WRAP_A: + ops += 2; + break; + case Fragment::WRAP_S: + ops += 1; + break; + case Fragment::WRAP_C: + ops += 1; + break; + case Fragment::WRAP_D: + ops += 3; + break; + case Fragment::WRAP_V: + // We don't account for OP_VERIFY here; that will be corrected for when the actual + // node is constructed below. + break; + case Fragment::WRAP_J: + ops += 4; + break; + case Fragment::WRAP_N: + ops += 1; + break; + } + if (ops > MAX_OPS_PER_SCRIPT) return {}; auto subtypes = node_info->subtypes; todo.back().second = std::move(node_info); todo.reserve(todo.size() + subtypes.size()); @@ -738,11 +868,11 @@ NodeRef GenNode(F ConsumeNode, Type root_type = ""_mst, bool strict_valid = fals NodeInfo& info = *todo.back().second; // Gather children from the back of stack. std::vector<NodeRef> sub; - sub.reserve(info.n_subs); - for (size_t i = 0; i < info.n_subs; ++i) { - sub.push_back(std::move(*(stack.end() - info.n_subs + i))); + sub.reserve(info.subtypes.size()); + for (size_t i = 0; i < info.subtypes.size(); ++i) { + sub.push_back(std::move(*(stack.end() - info.subtypes.size() + i))); } - stack.erase(stack.end() - info.n_subs, stack.end()); + stack.erase(stack.end() - info.subtypes.size(), stack.end()); // Construct new NodeRef. NodeRef node; if (info.keys.empty()) { @@ -753,17 +883,29 @@ NodeRef GenNode(F ConsumeNode, Type root_type = ""_mst, bool strict_valid = fals node = MakeNodeRef(info.fragment, std::move(info.keys), info.k); } // Verify acceptability. - if (!node || !(node->GetType() << type_needed)) { + if (!node || (node->GetType() & "KVWB"_mst) == ""_mst) { assert(!strict_valid); return {}; } + if (!(type_needed == ""_mst)) { + assert(node->GetType() << type_needed); + } if (!node->IsValid()) return {}; + // Update resource predictions. + if (node->fragment == Fragment::WRAP_V && node->subs[0]->GetType() << "x"_mst) { + ops += 1; + scriptsize += 1; + } + if (ops > MAX_OPS_PER_SCRIPT) return {}; + if (scriptsize > MAX_STANDARD_P2WSH_SCRIPT_SIZE) return {}; // Move it to the stack. stack.push_back(std::move(node)); todo.pop_back(); } } assert(stack.size() == 1); + assert(stack[0]->GetStaticOps() == ops); + assert(stack[0]->ScriptSize() == scriptsize); stack[0]->DuplicateKeyCheck(KEY_COMP); return std::move(stack[0]); } @@ -921,9 +1063,9 @@ void FuzzInitSmart() FUZZ_TARGET_INIT(miniscript_stable, FuzzInit) { FuzzedDataProvider provider(buffer.data(), buffer.size()); - TestNode(GenNode([&](Type) { - return ConsumeNodeStable(provider); - }), provider); + TestNode(GenNode([&](Type needed_type) { + return ConsumeNodeStable(provider, needed_type); + }, ""_mst), provider); } /** Fuzz target that runs TestNode on nodes generated using ConsumeNodeSmart. */ |