diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 16b10b0b451bbd..6c7f351b9d3a7b 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6776,6 +6776,13 @@ dependencies = [ "solana-program", ] +[[package]] +name = "solana-sbf-rust-divide-by-zero" +version = "2.2.0" +dependencies = [ + "solana-program", +] + [[package]] name = "solana-sbf-rust-dup-accounts" version = "2.2.0" diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index 7d48d1ab8efca2..3ffed90d468834 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -154,6 +154,7 @@ members = [ "rust/custom_heap", "rust/dep_crate", "rust/deprecated_loader", + "rust/divide_by_zero", "rust/dup_accounts", "rust/error_handling", "rust/external_spend", diff --git a/programs/sbf/rust/divide_by_zero/Cargo.toml b/programs/sbf/rust/divide_by_zero/Cargo.toml new file mode 100644 index 00000000000000..029b78eea386e3 --- /dev/null +++ b/programs/sbf/rust/divide_by_zero/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "solana-sbf-rust-divide-by-zero" +version = { workspace = true } +description = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +solana-program = { workspace = true } + +[lib] +crate-type = ["cdylib"] diff --git a/programs/sbf/rust/divide_by_zero/src/lib.rs b/programs/sbf/rust/divide_by_zero/src/lib.rs new file mode 100644 index 00000000000000..446faa0730dd2c --- /dev/null +++ b/programs/sbf/rust/divide_by_zero/src/lib.rs @@ -0,0 +1,27 @@ +//! Example/test program to trigger vm error by dividing by zero + +#![feature(asm_experimental_arch)] + +extern crate solana_program; +use { + solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}, + std::arch::asm, +}; + +solana_program::entrypoint_no_alloc!(process_instruction); +fn process_instruction( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + unsafe { + asm!( + " + mov64 r0, 0 + mov64 r1, 0 + div64 r0, r1 + " + ); + } + Ok(()) +} diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs index 55fb0ce9169b69..4611a8e328161e 100644 --- a/programs/sbf/tests/programs.rs +++ b/programs/sbf/tests/programs.rs @@ -94,17 +94,19 @@ fn process_transaction_and_record_inner( Result<(), TransactionError>, Vec>, Vec, + u64, ) { let commit_result = load_execute_and_commit_transaction(bank, tx); let CommittedTransaction { inner_instructions, log_messages, status, + executed_units, .. } = commit_result.unwrap(); let inner_instructions = inner_instructions.expect("cpi recording should be enabled"); let log_messages = log_messages.expect("log recording should be enabled"); - (status, inner_instructions, log_messages) + (status, inner_instructions, log_messages, executed_units) } #[cfg(feature = "sbf_rust")] @@ -789,7 +791,7 @@ fn test_program_sbf_invoke_sanity() { message.clone(), bank.last_blockhash(), ); - let (result, inner_instructions, _log_messages) = + let (result, inner_instructions, _log_messages, _executed_units) = process_transaction_and_record_inner(&bank, tx); assert_eq!(result, Ok(())); @@ -858,11 +860,12 @@ fn test_program_sbf_invoke_sanity() { // failure cases - let do_invoke_failure_test_local = + let do_invoke_failure_test_local_with_compute_check = |test: u8, expected_error: TransactionError, expected_invoked_programs: &[Pubkey], - expected_log_messages: Option>| { + expected_log_messages: Option>, + should_deplete_compute_meter: bool| { println!("Running failure test #{:?}", test); let instruction_data = &[test, bump_seed1, bump_seed2, bump_seed3]; let signers = vec![ @@ -871,14 +874,21 @@ fn test_program_sbf_invoke_sanity() { &invoked_argument_keypair, &from_keypair, ]; + let compute_unit_limit = 10_000; let instruction = Instruction::new_with_bytes( invoke_program_id, instruction_data, account_metas.clone(), ); - let message = Message::new(&[instruction], Some(&mint_pubkey)); + let message = Message::new( + &[ + instruction, + ComputeBudgetInstruction::set_compute_unit_limit(compute_unit_limit), + ], + Some(&mint_pubkey), + ); let tx = Transaction::new(&signers, message.clone(), bank.last_blockhash()); - let (result, inner_instructions, log_messages) = + let (result, inner_instructions, log_messages, executed_units) = process_transaction_and_record_inner(&bank, tx); let invoked_programs: Vec = inner_instructions[0] .iter() @@ -887,6 +897,11 @@ fn test_program_sbf_invoke_sanity() { .collect(); assert_eq!(result, Err(expected_error)); assert_eq!(invoked_programs, expected_invoked_programs); + if should_deplete_compute_meter { + assert_eq!(executed_units, compute_unit_limit as u64); + } else { + assert!(executed_units < compute_unit_limit as u64); + } if let Some(expected_log_messages) = expected_log_messages { assert_eq!(log_messages.len(), expected_log_messages.len()); expected_log_messages @@ -900,6 +915,20 @@ fn test_program_sbf_invoke_sanity() { } }; + let do_invoke_failure_test_local = + |test: u8, + expected_error: TransactionError, + expected_invoked_programs: &[Pubkey], + expected_log_messages: Option>| { + do_invoke_failure_test_local_with_compute_check( + test, + expected_error, + expected_invoked_programs, + expected_log_messages, + false, // should_deplete_compute_meter + ) + }; + let program_lang = match program.0 { Languages::Rust => "Rust", Languages::C => "C", @@ -1014,11 +1043,12 @@ fn test_program_sbf_invoke_sanity() { None, ); - do_invoke_failure_test_local( + do_invoke_failure_test_local_with_compute_check( TEST_WRITABLE_DEESCALATION_WRITABLE, TransactionError::InstructionError(0, InstructionError::ReadonlyDataModified), &[invoked_program_id.clone()], None, + true, // should_deplete_compute_meter ); do_invoke_failure_test_local( @@ -1100,7 +1130,7 @@ fn test_program_sbf_invoke_sanity() { message.clone(), bank.last_blockhash(), ); - let (result, inner_instructions, _log_messages) = + let (result, inner_instructions, _log_messages, _executed_units) = process_transaction_and_record_inner(&bank, tx); let invoked_programs: Vec = inner_instructions[0] .iter() @@ -1734,7 +1764,7 @@ fn test_program_sbf_invoke_stable_genesis_and_bank() { message.clone(), bank.last_blockhash(), ); - let (result, _, _) = process_transaction_and_record_inner(&bank, tx); + let (result, _, _, _) = process_transaction_and_record_inner(&bank, tx); assert_eq!( result.unwrap_err(), TransactionError::InstructionError(1, InstructionError::InvalidAccountData), @@ -1781,7 +1811,7 @@ fn test_program_sbf_invoke_stable_genesis_and_bank() { message.clone(), bank.last_blockhash(), ); - let (result, _, _) = process_transaction_and_record_inner(&bank, tx); + let (result, _, _, _) = process_transaction_and_record_inner(&bank, tx); assert_eq!( result.unwrap_err(), TransactionError::InstructionError(1, InstructionError::InvalidAccountData), @@ -1892,7 +1922,7 @@ fn test_program_sbf_invoke_in_same_tx_as_deployment() { Err(TransactionError::ProgramAccountNotFound), ); } else { - let (result, _, _) = process_transaction_and_record_inner(&bank, tx); + let (result, _, _, _) = process_transaction_and_record_inner(&bank, tx); assert_eq!( result.unwrap_err(), TransactionError::InstructionError(2, InstructionError::UnsupportedProgramId), @@ -2003,7 +2033,7 @@ fn test_program_sbf_invoke_in_same_tx_as_redeployment() { message.clone(), bank.last_blockhash(), ); - let (result, _, _) = process_transaction_and_record_inner(&bank, tx); + let (result, _, _, _) = process_transaction_and_record_inner(&bank, tx); assert_eq!( result.unwrap_err(), TransactionError::InstructionError(1, InstructionError::UnsupportedProgramId), @@ -2098,7 +2128,7 @@ fn test_program_sbf_invoke_in_same_tx_as_undeployment() { message.clone(), bank.last_blockhash(), ); - let (result, _, _) = process_transaction_and_record_inner(&bank, tx); + let (result, _, _, _) = process_transaction_and_record_inner(&bank, tx); assert_eq!( result.unwrap_err(), TransactionError::InstructionError(1, InstructionError::UnsupportedProgramId), @@ -4154,7 +4184,7 @@ fn test_cpi_account_ownership_writability() { ); let message = Message::new(&[instruction], Some(&mint_pubkey)); let tx = Transaction::new(&[&mint_keypair], message.clone(), bank.last_blockhash()); - let (result, _, logs) = process_transaction_and_record_inner(&bank, tx); + let (result, _, logs, _) = process_transaction_and_record_inner(&bank, tx); if direct_mapping { assert_eq!( result.unwrap_err(), @@ -4590,7 +4620,7 @@ fn test_cpi_invalid_account_info_pointers() { let message = Message::new(&[instruction], Some(&mint_pubkey)); let tx = Transaction::new(&[&mint_keypair], message.clone(), bank.last_blockhash()); - let (result, _, logs) = process_transaction_and_record_inner(&bank, tx); + let (result, _, logs, _) = process_transaction_and_record_inner(&bank, tx); assert!(result.is_err(), "{result:?}"); assert!( logs.iter().any(|log| log.contains("Invalid pointer")), @@ -4600,6 +4630,134 @@ fn test_cpi_invalid_account_info_pointers() { } } +#[test] +#[cfg(feature = "sbf_rust")] +fn test_deplete_cost_meter_with_access_violation() { + solana_logger::setup(); + let GenesisConfigInfo { + genesis_config, + mint_keypair, + .. + } = create_genesis_config(100_123_456_789); + + for apply_cost_tracker in [false, true] { + let mut bank = Bank::new_for_tests(&genesis_config); + let feature_set = Arc::make_mut(&mut bank.feature_set); + // by default test banks have all features enabled, so we only need to + // disable when needed + if !apply_cost_tracker { + feature_set.deactivate(&feature_set::apply_cost_tracker_during_replay::id()); + } + let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); + let mut bank_client = BankClient::new_shared(bank.clone()); + let authority_keypair = Keypair::new(); + let (bank, invoke_program_id) = load_upgradeable_program_and_advance_slot( + &mut bank_client, + bank_forks.as_ref(), + &mint_keypair, + &authority_keypair, + "solana_sbf_rust_invoke", + ); + + let account_keypair = Keypair::new(); + let mint_pubkey = mint_keypair.pubkey(); + let account_metas = vec![ + AccountMeta::new(mint_pubkey, true), + AccountMeta::new(account_keypair.pubkey(), false), + AccountMeta::new_readonly(invoke_program_id, false), + ]; + + let mut instruction_data = vec![TEST_WRITE_ACCOUNT, 2]; + instruction_data.extend_from_slice(3usize.to_le_bytes().as_ref()); + instruction_data.push(42); + + let instruction = Instruction::new_with_bytes( + invoke_program_id, + &instruction_data, + account_metas.clone(), + ); + + let compute_unit_limit = 1_000_000u32; + let message = Message::new( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(compute_unit_limit), + instruction, + ], + Some(&mint_keypair.pubkey()), + ); + let tx = Transaction::new(&[&mint_keypair], message, bank.last_blockhash()); + + let result = load_execute_and_commit_transaction(&bank, tx).unwrap(); + + assert_eq!( + result.status.unwrap_err(), + TransactionError::InstructionError(1, InstructionError::ReadonlyDataModified) + ); + + if apply_cost_tracker { + assert_eq!(result.executed_units, u64::from(compute_unit_limit)); + } else { + assert!(result.executed_units < u64::from(compute_unit_limit)); + } + } +} + +#[test] +#[cfg(feature = "sbf_rust")] +fn test_program_sbf_deplete_cost_meter_with_divide_by_zero() { + solana_logger::setup(); + + let GenesisConfigInfo { + genesis_config, + mint_keypair, + .. + } = create_genesis_config(50); + + for apply_cost_tracker in [false, true] { + let mut bank = Bank::new_for_tests(&genesis_config); + let feature_set = Arc::make_mut(&mut bank.feature_set); + // by default test banks have all features enabled, so we only need to + // disable when needed + if !apply_cost_tracker { + feature_set.deactivate(&feature_set::apply_cost_tracker_during_replay::id()); + } + let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); + let mut bank_client = BankClient::new_shared(bank.clone()); + let authority_keypair = Keypair::new(); + let (bank, program_id) = load_upgradeable_program_and_advance_slot( + &mut bank_client, + bank_forks.as_ref(), + &mint_keypair, + &authority_keypair, + "solana_sbf_rust_divide_by_zero", + ); + + let instruction = Instruction::new_with_bytes(program_id, &[], vec![]); + let compute_unit_limit = 10_000; + let message = Message::new( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(compute_unit_limit), + instruction, + ], + Some(&mint_keypair.pubkey()), + ); + let tx = Transaction::new(&[&mint_keypair], message, bank.last_blockhash()); + + let result = load_execute_and_commit_transaction(&bank, tx).unwrap(); + + assert_eq!( + result.status.unwrap_err(), + TransactionError::InstructionError(1, InstructionError::ProgramFailedToComplete) + ); + + if apply_cost_tracker { + assert_eq!(result.executed_units, u64::from(compute_unit_limit)); + } else { + assert!(result.executed_units < u64::from(compute_unit_limit)); + } + } +} + #[test] #[cfg(feature = "sbf_rust")] fn test_deny_executable_write() { @@ -5056,7 +5214,7 @@ fn test_account_info_rc_in_account() { let message = Message::new(&[instruction], Some(&mint_pubkey)); let tx = Transaction::new(&[&mint_keypair], message.clone(), bank.last_blockhash()); - let (result, _, logs) = process_transaction_and_record_inner(&bank, tx); + let (result, _, logs, _) = process_transaction_and_record_inner(&bank, tx); if direct_mapping { assert!( @@ -5079,7 +5237,7 @@ fn test_account_info_rc_in_account() { let message = Message::new(&[instruction], Some(&mint_pubkey)); let tx = Transaction::new(&[&mint_keypair], message.clone(), bank.last_blockhash()); - let (result, _, logs) = process_transaction_and_record_inner(&bank, tx); + let (result, _, logs, _) = process_transaction_and_record_inner(&bank, tx); if direct_mapping { assert!( @@ -5170,7 +5328,7 @@ fn test_clone_account_data() { let message = Message::new(&[instruction], Some(&mint_pubkey)); let tx = Transaction::new(&[&mint_keypair], message.clone(), bank.last_blockhash()); - let (result, _, logs) = process_transaction_and_record_inner(&bank, tx); + let (result, _, logs, _) = process_transaction_and_record_inner(&bank, tx); assert!(result.is_err(), "{result:?}"); let error = format!("Program {invoke_program_id} failed: instruction modified data of an account it does not own"); assert!(logs.iter().any(|log| log.contains(&error)), "{logs:?}"); @@ -5200,7 +5358,7 @@ fn test_clone_account_data() { let message = Message::new(&[instruction], Some(&mint_pubkey)); let tx = Transaction::new(&[&mint_keypair], message.clone(), bank.last_blockhash()); - let (result, _, logs) = process_transaction_and_record_inner(&bank, tx); + let (result, _, logs, _) = process_transaction_and_record_inner(&bank, tx); assert!(result.is_err(), "{result:?}"); let error = format!("Program {invoke_program_id} failed: instruction modified data of an account it does not own"); assert!(logs.iter().any(|log| log.contains(&error)), "{logs:?}"); @@ -5288,7 +5446,7 @@ fn test_stack_heap_zeroed() { Some(&mint_pubkey), ); let tx = Transaction::new(&[&mint_keypair], message.clone(), bank.last_blockhash()); - let (result, _, logs) = process_transaction_and_record_inner(&bank, tx); + let (result, _, logs, _) = process_transaction_and_record_inner(&bank, tx); assert!(result.is_err(), "{result:?}"); assert!( logs.iter()