From 36bdfad0738533a885f23fe9d86f59680020079b Mon Sep 17 00:00:00 2001 From: Arthur Chan Date: Fri, 18 Jul 2025 17:08:50 +0000 Subject: [PATCH] rpc-fuzz: Add new fuzzers for RPC endpoints Signed-off-by: Arthur Chan Add macro definition for fuzzers Signed-off-by: Arthur Chan Fix FuzzedDataProvider header missing problem Signed-off-by: Arthur Chan Add README Signed-off-by: Arthur Chan Provide static FuzzedDataProvider.h Signed-off-by: Arthur Chan Update and enhance fuzzer Signed-off-by: Arthur Chan Activate UBSan Signed-off-by: Arthur Chan Fix fuzz target retrieval Signed-off-by: Arthur Chan Remove bias selector and fix protocol lifespan Signed-off-by: Arthur Chan Drop SIGALARM handling and fix bug on selectors Signed-off-by: Arthur Chan Fix rpc request changes Signed-off-by: Arthur Chan Add a new fuzzer profile that catch all expcetions Signed-off-by: Arthur Chan Fix typo Signed-off-by: Arthur Chan Add warning Signed-off-by: Arthur Chan --- CMakeLists.txt | 4 +- src/common/perf_timer.cpp | 7 +- tests/fuzz/CMakeLists.txt | 69 ++ tests/fuzz/fuzz_rpc/README.md | 50 + tests/fuzz/fuzz_rpc/fuzz_rpc.cpp | 91 ++ tests/fuzz/fuzz_rpc/fuzz_zmq.cpp | 43 + tests/fuzz/fuzz_rpc/initialisation.cpp | 357 +++++++ tests/fuzz/fuzz_rpc/initialisation.h | 31 + tests/fuzz/fuzz_rpc/rpc_endpoints.cpp | 939 ++++++++++++++++++ tests/fuzz/fuzz_rpc/rpc_endpoints.h | 92 ++ tests/fuzz/fuzz_rpc/zmq_endpoints.cpp | 87 ++ tests/fuzz/fuzz_rpc/zmq_endpoints.h | 19 + .../fuzz/include/fuzzer/FuzzedDataProvider.h | 397 ++++++++ 13 files changed, 2183 insertions(+), 3 deletions(-) create mode 100644 tests/fuzz/fuzz_rpc/README.md create mode 100644 tests/fuzz/fuzz_rpc/fuzz_rpc.cpp create mode 100644 tests/fuzz/fuzz_rpc/fuzz_zmq.cpp create mode 100644 tests/fuzz/fuzz_rpc/initialisation.cpp create mode 100644 tests/fuzz/fuzz_rpc/initialisation.h create mode 100644 tests/fuzz/fuzz_rpc/rpc_endpoints.cpp create mode 100644 tests/fuzz/fuzz_rpc/rpc_endpoints.h create mode 100644 tests/fuzz/fuzz_rpc/zmq_endpoints.cpp create mode 100644 tests/fuzz/fuzz_rpc/zmq_endpoints.h create mode 100644 tests/fuzz/include/fuzzer/FuzzedDataProvider.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 417755848..71f0023c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -484,8 +484,8 @@ endif() option(SANITIZE "Use ASAN memory sanitizer" OFF) if(SANITIZE) message(STATUS "Using ASAN") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address,undefined") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined") endif() # Set default blockchain storage location: diff --git a/src/common/perf_timer.cpp b/src/common/perf_timer.cpp index 877b80d1c..a7acf111b 100644 --- a/src/common/perf_timer.cpp +++ b/src/common/perf_timer.cpp @@ -90,7 +90,12 @@ namespace tools el::Level performance_timer_log_level = el::Level::Info; -static __thread std::vector *performance_timers = NULL; +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + #warning "Building with fuzzing mode UNSAFE FOR PRODUCTION!" + __thread std::vector *performance_timers = NULL; +#else + static __thread std::vector *performance_timers = NULL; +#endif void set_performance_timer_log_level(el::Level level) { diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt index cd517a4ae..6009ead14 100644 --- a/tests/fuzz/CMakeLists.txt +++ b/tests/fuzz/CMakeLists.txt @@ -26,6 +26,75 @@ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# Add the include path for +include_directories(${CMAKE_SOURCE_DIR}/tests/fuzz/include) + +# Recompile perf_timer for fuzzing +add_library(fuzz_unsafe_macro OBJECT + ${CMAKE_SOURCE_DIR}/src/common/perf_timer.cpp) +target_compile_definitions(fuzz_unsafe_macro + PRIVATE FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) + +monero_add_minimal_executable(fuzz_rpc + fuzz_rpc/initialisation.cpp + fuzz_rpc/rpc_endpoints.cpp + fuzz_rpc/fuzz_rpc.cpp + $) +target_compile_definitions(fuzz_rpc PRIVATE SAFE) +target_link_libraries(fuzz_rpc + PRIVATE + rpc + ${CMAKE_THREAD_LIBS_INIT} + ${EXTRA_LIBRARIES} + $ENV{LIB_FUZZING_ENGINE}) +set_property(TARGET fuzz_rpc + PROPERTY + FOLDER "tests") + +monero_add_minimal_executable(fuzz_rpc_full + fuzz_rpc/initialisation.cpp + fuzz_rpc/rpc_endpoints.cpp + fuzz_rpc/fuzz_rpc.cpp + $) +target_link_libraries(fuzz_rpc_full + PRIVATE + rpc + ${CMAKE_THREAD_LIBS_INIT} + ${EXTRA_LIBRARIES} + $ENV{LIB_FUZZING_ENGINE}) +set_property(TARGET fuzz_rpc_full + PROPERTY + FOLDER "tests") + +monero_add_minimal_executable(fuzz_rpc_full_no_exceptions + fuzz_rpc/initialisation.cpp + fuzz_rpc/rpc_endpoints.cpp + fuzz_rpc/fuzz_rpc.cpp + $) +target_compile_definitions(fuzz_rpc_full_no_exceptions PRIVATE CATCH_ALL_EXCEPTIONS) +target_link_libraries(fuzz_rpc_full_no_exceptions + PRIVATE + rpc + ${CMAKE_THREAD_LIBS_INIT} + ${EXTRA_LIBRARIES} + $ENV{LIB_FUZZING_ENGINE}) +set_property(TARGET fuzz_rpc_full_no_exceptions + PROPERTY + FOLDER "tests") + +monero_add_minimal_executable(fuzz_zmq + fuzz_rpc/zmq_endpoints.cpp + fuzz_rpc/fuzz_zmq.cpp) +target_link_libraries(fuzz_zmq + PRIVATE + rpc_pub + ${CMAKE_THREAD_LIBS_INIT} + ${EXTRA_LIBRARIES} + $ENV{LIB_FUZZING_ENGINE}) +set_property(TARGET fuzz_zmq + PROPERTY + FOLDER "tests") + monero_add_minimal_executable(block_fuzz_tests block.cpp fuzzer.cpp) target_link_libraries(block_fuzz_tests PRIVATE diff --git a/tests/fuzz/fuzz_rpc/README.md b/tests/fuzz/fuzz_rpc/README.md new file mode 100644 index 000000000..05831c18e --- /dev/null +++ b/tests/fuzz/fuzz_rpc/README.md @@ -0,0 +1,50 @@ +# Fuzzing Harness Explanation + +The fuzzing harness skips the transport layer and directly initialises a `core_rpc_server` object. It then calls and fuzzes the RPC endpoint function handlers directly, removing the need to start a fake server while still allowing the handler logic for each RPC API to be fuzzed. + + +## `fuzz_rpc.cpp` / `fuzz_zmq.cpp` + +These are the main fuzzing entry points, containing `LLVMFuzzerTestOneInput`, which manages each iteration of the fuzzing process. + +## `initialisation.cpp` / `initialisation.h` + +This set of source files provides initialisation helper functions to configure the `core_rpc_server` class with dummy protocols, P2P, payment modules, and other components. It also includes functions to generate fake random blocks, miners, and transactions for use in fuzzing. + +## `rpc_endpoints.cpp` / `rpc_endpoints.h` / `zmq_endpoints.cpp` / `zmq_endpoints.h` + +These source files handle the creation of the necessary request and response objects and the invocation of specific endpoint functions. These simulate real RPC requests / ZMQ requests for the purpose of fuzzing. There are three categories of RPC endpoint functions and only one for ZMQ endpoint functions: + +* **Safe**: Considered stable and unlikely to fail. +* **Risky**: More prone to failure and early exits, especially if no valid blockchain is generated or no payment modules are configured. +* **Priority**: Most critical RPC endpoint functions that will at least run once per iteration. + +# Building the Fuzzing Harnesses + +The same `fuzz_rpc.cpp` is compiled into two versions of the fuzzer, one with the `SAFE` macro defined and one without. Meanwhile, `fuzz_zmq` is compiled into a single version. + +* With `SAFE` defined: Only safe endpoint functions are fuzzed. +* Without `SAFE`: Both safe and risky functions are included in the fuzzing process, and payment modules are configured. + +# Fuzzing Harness Flow + +## Select a Random RPC Endpoint Function / ZMQ Endpoint Function + +Random data is used to generate selectors that choose an RPC endpoint function / ZMQ endpoint function to fuzz, either from the safe or risky function maps (depending on whether the `SAFE` macro is defined). A function from the priority category is always included at least once. + + +## `initialise_rpc_core` / `initialise_rpc_server` + +At the start of each fuzzing iteration, a `core_rpc_server` object is created and initialised with dummy protocols, P2P, payment modules, and more. +If the `SAFE` macro is not defined, the payment module will also be initialised. +This step is skipped for `fuzz_zmq`. + +## `generate_random_blocks` + +The provided random data is used to initialise a set of random blocks, which are added to a dummy blockchain (initialised using `FAKECHAIN`). +If blocks are successfully added, a list of random transactions is then generated and also pushed to the blockchain. +This step is skipped for `fuzz_zmq`. + +## Real Fuzzing + +The selected RPC endpoint helper function in `rpc_endpoints` or `zmq_endpoints` generates a random request and response object for the specific call, and then invokes the corresponding handler in the `core_rpc_server` object. diff --git a/tests/fuzz/fuzz_rpc/fuzz_rpc.cpp b/tests/fuzz/fuzz_rpc/fuzz_rpc.cpp new file mode 100644 index 000000000..ba1969218 --- /dev/null +++ b/tests/fuzz/fuzz_rpc/fuzz_rpc.cpp @@ -0,0 +1,91 @@ +#include "initialisation.h" +#include "rpc_endpoints.h" +#include + +#include +#include +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + // In general an iteration needs a fair amount of data so ensure we have enough + // to work with, otherwise return 0 to skip this iteration. + if (size < 512) { + return 0; + } + + // Determine if this iteration should run in safe mode +#ifdef SAFE + constexpr bool is_safe_mode = true; +#else + constexpr bool is_safe_mode = false; +#endif + + // Retrieve a list of all fuzz targets + auto fuzz_targets = get_fuzz_targets(is_safe_mode); + + // Disable fatal exits for logging. + el::Loggers::addFlag(el::LoggingFlag::DisableApplicationAbortOnFatalLog); + + // Prepare base FuzzedDataProvider + FuzzedDataProvider provider(data, size); + + // Randomly choose multiple fuzz_targets to fuzz + int rpc_messages_to_send = provider.ConsumeIntegralInRange(1, 16); + std::vector selectors; + if (is_safe_mode) { + selectors.reserve(rpc_messages_to_send); + } else { + selectors.reserve(rpc_messages_to_send + priority_fuzz_targets.size()); + for (int i = 0; i < priority_fuzz_targets.size(); ++i) { + selectors.push_back(i); + } + + // Randomly shuffle the selectors for priority fuzz targets + for (int i = 0; i < priority_fuzz_targets.size(); i++) { + int target = provider.ConsumeIntegralInRange(0, priority_fuzz_targets.size() - 1); + std::swap(selectors[i], selectors[target]); + } + } + + // Randomly select rpc functions to call + for (int i = 0; i < rpc_messages_to_send && provider.remaining_bytes() >= 2; ++i) { + int selector = provider.ConsumeIntegralInRange(0, fuzz_targets.size() - 1); + selectors.push_back(selector); + } + + // Initialise core and core_rpc_server + auto core_env = initialise_rpc_core(); + auto& dummy_core = core_env->core; + auto rpc_handler = initialise_rpc_server(*dummy_core, provider, !is_safe_mode); + + // Generate random blocks/miners/transactions and push to the core blockchains + if (!generate_random_blocks(*dummy_core, provider)) { + // No randomised blocks have been successfully added, skipping this iteration + dummy_core->get_blockchain_storage().get_db().batch_stop(); + return 0; + } + + // Disable bootstrap daemon + disable_bootstrap_daemon(*rpc_handler->rpc); + + for (int selector : selectors) { + try { + // Fuzz the target function + fuzz_targets[selector](*rpc_handler->rpc, provider); + } catch (const std::runtime_error&) { + // Known runtime_error thrown from monero + } catch (const cryptonote::DB_ERROR& e) { + // Known error thrown from monero on internal blockchain DB check + // when fuzzing with random values +#ifdef CATCH_ALL_EXCEPTIONS + } catch (...) { + // Silent all exceptions +#endif + } + } + + dummy_core->get_blockchain_storage().get_db().batch_stop(); + return 0; +} diff --git a/tests/fuzz/fuzz_rpc/fuzz_zmq.cpp b/tests/fuzz/fuzz_rpc/fuzz_zmq.cpp new file mode 100644 index 000000000..c60c43643 --- /dev/null +++ b/tests/fuzz/fuzz_rpc/fuzz_zmq.cpp @@ -0,0 +1,43 @@ +#include +#include "zmq_endpoints.h" +#include +#include + +using namespace cryptonote; +using namespace cryptonote::listener; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + if (size < 64) { + return 0; + } + + FuzzedDataProvider provider(data, size); + + void* ctx = zmq_ctx_new(); + if (!ctx) { + return 0; + } + + // Randomly choose multiple zmq_targets to fuzz + int to_sent = provider.ConsumeIntegralInRange(1, 8); + std::vector selectors; + selectors.reserve(to_sent); + for (int i = 0; i < to_sent && provider.remaining_bytes() >= 2; ++i) { + uint16_t raw = provider.ConsumeIntegral(); + selectors.push_back(raw % zmq_targets.size()); + } + + try { + zmq_pub pub(ctx); + for (int selector : selectors) { + zmq_targets[selector](pub, provider); + } + } catch (const std::runtime_error& e) { + // Ignore known runtime_error from checking + } + + zmq_ctx_shutdown(ctx); + zmq_ctx_term(ctx); + + return 0; +} diff --git a/tests/fuzz/fuzz_rpc/initialisation.cpp b/tests/fuzz/fuzz_rpc/initialisation.cpp new file mode 100644 index 000000000..6c2b06e22 --- /dev/null +++ b/tests/fuzz/fuzz_rpc/initialisation.cpp @@ -0,0 +1,357 @@ +#include "initialisation.h" + +#include "crypto/crypto.h" +#include "cryptonote_config.h" +#include "cryptonote_basic/cryptonote_basic.h" +#include "cryptonote_basic/cryptonote_basic_impl.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_basic/tx_extra.h" +#include "cryptonote_core/cryptonote_core.h" +#include "cryptonote_core/cryptonote_tx_utils.h" +#include "cryptonote_core/tx_pool.h" +#include "cryptonote_protocol/cryptonote_protocol_handler.h" +#include "p2p/net_node.h" +#include "p2p/net_node.inl" +#include "ringct/rctTypes.h" +#include "serialization/binary_utils.h" +#include "serialization/variant.h" + +#include +#include +#include +#include +#include + +// For storing valid transaction hashes +std::vector cached_tx_hashes; + +// Configure a dummy protocol to avoid external requests +bool DummyProtocol::is_synchronized() const { + return true; +} + +bool DummyProtocol::relay_transactions(cryptonote::NOTIFY_NEW_TRANSACTIONS::request&, const boost::uuids::uuid&, epee::net_utils::zone, cryptonote::relay_method) { + return true; +} + +bool DummyProtocol::relay_block(cryptonote::NOTIFY_NEW_FLUFFY_BLOCK::request&, cryptonote::cryptonote_connection_context&) { + return true; +} + +// Function to create and initialise a dummy rpc core object +std::unique_ptr initialise_rpc_core() { + auto env = std::make_unique(); + + // Create fresh pointer for dummy protocol and core + env->protocol = std::make_unique(); + env->core = std::make_unique(env->protocol.get()); + + // Configure the rpc core object with default settings + boost::program_options::options_description desc; + cryptonote::core::init_options(desc); + boost::program_options::variables_map vm; + + // Add command line argument to configure the rpc core object to use regression testing mode (FAKECHAIN) + // Enabling FAKECHAIN mode allows skipping validation logic of the authors signature and transactions ID + // on valid blocks and transactions while keeping other logic + std::vector argv = {"fuzz", "--regtest"}; + boost::program_options::store(boost::program_options::parse_command_line(argv.size(), argv.data(), desc), vm); + boost::program_options::notify(vm); + + // Initialise the dummy core with all the above settings + env->core->init(vm); + + return env; +} + +// Build the core_rpc_server handler object with the configured dummy core object +std::unique_ptr initialise_rpc_server(cryptonote::core& dummy_core, FuzzedDataProvider& provider, bool need_payment) { + // Create rpc server bundle object + auto bundle = std::make_unique(); + + // Allocate new protocol and node server then create RPC server object + bundle->proto_handler = std::make_unique>(dummy_core, nullptr, true); + bundle->proto_handler->set_no_sync(false); + bundle->dummy_p2p = std::make_unique>>(*bundle->proto_handler); + bundle->rpc = std::make_unique(dummy_core, *bundle->dummy_p2p); + + // Set up dummy variable map for rpc initialisation with payment + if (need_payment) { + boost::program_options::variables_map vm; + boost::program_options::options_description desc{"fuzz"}; + command_line::add_arg(desc, cryptonote::arg_data_dir); + command_line::add_arg(desc, cryptonote::arg_testnet_on); + command_line::add_arg(desc, cryptonote::arg_stagenet_on); + cryptonote::core_rpc_server::init_options(desc); + + // Generate random address and use it if it is a valid address with valid format + std::string address_arg; + if (provider.remaining_bytes() > 95) { + std::string random_str = provider.ConsumeBytesAsString(95); + if (!random_str.empty() && std::all_of(random_str.begin(), random_str.end(), [](char c) { + return isalnum(c); + })) { + address_arg = "--rpc-payment-address=" + random_str; + } + } + + // Fall back to default hardcoded address if generated address is invalid + if (address_arg.empty()) { + address_arg = "--rpc-payment-address=44AFFq5kSiGBoZKfRLKFY7bUuS5JxqLkZ3Zf1diYv5ZdfTP7hS5gZtSGdgjNXmYGFzRiV3yTgF8Yf4zrhGcq14D3z8PUnHT"; + } + + // Provide needed payment related configuration for init of core_rpc_server + std::vector argv = { + "fuzz", + address_arg.c_str() + }; + boost::program_options::store(boost::program_options::parse_command_line(argv.size(), argv.data(), desc), vm); + boost::program_options::notify(vm); + bool success = bundle->rpc->init(vm, true, "18089", true, ""); + if (!success) { + // Revert back to a fresh core_rpc_server if payment module init is failed + bundle->rpc = std::make_unique(dummy_core, *bundle->dummy_p2p); + } + } + + return bundle; +} + +bool generate_random_blocks(cryptonote::core& core, FuzzedDataProvider& provider) { + static std::vector cached_blocks; + static std::vector cached_txs; + + bool use_cached_blocks = provider.ConsumeBool(); + bool use_cached_txs = provider.ConsumeBool(); + + if (!use_cached_blocks || cached_blocks.empty()) { + // Create new cached blocks + cached_blocks.clear(); + + // Prepare the genesis block (initial base block) in the block chains + cryptonote::block genesis_block; + cryptonote::generate_genesis_block(genesis_block, config::GENESIS_TX, config::GENESIS_NONCE); + cached_blocks.push_back(genesis_block); + + // Setup initial values + const size_t median_weight = 300000; + const size_t block_weight = 100000; + const uint8_t version = core.get_blockchain_storage().get_current_hard_fork_version(); + + // Accumulate coins for genesis block + uint64_t coins = 0; + for (const auto& o : genesis_block.miner_tx.vout) { + coins += o.amount; + } + + // Create random number of miners + size_t miner_count = provider.ConsumeIntegralInRange(1, 4); + std::vector miners(miner_count); + for (auto& miner : miners) { + miner.generate(); + } + + // Create random number of blocks + size_t total_blocks = provider.ConsumeIntegralInRange(2, 15); + for (size_t i = 0; i < total_blocks; ++i) { + // Stop generate blocks if no more bytes left + if (provider.remaining_bytes() <= 0) { + break; + } + + // Randomly link a miner identity to this block + cryptonote::block blk; + size_t selected_miner_index = provider.ConsumeIntegralInRange(0, miners.size() - 1); + cryptonote::account_base& miner = miners[selected_miner_index]; + + // Initialise block property with random value + uint64_t height = cached_blocks.size(); + crypto::hash prev_id = cached_blocks.back().hash; + blk.major_version = version; + blk.minor_version = version; + blk.timestamp = core.get_blockchain_storage().get_adjusted_time(height); + blk.prev_id = prev_id; + blk.nonce = provider.ConsumeIntegral(); + + // Calculate and accumulate reward + uint64_t base_reward = 0, fee = 0; + if (!cryptonote::get_block_reward(median_weight, block_weight, coins, base_reward, version)) { + break; + } + + // Link empty miner transaction to block + cryptonote::transaction miner_tx; + cryptonote::construct_miner_tx( + height, median_weight, coins, block_weight, + fee, miner.get_keys().m_account_address, + miner_tx, cryptonote::blobdata(), + 1, blk.major_version + ); + for (const auto& o : miner_tx.vout) { + coins += o.amount; + } + miner_tx.version = 2; + blk.miner_tx = miner_tx; + + // Add the block to the cache + cached_blocks.push_back(blk); + } + } + + if (cached_blocks.empty()) { + // No random block generated, exit early to next iteration + return false; + } + + if (!use_cached_txs || cached_txs.empty()) { + // Create new cache transactions + cached_txs.clear(); + cached_tx_hashes.clear(); + + // Random number of transactions to generate + const size_t tx_count = provider.ConsumeIntegralInRange(1, 4); + for (size_t i = 0; i < tx_count; ++i) { + // Stop generate transactions if no more bytes left + if (provider.remaining_bytes() <= 0) { + break; + } + + // Initialise variables + cryptonote::transaction tx; + std::vector sources; + std::vector destinations; + std::vector extra; + + // Generate sender account + cryptonote::account_base sender; + sender.generate(); + + // Generate receiver account + cryptonote::account_base receiver; + receiver.generate(); + + // Generate random transfer amount + uint64_t transfer_amount = provider.ConsumeIntegralInRange(10000, 1000000000); + uint64_t input_amount = transfer_amount + 10000; + + // Prepare source entry + cryptonote::tx_source_entry src{}; + src.amount = input_amount; + src.rct = false; + src.real_output = 0; + src.real_output_in_tx_index = 0; + + // Derive valid public key + hw::device &hwdev = sender.get_keys().get_device(); + cryptonote::keypair tx_key = cryptonote::keypair::generate(hwdev); + crypto::key_derivation derivation; + if (!crypto::generate_key_derivation(sender.get_keys().m_account_address.m_view_public_key, tx_key.sec, derivation)) { + continue; + } + crypto::public_key out_pub_key; + if (!crypto::derive_public_key(derivation, 0, sender.get_keys().m_account_address.m_spend_public_key, out_pub_key)) { + continue; + } + src.real_out_tx_key = tx_key.pub; + + // Configure output entry for source_entry + for (size_t j = 0; j < 10; ++j) { + cryptonote::tx_source_entry::output_entry oe; + oe.first = j; + crypto::public_key pk; + + if (j == src.real_output) { + pk = out_pub_key; + } else { + cryptonote::keypair fake = cryptonote::keypair::generate(hwdev); + pk = fake.pub; + } + + crypto::view_tag vtag{}; + vtag.data = static_cast(provider.ConsumeIntegral()); + + cryptonote::txout_to_tagged_key tagged{}; + tagged.key = pk; + tagged.view_tag = vtag; + + oe.second.dest = *reinterpret_cast(&tagged.key); + oe.second.mask = rct::identity(); + src.outputs.push_back(oe); + } + sources.push_back(src); + + // Prepare destination entry + cryptonote::tx_destination_entry dst; + dst.amount = transfer_amount; + dst.addr = receiver.get_keys().m_account_address; + dst.is_subaddress = false; + destinations.push_back(dst); + + // Construct the transaction + crypto::secret_key tx_secret_key; + std::vector additional_tx_keys; + std::unordered_map subaddresses; + subaddresses[sender.get_keys().m_account_address.m_spend_public_key] = {0, 0}; + std::vector destinations_copy = destinations; + + // Construct transaction objects + bool success = cryptonote::construct_tx_and_get_tx_key( + sender.get_keys(), + subaddresses, + sources, + destinations_copy, + boost::none, + extra, + tx, + tx_secret_key, + additional_tx_keys, + true, + {rct::RangeProofPaddedBulletproof, 0}, + true + ); + if (!success) { + continue; + } + + // Force version and unlock_time + tx.version = 2; + tx.unlock_time = 0; + + // Serialise the transaction + cryptonote::blobdata tx_blob; + if (!cryptonote::t_serializable_object_to_blob(tx, tx_blob)) { + continue; + } + + // Add the transaction to cache + cached_txs.push_back(tx_blob); + } + } + + core.get_blockchain_storage().get_db().batch_start(); + + bool added_block = false; + for (const auto& blk : cached_blocks) { + cryptonote::block_verification_context bvc{}; + if (core.get_blockchain_storage().add_new_block(blk, bvc)) { + added_block = true; + } + } + + bool added_txs = false; + for (const auto& tx_blob : cached_txs) { + cryptonote::tx_verification_context tvc; + bool accepted = core.handle_incoming_tx(tx_blob, tvc, cryptonote::relay_method::block, true); + if (accepted || tvc.m_added_to_pool) { + added_txs = true; + + // Store legit hashes + cryptonote::transaction tx; + if (cryptonote::parse_and_validate_tx_from_blob(tx_blob, tx)) { + cached_tx_hashes.push_back(cryptonote::get_transaction_hash(tx)); + } + } + } + + return added_block; +} diff --git a/tests/fuzz/fuzz_rpc/initialisation.h b/tests/fuzz/fuzz_rpc/initialisation.h new file mode 100644 index 000000000..e892c410e --- /dev/null +++ b/tests/fuzz/fuzz_rpc/initialisation.h @@ -0,0 +1,31 @@ +#pragma once +#include "crypto/hash.h" +#include "rpc/core_rpc_server.h" +#include + +// Define templates for dummy protocol object +template class nodetool::node_server>; + +struct DummyProtocol : public cryptonote::i_cryptonote_protocol { +public: + bool is_synchronized() const; + bool relay_transactions(cryptonote::NOTIFY_NEW_TRANSACTIONS::request&, const boost::uuids::uuid&, epee::net_utils::zone, cryptonote::relay_method); + bool relay_block(cryptonote::NOTIFY_NEW_FLUFFY_BLOCK::request&, cryptonote::cryptonote_connection_context&); +}; + +struct RpcServerBundle { + std::unique_ptr> proto_handler; + std::unique_ptr>> dummy_p2p; + std::unique_ptr rpc; +}; + +struct CoreEnv { + std::unique_ptr protocol; + std::unique_ptr core; +}; + +std::unique_ptr initialise_rpc_core(); +std::unique_ptr initialise_rpc_server(cryptonote::core&, FuzzedDataProvider&, bool); +bool generate_random_blocks(cryptonote::core&, FuzzedDataProvider&); + +extern std::vector cached_tx_hashes; diff --git a/tests/fuzz/fuzz_rpc/rpc_endpoints.cpp b/tests/fuzz/fuzz_rpc/rpc_endpoints.cpp new file mode 100644 index 000000000..028ea25f3 --- /dev/null +++ b/tests/fuzz/fuzz_rpc/rpc_endpoints.cpp @@ -0,0 +1,939 @@ +#include "rpc_endpoints.h" +#include "initialisation.h" +#include "rpc/rpc_payment_signature.h" +#include +#include + +// Initialising common objects for calling RPC endpoint functions +cryptonote::core_rpc_server::connection_context ctx; +epee::json_rpc::error error_resp; + +// Helper function to disable bootstrap daemon +void disable_bootstrap_daemon(cryptonote::core_rpc_server& rpc) { + cryptonote::COMMAND_RPC_SET_BOOTSTRAP_DAEMON::request req; + cryptonote::COMMAND_RPC_SET_BOOTSTRAP_DAEMON::response res; + + req.address = ""; + req.username = ""; + req.password = ""; + req.proxy = ""; + + rpc.on_set_bootstrap_daemon(req, res, &ctx); +} + +// Retrieve fuzz targets base on SAFE settings +std::map> get_fuzz_targets(bool safe) { + std::map> results; + + if (safe) { + // Only return safe and stable fuzz targets after re-indexing + for (const auto& kv : safe_fuzz_targets) { + results[kv.first - 14] = kv.second; + } + } else { + // Return the full list of fuzz targets + results.insert(priority_fuzz_targets.begin(), priority_fuzz_targets.end()); + results.insert(safe_fuzz_targets.begin(), safe_fuzz_targets.end()); + results.insert(risky_fuzz_targets.begin(), risky_fuzz_targets.end()); + } + + return results; +} + +// Fuzzing functions for different RPC endpoint functions +void fuzz_get_height(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_HEIGHT::request req; + cryptonote::COMMAND_RPC_GET_HEIGHT::response res; + + rpc.on_get_height(req, res, &ctx); +} + +void fuzz_get_blocks(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::request req; + cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::response res; + req.client = "fuzz"; + + req.requested_info = provider.ConsumeIntegralInRange(0, 2); + + size_t count = provider.ConsumeIntegralInRange(0, 16); + for (size_t i = 0; i < count; ++i) { + std::string hash_data = provider.ConsumeBytesAsString(sizeof(crypto::hash)); + if (hash_data.size() != sizeof(crypto::hash)) break; + crypto::hash h; + std::memcpy(&h, hash_data.data(), sizeof(h)); + req.block_ids.push_back(h); + } + + req.start_height = provider.ConsumeIntegral(); + req.pool_info_since = provider.ConsumeIntegral(); + req.max_block_count = provider.ConsumeIntegral(); + + req.prune = provider.ConsumeBool(); + req.no_miner_tx = provider.ConsumeBool(); + + rpc.on_get_blocks(req, res, &ctx); +} + +void fuzz_get_blocks_by_height(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_BLOCKS_BY_HEIGHT::request req; + cryptonote::COMMAND_RPC_GET_BLOCKS_BY_HEIGHT::response res; + req.client = "fuzz"; + + size_t count = provider.ConsumeIntegralInRange(0, 16); + for (size_t i = 0; i < count; ++i) { + uint64_t height = provider.ConsumeIntegral(); + req.heights.push_back(height); + } + + rpc.on_get_blocks_by_height(req, res, &ctx); +} + +void fuzz_get_hashes(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_HASHES_FAST::request req; + cryptonote::COMMAND_RPC_GET_HASHES_FAST::response res; + req.client = "fuzz"; + + size_t count = provider.ConsumeIntegralInRange(0, 16); + for (size_t i = 0; i < count; ++i) { + std::string hash_data = provider.ConsumeBytesAsString(sizeof(crypto::hash)); + if (hash_data.size() != sizeof(crypto::hash)) break; + crypto::hash h; + std::memcpy(&h, hash_data.data(), sizeof(h)); + req.block_ids.push_back(h); + } + + req.start_height = provider.ConsumeIntegral(); + + rpc.on_get_hashes(req, res, &ctx); +} + +void fuzz_get_indexes(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_TX_GLOBAL_OUTPUTS_INDEXES::request req; + cryptonote::COMMAND_RPC_GET_TX_GLOBAL_OUTPUTS_INDEXES::response res; + req.client = "fuzz"; + + std::string txid_data = provider.ConsumeBytesAsString(sizeof(crypto::hash)); + if (txid_data.size() == sizeof(crypto::hash)) { + std::memcpy(&req.txid, txid_data.data(), sizeof(req.txid)); + } + + rpc.on_get_indexes(req, res, &ctx); +} + +void fuzz_get_outs_bin(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_OUTPUTS_BIN::request req; + cryptonote::COMMAND_RPC_GET_OUTPUTS_BIN::response res; + req.client = "fuzz"; + + size_t count = provider.ConsumeIntegralInRange(0, 16); + for (size_t i = 0; i < count; ++i) { + cryptonote::get_outputs_out out; + out.amount = provider.ConsumeIntegral(); + out.index = provider.ConsumeIntegral(); + req.outputs.push_back(out); + } + + req.get_txid = provider.ConsumeBool(); + + rpc.on_get_outs_bin(req, res, &ctx); +} + +void fuzz_get_transactions(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_TRANSACTIONS::request req; + cryptonote::COMMAND_RPC_GET_TRANSACTIONS::response res; + req.client = "fuzz"; + + // Cached valid transaction hashes + size_t real_count = provider.ConsumeIntegralInRange(0, cached_tx_hashes.size()); + for (size_t i = 0; i < real_count; ++i) { + const crypto::hash& h = cached_tx_hashes[i]; + req.txs_hashes.emplace_back(epee::string_tools::pod_to_hex(h)); + } + + // Random transaction hashes (that still need 64 bytes hex + size_t junk_count = provider.ConsumeIntegralInRange(0, 3); + for (size_t i = 0; i < junk_count; ++i) { + std::string junk; + for (size_t j = 0; j < 64; ++j) { + junk += "0123456789abcdef"[provider.ConsumeIntegralInRange(0, 15)]; + } + req.txs_hashes.emplace_back(std::move(junk)); + } + + req.decode_as_json = provider.ConsumeBool(); + req.prune = provider.ConsumeBool(); + req.split = provider.ConsumeBool(); + + rpc.on_get_transactions(req, res, &ctx); +} + +void fuzz_get_alt_blocks_hashes(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_ALT_BLOCKS_HASHES::request req; + cryptonote::COMMAND_RPC_GET_ALT_BLOCKS_HASHES::response res; + req.client = "fuzz"; + + rpc.on_get_alt_blocks_hashes(req, res, &ctx); +} + +void fuzz_is_key_image_spent(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_IS_KEY_IMAGE_SPENT::request req; + cryptonote::COMMAND_RPC_IS_KEY_IMAGE_SPENT::response res; + req.client = "fuzz"; + + size_t count = provider.ConsumeIntegralInRange(0, 16); + for (size_t i = 0; i < count; ++i) { + req.key_images.push_back(provider.ConsumeRandomLengthString(64)); + } + + rpc.on_is_key_image_spent(req, res, &ctx); +} + +void fuzz_send_raw_tx(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_SEND_RAW_TX::request req; + cryptonote::COMMAND_RPC_SEND_RAW_TX::response res; + req.client = "fuzz"; + + const std::string raw_tx_blob = provider.ConsumeRandomLengthString(512); + req.tx_as_hex = epee::string_tools::buff_to_hex_nodelimer(raw_tx_blob); + req.do_not_relay = provider.ConsumeBool(); + req.do_sanity_checks = provider.ConsumeBool(); + + rpc.on_send_raw_tx(req, res, &ctx); +} + +void fuzz_start_mining(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_START_MINING::request req; + cryptonote::COMMAND_RPC_START_MINING::response res; + + req.miner_address = provider.ConsumeRandomLengthString(128); + req.threads_count = provider.ConsumeIntegralInRange(0, 256); + req.do_background_mining = provider.ConsumeBool(); + req.ignore_battery = provider.ConsumeBool(); + + rpc.on_start_mining(req, res, &ctx); +} + +void fuzz_stop_mining(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_STOP_MINING::request req; + cryptonote::COMMAND_RPC_STOP_MINING::response res; + + rpc.on_stop_mining(req, res, &ctx); +} + +void fuzz_mining_status(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_MINING_STATUS::request req; + cryptonote::COMMAND_RPC_MINING_STATUS::response res; + + rpc.on_mining_status(req, res, &ctx); +} + +void fuzz_save_bc(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_SAVE_BC::request req; + cryptonote::COMMAND_RPC_SAVE_BC::response res; + + rpc.on_save_bc(req, res, &ctx); +} + +void fuzz_get_peer_list(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_PEER_LIST::request req; + cryptonote::COMMAND_RPC_GET_PEER_LIST::response res; + + req.public_only = provider.ConsumeBool(); + req.include_blocked = provider.ConsumeBool(); + + rpc.on_get_peer_list(req, res, &ctx); +} + +void fuzz_get_public_nodes(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_PUBLIC_NODES::request req; + cryptonote::COMMAND_RPC_GET_PUBLIC_NODES::response res; + + req.gray = provider.ConsumeBool(); + req.white = provider.ConsumeBool(); + req.include_blocked = provider.ConsumeBool(); + + rpc.on_get_public_nodes(req, res, &ctx); +} + +void fuzz_set_log_hash_rate(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_SET_LOG_HASH_RATE::request req; + cryptonote::COMMAND_RPC_SET_LOG_HASH_RATE::response res; + + req.visible = provider.ConsumeBool(); + + rpc.on_set_log_hash_rate(req, res, &ctx); +} + +void fuzz_set_log_level(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_SET_LOG_LEVEL::request req; + cryptonote::COMMAND_RPC_SET_LOG_LEVEL::response res; + + req.level = provider.ConsumeIntegral(); + + rpc.on_set_log_level(req, res, &ctx); +} + +void fuzz_set_log_categories(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_SET_LOG_CATEGORIES::request req; + cryptonote::COMMAND_RPC_SET_LOG_CATEGORIES::response res; + + req.categories = provider.ConsumeRandomLengthString(32); + + rpc.on_set_log_categories(req, res, &ctx); +} + +void fuzz_get_transaction_pool(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL::request req; + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL::response res; + req.client = "fuzz"; + + rpc.on_get_transaction_pool(req, res, &ctx); +} + +void fuzz_get_transaction_pool_hashes_bin(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_HASHES_BIN::request req; + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_HASHES_BIN::response res; + req.client = "fuzz"; + + rpc.on_get_transaction_pool_hashes_bin(req, res, &ctx); +} + +void fuzz_get_transaction_pool_hashes(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_HASHES::request req; + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_HASHES::response res; + req.client = "fuzz"; + + rpc.on_get_transaction_pool_hashes(req, res, &ctx); +} + +void fuzz_get_transaction_pool_stats(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_STATS::request req; + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_STATS::response res; + req.client = "fuzz"; + + rpc.on_get_transaction_pool_stats(req, res, &ctx); +} + +void fuzz_set_bootstrap_daemon(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_SET_BOOTSTRAP_DAEMON::request req; + cryptonote::COMMAND_RPC_SET_BOOTSTRAP_DAEMON::response res; + + req.address = provider.ConsumeRandomLengthString(64); + req.username = provider.ConsumeRandomLengthString(32); + req.password = provider.ConsumeRandomLengthString(32); + req.proxy = provider.ConsumeRandomLengthString(32); + + rpc.on_set_bootstrap_daemon(req, res, &ctx); + + // Immediate reset bootstrap daemon to avoid affecting other fuzzing with external calls + disable_bootstrap_daemon(rpc); +} + +void fuzz_stop_daemon(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_STOP_DAEMON::request req; + cryptonote::COMMAND_RPC_STOP_DAEMON::response res; + + rpc.on_stop_daemon(req, res, &ctx); +} + +void fuzz_get_info(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_INFO::request req; + cryptonote::COMMAND_RPC_GET_INFO::response res; + req.client = "fuzz"; + + rpc.on_get_info(req, res, &ctx); +} + +void fuzz_get_net_stats(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_NET_STATS::request req; + cryptonote::COMMAND_RPC_GET_NET_STATS::response res; + + rpc.on_get_net_stats(req, res, &ctx); +} + +void fuzz_get_limit(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_LIMIT::request req; + cryptonote::COMMAND_RPC_GET_LIMIT::response res; + + rpc.on_get_limit(req, res, &ctx); +} + +void fuzz_set_limit(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_SET_LIMIT::request req; + cryptonote::COMMAND_RPC_SET_LIMIT::response res; + + req.limit_down = provider.ConsumeIntegral(); + req.limit_up = provider.ConsumeIntegral(); + + rpc.on_set_limit(req, res, &ctx); +} + +void fuzz_out_peers(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_OUT_PEERS::request req; + cryptonote::COMMAND_RPC_OUT_PEERS::response res; + + req.set = provider.ConsumeBool(); + req.out_peers = provider.ConsumeIntegral(); + + rpc.on_out_peers(req, res, &ctx); +} + +void fuzz_in_peers(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_IN_PEERS::request req; + cryptonote::COMMAND_RPC_IN_PEERS::response res; + + req.set = provider.ConsumeBool(); + req.in_peers = provider.ConsumeIntegral(); + + rpc.on_in_peers(req, res, &ctx); +} + +void fuzz_get_outs(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_OUTPUTS::request req; + cryptonote::COMMAND_RPC_GET_OUTPUTS::response res; + req.client = "fuzz"; + + size_t count = provider.ConsumeIntegralInRange(0, 5); + for (size_t i = 0; i < count; ++i) { + cryptonote::get_outputs_out out; + out.amount = provider.ConsumeIntegral(); + out.index = provider.ConsumeIntegral(); + req.outputs.push_back(out); + } + + req.get_txid = provider.ConsumeBool(); + + rpc.on_get_outs(req, res, &ctx); +} + +void fuzz_update(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_UPDATE::request req; + cryptonote::COMMAND_RPC_UPDATE::response res; + + req.command = provider.ConsumeRandomLengthString(16); + req.path = provider.ConsumeRandomLengthString(32); + + rpc.on_update(req, res, &ctx); +} + +void fuzz_get_output_distribution_bin(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::request req; + cryptonote::COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::response res; + req.client = "fuzz"; + req.amounts = {0}; + + req.from_height = provider.ConsumeIntegral(); + req.to_height = req.from_height + provider.ConsumeIntegralInRange(0, 1000); + req.cumulative = provider.ConsumeBool(); + req.binary = provider.ConsumeBool(); + req.compress = provider.ConsumeBool(); + + rpc.on_get_output_distribution_bin(req, res, &ctx); +} + +void fuzz_pop_blocks(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_POP_BLOCKS::request req; + cryptonote::COMMAND_RPC_POP_BLOCKS::response res; + + req.nblocks = provider.ConsumeIntegral(); + rpc.on_pop_blocks(req, res, &ctx); +} + +void fuzz_getblockcount(cryptonote::core_rpc_server& rpc, FuzzedDataProvider&) { + cryptonote::COMMAND_RPC_GETBLOCKCOUNT::request req; + cryptonote::COMMAND_RPC_GETBLOCKCOUNT::response res; + + rpc.on_getblockcount(req, res, &ctx); +} + + +// Fuzzing functions for RPC JSONAPI endpoint functions +void fuzz_getblockhash(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GETBLOCKHASH::request req; + cryptonote::COMMAND_RPC_GETBLOCKHASH::response res; + + size_t count = provider.ConsumeIntegralInRange(0, 10); + for (size_t i = 0; i < count; ++i) { + req.push_back(provider.ConsumeIntegral()); + } + + rpc.on_getblockhash(req, res, error_resp, &ctx); +} + +void fuzz_getblocktemplate(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GETBLOCKTEMPLATE::request req; + cryptonote::COMMAND_RPC_GETBLOCKTEMPLATE::response res; + + if (provider.ConsumeBool()) { + req.wallet_address = "9uVsvEryzpN8WH2t1WWhFFCG5tS8cBNdmJYNRuckLENFimfauV5pZKeS1P2CbxGkSDTUPHXWwiYE5ZGSXDAGbaZgDxobqDN"; + } else { + req.wallet_address = provider.ConsumeRandomLengthString(128); + } + + req.reserve_size = provider.ConsumeIntegral(); + req.prev_block = provider.ConsumeRandomLengthString(64); + req.extra_nonce = provider.ConsumeRandomLengthString(32); + + rpc.on_getblocktemplate(req, res, error_resp, &ctx); +} + +void fuzz_getminerdata(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GETMINERDATA::request req; + cryptonote::COMMAND_RPC_GETMINERDATA::response res; + + rpc.on_getminerdata(req, res, error_resp, &ctx); +} + +void fuzz_calcpow(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_CALCPOW::request req; + cryptonote::COMMAND_RPC_CALCPOW::response res; + + req.major_version = provider.ConsumeIntegral(); + req.height = provider.ConsumeIntegral(); + req.block_blob = provider.ConsumeRandomLengthString(128); + req.seed_hash = provider.ConsumeRandomLengthString(64); + + rpc.on_calcpow(req, res, error_resp, &ctx); +} + +void fuzz_add_aux_pow(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_ADD_AUX_POW::request req; + cryptonote::COMMAND_RPC_ADD_AUX_POW::response res; + + req.blocktemplate_blob = provider.ConsumeRandomLengthString(128); + + size_t count = provider.ConsumeIntegralInRange(0, 4); + for (size_t i = 0; i < count; ++i) { + cryptonote::COMMAND_RPC_ADD_AUX_POW::aux_pow_t aux; + aux.id = provider.ConsumeRandomLengthString(32); + aux.hash = provider.ConsumeRandomLengthString(64); + req.aux_pow.push_back(aux); + } + + rpc.on_add_aux_pow(req, res, error_resp, &ctx); +} + +void fuzz_submitblock(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_SUBMITBLOCK::request req; + cryptonote::COMMAND_RPC_SUBMITBLOCK::response res; + + std::string hash_data = provider.ConsumeBytesAsString(sizeof(crypto::hash)); + if (hash_data.size() != sizeof(crypto::hash)) return; + crypto::hash h; + std::memcpy(&h, hash_data.data(), sizeof(h)); + std::string hex_str = epee::string_tools::pod_to_hex(h); + req.push_back(hex_str); + + rpc.on_submitblock(req, res, error_resp, &ctx); +} + +void fuzz_generateblocks(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GENERATEBLOCKS::request req; + cryptonote::COMMAND_RPC_GENERATEBLOCKS::response res; + + req.amount_of_blocks = provider.ConsumeIntegral(); + req.wallet_address = provider.ConsumeRandomLengthString(128); + req.prev_block = provider.ConsumeRandomLengthString(128); + req.starting_nonce = provider.ConsumeIntegral(); + + rpc.on_generateblocks(req, res, error_resp, &ctx); +} + +void fuzz_get_last_block_header(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_LAST_BLOCK_HEADER::request req; + cryptonote::COMMAND_RPC_GET_LAST_BLOCK_HEADER::response res; + req.client = "fuzz"; + + req.fill_pow_hash = provider.ConsumeBool(); + + rpc.on_get_last_block_header(req, res, error_resp, &ctx); +} + +void fuzz_get_block_header_by_hash(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_BLOCK_HEADER_BY_HASH::request req; + cryptonote::COMMAND_RPC_GET_BLOCK_HEADER_BY_HASH::response res; + req.client = "fuzz"; + + req.hash = provider.ConsumeRandomLengthString(64); + + size_t count = provider.ConsumeIntegralInRange(0, 3); + for (size_t i = 0; i < count; ++i) { + req.hashes.push_back(provider.ConsumeRandomLengthString(64)); + } + + req.fill_pow_hash = provider.ConsumeBool(); + + rpc.on_get_block_header_by_hash(req, res, error_resp, &ctx); +} + +void fuzz_get_block_header_by_height(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_BLOCK_HEADER_BY_HEIGHT::request req; + cryptonote::COMMAND_RPC_GET_BLOCK_HEADER_BY_HEIGHT::response res; + req.client = "fuzz"; + + req.height = provider.ConsumeIntegral(); + req.fill_pow_hash = provider.ConsumeBool(); + + rpc.on_get_block_header_by_height(req, res, error_resp, &ctx); +} + +void fuzz_get_block_headers_range(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_BLOCK_HEADERS_RANGE::request req; + cryptonote::COMMAND_RPC_GET_BLOCK_HEADERS_RANGE::response res; + req.client = "fuzz"; + + req.start_height = provider.ConsumeIntegral(); + req.end_height = provider.ConsumeIntegral(); + req.fill_pow_hash = provider.ConsumeBool(); + + rpc.on_get_block_headers_range(req, res, error_resp, &ctx); +} + +void fuzz_get_block(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_BLOCK::request req; + cryptonote::COMMAND_RPC_GET_BLOCK::response res; + req.client = "fuzz"; + + req.hash = provider.ConsumeRandomLengthString(64); + req.height = provider.ConsumeIntegral(); + req.fill_pow_hash = provider.ConsumeBool(); + + rpc.on_get_block(req, res, error_resp, &ctx); +} + +void fuzz_get_connections(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_CONNECTIONS::request req; + cryptonote::COMMAND_RPC_GET_CONNECTIONS::response res; + + rpc.on_get_connections(req, res, error_resp, &ctx); +} + +void fuzz_get_info_json(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_INFO::request req; + cryptonote::COMMAND_RPC_GET_INFO::response res; + req.client = "fuzz"; + + rpc.on_get_info_json(req, res, error_resp, &ctx); +} + +void fuzz_hard_fork_info(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_HARD_FORK_INFO::request req; + cryptonote::COMMAND_RPC_HARD_FORK_INFO::response res; + + req.version = provider.ConsumeIntegral(); + + rpc.on_hard_fork_info(req, res, error_resp, &ctx); +} + +void fuzz_set_bans(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_SETBANS::request req; + cryptonote::COMMAND_RPC_SETBANS::response res; + + int count = provider.ConsumeIntegralInRange(0, 4); + for (int i = 0; i < count; ++i) { + cryptonote::COMMAND_RPC_SETBANS::ban entry; + entry.host = provider.ConsumeRandomLengthString(32); + entry.ip = provider.ConsumeIntegral(); + entry.ban = provider.ConsumeBool(); + entry.seconds = provider.ConsumeIntegral(); + req.bans.push_back(entry); + } + + rpc.on_set_bans(req, res, error_resp, &ctx); +} + +void fuzz_get_bans(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GETBANS::request req; + cryptonote::COMMAND_RPC_GETBANS::response res; + + rpc.on_get_bans(req, res, error_resp, &ctx); +} + +void fuzz_banned(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_BANNED::request req; + cryptonote::COMMAND_RPC_BANNED::response res; + + req.address = provider.ConsumeRandomLengthString(32); + + rpc.on_banned(req, res, error_resp, &ctx); +} + +void fuzz_flush_txpool(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_FLUSH_TRANSACTION_POOL::request req; + cryptonote::COMMAND_RPC_FLUSH_TRANSACTION_POOL::response res; + + int count = provider.ConsumeIntegralInRange(0, 8); + for (int i = 0; i < count; ++i) { + req.txids.push_back(provider.ConsumeRandomLengthString(64)); + } + + rpc.on_flush_txpool(req, res, error_resp, &ctx); +} + +void fuzz_get_output_histogram(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::request req; + cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::response res; + req.client = "fuzz"; + + int amount_count = provider.ConsumeIntegralInRange(0, 5); + for (int i = 0; i < amount_count; ++i) { + req.amounts.push_back(provider.ConsumeIntegral()); + } + req.min_count = provider.ConsumeIntegral(); + req.max_count = provider.ConsumeIntegral(); + req.unlocked = provider.ConsumeBool(); + req.recent_cutoff = provider.ConsumeIntegral(); + + rpc.on_get_output_histogram(req, res, error_resp, &ctx); +} + +void fuzz_get_version(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_VERSION::request req; + cryptonote::COMMAND_RPC_GET_VERSION::response res; + + rpc.on_get_version(req, res, error_resp, &ctx); +} + +void fuzz_get_coinbase_tx_sum(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_COINBASE_TX_SUM::request req; + cryptonote::COMMAND_RPC_GET_COINBASE_TX_SUM::response res; + req.client = "fuzz"; + + req.height = provider.ConsumeIntegral(); + req.count = provider.ConsumeIntegral(); + + rpc.on_get_coinbase_tx_sum(req, res, error_resp, &ctx); +} + +void fuzz_get_base_fee_estimate(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_BASE_FEE_ESTIMATE::request req; + cryptonote::COMMAND_RPC_GET_BASE_FEE_ESTIMATE::response res; + req.client = "fuzz"; + + req.grace_blocks = provider.ConsumeIntegral(); + + rpc.on_get_base_fee_estimate(req, res, error_resp, &ctx); +} + +void fuzz_get_alternate_chains(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_ALTERNATE_CHAINS::request req; + cryptonote::COMMAND_RPC_GET_ALTERNATE_CHAINS::response res; + + rpc.on_get_alternate_chains(req, res, error_resp, &ctx); +} + +void fuzz_relay_tx(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_RELAY_TX::request req; + cryptonote::COMMAND_RPC_RELAY_TX::response res; + req.client = "fuzz"; + + size_t count = provider.ConsumeIntegralInRange(0, 4); + for (size_t i = 0; i < count; ++i) { + std::string hash_data = provider.ConsumeBytesAsString(sizeof(crypto::hash)); + if (hash_data.size() != sizeof(crypto::hash)) break; + crypto::hash h; + std::memcpy(&h, hash_data.data(), sizeof(h)); + std::string hex_str = epee::string_tools::pod_to_hex(h); + req.txids.push_back(hex_str); + } + epee::json_rpc::error error_resp; + rpc.on_relay_tx(req, res, error_resp, &ctx); +} + +void fuzz_sync_info(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_SYNC_INFO::request req; + cryptonote::COMMAND_RPC_SYNC_INFO::response res; + req.client = "fuzz"; + + rpc.on_sync_info(req, res, error_resp, &ctx); +} + +void fuzz_get_txpool_backlog(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_BACKLOG::request req; + cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_BACKLOG::response res; + req.client = "fuzz"; + + rpc.on_get_txpool_backlog(req, res, error_resp, &ctx); +} + +void fuzz_get_output_distribution(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::request req; + cryptonote::COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::response res; + req.client = "fuzz"; + req.amounts = {0}; + + req.from_height = provider.ConsumeIntegral(); + req.to_height = req.from_height + provider.ConsumeIntegralInRange(0, 1000); + req.cumulative = provider.ConsumeBool(); + req.binary = provider.ConsumeBool(); + req.compress = provider.ConsumeBool(); + + rpc.on_get_output_distribution(req, res, error_resp, &ctx); +} + +void fuzz_prune_blockchain(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_PRUNE_BLOCKCHAIN::request req; + cryptonote::COMMAND_RPC_PRUNE_BLOCKCHAIN::response res; + + req.check = provider.ConsumeBool(); + + rpc.on_prune_blockchain(req, res, error_resp, &ctx); +} + +void fuzz_flush_cache(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_FLUSH_CACHE::request req; + cryptonote::COMMAND_RPC_FLUSH_CACHE::response res; + + req.bad_blocks = provider.ConsumeBool(); + + rpc.on_flush_cache(req, res, error_resp, &ctx); +} + +void fuzz_get_txids_loose(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_GET_TXIDS_LOOSE::request req; + cryptonote::COMMAND_RPC_GET_TXIDS_LOOSE::response res; + + req.txid_template = provider.ConsumeRandomLengthString(64); + req.num_matching_bits = provider.ConsumeIntegral(); + + rpc.on_get_txids_loose(req, res, error_resp, &ctx); +} + +void fuzz_rpc_access_info(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_ACCESS_INFO::request req; + cryptonote::COMMAND_RPC_ACCESS_INFO::response res; + req.client = "fuzz"; + + rpc.on_rpc_access_info(req, res, error_resp, &ctx); +} + +void fuzz_rpc_access_submit_nonce(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_ACCESS_SUBMIT_NONCE::request req; + cryptonote::COMMAND_RPC_ACCESS_SUBMIT_NONCE::response res; + req.client = "fuzz"; + + req.nonce = provider.ConsumeIntegral(); + req.cookie = provider.ConsumeIntegral(); + + rpc.on_rpc_access_submit_nonce(req, res, error_resp, &ctx); +} + +void fuzz_rpc_access_pay(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_ACCESS_PAY::request req; + cryptonote::COMMAND_RPC_ACCESS_PAY::response res; + req.client = "fuzz"; + + req.paying_for = provider.ConsumeRandomLengthString(32); + req.payment = provider.ConsumeIntegral(); + + rpc.on_rpc_access_pay(req, res, error_resp, &ctx); +} + +void fuzz_rpc_access_tracking(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_ACCESS_TRACKING::request req; + cryptonote::COMMAND_RPC_ACCESS_TRACKING::response res; + + req.clear = provider.ConsumeBool(); + + rpc.on_rpc_access_tracking(req, res, error_resp, &ctx); +} + +void fuzz_rpc_access_data(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_ACCESS_DATA::request req; + cryptonote::COMMAND_RPC_ACCESS_DATA::response res; + + rpc.on_rpc_access_data(req, res, error_resp, &ctx); +} + +void fuzz_rpc_access_account(cryptonote::core_rpc_server& rpc, FuzzedDataProvider& provider) { + cryptonote::COMMAND_RPC_ACCESS_ACCOUNT::request req; + cryptonote::COMMAND_RPC_ACCESS_ACCOUNT::response res; + req.client = "fuzz"; + + req.delta_balance = provider.ConsumeIntegral(); + + rpc.on_rpc_access_account(req, res, error_resp, &ctx); +} + +// Maps storing all fuzzing functions +std::map> priority_fuzz_targets = { + {0, fuzz_get_blocks}, + {1, fuzz_get_blocks_by_height}, + {2, fuzz_get_hashes}, + {3, fuzz_get_outs_bin}, + {4, fuzz_get_transactions}, + {5, fuzz_is_key_image_spent}, + {6, fuzz_send_raw_tx}, + {7, fuzz_get_output_distribution_bin}, + {8, fuzz_pop_blocks}, + {9, fuzz_getblocktemplate}, + {10, fuzz_submitblock}, + {11, fuzz_generateblocks}, + {12, fuzz_relay_tx}, + {13, fuzz_get_output_distribution}, +}; + +std::map> safe_fuzz_targets = { + {14, fuzz_get_height}, + {15, fuzz_get_indexes}, + {16, fuzz_get_alt_blocks_hashes}, + {17, fuzz_get_peer_list}, + {18, fuzz_get_public_nodes}, + {19, fuzz_set_log_hash_rate}, + {20, fuzz_set_log_level}, + {21, fuzz_set_log_categories}, + {22, fuzz_get_transaction_pool}, + {23, fuzz_get_transaction_pool_hashes_bin}, + {24, fuzz_get_transaction_pool_hashes}, + {25, fuzz_get_transaction_pool_stats}, + {26, fuzz_get_info}, + {27, fuzz_get_net_stats}, + {28, fuzz_get_limit}, + {29, fuzz_set_limit}, + {30, fuzz_out_peers}, + {31, fuzz_in_peers}, + {32, fuzz_get_outs}, + {33, fuzz_getblockcount}, + {34, fuzz_getblockhash}, + {35, fuzz_getminerdata}, + {36, fuzz_calcpow}, + {37, fuzz_get_last_block_header}, + {38, fuzz_get_block_header_by_hash}, + {39, fuzz_get_block_header_by_height}, + {40, fuzz_get_block_headers_range}, + {41, fuzz_get_block}, + {42, fuzz_get_connections}, + {43, fuzz_get_info_json}, + {44, fuzz_hard_fork_info}, + {45, fuzz_set_bans}, + {46, fuzz_get_bans}, + {47, fuzz_banned}, + {48, fuzz_get_output_histogram}, + {49, fuzz_get_version}, + {50, fuzz_get_coinbase_tx_sum}, + {51, fuzz_get_base_fee_estimate}, + {52, fuzz_get_alternate_chains}, + {53, fuzz_sync_info}, + {54, fuzz_get_txpool_backlog}, +}; + +std::map> risky_fuzz_targets = { + {55, fuzz_start_mining}, + {56, fuzz_stop_mining}, + {57, fuzz_mining_status}, + {58, fuzz_save_bc}, + {59, fuzz_set_bootstrap_daemon}, + {60, fuzz_stop_daemon}, + {61, fuzz_update}, + {62, fuzz_add_aux_pow}, + {63, fuzz_flush_txpool}, + {64, fuzz_flush_cache}, + {65, fuzz_get_txids_loose}, + {66, fuzz_rpc_access_info}, + {67, fuzz_rpc_access_submit_nonce}, + {68, fuzz_rpc_access_pay}, + {69, fuzz_rpc_access_tracking}, + {70, fuzz_rpc_access_data}, + {71, fuzz_rpc_access_account}, +// {72, fuzz_prune_blockchain}, +}; diff --git a/tests/fuzz/fuzz_rpc/rpc_endpoints.h b/tests/fuzz/fuzz_rpc/rpc_endpoints.h new file mode 100644 index 000000000..312313160 --- /dev/null +++ b/tests/fuzz/fuzz_rpc/rpc_endpoints.h @@ -0,0 +1,92 @@ +#pragma once +#include "common/perf_timer.h" +#include "rpc/core_rpc_server.h" +#include "fuzzer/FuzzedDataProvider.h" +#include +#include + +namespace tools { + extern __thread std::vector *performance_timers; +} + +void disable_bootstrap_daemon(cryptonote::core_rpc_server& rpc); +std::map> get_fuzz_targets(bool safe); + +void fuzz_get_height(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_blocks(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_blocks_by_height(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_hashes(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_indexes(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_outs_bin(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_transactions(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_alt_blocks_hashes(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_is_key_image_spent(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_send_raw_tx(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_start_mining(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_stop_mining(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_mining_status(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_save_bc(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_peer_list(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_public_nodes(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_set_log_hash_rate(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_set_log_level(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_set_log_categories(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_transaction_pool(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_transaction_pool_hashes_bin(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_transaction_pool_hashes(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_transaction_pool_stats(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_set_bootstrap_daemon(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_stop_daemon(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_info(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_net_stats(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_limit(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_set_limit(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_out_peers(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_in_peers(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_outs(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_update(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_output_distribution_bin(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_pop_blocks(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_getblockcount(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_getblockhash(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_getblocktemplate(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_getminerdata(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_calcpow(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_add_aux_pow(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_submitblock(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_generateblocks(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_last_block_header(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_block_header_by_hash(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_block_header_by_height(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_block_headers_range(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_block(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_connections(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_info_json(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_hard_fork_info(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_set_bans(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_bans(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_banned(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_flush_txpool(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_output_histogram(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_version(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_coinbase_tx_sum(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_base_fee_estimate(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_alternate_chains(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_relay_tx(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_sync_info(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_txpool_backlog(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_output_distribution(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_prune_blockchain(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_flush_cache(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_txids_loose(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_rpc_access_info(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_rpc_access_submit_nonce(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_rpc_access_pay(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_rpc_access_tracking(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_rpc_access_data(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_rpc_access_account(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); +void fuzz_get_height(cryptonote::core_rpc_server&, FuzzedDataProvider& provider); + +extern std::map> priority_fuzz_targets; +extern std::map> safe_fuzz_targets; +extern std::map> risky_fuzz_targets; diff --git a/tests/fuzz/fuzz_rpc/zmq_endpoints.cpp b/tests/fuzz/fuzz_rpc/zmq_endpoints.cpp new file mode 100644 index 000000000..d13e59249 --- /dev/null +++ b/tests/fuzz/fuzz_rpc/zmq_endpoints.cpp @@ -0,0 +1,87 @@ +#include "cryptonote_basic/events.h" +#include "cryptonote_core/tx_pool.h" +#include "rpc/zmq_pub.h" +#include "rpc_endpoints.h" +#include +#include + +using namespace cryptonote; +using namespace cryptonote::listener; + +void fuzz_sub_request(zmq_pub& pub, FuzzedDataProvider& provider) { + std::string sub = provider.ConsumeRandomLengthString(64); + pub.sub_request(boost::string_ref(sub)); +} + +void fuzz_send_chain_main(zmq_pub& pub, FuzzedDataProvider& provider) { + uint64_t height = provider.ConsumeIntegral(); + size_t blk_count = provider.ConsumeIntegralInRange(0, 4); + std::vector blocks; + + for (size_t i = 0; i < blk_count; ++i) { + block b{}; + b.major_version = provider.ConsumeIntegral(); + b.minor_version = provider.ConsumeIntegral(); + b.timestamp = provider.ConsumeIntegral(); + b.prev_id = crypto::null_hash; + b.nonce = provider.ConsumeIntegral(); + blocks.push_back(b); + } + + pub.send_chain_main(height, epee::span(blocks.data(), blocks.size())); +} + +void fuzz_send_miner_data(zmq_pub& pub, FuzzedDataProvider& provider) { + uint8_t major = provider.ConsumeIntegral(); + uint64_t h = provider.ConsumeIntegral(); + + crypto::hash prev_id, seed_hash; + std::memset(&prev_id, 0x01, sizeof(prev_id)); + std::memset(&seed_hash, 0x02, sizeof(seed_hash)); + difficulty_type diff = provider.ConsumeIntegral(); + + uint64_t median_weight = provider.ConsumeIntegral(); + uint64_t coins = provider.ConsumeIntegral(); + std::vector backlog; + size_t count = provider.ConsumeIntegralInRange(0, 3); + + for (size_t i = 0; i < count; ++i) { + tx_block_template_backlog_entry entry; + entry.weight = provider.ConsumeIntegral(); + entry.fee = provider.ConsumeIntegral(); + backlog.push_back(entry); + } + + pub.send_miner_data(major, h, prev_id, seed_hash, diff, median_weight, coins, backlog); +} + +void fuzz_send_txpool_add(zmq_pub& pub, FuzzedDataProvider& provider) { + size_t count = provider.ConsumeIntegralInRange(0, 3); + std::vector events; + for (size_t i = 0; i < count; ++i) { + txpool_event evt{}; + + evt.res = provider.ConsumeBool(); + + auto blob = provider.ConsumeRandomLengthString(128); + if (!parse_and_validate_tx_from_blob(blob, evt.tx)) { + continue; + } + + evt.hash = get_transaction_hash(evt.tx); + evt.blob_size = blob.size(); + evt.weight = provider.ConsumeIntegral(); + + events.push_back(std::move(evt)); + } + + pub.send_txpool_add(std::move(events)); +} + +// Map all zmq targets +std::map> zmq_targets = { + {0, fuzz_sub_request}, + {1, fuzz_send_chain_main}, + {2, fuzz_send_miner_data}, + {3, fuzz_send_txpool_add}, +}; diff --git a/tests/fuzz/fuzz_rpc/zmq_endpoints.h b/tests/fuzz/fuzz_rpc/zmq_endpoints.h new file mode 100644 index 000000000..fff4deecc --- /dev/null +++ b/tests/fuzz/fuzz_rpc/zmq_endpoints.h @@ -0,0 +1,19 @@ +#include "rpc/zmq_pub.h" +#include "cryptonote_basic/cryptonote_basic.h" +#include "crypto/crypto.h" +#include "crypto/hash.h" +#include "span.h" +#include +#include +#include +#include + +using namespace cryptonote; +using namespace cryptonote::listener; + +void fuzz_sub_request(zmq_pub&, FuzzedDataProvider&); +void fuzz_send_chain_main(zmq_pub&, FuzzedDataProvider&); +void fuzz_send_miner_data(zmq_pub&, FuzzedDataProvider&); +void fuzz_send_txpool_add(zmq_pub&, FuzzedDataProvider&); + +extern std::map> zmq_targets; diff --git a/tests/fuzz/include/fuzzer/FuzzedDataProvider.h b/tests/fuzz/include/fuzzer/FuzzedDataProvider.h new file mode 100644 index 000000000..33ffa8bc0 --- /dev/null +++ b/tests/fuzz/include/fuzzer/FuzzedDataProvider.h @@ -0,0 +1,397 @@ +//===- FuzzedDataProvider.h - Utility header for fuzz targets ---*- C++ -* ===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// A single header library providing an utility class to break up an array of +// bytes. Whenever run on the same input, provides the same output, as long as +// its methods are called in the same order, with the same arguments. +//===----------------------------------------------------------------------===// + +#ifndef LLVM_FUZZER_FUZZED_DATA_PROVIDER_H_ +#define LLVM_FUZZER_FUZZED_DATA_PROVIDER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// In addition to the comments below, the API is also briefly documented at +// https://github.com/google/fuzzing/blob/master/docs/split-inputs.md#fuzzed-data-provider +class FuzzedDataProvider { + public: + // |data| is an array of length |size| that the FuzzedDataProvider wraps to + // provide more granular access. |data| must outlive the FuzzedDataProvider. + FuzzedDataProvider(const uint8_t *data, size_t size) + : data_ptr_(data), remaining_bytes_(size) {} + ~FuzzedDataProvider() = default; + + // See the implementation below (after the class definition) for more verbose + // comments for each of the methods. + + // Methods returning std::vector of bytes. These are the most popular choice + // when splitting fuzzing input into pieces, as every piece is put into a + // separate buffer (i.e. ASan would catch any under-/overflow) and the memory + // will be released automatically. + template std::vector ConsumeBytes(size_t num_bytes); + template + std::vector ConsumeBytesWithTerminator(size_t num_bytes, T terminator = 0); + template std::vector ConsumeRemainingBytes(); + + // Methods returning strings. Use only when you need a std::string or a null + // terminated C-string. Otherwise, prefer the methods returning std::vector. + std::string ConsumeBytesAsString(size_t num_bytes); + std::string ConsumeRandomLengthString(size_t max_length); + std::string ConsumeRandomLengthString(); + std::string ConsumeRemainingBytesAsString(); + + // Methods returning integer values. + template T ConsumeIntegral(); + template T ConsumeIntegralInRange(T min, T max); + + // Methods returning floating point values. + template T ConsumeFloatingPoint(); + template T ConsumeFloatingPointInRange(T min, T max); + + // 0 <= return value <= 1. + template T ConsumeProbability(); + + bool ConsumeBool(); + + // Returns a value chosen from the given enum. + template T ConsumeEnum(); + + // Returns a value from the given array. + template T PickValueInArray(const T (&array)[size]); + template + T PickValueInArray(const std::array &array); + template T PickValueInArray(std::initializer_list list); + + // Writes data to the given destination and returns number of bytes written. + size_t ConsumeData(void *destination, size_t num_bytes); + + // Reports the remaining bytes available for fuzzed input. + size_t remaining_bytes() { return remaining_bytes_; } + + private: + FuzzedDataProvider(const FuzzedDataProvider &) = delete; + FuzzedDataProvider &operator=(const FuzzedDataProvider &) = delete; + + void CopyAndAdvance(void *destination, size_t num_bytes); + + void Advance(size_t num_bytes); + + template + std::vector ConsumeBytes(size_t size, size_t num_bytes); + + template TS ConvertUnsignedToSigned(TU value); + + const uint8_t *data_ptr_; + size_t remaining_bytes_; +}; + +// Returns a std::vector containing |num_bytes| of input data. If fewer than +// |num_bytes| of data remain, returns a shorter std::vector containing all +// of the data that's left. Can be used with any byte sized type, such as +// char, unsigned char, uint8_t, etc. +template +std::vector FuzzedDataProvider::ConsumeBytes(size_t num_bytes) { + num_bytes = std::min(num_bytes, remaining_bytes_); + return ConsumeBytes(num_bytes, num_bytes); +} + +// Similar to |ConsumeBytes|, but also appends the terminator value at the end +// of the resulting vector. Useful, when a mutable null-terminated C-string is +// needed, for example. But that is a rare case. Better avoid it, if possible, +// and prefer using |ConsumeBytes| or |ConsumeBytesAsString| methods. +template +std::vector FuzzedDataProvider::ConsumeBytesWithTerminator(size_t num_bytes, + T terminator) { + num_bytes = std::min(num_bytes, remaining_bytes_); + std::vector result = ConsumeBytes(num_bytes + 1, num_bytes); + result.back() = terminator; + return result; +} + +// Returns a std::vector containing all remaining bytes of the input data. +template +std::vector FuzzedDataProvider::ConsumeRemainingBytes() { + return ConsumeBytes(remaining_bytes_); +} + +// Returns a std::string containing |num_bytes| of input data. Using this and +// |.c_str()| on the resulting string is the best way to get an immutable +// null-terminated C string. If fewer than |num_bytes| of data remain, returns +// a shorter std::string containing all of the data that's left. +inline std::string FuzzedDataProvider::ConsumeBytesAsString(size_t num_bytes) { + static_assert(sizeof(std::string::value_type) == sizeof(uint8_t), + "ConsumeBytesAsString cannot convert the data to a string."); + + num_bytes = std::min(num_bytes, remaining_bytes_); + std::string result( + reinterpret_cast(data_ptr_), num_bytes); + Advance(num_bytes); + return result; +} + +// Returns a std::string of length from 0 to |max_length|. When it runs out of +// input data, returns what remains of the input. Designed to be more stable +// with respect to a fuzzer inserting characters than just picking a random +// length and then consuming that many bytes with |ConsumeBytes|. +inline std::string +FuzzedDataProvider::ConsumeRandomLengthString(size_t max_length) { + // Reads bytes from the start of |data_ptr_|. Maps "\\" to "\", and maps "\" + // followed by anything else to the end of the string. As a result of this + // logic, a fuzzer can insert characters into the string, and the string + // will be lengthened to include those new characters, resulting in a more + // stable fuzzer than picking the length of a string independently from + // picking its contents. + std::string result; + + // Reserve the anticipated capacity to prevent several reallocations. + result.reserve(std::min(max_length, remaining_bytes_)); + for (size_t i = 0; i < max_length && remaining_bytes_ != 0; ++i) { + char next = ConvertUnsignedToSigned(data_ptr_[0]); + Advance(1); + if (next == '\\' && remaining_bytes_ != 0) { + next = ConvertUnsignedToSigned(data_ptr_[0]); + Advance(1); + if (next != '\\') + break; + } + result += next; + } + + result.shrink_to_fit(); + return result; +} + +// Returns a std::string of length from 0 to |remaining_bytes_|. +inline std::string FuzzedDataProvider::ConsumeRandomLengthString() { + return ConsumeRandomLengthString(remaining_bytes_); +} + +// Returns a std::string containing all remaining bytes of the input data. +// Prefer using |ConsumeRemainingBytes| unless you actually need a std::string +// object. +inline std::string FuzzedDataProvider::ConsumeRemainingBytesAsString() { + return ConsumeBytesAsString(remaining_bytes_); +} + +// Returns a number in the range [Type's min, Type's max]. The value might +// not be uniformly distributed in the given range. If there's no input data +// left, always returns |min|. +template T FuzzedDataProvider::ConsumeIntegral() { + return ConsumeIntegralInRange(std::numeric_limits::min(), + std::numeric_limits::max()); +} + +// Returns a number in the range [min, max] by consuming bytes from the +// input data. The value might not be uniformly distributed in the given +// range. If there's no input data left, always returns |min|. |min| must +// be less than or equal to |max|. +template +T FuzzedDataProvider::ConsumeIntegralInRange(T min, T max) { + static_assert(std::is_integral_v, "An integral type is required."); + static_assert(sizeof(T) <= sizeof(uint64_t), "Unsupported integral type."); + + if (min > max) + abort(); + + // Use the biggest type possible to hold the range and the result. + uint64_t range = static_cast(max) - static_cast(min); + uint64_t result = 0; + size_t offset = 0; + + while (offset < sizeof(T) * CHAR_BIT && (range >> offset) > 0 && + remaining_bytes_ != 0) { + // Pull bytes off the end of the seed data. Experimentally, this seems to + // allow the fuzzer to more easily explore the input space. This makes + // sense, since it works by modifying inputs that caused new code to run, + // and this data is often used to encode length of data read by + // |ConsumeBytes|. Separating out read lengths makes it easier modify the + // contents of the data that is actually read. + --remaining_bytes_; + result = (result << CHAR_BIT) | data_ptr_[remaining_bytes_]; + offset += CHAR_BIT; + } + + // Avoid division by 0, in case |range + 1| results in overflow. + if (range != std::numeric_limits::max()) + result = result % (range + 1); + + return static_cast(static_cast(min) + result); +} + +// Returns a floating point value in the range [Type's lowest, Type's max] by +// consuming bytes from the input data. If there's no input data left, always +// returns approximately 0. +template T FuzzedDataProvider::ConsumeFloatingPoint() { + return ConsumeFloatingPointInRange(std::numeric_limits::lowest(), + std::numeric_limits::max()); +} + +// Returns a floating point value in the given range by consuming bytes from +// the input data. If there's no input data left, returns |min|. Note that +// |min| must be less than or equal to |max|. +template +T FuzzedDataProvider::ConsumeFloatingPointInRange(T min, T max) { + if (min > max) + abort(); + + T range = .0; + T result = min; + constexpr T zero(.0); + if (max > zero && min < zero && max > min + std::numeric_limits::max()) { + // The diff |max - min| would overflow the given floating point type. Use + // the half of the diff as the range and consume a bool to decide whether + // the result is in the first of the second part of the diff. + range = (max / 2.0) - (min / 2.0); + if (ConsumeBool()) { + result += range; + } + } else { + range = max - min; + } + + return result + range * ConsumeProbability(); +} + +// Returns a floating point number in the range [0.0, 1.0]. If there's no +// input data left, always returns 0. +template T FuzzedDataProvider::ConsumeProbability() { + static_assert(std::is_floating_point_v, + "A floating point type is required."); + + // Use different integral types for different floating point types in order + // to provide better density of the resulting values. + using IntegralType = + typename std::conditional_t<(sizeof(T) <= sizeof(uint32_t)), uint32_t, + uint64_t>; + + T result = static_cast(ConsumeIntegral()); + result /= static_cast(std::numeric_limits::max()); + return result; +} + +// Reads one byte and returns a bool, or false when no data remains. +inline bool FuzzedDataProvider::ConsumeBool() { + return 1 & ConsumeIntegral(); +} + +// Returns an enum value. The enum must start at 0 and be contiguous. It must +// also contain |kMaxValue| aliased to its largest (inclusive) value. Such as: +// enum class Foo { SomeValue, OtherValue, kMaxValue = OtherValue }; +template T FuzzedDataProvider::ConsumeEnum() { + static_assert(std::is_enum_v, "|T| must be an enum type."); + return static_cast( + ConsumeIntegralInRange(0, static_cast(T::kMaxValue))); +} + +// Returns a copy of the value selected from the given fixed-size |array|. +template +T FuzzedDataProvider::PickValueInArray(const T (&array)[size]) { + static_assert(size > 0, "The array must be non empty."); + return array[ConsumeIntegralInRange(0, size - 1)]; +} + +template +T FuzzedDataProvider::PickValueInArray(const std::array &array) { + static_assert(size > 0, "The array must be non empty."); + return array[ConsumeIntegralInRange(0, size - 1)]; +} + +template +T FuzzedDataProvider::PickValueInArray(std::initializer_list list) { + if (!list.size()) + abort(); + + return *(list.begin() + ConsumeIntegralInRange(0, list.size() - 1)); +} + +// Writes |num_bytes| of input data to the given destination pointer. If there +// is not enough data left, writes all remaining bytes. Return value is the +// number of bytes written. +// In general, it's better to avoid using this function, but it may be useful +// in cases when it's necessary to fill a certain buffer or object with +// fuzzing data. +inline size_t FuzzedDataProvider::ConsumeData(void *destination, + size_t num_bytes) { + num_bytes = std::min(num_bytes, remaining_bytes_); + CopyAndAdvance(destination, num_bytes); + return num_bytes; +} + +// Private methods. +inline void FuzzedDataProvider::CopyAndAdvance(void *destination, + size_t num_bytes) { + std::memcpy(destination, data_ptr_, num_bytes); + Advance(num_bytes); +} + +inline void FuzzedDataProvider::Advance(size_t num_bytes) { + if (num_bytes > remaining_bytes_) + abort(); + + data_ptr_ += num_bytes; + remaining_bytes_ -= num_bytes; +} + +template +std::vector FuzzedDataProvider::ConsumeBytes(size_t size, size_t num_bytes) { + static_assert(sizeof(T) == sizeof(uint8_t), "Incompatible data type."); + + // The point of using the size-based constructor below is to increase the + // odds of having a vector object with capacity being equal to the length. + // That part is always implementation specific, but at least both libc++ and + // libstdc++ allocate the requested number of bytes in that constructor, + // which seems to be a natural choice for other implementations as well. + // To increase the odds even more, we also call |shrink_to_fit| below. + std::vector result(size); + if (size == 0) { + if (num_bytes != 0) + abort(); + return result; + } + + CopyAndAdvance(result.data(), num_bytes); + + // Even though |shrink_to_fit| is also implementation specific, we expect it + // to provide an additional assurance in case vector's constructor allocated + // a buffer which is larger than the actual amount of data we put inside it. + result.shrink_to_fit(); + return result; +} + +template +TS FuzzedDataProvider::ConvertUnsignedToSigned(TU value) { + static_assert(sizeof(TS) == sizeof(TU), "Incompatible data types."); + static_assert(!std::numeric_limits::is_signed, + "Source type must be unsigned."); + + // TODO(Dor1s): change to `if constexpr` once C++17 becomes mainstream. + if (std::numeric_limits::is_modulo) + return static_cast(value); + + // Avoid using implementation-defined unsigned to signed conversions. + // To learn more, see https://stackoverflow.com/questions/13150449. + if (value <= std::numeric_limits::max()) { + return static_cast(value); + } else { + constexpr auto TS_min = std::numeric_limits::min(); + return TS_min + static_cast(value - TS_min); + } +} + +#endif // LLVM_FUZZER_FUZZED_DATA_PROVIDER_H_