Browse Source

lang: Fuzzing host

armaniferrante 4 years ago
parent
commit
f2ba0cbe56
5 changed files with 304 additions and 28 deletions
  1. 11 0
      Cargo.lock
  2. 8 0
      lang/Cargo.toml
  3. 226 0
      lang/src/fuzzing.rs
  4. 33 1
      lang/src/lib.rs
  5. 26 27
      lang/syn/src/codegen/program.rs

+ 11 - 0
Cargo.lock

@@ -181,7 +181,12 @@ dependencies = [
  "anchor-derive-accounts",
  "base64 0.13.0",
  "borsh",
+ "bumpalo",
+ "lazy_static",
+ "rand 0.7.3",
+ "safe-transmute",
  "solana-program",
+ "spl-token 3.1.0",
  "thiserror",
 ]
 
@@ -2526,6 +2531,12 @@ version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
 
+[[package]]
+name = "safe-transmute"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d95e7284b4bd97e24af76023904cd0157c9cc9da0310beb4139a1e88a748d47"
+
 [[package]]
 name = "same-file"
 version = "1.0.6"

+ 8 - 0
lang/Cargo.toml

@@ -9,6 +9,7 @@ description = "Solana Sealevel eDSL"
 
 [features]
 derive = []
+fuzzing = ["lazy_static", "spl-token", "bumpalo", "rand", "safe-transmute"]
 default = []
 
 [dependencies]
@@ -24,3 +25,10 @@ borsh = "0.8.2"
 solana-program = "=1.5.15"
 thiserror = "1.0.20"
 base64 = "0.13.0"
+
+# Fuzz deps.
+lazy_static = { version = "1.4.0", optional = true }
+spl-token = { version = "3.0.1", features = ["no-entrypoint"], optional = true }
+bumpalo = { version = "3.4.0", features = ["collections", "boxed"], optional = true }
+rand = { version = "0.7.3", optional = true }
+safe-transmute = { version = "0.11.0", optional = true }

+ 226 - 0
lang/src/fuzzing.rs

