From d858d6981cafe2c1d9ef775f097af4aa7bdd7365 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sun, 22 Feb 2026 13:22:23 -0600 Subject: [PATCH 1/5] test(fuzz): add governance proposal validator target --- src/Makefile.test.include | 1 + .../fuzz/governance_proposal_validator.cpp | 141 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/test/fuzz/governance_proposal_validator.cpp diff --git a/src/Makefile.test.include b/src/Makefile.test.include index dd6dda7178c3..9f2aec907efb 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -312,6 +312,7 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/flatfile.cpp \ test/fuzz/float.cpp \ test/fuzz/golomb_rice.cpp \ + test/fuzz/governance_proposal_validator.cpp \ test/fuzz/hex.cpp \ test/fuzz/http_request.cpp \ test/fuzz/integer.cpp \ diff --git a/src/test/fuzz/governance_proposal_validator.cpp b/src/test/fuzz/governance_proposal_validator.cpp new file mode 100644 index 000000000000..7ca1b8415fe2 --- /dev/null +++ b/src/test/fuzz/governance_proposal_validator.cpp @@ -0,0 +1,141 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { +std::string HexEncodeString(const std::string& input) +{ + static constexpr char HEX_DIGITS[] = "0123456789abcdef"; + std::string out; + out.reserve(input.size() * 2); + for (const unsigned char ch : input) { + out.push_back(HEX_DIGITS[ch >> 4]); + out.push_back(HEX_DIGITS[ch & 0x0f]); + } + return out; +} + +std::string SanitizeJsonString(std::string input) +{ + for (char& ch : input) { + if (ch == '"' || ch == '\\' || static_cast(ch) < 0x20) { + ch = 'x'; + } + } + return input; +} + +std::string MakeProposalJson( + int64_t type, + const std::string& name, + int64_t start_epoch, + int64_t end_epoch, + double payment_amount, + const std::string& payment_address, + const std::string& url) +{ + return strprintf( + "{\"type\":%" PRId64 ",\"name\":\"%s\",\"start_epoch\":%" PRId64 ",\"end_epoch\":%" PRId64 ",\"payment_amount\":%.17g,\"payment_address\":\"%s\",\"url\":\"%s\"}", + type, + SanitizeJsonString(name), + start_epoch, + end_epoch, + payment_amount, + SanitizeJsonString(payment_address), + SanitizeJsonString(url)); +} + +void RunValidatorCase(const std::string& hex_data, bool allow_script, bool check_expiration) +{ + try { + CProposalValidator validator(hex_data, allow_script); + (void)validator.Validate(check_expiration); + (void)validator.GetErrorMessages(); + } catch (const std::exception&) { + } catch (...) { + } +} +} // namespace + +void initialize_governance_proposal_validator() +{ + SelectParams(CBaseChainParams::MAIN); +} + +FUZZ_TARGET(governance_proposal_validator, .init = initialize_governance_proposal_validator) +{ + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + + constexpr std::array kPaymentAddresses{ + "Xs7iEDx8nMwJHdiQnwvCnLBTP2sjmDGTJA", // P2PKH (mainnet) + "7XuP9xVGyvkCAfW84QJkGfbiR7dX9TYaPH", // P2SH (mainnet) + }; + + const int64_t type = fuzzed_data_provider.ConsumeBool() + ? ToUnderlying(GovernanceObject::PROPOSAL) + : fuzzed_data_provider.ConsumeIntegral(); + + const int64_t start_epoch = fuzzed_data_provider.ConsumeIntegral(); + const int64_t end_epoch = start_epoch + fuzzed_data_provider.ConsumeIntegralInRange(-4, 1024); + + double payment_amount = fuzzed_data_provider.ConsumeFloatingPointInRange(-1000.0, 1000.0); + if (fuzzed_data_provider.ConsumeBool()) { + payment_amount = 1.0; + } + + std::string random_name = fuzzed_data_provider.ConsumeRandomLengthString(96); + std::string random_url = fuzzed_data_provider.ConsumeRandomLengthString(256); + + if (fuzzed_data_provider.ConsumeBool()) { + random_name = "dash-proposal-" + random_name; + } + + constexpr std::array kUrls{ + "https://dash.org/proposals/1", + "http://[::1]/path", + "http://[broken/path", + "http://broken]/path", + }; + + const std::string payment_address = fuzzed_data_provider.ConsumeBool() + ? std::string(kPaymentAddresses[fuzzed_data_provider.ConsumeIntegralInRange(0, kPaymentAddresses.size() - 1)]) + : fuzzed_data_provider.ConsumeRandomLengthString(96); + const std::string url = fuzzed_data_provider.ConsumeBool() + ? std::string(kUrls[fuzzed_data_provider.ConsumeIntegralInRange(0, kUrls.size() - 1)]) + : random_url; + + const std::string json_hex = HexEncodeString(MakeProposalJson( + type, + random_name, + start_epoch, + end_epoch, + payment_amount, + payment_address, + url)); + + const std::string malformed_json_hex = HexEncodeString("{" + fuzzed_data_provider.ConsumeRandomLengthString(128)); + const size_t oversized_payload_size = fuzzed_data_provider.ConsumeIntegralInRange(513, 2048); + const std::string oversized_hex(oversized_payload_size * 2, 'a'); + const std::string random_hex = fuzzed_data_provider.ConsumeRandomLengthString(2048); + + for (const bool allow_script : {false, true}) { + for (const bool check_expiration : {false, true}) { + RunValidatorCase(json_hex, allow_script, check_expiration); + RunValidatorCase(malformed_json_hex, allow_script, check_expiration); + RunValidatorCase(oversized_hex, allow_script, check_expiration); + RunValidatorCase(random_hex, allow_script, check_expiration); + } + } +} From dd52d121cbadf54ea57e6de2254c6c25f5858efd Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sun, 22 Feb 2026 20:57:05 -0600 Subject: [PATCH 2/5] fix: clamp start_epoch to avoid signed overflow UB Address CodeRabbit feedback: adding offset [-4, 1024] to an unconstrained int64_t start_epoch can overflow, which is UB under UBSan. Clamp to safe range and add explicit include. --- src/test/fuzz/governance_proposal_validator.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/fuzz/governance_proposal_validator.cpp b/src/test/fuzz/governance_proposal_validator.cpp index 7ca1b8415fe2..2a835095db04 100644 --- a/src/test/fuzz/governance_proposal_validator.cpp +++ b/src/test/fuzz/governance_proposal_validator.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace { @@ -87,7 +88,10 @@ FUZZ_TARGET(governance_proposal_validator, .init = initialize_governance_proposa ? ToUnderlying(GovernanceObject::PROPOSAL) : fuzzed_data_provider.ConsumeIntegral(); - const int64_t start_epoch = fuzzed_data_provider.ConsumeIntegral(); + // Clamp start_epoch to avoid signed overflow UB when adding offset + const int64_t start_epoch = fuzzed_data_provider.ConsumeIntegralInRange( + std::numeric_limits::min() + 1024, + std::numeric_limits::max() - 1024); const int64_t end_epoch = start_epoch + fuzzed_data_provider.ConsumeIntegralInRange(-4, 1024); double payment_amount = fuzzed_data_provider.ConsumeFloatingPointInRange(-1000.0, 1000.0); From 74d5d52e11b5fe019fd3886995229f25c7a05fa8 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sun, 22 Feb 2026 21:02:28 -0600 Subject: [PATCH 3/5] fix: add fuzz target to non-backported.txt Required for clang-format CI enforcement on Dash-specific files. --- test/util/data/non-backported.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/util/data/non-backported.txt b/test/util/data/non-backported.txt index 79ba20f5ec2d..f81cb8e9a1d6 100644 --- a/test/util/data/non-backported.txt +++ b/test/util/data/non-backported.txt @@ -50,7 +50,7 @@ src/stacktraces.* src/stats/*.cpp src/stats/*.h src/test/block_reward_reallocation_tests.cpp -src/test/bls_tests.cpp +src/test/fuzz/governance_proposal_validator.cppsrc/test/bls_tests.cpp src/test/coinjoin_*.cpp src/test/dip0020opcodes_tests.cpp src/test/dynamic_activation*.cpp From f306b73d3097e121c85f21c2dbb4da3ccdaea280 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sun, 22 Feb 2026 21:46:56 -0600 Subject: [PATCH 4/5] fix: add missing newline in non-backported.txt --- test/util/data/non-backported.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/util/data/non-backported.txt b/test/util/data/non-backported.txt index f81cb8e9a1d6..d5277201a711 100644 --- a/test/util/data/non-backported.txt +++ b/test/util/data/non-backported.txt @@ -50,7 +50,8 @@ src/stacktraces.* src/stats/*.cpp src/stats/*.h src/test/block_reward_reallocation_tests.cpp -src/test/fuzz/governance_proposal_validator.cppsrc/test/bls_tests.cpp +src/test/fuzz/governance_proposal_validator.cpp +src/test/bls_tests.cpp src/test/coinjoin_*.cpp src/test/dip0020opcodes_tests.cpp src/test/dynamic_activation*.cpp From 3db377c4c9f934b61661f46a8f462b6ca5d68135 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sun, 22 Feb 2026 21:51:30 -0600 Subject: [PATCH 5/5] style: apply clang-format to governance fuzz target --- .../fuzz/governance_proposal_validator.cpp | 61 +++++++------------ 1 file changed, 21 insertions(+), 40 deletions(-) diff --git a/src/test/fuzz/governance_proposal_validator.cpp b/src/test/fuzz/governance_proposal_validator.cpp index 2a835095db04..1f04aa8a25f9 100644 --- a/src/test/fuzz/governance_proposal_validator.cpp +++ b/src/test/fuzz/governance_proposal_validator.cpp @@ -38,24 +38,13 @@ std::string SanitizeJsonString(std::string input) return input; } -std::string MakeProposalJson( - int64_t type, - const std::string& name, - int64_t start_epoch, - int64_t end_epoch, - double payment_amount, - const std::string& payment_address, - const std::string& url) +std::string MakeProposalJson(int64_t type, const std::string& name, int64_t start_epoch, int64_t end_epoch, + double payment_amount, const std::string& payment_address, const std::string& url) { - return strprintf( - "{\"type\":%" PRId64 ",\"name\":\"%s\",\"start_epoch\":%" PRId64 ",\"end_epoch\":%" PRId64 ",\"payment_amount\":%.17g,\"payment_address\":\"%s\",\"url\":\"%s\"}", - type, - SanitizeJsonString(name), - start_epoch, - end_epoch, - payment_amount, - SanitizeJsonString(payment_address), - SanitizeJsonString(url)); + return strprintf("{\"type\":%" PRId64 ",\"name\":\"%s\",\"start_epoch\":%" PRId64 ",\"end_epoch\":%" PRId64 + ",\"payment_amount\":%.17g,\"payment_address\":\"%s\",\"url\":\"%s\"}", + type, SanitizeJsonString(name), start_epoch, end_epoch, payment_amount, + SanitizeJsonString(payment_address), SanitizeJsonString(url)); } void RunValidatorCase(const std::string& hex_data, bool allow_script, bool check_expiration) @@ -70,10 +59,7 @@ void RunValidatorCase(const std::string& hex_data, bool allow_script, bool check } } // namespace -void initialize_governance_proposal_validator() -{ - SelectParams(CBaseChainParams::MAIN); -} +void initialize_governance_proposal_validator() { SelectParams(CBaseChainParams::MAIN); } FUZZ_TARGET(governance_proposal_validator, .init = initialize_governance_proposal_validator) { @@ -84,14 +70,12 @@ FUZZ_TARGET(governance_proposal_validator, .init = initialize_governance_proposa "7XuP9xVGyvkCAfW84QJkGfbiR7dX9TYaPH", // P2SH (mainnet) }; - const int64_t type = fuzzed_data_provider.ConsumeBool() - ? ToUnderlying(GovernanceObject::PROPOSAL) - : fuzzed_data_provider.ConsumeIntegral(); + const int64_t type = fuzzed_data_provider.ConsumeBool() ? ToUnderlying(GovernanceObject::PROPOSAL) + : fuzzed_data_provider.ConsumeIntegral(); // Clamp start_epoch to avoid signed overflow UB when adding offset const int64_t start_epoch = fuzzed_data_provider.ConsumeIntegralInRange( - std::numeric_limits::min() + 1024, - std::numeric_limits::max() - 1024); + std::numeric_limits::min() + 1024, std::numeric_limits::max() - 1024); const int64_t end_epoch = start_epoch + fuzzed_data_provider.ConsumeIntegralInRange(-4, 1024); double payment_amount = fuzzed_data_provider.ConsumeFloatingPointInRange(-1000.0, 1000.0); @@ -113,21 +97,18 @@ FUZZ_TARGET(governance_proposal_validator, .init = initialize_governance_proposa "http://broken]/path", }; - const std::string payment_address = fuzzed_data_provider.ConsumeBool() - ? std::string(kPaymentAddresses[fuzzed_data_provider.ConsumeIntegralInRange(0, kPaymentAddresses.size() - 1)]) - : fuzzed_data_provider.ConsumeRandomLengthString(96); + const std::string payment_address = + fuzzed_data_provider.ConsumeBool() + ? std::string( + kPaymentAddresses[fuzzed_data_provider.ConsumeIntegralInRange(0, kPaymentAddresses.size() - 1)]) + : fuzzed_data_provider.ConsumeRandomLengthString(96); const std::string url = fuzzed_data_provider.ConsumeBool() - ? std::string(kUrls[fuzzed_data_provider.ConsumeIntegralInRange(0, kUrls.size() - 1)]) - : random_url; - - const std::string json_hex = HexEncodeString(MakeProposalJson( - type, - random_name, - start_epoch, - end_epoch, - payment_amount, - payment_address, - url)); + ? std::string( + kUrls[fuzzed_data_provider.ConsumeIntegralInRange(0, kUrls.size() - 1)]) + : random_url; + + const std::string json_hex = HexEncodeString( + MakeProposalJson(type, random_name, start_epoch, end_epoch, payment_amount, payment_address, url)); const std::string malformed_json_hex = HexEncodeString("{" + fuzzed_data_provider.ConsumeRandomLengthString(128)); const size_t oversized_payload_size = fuzzed_data_provider.ConsumeIntegralInRange(513, 2048);