Skip to main content

krastor_fuzz_core/
lib.rs

1//! Krastor Fuzz Core — Coverage-guided execution engine for Solana programs.
2//!
3//! ## Architecture
4//! ```text
5//! Fuzzer::run_one_round()
6//!   ├─ random_action()         → pick random instruction + account params
7//!   ├─ mutate_accounts()       → Solana-aware directed mutations
8//!   ├─ LiteSVM::execute()      → deploy + construct + submit transaction
9//!   ├─ check_invariants()      → user-defined post-condition checks
10//!   └─ log_coverage()          → (optional) coverage bitmap collection
11//! ```
12
13pub mod fuzzer;
14pub mod mutator;
15pub use fuzzer::Fuzzer;
16pub mod crash;
17pub mod executor;
18pub mod invariant;
19
20use rand::Rng;
21use serde::{Deserialize, Serialize};
22
23// ============ FuzzAccount ============
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct FuzzAccount {
26    /// Base58-encoded public key
27    pub key: String,
28    /// Account data as base64-encoded bytes
29    pub data: Vec<u8>,
30    /// Owner program (base58)
31    pub owner: String,
32    /// Lamports balance
33    pub lamports: u64,
34    /// Is this account rent-exempt?
35    pub rent_epoch: u64,
36    /// Is this account writable?
37    pub is_writable: bool,
38    /// Is this account a signer?
39    pub is_signer: bool,
40    /// PDA seeds if derived
41    pub seeds: Option<Vec<Vec<u8>>>,
42}
43
44impl Default for FuzzAccount {
45    fn default() -> Self {
46        Self {
47            key: String::new(),
48            data: vec![0u8; 32],
49            owner: "11111111111111111111111111111111".to_string(),
50            lamports: 1_000_000,
51            rent_epoch: u64::MAX,
52            is_writable: true,
53            is_signer: false,
54            seeds: None,
55        }
56    }
57}
58
59impl FuzzAccount {
60    pub fn random(rng: &mut impl Rng) -> Self {
61        let data_len: usize = rng.gen_range(1..=1024);
62        let mut data = vec![0u8; data_len];
63        rng.fill(&mut data[..]);
64
65        Self {
66            key: bs58_encode(&random_bytes(rng, 32)),
67            data,
68            owner: bs58_encode(&random_bytes(rng, 32)),
69            lamports: rng.gen_range(1..10_000_000),
70            rent_epoch: u64::MAX,
71            is_writable: rng.gen_bool(0.8),
72            is_signer: rng.gen_bool(0.1),
73            seeds: if rng.gen_bool(0.3) {
74                Some(vec![random_bytes(rng, 16)])
75            } else {
76                None
77            },
78        }
79    }
80
81    /// Check if an account is rent-exempt based on current rent settings
82    pub fn is_rent_exempt(&self) -> bool {
83        // UNCERTAINTY: rent-exempt threshold calculation depends on exact Solana's
84        // rent sysvar format. Current formula: data_len * rent_per_byte_year * 2 years
85        // Correct implementation requires reading Rent sysvar or using LiteSVM helper.
86        let min_lamports = self.data.len() as u64 * 3480 * 2; // approx rent
87        self.lamports >= min_lamports && self.rent_epoch != 0
88    }
89}
90
91// ============ FuzzAction ============
92/// A single fuzzing action: one instruction invocation with specific accounts
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct FuzzAction {
95    /// Anchor instruction discriminator (8 bytes, hex)
96    pub ix_discriminator: [u8; 8],
97    /// Instruction name (from IDL)
98    pub ix_name: String,
99    /// Program ID to invoke
100    pub program_id: String,
101    /// Accounts passed to the instruction
102    pub accounts: Vec<FuzzAccount>,
103    /// Serialized instruction data
104    pub ix_data: Vec<u8>,
105}
106
107// ============ FuzzActionSequence ============
108/// Full execution round: multiple instructions in sequence
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct FuzzActionSequence {
111    pub actions: Vec<FuzzAction>,
112    pub initial_accounts: Vec<FuzzAccount>,
113}
114
115// ============ CoverageBitmap ============
116/// AFL-style coverage bitmap (65536 entries is standard)
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct CoverageBitmap {
119    /// Hit counts for each edge (reduce to 0-255 in AFL style)
120    pub edges: Vec<u8>,
121    /// Total edges with non-zero hit count
122    pub covered_edges: usize,
123}
124
125impl Default for CoverageBitmap {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131impl CoverageBitmap {
132    pub fn new() -> Self {
133        Self {
134            edges: vec![0u8; 65536],
135            covered_edges: 0,
136        }
137    }
138
139    /// Record an edge transition (prev → cur)
140    pub fn record_edge(&mut self, prev: usize, cur: usize) -> bool {
141        let idx = (prev ^ cur) % self.edges.len();
142        let was_zero = self.edges[idx] == 0;
143        if self.edges[idx] < u8::MAX {
144            self.edges[idx] = self.edges[idx].saturating_add(1);
145        }
146        if was_zero && self.edges[idx] > 0 {
147            self.covered_edges += 1;
148        }
149        was_zero // new edge discovered?
150    }
151
152    /// Check if this bitmap has new coverage compared to the global one
153    pub fn has_new_coverage(&self, global: &CoverageBitmap) -> bool {
154        self.edges
155            .iter()
156            .zip(global.edges.iter())
157            .any(|(a, b)| a > b)
158    }
159
160    /// Merge global coverage with this bitmap
161    pub fn merge(&mut self, global: &CoverageBitmap) {
162        for (a, b) in self.edges.iter_mut().zip(global.edges.iter()) {
163            *a = a.saturating_add(*b);
164        }
165    }
166}
167
168// ============ Helpers ============
169fn random_bytes(rng: &mut impl Rng, len: usize) -> Vec<u8> {
170    (0..len).map(|_| rng.gen()).collect()
171}
172
173// Simple base58 encode (placeholder — real impl would use bs58 crate)
174pub fn bs58_encode(data: &[u8]) -> String {
175    const ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
176    let mut result = String::new();
177    let mut num = 0u128;
178    let mut count = 0;
179    for &byte in data {
180        num = num * 256 + byte as u128;
181        count += 1;
182        if count >= 16 {
183            while num > 0 {
184                result.push(ALPHABET[(num % 58) as usize] as char);
185                num /= 58;
186            }
187            count = 0;
188        }
189    }
190    if result.is_empty() && data.is_empty() {
191        result.push('1');
192    }
193    result
194}