|
11 | 11 |
|
12 | 12 | using namespace bb::avm2::fuzzer; |
13 | 13 |
|
| 14 | +// Extra counters to guide libfuzzer towards inputs with more enqueued calls. |
| 15 | +// Index 0 = 1 call, index 1 = 2 calls, etc. When an input has N enqueued calls, |
| 16 | +// we increment counter[N-1], signaling new coverage to libfuzzer. |
| 17 | +constexpr size_t MAX_ENQUEUED_CALLS_COUNTER = 32; |
| 18 | +__attribute__((section("__libfuzzer_extra_counters"))) uint8_t enqueued_calls_counter[MAX_ENQUEUED_CALLS_COUNTER]; |
| 19 | + |
| 20 | +// Counters for tracking transaction effects to guide libfuzzer. |
| 21 | +__attribute__((section( |
| 22 | + "__libfuzzer_extra_counters"))) uint8_t public_data_writes_counter[MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX]; |
| 23 | +__attribute__((section("__libfuzzer_extra_counters"))) uint8_t note_hashes_counter[MAX_NOTE_HASHES_PER_TX]; |
| 24 | +__attribute__((section("__libfuzzer_extra_counters"))) uint8_t nullifiers_counter[MAX_NULLIFIERS_PER_TX]; |
| 25 | +__attribute__((section("__libfuzzer_extra_counters"))) uint8_t l2_to_l1_msgs_counter[MAX_L2_TO_L1_MSGS_PER_TX]; |
| 26 | + |
| 27 | +// Public logs use logarithmic bucketing due to large range |
| 28 | +constexpr size_t MAX_PUBLIC_LOGS_COUNTER = 16; |
| 29 | +__attribute__((section("__libfuzzer_extra_counters"))) uint8_t public_logs_counter[MAX_PUBLIC_LOGS_COUNTER]; |
| 30 | + |
| 31 | +namespace { |
| 32 | + |
| 33 | +void update_effects_counters(const TxSimulationResult& result) |
| 34 | +{ |
| 35 | + const auto& tx_effect = result.public_tx_effect; |
| 36 | + |
| 37 | + size_t public_data_writes_size = tx_effect.public_data_writes.size(); |
| 38 | + if (public_data_writes_size > 0) { |
| 39 | + if (public_data_writes_size > MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX) { |
| 40 | + throw std::runtime_error( |
| 41 | + "Should be unreachable: generated " + std::to_string(public_data_writes_size) + |
| 42 | + " public data writes, max: " + std::to_string(MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX)); |
| 43 | + } |
| 44 | + public_data_writes_counter[public_data_writes_size - 1]++; |
| 45 | + } |
| 46 | + |
| 47 | + size_t note_hashes_size = tx_effect.note_hashes.size(); |
| 48 | + if (note_hashes_size > 0) { |
| 49 | + if (note_hashes_size > MAX_NOTE_HASHES_PER_TX) { |
| 50 | + throw std::runtime_error("Should be unreachable: generated " + std::to_string(note_hashes_size) + |
| 51 | + " note hashes, max: " + std::to_string(MAX_NOTE_HASHES_PER_TX)); |
| 52 | + } |
| 53 | + note_hashes_counter[note_hashes_size - 1]++; |
| 54 | + } |
| 55 | + |
| 56 | + size_t nullifiers_size = tx_effect.nullifiers.size(); |
| 57 | + if (nullifiers_size > 0) { |
| 58 | + if (nullifiers_size > MAX_NULLIFIERS_PER_TX) { |
| 59 | + throw std::runtime_error("Should be unreachable: generated " + std::to_string(nullifiers_size) + |
| 60 | + " nullifiers, max: " + std::to_string(MAX_NULLIFIERS_PER_TX)); |
| 61 | + } |
| 62 | + nullifiers_counter[nullifiers_size - 1]++; |
| 63 | + } |
| 64 | + |
| 65 | + size_t l2_to_l1_size = tx_effect.l2_to_l1_msgs.size(); |
| 66 | + if (l2_to_l1_size > 0) { |
| 67 | + if (l2_to_l1_size > MAX_L2_TO_L1_MSGS_PER_TX) { |
| 68 | + throw std::runtime_error("Should be unreachable: generated " + std::to_string(l2_to_l1_size) + |
| 69 | + " L2-to-L1 messages, max: " + std::to_string(MAX_L2_TO_L1_MSGS_PER_TX)); |
| 70 | + } |
| 71 | + l2_to_l1_msgs_counter[l2_to_l1_size - 1]++; |
| 72 | + } |
| 73 | + |
| 74 | + // Public logs: calculate total field count across all logs. |
| 75 | + // Each log contributes 2 header fields (length + contract_address) plus data fields. |
| 76 | + uint32_t logs_field_count = 0; |
| 77 | + for (const auto& log : tx_effect.public_logs) { |
| 78 | + logs_field_count += 2 + static_cast<uint32_t>(log.fields.size()); |
| 79 | + } |
| 80 | + if (logs_field_count > 0) { |
| 81 | + uint8_t bucket = static_cast<uint8_t>(31 - std::countl_zero(logs_field_count)); |
| 82 | + if (bucket >= MAX_PUBLIC_LOGS_COUNTER) { |
| 83 | + throw std::runtime_error("Should be unreachable: generated " + std::to_string(logs_field_count) + |
| 84 | + " fields, max log2(count): " + std::to_string(MAX_PUBLIC_LOGS_COUNTER)); |
| 85 | + } |
| 86 | + public_logs_counter[bucket]++; |
| 87 | + } |
| 88 | +} |
| 89 | + |
| 90 | +} // namespace |
| 91 | + |
14 | 92 | extern "C" int LLVMFuzzerInitialize(int*, char***) |
15 | 93 | { |
| 94 | + // Zero all counters |
| 95 | + memset(enqueued_calls_counter, 0, sizeof(enqueued_calls_counter)); |
| 96 | + memset(public_data_writes_counter, 0, sizeof(public_data_writes_counter)); |
| 97 | + memset(note_hashes_counter, 0, sizeof(note_hashes_counter)); |
| 98 | + memset(nullifiers_counter, 0, sizeof(nullifiers_counter)); |
| 99 | + memset(l2_to_l1_msgs_counter, 0, sizeof(l2_to_l1_msgs_counter)); |
| 100 | + memset(public_logs_counter, 0, sizeof(public_logs_counter)); |
| 101 | + |
16 | 102 | FuzzerWorldStateManager::initialize(); |
17 | 103 | return 0; |
18 | 104 | } |
@@ -45,12 +131,19 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) |
45 | 131 | tx_data = create_default_tx_data(context); |
46 | 132 | } |
47 | 133 |
|
| 134 | + // Signal coverage for number of enqueued calls to guide fuzzer towards more calls |
| 135 | + size_t num_calls = tx_data.tx.setup_enqueued_calls.size() + tx_data.tx.app_logic_enqueued_calls.size(); |
| 136 | + if (num_calls > 0 && num_calls <= MAX_ENQUEUED_CALLS_COUNTER) { |
| 137 | + enqueued_calls_counter[num_calls - 1]++; |
| 138 | + } |
| 139 | + |
48 | 140 | // Setup contracts and fund fee payer |
49 | 141 | // Fuzzer state is dependent on the tx data |
50 | 142 | setup_fuzzer_state(*ws_mgr, contract_db, tx_data); |
51 | 143 | fund_fee_payer(*ws_mgr, tx_data.tx); |
52 | 144 |
|
53 | | - fuzz_prover(*ws_mgr, contract_db, tx_data); |
| 145 | + TxSimulationResult result = fuzz_prover(*ws_mgr, contract_db, tx_data); |
| 146 | + update_effects_counters(result); |
54 | 147 |
|
55 | 148 | // Print timing stats for this iteration |
56 | 149 | vinfo("Timing stats:\n", bb::avm2::Stats::get().to_string()); |
|
0 commit comments