Răsfoiți Sursa

svm: integrate Firedancer's fuzz harness for instructions (#8047)

* init fixture lib

* init instr lib

* hook up bin

* fix ci

* expose entrypoints into single .so file (#33)

* expose entrypoints into single .so file

* test

* fix

* fix submodule

* fix submodule path

* remove bad file

* whitespace

* update manifests

* clean up directories

* remove outdated submodule (#34)

* clean up directories

* update submodules

* update script

* cleaned up feature comment

* cleanups to fuzz harness instrcontext and sysvar cache setup (#35)

* remove `rlib` target

* Revert "remove `rlib` target"

This reverts commit 1543c0117a5b962049a2d262a215b9e6559d73b8.

* fix sysvars in test

* configure instruction changes from rebase

* rebase updates

---------

Co-authored-by: mjain-jump <150074777+mjain-jump@users.noreply.github.com>
Joe C 1 lună în urmă
părinte
comite
d8fe32ed87

+ 2 - 0
.github/workflows/cargo.yml

@@ -55,6 +55,8 @@ jobs:
           apk add bash git
 
       - uses: actions/checkout@v5
+        with:
+          submodules: recursive
 
       - uses: mozilla-actions/sccache-action@v0.0.9
         with:

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "svm-fuzz-harness/protosol"]
+	path = svm-fuzz-harness/protosol
+	url = https://github.com/firedancer-io/protosol.git

+ 36 - 0
Cargo.lock

@@ -11053,6 +11053,42 @@ 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",
+ "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"

+ 2 - 0
Cargo.toml

@@ -104,6 +104,7 @@ members = [
     "svm",
     "svm-callback",
     "svm-feature-set",
+    "svm-fuzz-harness",
     "svm-log-collector",
     "svm-measure",
     "svm-timings",
@@ -529,6 +530,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" }

+ 8 - 1
programs/bpf_loader/src/lib.rs

@@ -1550,7 +1550,14 @@ fn execute<'a, 'b: 'a>(
                 Err(Box::new(error) as Box<dyn std::error::Error>)
             }
             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

+ 1 - 0
svm-fuzz-harness/.gitignore

@@ -0,0 +1 @@
+dump

+ 55 - 0
svm-fuzz-harness/Cargo.toml

@@ -0,0 +1,55 @@
+[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 }
+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

+ 10 - 0
svm-fuzz-harness/Makefile

@@ -0,0 +1,10 @@
+
+CARGO?=cargo
+
+test: | binaries tests/run_test_vectors
+
+binaries:
+	$(CARGO) build --manifest-path ./Cargo.toml --bins --release
+
+tests/run_test_vectors:
+	scripts/run_test_vectors.sh

+ 52 - 0
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<PathBuf>,
+}
+
+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);
+}

+ 18 - 0
svm-fuzz-harness/build.rs

@@ -0,0 +1,18 @@
+use std::io::Result;
+
+fn main() -> Result<()> {
+    let proto_base_path = std::path::PathBuf::from("protosol/proto");
+
+    let protos = &[
+        proto_base_path.join("context.proto"),
+        proto_base_path.join("invoke.proto"),
+    ];
+
+    protos
+        .iter()
+        .for_each(|proto| println!("cargo:rerun-if-changed={}", proto.display()));
+
+    prost_build::Config::new().compile_protos(protos, &[proto_base_path])?;
+
+    Ok(())
+}

+ 1 - 0
svm-fuzz-harness/protosol

@@ -0,0 +1 @@
+Subproject commit 7064b0b09062de4d2e24ee4709cbe160e541bb0e

+ 44 - 0
svm-fuzz-harness/scripts/run_test_vectors.sh

@@ -0,0 +1,44 @@
+#!/bin/bash
+
+
+set -ex
+
+
+if [ "$LOG_PATH" == "" ]; then
+  LOG_PATH="$(mktemp -d)"
+else
+  rm    -rf "$LOG_PATH"
+  mkdir -pv "$LOG_PATH"
+fi
+
+
+
+
+mkdir -p dump
+
+
+if [ ! -d dump/test-vectors ]; then
+  cd dump
+  git clone --depth=1 -q https://github.com/firedancer-io/test-vectors.git
+  cd ..
+else
+  cd dump/test-vectors
+  git pull -q
+  cd ../..
+fi
+
+
+find dump/test-vectors/instr/fixtures -type f -name '*.fix' -print0 | \
+  xargs -0 -n 1000 -P 32 -I {} ../target/release/test_exec_instr {} > "$LOG_PATH/test_exec_instr.log" 2>&1
+# Other tests will be included here...
+
+
+failed=$(grep -wR FAIL "$LOG_PATH" | wc -l)
+passed=$(grep -wR OK "$LOG_PATH" | wc -l)
+
+
+echo "PASSED: $passed"
+echo "FAILED: $failed"
+
+
+echo Test vectors success

+ 58 - 0
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<ProtoAccount> for (Pubkey, Account) {
+    type Error = FixtureError;
+
+    fn try_from(value: ProtoAccount) -> Result<Self, Self::Error> {
+        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,
+        }
+    }
+}

+ 13 - 0
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<u8>),
+
+    #[error("An account is missing for instruction account index {0}")]
+    AccountMissingForInstrAccount(usize),
+}

+ 37 - 0
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<HashMap<u64, Pubkey>> = 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
+    }
+}

+ 78 - 0
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<ProtoInstrContext> for InstrContext {
+    type Error = FixtureError;
+
+    fn try_from(value: ProtoInstrContext) -> Result<Self, Self::Error> {
+        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::<Result<Vec<_>, _>>()?;
+
+        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::<Result<Vec<_>, _>>()?;
+
+        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,
+        })
+    }
+}

+ 40 - 0
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<InstructionError>,
+    pub custom_err: Option<u32>,
+    pub modified_accounts: Vec<(Pubkey, Account)>,
+    pub cu_avail: u64,
+    pub return_data: Vec<u8>,
+}
+
+impl From<InstrEffects> 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)
+}

+ 11 - 0
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"));
+}

+ 453 - 0
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<KeyedAccountSharedData> = 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::<Pubkey>::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<CB: TransactionProcessingCallback, FG: ForkGraph>(
+                callbacks: &CB,
+                program_cache: &ProgramCache<FG>,
+                pubkey: &Pubkey,
+                slot: Slot,
+                effective_epoch: Epoch,
+                epoch_schedule: &EpochSchedule,
+                reload: bool,
+            ) -> Option<Arc<ProgramCacheEntry>> { */
+            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<AccountMeta>,
+) -> Vec<InstructionAccount> {
+    let mut instruction_accounts: Vec<InstructionAccount> =
+        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<InstrEffects> {
+    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::<Vec<_>>();
+            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<Pubkey> = (0..transaction_context.get_number_of_accounts())
+        .map(|index| {
+            *transaction_context
+                .get_key_of_account_at_index(index)
+                .clone()
+                .unwrap()
+        })
+        .collect::<Vec<_>>();
+
+    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<ProtoInstrEffects> {
+    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![],
+            })
+        );
+    }
+}

+ 50 - 0
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
+}

+ 81 - 0
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
+}

+ 19 - 0
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
+}