@@ -0,0 +1,226 @@
+//! The fuzz modules provides utilities to facilitate fuzzing anchor programs.
+
+use bumpalo::Bump;
+use safe_transmute::to_bytes::transmute_to_bytes;
+use solana_program::account_info::AccountInfo;
+use solana_program::bpf_loader;
+use solana_program::clock::Epoch;
+use solana_program::entrypoint::ProgramResult;
+use solana_program::instruction::Instruction;
+use solana_program::program_pack::Pack;
+use solana_program::pubkey::Pubkey;
+use solana_program::rent::Rent;
+use solana_program::system_program;
+use solana_program::sysvar::{self, Sysvar};
+use spl_token::state::Account as TokenAccount;
+use spl_token::state::Mint;
+use std::collections::HashMap;
+use std::fmt::Debug;
+use std::mem::size_of;
+use std::sync::{Arc, Mutex, MutexGuard};
+
+lazy_static::lazy_static! {
+    static ref ENV: Arc<Mutex<Environment>> = Arc::new(Mutex::new(Environment::new()));
+}
+
+// Global host environment.
+pub fn env() -> MutexGuard<'static, Environment> {
+    ENV.lock().unwrap()
+}
+
+// The host execution environment.
+pub struct Environment {
+    // All registered programs that can be invoked.
+    programs: HashMap<Pubkey, Box<dyn Program>>,
+    // The currently executing program.
+    current_program: Option<Pubkey>,
+    // Account storage.
+    accounts: AccountStore,
+}
+
+impl Environment {
+    pub fn new() -> Environment {
+        let mut env = Environment {
+            programs: HashMap::new(),
+            current_program: None,
+            accounts: AccountStore::new(),
+        };
+        env.register(Box::new(SplToken));
+        env
+    }
+
+    // Registers the program on the environment so that it can be invoked via
+    // CPI.
+    pub fn register(&mut self, program: Box<dyn Program>) {
+        self.programs.insert(program.id(), program);
+    }
+
+    // Performs a cross program invocation.
+    pub fn invoke<'info>(
+        &mut self,
+        ix: &Instruction,
+        accounts: &[AccountInfo<'info>],
+        seeds: &[&[&[u8]]],
+    ) -> ProgramResult {
+        let current_program = self.current_program.unwrap();
+
+        // If seeds were given, then calculate the expected PDA.
+        let pda = match seeds.len() > 0 {
+            false => None,
+            true => Some(Pubkey::create_program_address(seeds[0], &current_program).unwrap()),
+        };
+
+        self.current_program = Some(ix.program_id);
+        let program = self.programs.get(&ix.program_id).unwrap();
+        let account_infos: Vec<AccountInfo> = ix
+            .accounts
+            .iter()
+            .map(|meta| {
+                let mut acc_info = accounts
+                    .iter()
+                    .find(|info| *info.key == meta.pubkey)
+                    .unwrap()
+                    .clone();
+                // If a PDA was given, market it as signer.
+                if let Some(pda) = pda {
+                    if acc_info.key == &pda {
+                        acc_info.is_signer = true;
+                    }
+                }
+                acc_info
+            })
+            .collect();
+        program.entry(&ix.program_id, &account_infos, &ix.data)
+    }
+}
+
+struct AccountStore {
+    // Storage bytes.
+    storage: Bump,
+}
+
+impl AccountStore {
+    pub fn new() -> Self {
+        Self {
+            storage: Bump::new(),
+        }
+    }
+    pub fn new_sol_account(&self, lamports: u64) -> AccountInfo {
+        AccountInfo::new(
+            random_pubkey(&self.storage),
+            true,
+            false,
+            self.storage.alloc(lamports),
+            &mut [],
+            &system_program::ID,
+            false,
+            Epoch::default(),
+        )
+    }
+
+    pub fn new_token_mint(&self) -> AccountInfo {
+        let rent = Rent::default();
+        let data = self.storage.alloc_slice_fill_copy(Mint::LEN, 0u8);
+        let mut mint = Mint::default();
+        mint.is_initialized = true;
+        Mint::pack(mint, data).unwrap();
+        AccountInfo::new(
+            random_pubkey(&self.storage),
+            false,
+            true,
+            self.storage.alloc(rent.minimum_balance(data.len())),
+            data,
+            &spl_token::ID,
+            false,
+            Epoch::default(),
+        )
+    }
+
+    pub fn new_token_account<'a, 'b>(
+        &self,
+        mint_pubkey: &'a Pubkey,
+        owner_pubkey: &'b Pubkey,
+        balance: u64,
+    ) -> AccountInfo {
+        let rent = Rent::default();
+        let data = self.storage.alloc_slice_fill_copy(TokenAccount::LEN, 0u8);
+        let mut account = TokenAccount::default();
+        account.state = spl_token::state::AccountState::Initialized;
+        account.mint = *mint_pubkey;
+        account.owner = *owner_pubkey;
+        account.amount = balance;
+        TokenAccount::pack(account, data).unwrap();
+        AccountInfo::new(
+            random_pubkey(&self.storage),
+            false,
+            true,
+            self.storage.alloc(rent.minimum_balance(data.len())),
+            data,
+            &spl_token::ID,
+            false,
+            Epoch::default(),
+        )
+    }
+
+    pub fn new_program(&self) -> AccountInfo {
+        AccountInfo::new(
+            random_pubkey(&self.storage),
+            false,
+            false,
+            self.storage.alloc(0),
+            &mut [],
+            &bpf_loader::ID,
+            true,
+            Epoch::default(),
+        )
+    }
+
+    fn new_rent_sysvar_account(&self) -> AccountInfo {
+        let lamports = 100000;
+        let data = self.storage.alloc_slice_fill_copy(size_of::<Rent>(), 0u8);
+        let mut account_info = AccountInfo::new(
+            &sysvar::rent::ID,
+            false,
+            false,
+            self.storage.alloc(lamports),
+            data,
+            &sysvar::ID,
+            false,
+            Epoch::default(),
+        );
+        let rent = Rent::default();
+        rent.to_account_info(&mut account_info).unwrap();
+        account_info
+    }
+}
+
+fn random_pubkey(bump: &Bump) -> &Pubkey {
+    bump.alloc(Pubkey::new(transmute_to_bytes(&rand::random::<[u64; 4]>())))
+}
+
+// Program that can be executed in the environment.
+pub trait Program: Send + Sync + Debug {
+    // The program's ID.
+    fn id(&self) -> Pubkey;
+
+    // Entrypoint to start executing the program.
+    fn entry(&self, program_id: &Pubkey, accounts: &[AccountInfo], ix_data: &[u8])
+        -> ProgramResult;
+}
+
+#[derive(Debug)]
+struct SplToken;
+
+impl Program for SplToken {
+    fn entry(
+        &self,
+        program_id: &Pubkey,
+        accounts: &[AccountInfo],
+        ix_data: &[u8],
+    ) -> ProgramResult {
+        spl_token::processor::Processor::process(program_id, accounts, ix_data)
+    }
+    fn id(&self) -> Pubkey {
+        spl_token::ID
+    }
+}

+ 33 - 1
lang/src/lib.rs

@@ -24,7 +24,8 @@
 extern crate self as anchor_lang;
 
 use solana_program::account_info::AccountInfo;
-use solana_program::instruction::AccountMeta;
+use solana_program::entrypoint::ProgramResult;
+use solana_program::instruction::{AccountMeta, Instruction};
 use solana_program::program_error::ProgramError;
 use solana_program::pubkey::Pubkey;
 use std::io::Write;
@@ -35,6 +36,8 @@ mod context;
 mod cpi_account;
 mod ctor;
 mod error;
+#[cfg(fuzzing)]
+pub mod fuzzing;
 pub mod idl;
 mod program_account;
 mod state;
