Add simulation detection countermeasure (#22880)

* Add simulation detection countermeasures

* Add program and test using TestValidator

* Remove incinerator deposit

* Remove incinerator

* Update Cargo.lock

* Add more features to simulation bank

* Update Cargo.lock per rebase

Co-authored-by: Jon Cinque <jon.cinque@gmail.com>
This commit is contained in:
Michael Vines 2022-02-15 04:09:59 -08:00 committed by GitHub
parent d2a407a9a7
commit c42b80f099
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 2824 additions and 166 deletions

View File

@ -1587,7 +1587,10 @@ impl ReplayStage {
root_slot,
my_pubkey,
rpc_subscriptions,
NewBankOptions { vote_only_bank },
NewBankOptions {
vote_only_bank,
simulation_bank: false,
},
);
let tpu_bank = bank_forks.write().unwrap().insert(tpu_bank);

2757
programs/bpf/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@ itertools = "0.10.1"
log = "0.4.11"
miow = "0.3.6"
net2 = "0.2.37"
solana-account-decoder = { path = "../../account-decoder", version = "=1.10.0" }
solana-bpf-rust-invoke = { path = "rust/invoke", version = "=1.10.0"}
solana-bpf-loader-program = { path = "../bpf_loader", version = "=1.10.0"}
solana-bpf-rust-realloc = { path = "rust/realloc", version = "=1.10.0"}
@ -38,7 +39,6 @@ solana-runtime = { path = "../../runtime", version = "=1.10.0" }
solana-program-runtime = { path = "../../program-runtime", version = "=1.10.0" }
solana-sdk = { path = "../../sdk", version = "=1.10.0" }
solana-transaction-status = { path = "../../transaction-status", version = "=1.10.0" }
solana-account-decoder = { path = "../../account-decoder", version = "=1.10.0" }
[[bench]]
name = "bpf_loader"
@ -83,6 +83,7 @@ members = [
"rust/sha",
"rust/sibling_inner_instruction",
"rust/sibling_instruction",
"rust/simulation",
"rust/spoof1",
"rust/spoof1_system",
"rust/sysvar",

View File

@ -93,6 +93,7 @@ fn main() {
"sha",
"sibling_inner_instruction",
"sibling_instruction",
"simulation",
"spoof1",
"spoof1_system",
"upgradeable",

View File

@ -0,0 +1,28 @@
[package]
name = "solana-bpf-rust-simulation"
version = "1.10.0"
description = "Solana BPF Program Simulation Differences"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
documentation = "https://docs.rs/solana-bpf-rust-simulation"
edition = "2021"
[features]
test-bpf = []
[dependencies]
solana-program = { path = "../../../../sdk/program", version = "=1.10.0" }
[dev-dependencies]
solana-logger = { path = "../../../../logger", version = "=1.10.0" }
solana-program-test = { path = "../../../../program-test", version = "=1.10.0" }
solana-sdk = { path = "../../../../sdk", version = "=1.10.0" }
solana-validator = { path = "../../../../validator", version = "=1.10.0" }
[lib]
crate-type = ["cdylib", "lib"]
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@ -0,0 +1,39 @@
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
declare_id, entrypoint,
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
sysvar::Sysvar,
};
use std::convert::TryInto;
declare_id!("Sim1jD5C35odT8mzctm8BWnjic8xW5xgeb5MbcbErTo");
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let slot_account = next_account_info(account_info_iter)?;
// Slot is an u64 at the end of the structure
let data = slot_account.data.borrow();
let slot: u64 = u64::from_le_bytes(data[data.len() - 8..].try_into().unwrap());
let clock = Clock::get().unwrap();
msg!("next_slot is {:?} ", slot);
msg!("clock is in slot {:?} ", clock.slot);
if clock.slot >= slot {
msg!("On-chain");
} else {
panic!("Simulation");
}
Ok(())
}

View File

@ -0,0 +1,41 @@
#![cfg(feature = "test-bpf")]
use {
solana_bpf_rust_simulation::process_instruction,
solana_program_test::{processor, tokio, ProgramTest},
solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signature::Signer,
sysvar,
transaction::Transaction,
},
};
#[tokio::test]
async fn no_panic() {
let program_id = Pubkey::new_unique();
let program_test = ProgramTest::new(
"solana_bpf_rust_simulation",
program_id,
processor!(process_instruction),
);
let mut context = program_test.start_with_context().await;
let transaction = Transaction::new_signed_with_payer(
&[Instruction {
program_id,
accounts: vec![AccountMeta::new_readonly(sysvar::slot_history::id(), false)],
data: vec![],
}],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
);
context
.banks_client
.process_transaction_with_preflight(transaction)
.await
.unwrap();
}

View File

@ -0,0 +1,38 @@
#![cfg(feature = "test-bpf")]
use {
solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
sysvar,
},
solana_sdk::{signature::Signer, transaction::Transaction},
solana_validator::test_validator::*,
};
#[test]
fn no_panic() {
solana_logger::setup_with_default("solana_program_runtime=debug");
let program_id = Pubkey::new_unique();
let (test_validator, payer) = TestValidatorGenesis::default()
.add_program("solana_bpf_rust_simulation", program_id)
.start();
let rpc_client = test_validator.get_rpc_client();
let blockhash = rpc_client.get_latest_blockhash().unwrap();
let transaction = Transaction::new_signed_with_payer(
&[Instruction {
program_id,
accounts: vec![AccountMeta::new_readonly(sysvar::slot_history::id(), false)],
data: vec![],
}],
Some(&payer.pubkey()),
&[&payer],
blockhash,
);
rpc_client
.send_and_confirm_transaction(&transaction)
.unwrap();
}

