diff --git a/Cargo.lock b/Cargo.lock index 6d3d344a060757..4cf72f1a264c20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5614,6 +5614,15 @@ dependencies = [ "autotools", ] +[[package]] +name = "protosol" +version = "1.0.1" +source = "git+https://github.com/firedancer-io/protosol?tag=v1.0.2#56629ce3939dd8059acd900b74e29e3aaa1b178b" +dependencies = [ + "prost", + "prost-build", +] + [[package]] name = "qstring" version = "0.7.2" @@ -11074,6 +11083,43 @@ dependencies = [ name = "solana-svm-feature-set" version = "3.1.0" +[[package]] +name = "solana-svm-fuzz-harness" +version = "3.1.0" +dependencies = [ + "agave-feature-set", + "agave-precompiles", + "agave-syscalls", + "bincode", + "clap 4.5.31", + "prost", + "prost-build", + "protosol", + "solana-account", + "solana-builtins", + "solana-clock", + "solana-compute-budget", + "solana-epoch-schedule", + "solana-hash", + "solana-instruction", + "solana-instruction-error", + "solana-last-restart-slot", + "solana-logger", + "solana-precompile-error", + "solana-program-runtime", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-stable-layout", + "solana-svm", + "solana-svm-callback", + "solana-svm-log-collector", + "solana-svm-timings", + "solana-sysvar-id", + "solana-transaction-context", + "thiserror 2.0.17", +] + [[package]] name = "solana-svm-log-collector" version = "3.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8f0f43b06970ce..551b8a9117afba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ members = [ "svm", "svm-callback", "svm-feature-set", + "svm-fuzz-harness", "svm-log-collector", "svm-measure", "svm-timings", @@ -531,6 +532,7 @@ solana-streamer = { path = "streamer", version = "=3.1.0" } solana-svm = { path = "svm", version = "=3.1.0" } solana-svm-callback = { path = "svm-callback", version = "=3.1.0" } solana-svm-feature-set = { path = "svm-feature-set", version = "=3.1.0" } +solana-svm-fuzz-harness = { path = "svm-fuzz-harness", version = "=3.1.0" } solana-svm-log-collector = { path = "svm-log-collector", version = "=3.1.0" } solana-svm-measure = { path = "svm-measure", version = "=3.1.0" } solana-svm-timings = { path = "svm-timings", version = "=3.1.0" } diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index d4478b4b3574f1..a0d8c419ae900a 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -1550,7 +1550,14 @@ fn execute<'a, 'b: 'a>( Err(Box::new(error) as Box) } ProgramResult::Err(mut error) => { - if !matches!(error, EbpfError::SyscallError(_)) { + // Don't clean me up!! + // This feature is active on all networks, but we still toggle + // it off during fuzzing. + if invoke_context + .get_feature_set() + .deplete_cu_meter_on_vm_failure + && !matches!(error, EbpfError::SyscallError(_)) + { // when an exception is thrown during the execution of a // Basic Block (e.g., a null memory dereference or other // faults), determining the exact number of CUs consumed diff --git a/svm-fuzz-harness/.gitignore b/svm-fuzz-harness/.gitignore new file mode 100644 index 00000000000000..995bba6cb2c9b0 --- /dev/null +++ b/svm-fuzz-harness/.gitignore @@ -0,0 +1 @@ +dump diff --git a/svm-fuzz-harness/Cargo.toml b/svm-fuzz-harness/Cargo.toml new file mode 100644 index 00000000000000..c346cd4e3ee2cf --- /dev/null +++ b/svm-fuzz-harness/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "solana-svm-fuzz-harness" +description = "Solana SVM fuzzing harnesses." +documentation = "https://docs.rs/solana-svm-fuzz-harness" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "test_exec_instr" +path = "bin/test_exec_instr.rs" + +[dependencies] +agave-feature-set = { workspace = true } +agave-precompiles = { workspace = true } +agave-syscalls = { workspace = true } +bincode = { workspace = true } +clap = { version = "4.5.2", features = ["derive"] } +prost = { workspace = true } +protosol = { git = "https://github.com/firedancer-io/protosol", tag = "v1.0.2" } +solana-account = { workspace = true } +solana-builtins = { workspace = true } +solana-clock = { workspace = true, features = ["sysvar"] } +solana-compute-budget = { workspace = true } +solana-epoch-schedule = { workspace = true, features = ["sysvar"] } +solana-hash = { workspace = true } +solana-instruction = { workspace = true } +solana-instruction-error = { workspace = true, features = ["serde"] } +solana-last-restart-slot = { workspace = true, features = ["sysvar"] } +solana-logger = { workspace = true } +solana-precompile-error = { workspace = true } +solana-program-runtime = { workspace = true } +solana-pubkey = { workspace = true } +solana-rent = { workspace = true, features = ["sysvar"] } +solana-sdk-ids = { workspace = true } +solana-stable-layout = { workspace = true } +solana-svm = { workspace = true } +solana-svm-callback = { workspace = true } +solana-svm-log-collector = { workspace = true } +solana-svm-timings = { workspace = true } +solana-sysvar-id = { workspace = true } +solana-transaction-context = { workspace = true } +thiserror = { workspace = true } + +[build-dependencies] +prost-build = { workspace = true } + +[lints] +workspace = true diff --git a/svm-fuzz-harness/Makefile b/svm-fuzz-harness/Makefile new file mode 100644 index 00000000000000..aea985f85d01ea --- /dev/null +++ b/svm-fuzz-harness/Makefile @@ -0,0 +1,7 @@ + +CARGO?=cargo + +test: | binaries + +binaries: + $(CARGO) build --manifest-path ./Cargo.toml --bins --release diff --git a/svm-fuzz-harness/bin/test_exec_instr.rs b/svm-fuzz-harness/bin/test_exec_instr.rs new file mode 100644 index 00000000000000..c287b36553d1a7 --- /dev/null +++ b/svm-fuzz-harness/bin/test_exec_instr.rs @@ -0,0 +1,52 @@ +use { + clap::Parser, + prost::Message, + solana_svm_fuzz_harness::{ + fixture::proto::InstrFixture as ProtoInstrFixture, instr::execute_instr_proto, + }, + std::path::PathBuf, +}; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Cli { + inputs: Vec, +} + +fn exec(input: &PathBuf) -> bool { + let blob = std::fs::read(input).unwrap(); + let fixture = ProtoInstrFixture::decode(&blob[..]).unwrap(); + let Some(context) = fixture.input else { + println!("No context found."); + return false; + }; + + let Some(expected) = fixture.output else { + println!("No fixture found."); + return false; + }; + let Some(effects) = execute_instr_proto(context) else { + println!("FAIL: No instruction effects returned for input: {input:?}",); + return false; + }; + + let ok = effects == expected; + + if ok { + println!("OK: {input:?}"); + } else { + println!("FAIL: {input:?}"); + } + ok +} + +fn main() { + let cli = Cli::parse(); + let mut fail_cnt: i32 = 0; + for input in cli.inputs { + if !exec(&input) { + fail_cnt = fail_cnt.saturating_add(1); + } + } + std::process::exit(fail_cnt); +} diff --git a/svm-fuzz-harness/build.rs b/svm-fuzz-harness/build.rs new file mode 100644 index 00000000000000..bcc396b2a25f6c --- /dev/null +++ b/svm-fuzz-harness/build.rs @@ -0,0 +1,40 @@ +use std::{env, fs, path::PathBuf}; + +fn main() -> Result<(), Box> { + // Get absolute proto dir from producer + let proto_dir = PathBuf::from( + env::var("DEP_PROTOSOL_PROTO_DIR") + .expect("protosol did not expose PROTO_DIR, did protosol build.rs run first?"), + ); + + println!("cargo:rerun-if-env-changed=DEP_PROTOSOL_PROTO_DIR"); + println!("cargo:rerun-if-changed={}", proto_dir.display()); + + // Collect absolute .proto paths + let mut proto_files = vec![]; + for entry in fs::read_dir(&proto_dir)? { + let path = entry?.path(); + if path.extension().and_then(|e| e.to_str()) == Some("proto") { + println!("cargo:rerun-if-changed={}", path.display()); + proto_files.push(path); + } + } + + // Ensure deterministic order for rebuilds + proto_files.sort(); + + // Compile protos into Rust + let out_dir = PathBuf::from(env::var("OUT_DIR")?); + let mut config = prost_build::Config::new(); + config.out_dir(&out_dir); + + config.compile_protos( + &proto_files + .iter() + .map(|p| p.display().to_string()) + .collect::>(), + &[proto_dir.to_str().unwrap()], + )?; + + Ok(()) +} diff --git a/svm-fuzz-harness/src/fixture/account_state.rs b/svm-fuzz-harness/src/fixture/account_state.rs new file mode 100644 index 00000000000000..87e088bea988db --- /dev/null +++ b/svm-fuzz-harness/src/fixture/account_state.rs @@ -0,0 +1,58 @@ +use { + super::{error::FixtureError, proto::AcctState as ProtoAccount}, + solana_account::Account, + solana_pubkey::Pubkey, +}; + +// Default `rent_epoch` field value for all accounts. +const RENT_EXEMPT_RENT_EPOCH: u64 = u64::MAX; + +impl TryFrom for (Pubkey, Account) { + type Error = FixtureError; + + fn try_from(value: ProtoAccount) -> Result { + let ProtoAccount { + address, + owner, + lamports, + data, + executable, + .. + } = value; + + let pubkey = Pubkey::try_from(address).map_err(FixtureError::InvalidPubkeyBytes)?; + let owner = Pubkey::try_from(owner).map_err(FixtureError::InvalidPubkeyBytes)?; + + Ok(( + pubkey, + Account { + data, + executable, + lamports, + owner, + rent_epoch: RENT_EXEMPT_RENT_EPOCH, + }, + )) + } +} + +impl From<(Pubkey, Account)> for ProtoAccount { + fn from(value: (Pubkey, Account)) -> Self { + let Account { + lamports, + data, + owner, + executable, + .. + } = value.1; + + ProtoAccount { + address: value.0.to_bytes().to_vec(), + owner: owner.to_bytes().to_vec(), + lamports, + data, + executable, + seed_addr: None, + } + } +} diff --git a/svm-fuzz-harness/src/fixture/error.rs b/svm-fuzz-harness/src/fixture/error.rs new file mode 100644 index 00000000000000..b6c8db64c2694e --- /dev/null +++ b/svm-fuzz-harness/src/fixture/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Debug, Error, PartialEq)] +pub enum FixtureError { + #[error("Invalid fixture input")] + InvalidFixtureInput, + + #[error("Invalid public key bytes")] + InvalidPubkeyBytes(Vec), + + #[error("An account is missing for instruction account index {0}")] + AccountMissingForInstrAccount(usize), +} diff --git a/svm-fuzz-harness/src/fixture/feature_set.rs b/svm-fuzz-harness/src/fixture/feature_set.rs new file mode 100644 index 00000000000000..671daa674a19e5 --- /dev/null +++ b/svm-fuzz-harness/src/fixture/feature_set.rs @@ -0,0 +1,37 @@ +use { + super::proto::FeatureSet as ProtoFeatureSet, + agave_feature_set::{FeatureSet, FEATURE_NAMES}, + solana_pubkey::Pubkey, + std::{collections::HashMap, sync::LazyLock}, +}; + +const fn feature_u64(feature: &Pubkey) -> u64 { + let feature_id = feature.to_bytes(); + feature_id[0] as u64 + | (feature_id[1] as u64) << 8 + | (feature_id[2] as u64) << 16 + | (feature_id[3] as u64) << 24 + | (feature_id[4] as u64) << 32 + | (feature_id[5] as u64) << 40 + | (feature_id[6] as u64) << 48 + | (feature_id[7] as u64) << 56 +} + +static INDEXED_FEATURES: LazyLock> = LazyLock::new(|| { + FEATURE_NAMES + .iter() + .map(|(pubkey, _)| (feature_u64(pubkey), *pubkey)) + .collect() +}); + +impl From<&ProtoFeatureSet> for FeatureSet { + fn from(value: &ProtoFeatureSet) -> Self { + let mut feature_set = FeatureSet::default(); + for id in &value.features { + if let Some(pubkey) = INDEXED_FEATURES.get(id) { + feature_set.activate(pubkey, 0); + } + } + feature_set + } +} diff --git a/svm-fuzz-harness/src/fixture/instr_context.rs b/svm-fuzz-harness/src/fixture/instr_context.rs new file mode 100644 index 00000000000000..c9025cbffe008e --- /dev/null +++ b/svm-fuzz-harness/src/fixture/instr_context.rs @@ -0,0 +1,78 @@ +//! Instruction context (input). + +use { + super::{error::FixtureError, proto::InstrContext as ProtoInstrContext}, + agave_feature_set::FeatureSet, + solana_account::Account, + solana_instruction::AccountMeta, + solana_pubkey::Pubkey, + solana_stable_layout::stable_instruction::StableInstruction, +}; + +/// Instruction context fixture. +pub struct InstrContext { + pub feature_set: FeatureSet, + pub accounts: Vec<(Pubkey, Account)>, + pub instruction: StableInstruction, + pub cu_avail: u64, +} + +impl TryFrom for InstrContext { + type Error = FixtureError; + + fn try_from(value: ProtoInstrContext) -> Result { + let program_id = Pubkey::new_from_array( + value + .program_id + .try_into() + .map_err(FixtureError::InvalidPubkeyBytes)?, + ); + + let feature_set: FeatureSet = value + .epoch_context + .as_ref() + .and_then(|epoch_ctx| epoch_ctx.features.as_ref()) + .map(|fs| fs.into()) + .unwrap_or_default(); + + let accounts: Vec<(Pubkey, Account)> = value + .accounts + .into_iter() + .map(|acct_state| acct_state.try_into()) + .collect::, _>>()?; + + let instruction_accounts = value + .instr_accounts + .into_iter() + .map(|acct| { + if acct.index as usize >= accounts.len() { + return Err(FixtureError::AccountMissingForInstrAccount( + acct.index as usize, + )); + } + Ok(AccountMeta { + pubkey: accounts[acct.index as usize].0, + is_signer: acct.is_signer, + is_writable: acct.is_writable, + }) + }) + .collect::, _>>()?; + + if instruction_accounts.len() > 128 { + return Err(FixtureError::InvalidFixtureInput); + } + + let instruction = StableInstruction { + accounts: instruction_accounts.into(), + data: value.data.into(), + program_id, + }; + + Ok(Self { + feature_set, + accounts, + instruction, + cu_avail: value.cu_avail, + }) + } +} diff --git a/svm-fuzz-harness/src/fixture/instr_effects.rs b/svm-fuzz-harness/src/fixture/instr_effects.rs new file mode 100644 index 00000000000000..6de160ba72a5dc --- /dev/null +++ b/svm-fuzz-harness/src/fixture/instr_effects.rs @@ -0,0 +1,40 @@ +//! Instruction effects (output). +use { + super::proto::InstrEffects as ProtoInstrEffects, solana_account::Account, + solana_instruction_error::InstructionError, solana_pubkey::Pubkey, +}; + +/// Represents the effects of a single instruction. +pub struct InstrEffects { + pub result: Option, + pub custom_err: Option, + pub modified_accounts: Vec<(Pubkey, Account)>, + pub cu_avail: u64, + pub return_data: Vec, +} + +impl From for ProtoInstrEffects { + fn from(value: InstrEffects) -> Self { + let InstrEffects { + result, + custom_err, + modified_accounts, + cu_avail, + return_data, + .. + } = value; + + Self { + result: result.as_ref().map(instr_err_to_num).unwrap_or_default(), + custom_err: custom_err.unwrap_or_default(), + modified_accounts: modified_accounts.into_iter().map(Into::into).collect(), + cu_avail, + return_data, + } + } +} + +fn instr_err_to_num(error: &InstructionError) -> i32 { + let serialized_err = bincode::serialize(error).unwrap(); + i32::from_le_bytes((&serialized_err[0..4]).try_into().unwrap()).saturating_add(1) +} diff --git a/svm-fuzz-harness/src/fixture/mod.rs b/svm-fuzz-harness/src/fixture/mod.rs new file mode 100644 index 00000000000000..f23dde8a3dbfe2 --- /dev/null +++ b/svm-fuzz-harness/src/fixture/mod.rs @@ -0,0 +1,11 @@ +//! Converts between Firedancer's protobuf payloads and Solana SDK types for +//! use in Agave's SVM. + +pub mod account_state; +pub mod error; +pub mod feature_set; +pub mod instr_context; +pub mod instr_effects; +pub mod proto { + include!(concat!(env!("OUT_DIR"), "/org.solana.sealevel.v1.rs")); +} diff --git a/svm-fuzz-harness/src/instr.rs b/svm-fuzz-harness/src/instr.rs new file mode 100644 index 00000000000000..138dbb7d5ba28d --- /dev/null +++ b/svm-fuzz-harness/src/instr.rs @@ -0,0 +1,453 @@ +//! Solana SVM fuzz harness for instructions. +//! +//! This entrypoint provides an API for Agave's program runtime in order to +//! execute program instructions directly against the VM. + +#![allow(clippy::missing_safety_doc)] + +use { + crate::fixture::{ + instr_context::InstrContext, + instr_effects::InstrEffects, + proto::{InstrContext as ProtoInstrContext, InstrEffects as ProtoInstrEffects}, + }, + agave_precompiles::{get_precompile, is_precompile}, + solana_account::AccountSharedData, + solana_compute_budget::compute_budget::{ComputeBudget, SVMTransactionExecutionCost}, + solana_hash::Hash, + solana_instruction::AccountMeta, + solana_instruction_error::InstructionError, + solana_precompile_error::PrecompileError, + solana_program_runtime::{ + invoke_context::{EnvironmentConfig, InvokeContext}, + loaded_programs::ProgramCacheForTxBatch, + sysvar_cache::SysvarCache, + }, + solana_pubkey::Pubkey, + solana_stable_layout::stable_vec::StableVec, + solana_svm_callback::{InvokeContextCallback, TransactionProcessingCallback}, + solana_svm_log_collector::LogCollector, + solana_svm_timings::ExecuteTimings, + solana_transaction_context::{ + transaction_accounts::KeyedAccountSharedData, IndexOfAccount, InstructionAccount, + TransactionContext, + }, + std::collections::HashSet, +}; + +/// Implement the callback trait so that the SVM API can be used to load +/// program ELFs from accounts (ie. `load_program_with_pubkey`). +struct InstrContextCallback<'a>(&'a InstrContext); + +impl InvokeContextCallback for InstrContextCallback<'_> { + fn is_precompile(&self, program_id: &Pubkey) -> bool { + is_precompile(program_id, |feature_id: &Pubkey| { + self.0.feature_set.is_active(feature_id) + }) + } + + fn process_precompile( + &self, + program_id: &Pubkey, + data: &[u8], + instruction_datas: Vec<&[u8]>, + ) -> std::result::Result<(), PrecompileError> { + if let Some(precompile) = get_precompile(program_id, |feature_id: &Pubkey| { + self.0.feature_set.is_active(feature_id) + }) { + precompile.verify(data, &instruction_datas, &self.0.feature_set) + } else { + Err(PrecompileError::InvalidPublicKey) + } + } +} + +impl TransactionProcessingCallback for InstrContextCallback<'_> { + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, u64)> { + self.0 + .accounts + .iter() + .find(|(found_pubkey, _)| *found_pubkey == *pubkey) + .map(|(_, account)| (AccountSharedData::from(account.clone()), 0u64)) + } +} + +fn create_invoke_context_fields( + input: &mut InstrContext, +) -> Option<( + TransactionContext, + SysvarCache, + ProgramCacheForTxBatch, + Hash, + u64, + ComputeBudget, +)> { + let compute_budget = { + let mut budget = ComputeBudget::new_with_defaults(false); + budget.compute_unit_limit = input.cu_avail; + budget + }; + + let sysvar_cache = crate::sysvar_cache::setup_sysvar_cache(&input.accounts); + + let clock = sysvar_cache.get_clock().unwrap(); + let rent = sysvar_cache.get_rent().unwrap(); + + if !input + .accounts + .iter() + .any(|(pubkey, _)| pubkey == &input.instruction.program_id) + { + input.accounts.push(( + input.instruction.program_id, + AccountSharedData::default().into(), + )); + } + + let transaction_accounts: Vec = input + .accounts + .iter() + .map(|(pubkey, account)| (*pubkey, AccountSharedData::from(account.clone()))) + .collect(); + + let transaction_context = TransactionContext::new( + transaction_accounts.clone(), + (*rent).clone(), + compute_budget.max_instruction_stack_depth, + compute_budget.max_instruction_trace_length, + ); + + // Set up the program cache, which will include all builtins by default. + let mut program_cache = + crate::program_cache::setup_program_cache(&input.feature_set, &compute_budget, clock.slot); + + let environments = program_cache.environments.clone(); + + #[allow(deprecated)] + let (blockhash, lamports_per_signature) = sysvar_cache + .get_recent_blockhashes() + .ok() + .and_then(|x| (*x).last().cloned()) + .map(|x| (x.blockhash, x.fee_calculator.lamports_per_signature)) + .unwrap_or_default(); + + let mut newly_loaded_programs = HashSet::::new(); + + for acc in &input.accounts { + // FD rejects duplicate account loads + if !newly_loaded_programs.insert(acc.0) { + return None; + } + + if program_cache.find(&acc.0).is_none() { + // load_program_with_pubkey expects the owner to be one of the bpf loader + if !solana_sdk_ids::loader_v4::check_id(&acc.1.owner) + && !solana_sdk_ids::bpf_loader_deprecated::check_id(&acc.1.owner) + && !solana_sdk_ids::bpf_loader::check_id(&acc.1.owner) + && !solana_sdk_ids::bpf_loader_upgradeable::check_id(&acc.1.owner) + { + continue; + } + // https://github.com/anza-xyz/agave/blob/af6930da3a99fd0409d3accd9bbe449d82725bd6/svm/src/program_loader.rs#L124 + /* pub fn load_program_with_pubkey( + callbacks: &CB, + program_cache: &ProgramCache, + pubkey: &Pubkey, + slot: Slot, + effective_epoch: Epoch, + epoch_schedule: &EpochSchedule, + reload: bool, + ) -> Option> { */ + if let Some(loaded_program) = solana_svm::program_loader::load_program_with_pubkey( + &InstrContextCallback(input), + &environments, + &acc.0, + clock.slot, + &mut ExecuteTimings::default(), + false, + ) { + program_cache.replenish(acc.0, loaded_program); + } + } + } + + Some(( + transaction_context, + sysvar_cache, + program_cache, + blockhash, + lamports_per_signature, + compute_budget, + )) +} + +fn get_instr_accounts( + txn_context: &TransactionContext, + acct_metas: &StableVec, +) -> Vec { + let mut instruction_accounts: Vec = + Vec::with_capacity(acct_metas.len().try_into().unwrap()); + for account_meta in acct_metas.iter() { + let index_in_transaction = txn_context + .find_index_of_account(&account_meta.pubkey) + .unwrap_or(txn_context.get_number_of_accounts()) + as IndexOfAccount; + instruction_accounts.push(InstructionAccount::new( + index_in_transaction, + account_meta.is_signer, + account_meta.is_writable, + )); + } + instruction_accounts +} + +fn execute_instr(mut input: InstrContext) -> Option { + let log_collector = LogCollector::new_ref(); + + let ( + mut transaction_context, + sysvar_cache, + mut program_cache, + blockhash, + lamports_per_signature, + compute_budget, + ) = create_invoke_context_fields(&mut input)?; + + let mut compute_units_consumed = 0u64; + let runtime_features = input.feature_set.runtime_features(); + + let result = { + let callback = InstrContextCallback(&input); + + let environment_config = EnvironmentConfig::new( + blockhash, + lamports_per_signature, + &callback, + &runtime_features, + &sysvar_cache, + ); + + let program_idx = + transaction_context.find_index_of_account(&input.instruction.program_id)?; + + let instruction_accounts = + get_instr_accounts(&transaction_context, &input.instruction.accounts); + + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache, + environment_config, + Some(log_collector.clone()), + compute_budget.to_budget(), + SVMTransactionExecutionCost::default(), + ); + + invoke_context + .transaction_context + .configure_next_instruction_for_tests( + program_idx, + instruction_accounts, + input.instruction.data.to_vec(), + ) + .unwrap(); + + if invoke_context.is_precompile(&input.instruction.program_id) { + let instruction_data = input.instruction.data.iter().copied().collect::>(); + invoke_context.process_precompile( + &input.instruction.program_id, + &input.instruction.data, + [instruction_data.as_slice()].into_iter(), + ) + } else { + invoke_context + .process_instruction(&mut compute_units_consumed, &mut ExecuteTimings::default()) + } + }; + + let cu_avail = input.cu_avail.saturating_sub(compute_units_consumed); + let return_data = transaction_context.get_return_data().1.to_vec(); + + let account_keys: Vec = (0..transaction_context.get_number_of_accounts()) + .map(|index| { + *transaction_context + .get_key_of_account_at_index(index) + .clone() + .unwrap() + }) + .collect::>(); + + Some(InstrEffects { + custom_err: if let Err(InstructionError::Custom(code)) = result { + if get_precompile(&input.instruction.program_id, |_| true).is_some() { + Some(0) + } else { + Some(code) + } + } else { + None + }, + result: result.err(), + modified_accounts: transaction_context + .deconstruct_without_keys() + .unwrap() + .into_iter() + .zip(account_keys) + .map(|(account, key)| (key, account.into())) + .collect(), + cu_avail, + return_data, + }) +} + +pub fn execute_instr_proto(input: ProtoInstrContext) -> Option { + let Ok(instr_context) = InstrContext::try_from(input) else { + return None; + }; + let instr_effects = execute_instr(instr_context); + instr_effects.map(Into::into) +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::fixture::proto::{AcctState as ProtoAcctState, InstrAcct as ProtoInstrAcct}, + solana_sysvar_id::SysvarId, + }; + + #[test] + fn test_system_program_exec() { + let native_loader_id = solana_sdk_ids::native_loader::id().to_bytes().to_vec(); + let sysvar_id = solana_sysvar_id::id().to_bytes().to_vec(); + + // Create Clock sysvar + let clock = solana_clock::Clock { + slot: 10, + ..Default::default() + }; + let clock_data = bincode::serialize(&clock).unwrap(); + + // Create Rent sysvar + let rent = solana_rent::Rent::default(); + let rent_data = bincode::serialize(&rent).unwrap(); + + // Ensure that a basic account transfer works + let input = ProtoInstrContext { + program_id: vec![0u8; 32], + accounts: vec![ + ProtoAcctState { + address: vec![1u8; 32], + owner: vec![0u8; 32], + lamports: 1000, + data: vec![], + executable: false, + seed_addr: None, + }, + ProtoAcctState { + address: vec![2u8; 32], + owner: vec![0u8; 32], + lamports: 0, + data: vec![], + executable: false, + seed_addr: None, + }, + ProtoAcctState { + address: vec![0u8; 32], + owner: native_loader_id.clone(), + lamports: 10000000, + data: b"Solana Program".to_vec(), + executable: true, + seed_addr: None, + }, + ProtoAcctState { + address: solana_clock::Clock::id().to_bytes().to_vec(), + owner: sysvar_id.clone(), + lamports: 1, + data: clock_data.clone(), + executable: false, + seed_addr: None, + }, + ProtoAcctState { + address: solana_rent::Rent::id().to_bytes().to_vec(), + owner: sysvar_id.clone(), + lamports: 1, + data: rent_data.clone(), + executable: false, + seed_addr: None, + }, + ], + instr_accounts: vec![ + ProtoInstrAcct { + index: 0, + is_signer: true, + is_writable: true, + }, + ProtoInstrAcct { + index: 1, + is_signer: false, + is_writable: true, + }, + ], + data: vec![ + // Transfer + 0x02, 0x00, 0x00, 0x00, // Lamports + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + cu_avail: 10000u64, + epoch_context: None, + slot_context: None, + }; + let output = execute_instr_proto(input.clone()); + assert_eq!( + output, + Some(ProtoInstrEffects { + result: 0, + custom_err: 0, + modified_accounts: vec![ + ProtoAcctState { + address: vec![1u8; 32], + owner: vec![0u8; 32], + lamports: 999, + data: vec![], + executable: false, + seed_addr: None, + }, + ProtoAcctState { + address: vec![2u8; 32], + owner: vec![0u8; 32], + lamports: 1, + data: vec![], + executable: false, + seed_addr: None, + }, + ProtoAcctState { + address: vec![0u8; 32], + owner: native_loader_id.clone(), + lamports: 10000000, + data: b"Solana Program".to_vec(), + executable: true, + seed_addr: None, + }, + ProtoAcctState { + address: solana_clock::Clock::id().to_bytes().to_vec(), + owner: sysvar_id.clone(), + lamports: 1, + data: clock_data, + executable: false, + seed_addr: None, + }, + ProtoAcctState { + address: solana_rent::Rent::id().to_bytes().to_vec(), + owner: sysvar_id.clone(), + lamports: 1, + data: rent_data, + executable: false, + seed_addr: None, + }, + ], + cu_avail: 9850u64, + return_data: vec![], + }) + ); + } +} diff --git a/svm-fuzz-harness/src/lib.rs b/svm-fuzz-harness/src/lib.rs new file mode 100644 index 00000000000000..fac27a54318ba0 --- /dev/null +++ b/svm-fuzz-harness/src/lib.rs @@ -0,0 +1,50 @@ +#![allow(clippy::missing_safety_doc)] + +pub mod fixture; +pub mod instr; +pub mod program_cache; +pub mod sysvar_cache; + +use { + fixture::proto::InstrContext as ProtoInstrContext, + prost::Message, + std::{env, ffi::c_int}, +}; + +#[no_mangle] +pub unsafe extern "C" fn sol_compat_init(_log_level: i32) { + env::set_var("SOLANA_RAYON_THREADS", "1"); + env::set_var("RAYON_NUM_THREADS", "1"); + if env::var("ENABLE_SOLANA_LOGGER").is_ok() { + /* Pairs with RUST_LOG={trace,debug,info,etc} */ + solana_logger::setup(); + } +} + +#[no_mangle] +pub unsafe extern "C" fn sol_compat_fini() {} + +#[no_mangle] +pub unsafe extern "C" fn sol_compat_instr_execute_v1( + out_ptr: *mut u8, + out_psz: *mut u64, + in_ptr: *mut u8, + in_sz: u64, +) -> c_int { + let in_slice = std::slice::from_raw_parts(in_ptr, in_sz as usize); + let Ok(instr_context) = ProtoInstrContext::decode(in_slice) else { + return 0; + }; + let Some(instr_effects) = instr::execute_instr_proto(instr_context) else { + return 0; + }; + let out_slice = std::slice::from_raw_parts_mut(out_ptr, (*out_psz) as usize); + let out_vec = instr_effects.encode_to_vec(); + if out_vec.len() > out_slice.len() { + return 0; + } + out_slice[..out_vec.len()].copy_from_slice(&out_vec); + *out_psz = out_vec.len() as u64; + + 1 +} diff --git a/svm-fuzz-harness/src/program_cache.rs b/svm-fuzz-harness/src/program_cache.rs new file mode 100644 index 00000000000000..08e6679596a85e --- /dev/null +++ b/svm-fuzz-harness/src/program_cache.rs @@ -0,0 +1,81 @@ +use { + agave_feature_set::{ + enable_loader_v4, zk_elgamal_proof_program_enabled, zk_token_sdk_enabled, FeatureSet, + }, + agave_syscalls::create_program_runtime_environment_v1, + solana_builtins::BUILTINS, + solana_compute_budget::compute_budget::ComputeBudget, + solana_program_runtime::loaded_programs::{ + ProgramCacheEntry, ProgramCacheForTxBatch, ProgramRuntimeEnvironments, + }, + solana_pubkey::Pubkey, + std::sync::Arc, +}; + +// These programs have been migrated to Core BPF, and therefore should not be +// included in the fuzzing harness. +const MIGRATED_BUILTINS: &[Pubkey] = &[ + solana_sdk_ids::address_lookup_table::id(), + solana_sdk_ids::config::id(), + solana_sdk_ids::stake::id(), +]; + +pub fn setup_program_cache( + feature_set: &FeatureSet, + compute_budget: &ComputeBudget, + slot: u64, +) -> ProgramCacheForTxBatch { + let mut cache = ProgramCacheForTxBatch::default(); + + let environments = ProgramRuntimeEnvironments { + program_runtime_v1: Arc::new( + create_program_runtime_environment_v1( + &feature_set.runtime_features(), + &compute_budget.to_budget(), + false, /* deployment */ + false, /* debugging_features */ + ) + .unwrap(), + ), + ..ProgramRuntimeEnvironments::default() + }; + + cache.set_slot_for_tests(slot); + cache.environments = environments.clone(); + cache.upcoming_environments = Some(environments); + + for builtin in BUILTINS { + // Skip migrated builtins. + if MIGRATED_BUILTINS.contains(&builtin.program_id) { + continue; + } + + // Only activate feature-gated builtins if the feature is active. + if builtin.program_id == solana_sdk_ids::loader_v4::id() + && !feature_set.is_active(&enable_loader_v4::id()) + { + continue; + } + if builtin.program_id == solana_sdk_ids::zk_elgamal_proof_program::id() + && !feature_set.is_active(&zk_elgamal_proof_program_enabled::id()) + { + continue; + } + if builtin.program_id == solana_sdk_ids::zk_token_proof_program::id() + && !feature_set.is_active(&zk_token_sdk_enabled::id()) + { + continue; + } + + cache.replenish( + builtin.program_id, + Arc::new(ProgramCacheEntry::new_builtin( + 0u64, + builtin.name.len(), + builtin.entrypoint, + )), + ); + } + + cache +} diff --git a/svm-fuzz-harness/src/sysvar_cache.rs b/svm-fuzz-harness/src/sysvar_cache.rs new file mode 100644 index 00000000000000..9f6b3affc11b26 --- /dev/null +++ b/svm-fuzz-harness/src/sysvar_cache.rs @@ -0,0 +1,19 @@ +use { + solana_account::{Account, ReadableAccount}, + solana_program_runtime::sysvar_cache::SysvarCache, + solana_pubkey::Pubkey, +}; + +pub fn setup_sysvar_cache(input_accounts: &[(Pubkey, Account)]) -> SysvarCache { + let mut sysvar_cache = SysvarCache::default(); + + sysvar_cache.fill_missing_entries(|pubkey, callbackback| { + if let Some(account) = input_accounts.iter().find(|(key, _)| key == pubkey) { + if account.1.lamports() > 0 { + callbackback(account.1.data()); + } + } + }); + + sysvar_cache +}