@@ -191,6 +194,35 @@ pub trait Discriminator {
     fn discriminator() -> [u8; 8];
 }
 
+/// The cpi module provides wrapped utilities for cross program invocation.
+/// These should be used instead of the underlying `solana_program::program`
+/// methods, so that Anchor programs can be fuzzed without program modification.
+pub mod cpi {
+    use super::*;
+
+    pub fn invoke<'info>(ix: &Instruction, accounts: &[AccountInfo<'info>]) -> ProgramResult {
+        invoke_signed(ix, accounts, &[])
+    }
+
+    #[cfg(not(fuzzing))]
+    pub fn invoke_signed<'info>(
+        ix: &Instruction,
+        accounts: &[AccountInfo<'info>],
+        seeds: &[&[&[u8]]],
+    ) -> ProgramResult {
+        solana_program::program::invoke_signed(ix, accounts, seeds)
+    }
+
+    #[cfg(fuzzing)]
+    pub fn invoke_signed<'info>(
+        ix: &Instruction,
+        accounts: &[AccountInfo<'info>],
+        seeds: &[&[&[u8]]],
+    ) -> ProgramResult {
+        fuzzing::env().invoke(ix, accounts, seeds)
+    }
+}
+
 /// The prelude contains all commonly used components of the crate.
 /// All programs should include it via `anchor_lang::prelude::*;`.
 pub mod prelude {

+ 26 - 27
lang/syn/src/codegen/program.rs

@@ -12,7 +12,7 @@ const SIGHASH_GLOBAL_NAMESPACE: &str = "global";
 
 pub fn generate(program: Program) -> proc_macro2::TokenStream {
     let mod_name = &program.name;
-    let dispatch = generate_dispatch(&program);
+    let entry = generate_entry(&program);
     let handlers_non_inlined = generate_non_inlined_handlers(&program);
     let methods = generate_methods(&program);
     let ixs = generate_ixs(&program);
@@ -23,10 +23,31 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
         // TODO: remove once we allow segmented paths in `Accounts` structs.
         use #mod_name::*;
 
+        #entry
+
+        // Create a private module to not clutter the program's namespace.
+        mod __private {
+            use super::*;
+
+            #handlers_non_inlined
+        }
+
+        #accounts
+
+        #ixs
+
+        #methods
+
+        #cpi
+    }
+}
+
+fn generate_entry(program: &Program) -> proc_macro2::TokenStream {
+    let dispatch = generate_dispatch(&program);
+    quote! {
         #[cfg(not(feature = "no-entrypoint"))]
         anchor_lang::solana_program::entrypoint!(entry);
-        #[cfg(not(feature = "no-entrypoint"))]
-        fn entry(program_id: &Pubkey, accounts: &[AccountInfo], ix_data: &[u8]) -> ProgramResult {
+        pub fn entry(program_id: &Pubkey, accounts: &[AccountInfo], ix_data: &[u8]) -> ProgramResult {
             if ix_data.len() < 8 {
                 return Err(ProgramError::Custom(99));
             }
@@ -47,21 +68,6 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
 
             #dispatch
         }
-
-        // Create a private module to not clutter the program's namespace.
-        mod __private {
-            use super::*;
-
-            #handlers_non_inlined
-        }
-
-        #accounts
-
-        #ixs
-
-        #methods
-
-        #cpi
     }
 }
 
@@ -432,7 +438,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
                             space,
                             owner,
                         );
-                        anchor_lang::solana_program::program::invoke_signed(
+                        anchor_lang::cpi::invoke_signed(
                             &ix,
                             &[
                                 ctor_accounts.from.clone(),
@@ -1001,10 +1007,6 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
                     generate_ix_variant(ix.raw_method.sig.ident.to_string(), &ix.args, false);
                 let method_name = &ix.ident;
                 let args: Vec<&syn::PatType> = ix.args.iter().map(|arg| &arg.raw_arg).collect();
-                let name = &ix.raw_method.sig.ident.to_string();
-                let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &name);
-                let sighash_tts: proc_macro2::TokenStream =
-                    format!("{:?}", sighash_arr).parse().unwrap();
                 quote! {
                     pub fn #method_name<'a, 'b, 'c, 'info>(
                         ctx: CpiContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>,
@@ -1012,10 +1014,7 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
                     ) -> ProgramResult {
                         let ix = {
                             let ix = instruction::#ix_variant;
-                            let mut ix_data = AnchorSerialize::try_to_vec(&ix)
-                                .map_err(|_| ProgramError::InvalidInstructionData)?;
-                            let mut data = #sighash_tts.to_vec();
-                            data.append(&mut ix_data);
+                            let data = anchor_lang::InstructionData::data(&ix);
                             let accounts = ctx.accounts.to_account_metas(None);
                             anchor_lang::solana_program::instruction::Instruction {
                                 program_id: *ctx.program.key,