View File

@ -3472,7 +3472,7 @@ pub mod rpc_full {
let preflight_commitment = config
.preflight_commitment
.map(|commitment| CommitmentConfig { commitment });
let preflight_bank = &*meta.bank(preflight_commitment);
let preflight_bank = meta.bank(preflight_commitment);
let transaction = sanitize_transaction(unsanitized_tx)?;
let signature = *transaction.signature();
@ -3569,7 +3569,7 @@ pub mod rpc_full {
let (_, mut unsanitized_tx) =
decode_and_deserialize::<VersionedTransaction>(data, encoding)?;
let bank = &*meta.bank(config.commitment);
let bank = meta.bank(config.commitment);
if config.replace_recent_blockhash {
if config.sig_verify {
return Err(Error::invalid_params(
@ -3637,7 +3637,7 @@ pub mod rpc_full {
};
Ok(new_response(
bank,
&bank,
RpcSimulateTransactionResult {
err: result.err(),
logs: Some(logs),

View File

@ -199,9 +199,16 @@ impl Accounts {
}
}
pub fn new_from_parent(parent: &Accounts, slot: Slot, parent_slot: Slot) -> Self {
pub fn new_from_parent(
parent: &Accounts,
slot: Slot,
parent_slot: Slot,
simulation_bank: bool,
) -> Self {
let accounts_db = parent.accounts_db.clone();
accounts_db.set_hash(slot, parent_slot);
if !simulation_bank {
accounts_db.set_hash(slot, parent_slot);
}
Self {
accounts_db,
account_locks: Mutex::new(AccountLocks::default()),

View File

@ -1035,6 +1035,18 @@ pub trait DropCallback: fmt::Debug {
fn clone_box(&self) -> Box<dyn DropCallback + Send + Sync>;
}
/// Noop callback on dropping banks is useful for simulation banks, which are
/// new banks created from a frozen bank, but should not be purged in the same
/// way.
#[derive(Debug, Clone)]
struct NoopDropCallback;
impl DropCallback for NoopDropCallback {
fn callback(&self, _b: &Bank) {}
fn clone_box(&self) -> Box<dyn DropCallback + Send + Sync> {
Box::new(self.clone())
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize, AbiExample, Clone, Copy)]
pub struct RewardInfo {
pub reward_type: RewardType,
@ -1253,6 +1265,7 @@ struct LoadVoteAndStakeAccountsResult {
#[derive(Debug, Default)]
pub struct NewBankOptions {
pub vote_only_bank: bool,
pub simulation_bank: bool,
}
impl Bank {
@ -1527,7 +1540,10 @@ impl Bank {
new_bank_options: NewBankOptions,
) -> Self {
let mut time = Measure::start("bank::new_from_parent");
let NewBankOptions { vote_only_bank } = new_bank_options;
let NewBankOptions {
vote_only_bank,
simulation_bank,
} = new_bank_options;
parent.freeze();
assert_ne!(slot, parent.slot());
@ -1541,6 +1557,7 @@ impl Bank {
&parent.rc.accounts,
slot,
parent.slot(),
simulation_bank,
)),
parent: RwLock::new(Some(parent.clone())),
slot,
@ -1630,6 +1647,20 @@ impl Bank {
let (feature_set, feature_set_time) =
Measure::this(|_| parent.feature_set.clone(), (), "feature_set_creation");
let drop_callback = if simulation_bank {
RwLock::new(OptionalDropCallback(Some(Box::new(NoopDropCallback))))
} else {
RwLock::new(OptionalDropCallback(
parent
.drop_callback
.read()
.unwrap()
.0
.as_ref()
.map(|drop_callback| drop_callback.clone_box()),
))
};
let mut new = Bank {
rc,
src,
@ -1683,15 +1714,7 @@ impl Bank {
transaction_log_collector_config,
transaction_log_collector: Arc::new(RwLock::new(TransactionLogCollector::default())),
feature_set,
drop_callback: RwLock::new(OptionalDropCallback(
parent
.drop_callback
.read()
.unwrap()
.0
.as_ref()
.map(|drop_callback| drop_callback.clone_box()),
)),
drop_callback,
freeze_started: AtomicBool::new(false),
cost_tracker: RwLock::new(CostTracker::default()),
sysvar_cache: RwLock::new(SysvarCache::default()),
@ -3472,12 +3495,26 @@ impl Bank {
/// Run transactions against a frozen bank without committing the results
pub fn simulate_transaction(
&self,
self: &Arc<Bank>,
transaction: SanitizedTransaction,
) -> TransactionSimulationResult {
assert!(self.is_frozen(), "simulation bank must be frozen");
self.simulate_transaction_unchecked(transaction)
// Simulation detection countermeasure 1: Create a new child bank for the simulation. This
// ensures comparing the slot values between the Clock and SlotHistory sysvars does not
// reveal that the program is running in simulation.
//
// Reference: https://opcodes.fr/en/publications/2022-01/detecting-transaction-simulation/
let bank = Bank::new_from_parent_with_options(
self,
&Pubkey::default(),
self.slot().saturating_add(1),
NewBankOptions {
simulation_bank: true,
..NewBankOptions::default()
},
);
bank.simulate_transaction_unchecked(transaction)
}
/// Run transactions against a bank without committing the results; does not check if the bank