From d9ee5b87a9e7db0ecb3087076a728278a1703ee9 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Thu, 15 Sep 2022 13:50:14 -0400 Subject: [PATCH] Increase transaction account lock limit from 64 to 128 (#27242) * Increase transaction account lock limit from 64 to 256 * fix bpf test * Reduce lock limit from 256 to 128 (cherry picked from commit b9700244b5c6e9b4b8244def1ab01c075f3f6173) # Conflicts: # programs/bpf/tests/programs.rs # programs/bpf_loader/src/syscalls/cpi.rs # runtime/src/bank.rs # sdk/bpf/c/inc/sol/cpi.h # sdk/bpf/c/inc/sol/inc/cpi.inc # sdk/program/src/syscalls/mod.rs # sdk/src/feature_set.rs --- programs/bpf/tests/programs.rs | 11 + programs/bpf_loader/src/syscalls/cpi.rs | 997 ++++++++++++++++++++++++ runtime/src/bank.rs | 34 +- sdk/bpf/c/inc/sol/cpi.h | 25 + sdk/bpf/c/inc/sol/inc/cpi.inc | 117 +++ sdk/program/src/syscalls/mod.rs | 21 + sdk/src/feature_set.rs | 56 ++ sdk/src/transaction/sanitized.rs | 6 +- 8 files changed, 1263 insertions(+), 4 deletions(-) create mode 100644 programs/bpf_loader/src/syscalls/cpi.rs create mode 100644 sdk/bpf/c/inc/sol/inc/cpi.inc create mode 100644 sdk/program/src/syscalls/mod.rs diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs index 7e59d0271f7dd5..f88a6d6473ac45 100644 --- a/programs/bpf/tests/programs.rs +++ b/programs/bpf/tests/programs.rs @@ -1072,6 +1072,17 @@ fn test_program_bpf_invoke_sanity() { TEST_INSTRUCTION_META_TOO_LARGE, TransactionError::InstructionError(0, InstructionError::ProgramFailedToComplete), &[], +<<<<<<< HEAD +======= + Some(vec![ + format!("Program {invoke_program_id} invoke [1]"), + format!("Program log: invoke {program_lang} program"), + "Program log: Test max account infos exceeded".into(), + "skip".into(), // don't compare compute consumption logs + "Program failed to complete: Invoked an instruction with too many account info's (129 > 128)".into(), + format!("Program {invoke_program_id} failed: Program failed to complete"), + ]), +>>>>>>> b9700244b (Increase transaction account lock limit from 64 to 128 (#27242)) ); do_invoke_failure_test_local( diff --git a/programs/bpf_loader/src/syscalls/cpi.rs b/programs/bpf_loader/src/syscalls/cpi.rs new file mode 100644 index 00000000000000..f11c7981571d7e --- /dev/null +++ b/programs/bpf_loader/src/syscalls/cpi.rs @@ -0,0 +1,997 @@ +use { + super::*, + crate::declare_syscall, + solana_sdk::syscalls::{ + MAX_CPI_ACCOUNT_INFOS, MAX_CPI_INSTRUCTION_ACCOUNTS, MAX_CPI_INSTRUCTION_DATA_LEN, + }, +}; + +struct CallerAccount<'a> { + lamports: &'a mut u64, + owner: &'a mut Pubkey, + original_data_len: usize, + data: &'a mut [u8], + vm_data_addr: u64, + ref_to_len_in_vm: &'a mut u64, + executable: bool, + rent_epoch: u64, +} +type TranslatedAccounts<'a> = Vec<(IndexOfAccount, Option>)>; + +/// Implemented by language specific data structure translators +trait SyscallInvokeSigned<'a, 'b> { + fn get_context_mut(&self) -> Result>, EbpfError>; + fn translate_instruction( + &self, + addr: u64, + memory_mapping: &mut MemoryMapping, + invoke_context: &mut InvokeContext, + ) -> Result>; + fn translate_accounts<'c>( + &'c self, + instruction_accounts: &[InstructionAccount], + program_indices: &[IndexOfAccount], + account_infos_addr: u64, + account_infos_len: u64, + memory_mapping: &mut MemoryMapping, + invoke_context: &mut InvokeContext, + ) -> Result, EbpfError>; + fn translate_signers( + &self, + program_id: &Pubkey, + signers_seeds_addr: u64, + signers_seeds_len: u64, + memory_mapping: &mut MemoryMapping, + invoke_context: &InvokeContext, + ) -> Result, EbpfError>; +} + +declare_syscall!( + /// Cross-program invocation called from Rust + SyscallInvokeSignedRust, + fn call( + &mut self, + instruction_addr: u64, + account_infos_addr: u64, + account_infos_len: u64, + signers_seeds_addr: u64, + signers_seeds_len: u64, + memory_mapping: &mut MemoryMapping, + result: &mut Result>, + ) { + *result = call( + self, + instruction_addr, + account_infos_addr, + account_infos_len, + signers_seeds_addr, + signers_seeds_len, + memory_mapping, + ); + } +); + +impl<'a, 'b> SyscallInvokeSigned<'a, 'b> for SyscallInvokeSignedRust<'a, 'b> { + fn get_context_mut(&self) -> Result>, EbpfError> { + self.invoke_context + .try_borrow_mut() + .map_err(|_| SyscallError::InvokeContextBorrowFailed.into()) + } + + fn translate_instruction( + &self, + addr: u64, + memory_mapping: &mut MemoryMapping, + invoke_context: &mut InvokeContext, + ) -> Result> { + let ix = translate_type::( + memory_mapping, + addr, + invoke_context.get_check_aligned(), + )?; + + check_instruction_size(ix.accounts.len(), ix.data.len(), invoke_context)?; + + let accounts = translate_slice::( + memory_mapping, + ix.accounts.as_ptr() as u64, + ix.accounts.len() as u64, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )? + .to_vec(); + + let ix_data_len = ix.data.len() as u64; + if invoke_context + .feature_set + .is_active(&feature_set::loosen_cpi_size_restriction::id()) + { + invoke_context.get_compute_meter().consume( + (ix_data_len) + .saturating_div(invoke_context.get_compute_budget().cpi_bytes_per_unit), + )?; + } + + let data = translate_slice::( + memory_mapping, + ix.data.as_ptr() as u64, + ix_data_len, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )? + .to_vec(); + Ok(Instruction { + program_id: ix.program_id, + accounts, + data, + }) + } + + fn translate_accounts<'c>( + &'c self, + instruction_accounts: &[InstructionAccount], + program_indices: &[IndexOfAccount], + account_infos_addr: u64, + account_infos_len: u64, + memory_mapping: &mut MemoryMapping, + invoke_context: &mut InvokeContext, + ) -> Result, EbpfError> { + let account_infos = translate_slice::( + memory_mapping, + account_infos_addr, + account_infos_len, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )?; + check_account_infos(account_infos.len(), invoke_context)?; + let account_info_keys = account_infos + .iter() + .map(|account_info| { + translate_type::( + memory_mapping, + account_info.key as *const _ as u64, + invoke_context.get_check_aligned(), + ) + }) + .collect::, EbpfError>>()?; + + let translate = |account_info: &AccountInfo, invoke_context: &InvokeContext| { + // Translate the account from user space + + let lamports = { + // Double translate lamports out of RefCell + let ptr = translate_type::( + memory_mapping, + account_info.lamports.as_ptr() as u64, + invoke_context.get_check_aligned(), + )?; + translate_type_mut::(memory_mapping, *ptr, invoke_context.get_check_aligned())? + }; + let owner = translate_type_mut::( + memory_mapping, + account_info.owner as *const _ as u64, + invoke_context.get_check_aligned(), + )?; + + let (data, vm_data_addr, ref_to_len_in_vm) = { + // Double translate data out of RefCell + let data = *translate_type::<&[u8]>( + memory_mapping, + account_info.data.as_ptr() as *const _ as u64, + invoke_context.get_check_aligned(), + )?; + + invoke_context.get_compute_meter().consume( + (data.len() as u64) + .saturating_div(invoke_context.get_compute_budget().cpi_bytes_per_unit), + )?; + + let translated = translate( + memory_mapping, + AccessType::Store, + (account_info.data.as_ptr() as *const u64 as u64) + .saturating_add(size_of::() as u64), + 8, + )? as *mut u64; + let ref_to_len_in_vm = unsafe { &mut *translated }; + let vm_data_addr = data.as_ptr() as u64; + ( + translate_slice_mut::( + memory_mapping, + vm_data_addr, + data.len() as u64, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )?, + vm_data_addr, + ref_to_len_in_vm, + ) + }; + + Ok(CallerAccount { + lamports, + owner, + original_data_len: 0, // set later + data, + vm_data_addr, + ref_to_len_in_vm, + executable: account_info.executable, + rent_epoch: account_info.rent_epoch, + }) + }; + + get_translated_accounts( + instruction_accounts, + program_indices, + &account_info_keys, + account_infos, + invoke_context, + translate, + ) + } + + fn translate_signers( + &self, + program_id: &Pubkey, + signers_seeds_addr: u64, + signers_seeds_len: u64, + memory_mapping: &mut MemoryMapping, + invoke_context: &InvokeContext, + ) -> Result, EbpfError> { + let mut signers = Vec::new(); + if signers_seeds_len > 0 { + let signers_seeds = translate_slice::<&[&[u8]]>( + memory_mapping, + signers_seeds_addr, + signers_seeds_len, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )?; + if signers_seeds.len() > MAX_SIGNERS { + return Err(SyscallError::TooManySigners.into()); + } + for signer_seeds in signers_seeds.iter() { + let untranslated_seeds = translate_slice::<&[u8]>( + memory_mapping, + signer_seeds.as_ptr() as *const _ as u64, + signer_seeds.len() as u64, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )?; + if untranslated_seeds.len() > MAX_SEEDS { + return Err(SyscallError::InstructionError( + InstructionError::MaxSeedLengthExceeded, + ) + .into()); + } + let seeds = untranslated_seeds + .iter() + .map(|untranslated_seed| { + translate_slice::( + memory_mapping, + untranslated_seed.as_ptr() as *const _ as u64, + untranslated_seed.len() as u64, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + ) + }) + .collect::, EbpfError>>()?; + let signer = Pubkey::create_program_address(&seeds, program_id) + .map_err(SyscallError::BadSeeds)?; + signers.push(signer); + } + Ok(signers) + } else { + Ok(vec![]) + } + } +} + +/// Rust representation of C's SolInstruction +#[derive(Debug)] +#[repr(C)] +struct SolInstruction { + program_id_addr: u64, + accounts_addr: u64, + accounts_len: u64, + data_addr: u64, + data_len: u64, +} + +/// Rust representation of C's SolAccountMeta +#[derive(Debug)] +#[repr(C)] +struct SolAccountMeta { + pubkey_addr: u64, + is_writable: bool, + is_signer: bool, +} + +/// Rust representation of C's SolAccountInfo +#[derive(Debug)] +#[repr(C)] +struct SolAccountInfo { + key_addr: u64, + lamports_addr: u64, + data_len: u64, + data_addr: u64, + owner_addr: u64, + rent_epoch: u64, + #[allow(dead_code)] + is_signer: bool, + #[allow(dead_code)] + is_writable: bool, + executable: bool, +} + +/// Rust representation of C's SolSignerSeed +#[derive(Debug)] +#[repr(C)] +struct SolSignerSeedC { + addr: u64, + len: u64, +} + +/// Rust representation of C's SolSignerSeeds +#[derive(Debug)] +#[repr(C)] +struct SolSignerSeedsC { + addr: u64, + len: u64, +} + +declare_syscall!( + /// Cross-program invocation called from C + SyscallInvokeSignedC, + fn call( + &mut self, + instruction_addr: u64, + account_infos_addr: u64, + account_infos_len: u64, + signers_seeds_addr: u64, + signers_seeds_len: u64, + memory_mapping: &mut MemoryMapping, + result: &mut Result>, + ) { + *result = call( + self, + instruction_addr, + account_infos_addr, + account_infos_len, + signers_seeds_addr, + signers_seeds_len, + memory_mapping, + ); + } +); + +impl<'a, 'b> SyscallInvokeSigned<'a, 'b> for SyscallInvokeSignedC<'a, 'b> { + fn get_context_mut(&self) -> Result>, EbpfError> { + self.invoke_context + .try_borrow_mut() + .map_err(|_| SyscallError::InvokeContextBorrowFailed.into()) + } + + fn translate_instruction( + &self, + addr: u64, + memory_mapping: &mut MemoryMapping, + invoke_context: &mut InvokeContext, + ) -> Result> { + let ix_c = translate_type::( + memory_mapping, + addr, + invoke_context.get_check_aligned(), + )?; + + check_instruction_size( + ix_c.accounts_len as usize, + ix_c.data_len as usize, + invoke_context, + )?; + let program_id = translate_type::( + memory_mapping, + ix_c.program_id_addr, + invoke_context.get_check_aligned(), + )?; + let meta_cs = translate_slice::( + memory_mapping, + ix_c.accounts_addr, + ix_c.accounts_len as u64, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )?; + + let ix_data_len = ix_c.data_len as u64; + if invoke_context + .feature_set + .is_active(&feature_set::loosen_cpi_size_restriction::id()) + { + invoke_context.get_compute_meter().consume( + (ix_data_len) + .saturating_div(invoke_context.get_compute_budget().cpi_bytes_per_unit), + )?; + } + + let data = translate_slice::( + memory_mapping, + ix_c.data_addr, + ix_data_len, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )? + .to_vec(); + let accounts = meta_cs + .iter() + .map(|meta_c| { + let pubkey = translate_type::( + memory_mapping, + meta_c.pubkey_addr, + invoke_context.get_check_aligned(), + )?; + Ok(AccountMeta { + pubkey: *pubkey, + is_signer: meta_c.is_signer, + is_writable: meta_c.is_writable, + }) + }) + .collect::, EbpfError>>()?; + + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) + } + + fn translate_accounts<'c>( + &'c self, + instruction_accounts: &[InstructionAccount], + program_indices: &[IndexOfAccount], + account_infos_addr: u64, + account_infos_len: u64, + memory_mapping: &mut MemoryMapping, + invoke_context: &mut InvokeContext, + ) -> Result, EbpfError> { + let account_infos = translate_slice::( + memory_mapping, + account_infos_addr, + account_infos_len, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )?; + check_account_infos(account_infos.len(), invoke_context)?; + let account_info_keys = account_infos + .iter() + .map(|account_info| { + translate_type::( + memory_mapping, + account_info.key_addr, + invoke_context.get_check_aligned(), + ) + }) + .collect::, EbpfError>>()?; + + let translate = |account_info: &SolAccountInfo, invoke_context: &InvokeContext| { + // Translate the account from user space + + let lamports = translate_type_mut::( + memory_mapping, + account_info.lamports_addr, + invoke_context.get_check_aligned(), + )?; + let owner = translate_type_mut::( + memory_mapping, + account_info.owner_addr, + invoke_context.get_check_aligned(), + )?; + let vm_data_addr = account_info.data_addr; + + invoke_context.get_compute_meter().consume( + account_info + .data_len + .saturating_div(invoke_context.get_compute_budget().cpi_bytes_per_unit), + )?; + + let data = translate_slice_mut::( + memory_mapping, + vm_data_addr, + account_info.data_len, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )?; + + let first_info_addr = account_infos.first().ok_or(SyscallError::InstructionError( + InstructionError::InvalidArgument, + ))? as *const _ as u64; + let addr = &account_info.data_len as *const u64 as u64; + let vm_addr = if invoke_context + .feature_set + .is_active(&syscall_saturated_math::id()) + { + account_infos_addr.saturating_add(addr.saturating_sub(first_info_addr)) + } else { + #[allow(clippy::integer_arithmetic)] + { + account_infos_addr + (addr - first_info_addr) + } + }; + let _ = translate( + memory_mapping, + AccessType::Store, + vm_addr, + size_of::() as u64, + )?; + let ref_to_len_in_vm = unsafe { &mut *(addr as *mut u64) }; + + Ok(CallerAccount { + lamports, + owner, + original_data_len: 0, // set later + data, + vm_data_addr, + ref_to_len_in_vm, + executable: account_info.executable, + rent_epoch: account_info.rent_epoch, + }) + }; + + get_translated_accounts( + instruction_accounts, + program_indices, + &account_info_keys, + account_infos, + invoke_context, + translate, + ) + } + + fn translate_signers( + &self, + program_id: &Pubkey, + signers_seeds_addr: u64, + signers_seeds_len: u64, + memory_mapping: &mut MemoryMapping, + invoke_context: &InvokeContext, + ) -> Result, EbpfError> { + if signers_seeds_len > 0 { + let signers_seeds = translate_slice::( + memory_mapping, + signers_seeds_addr, + signers_seeds_len, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )?; + if signers_seeds.len() > MAX_SIGNERS { + return Err(SyscallError::TooManySigners.into()); + } + Ok(signers_seeds + .iter() + .map(|signer_seeds| { + let seeds = translate_slice::( + memory_mapping, + signer_seeds.addr, + signer_seeds.len, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + )?; + if seeds.len() > MAX_SEEDS { + return Err(SyscallError::InstructionError( + InstructionError::MaxSeedLengthExceeded, + ) + .into()); + } + let seeds_bytes = seeds + .iter() + .map(|seed| { + translate_slice::( + memory_mapping, + seed.addr, + seed.len, + invoke_context.get_check_aligned(), + invoke_context.get_check_size(), + ) + }) + .collect::, EbpfError>>()?; + Pubkey::create_program_address(&seeds_bytes, program_id) + .map_err(|err| SyscallError::BadSeeds(err).into()) + }) + .collect::, EbpfError>>()?) + } else { + Ok(vec![]) + } + } +} + +fn get_translated_accounts<'a, T, F>( + instruction_accounts: &[InstructionAccount], + program_indices: &[IndexOfAccount], + account_info_keys: &[&Pubkey], + account_infos: &[T], + invoke_context: &mut InvokeContext, + do_translate: F, +) -> Result, EbpfError> +where + F: Fn(&T, &InvokeContext) -> Result, EbpfError>, +{ + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context + .get_current_instruction_context() + .map_err(SyscallError::InstructionError)?; + let mut accounts = Vec::with_capacity(instruction_accounts.len().saturating_add(1)); + let is_disable_cpi_setting_executable_and_rent_epoch_active = invoke_context + .feature_set + .is_active(&disable_cpi_setting_executable_and_rent_epoch::id()); + + let program_account_index = program_indices + .last() + .ok_or(SyscallError::InstructionError( + InstructionError::MissingAccount, + ))?; + accounts.push((*program_account_index, None)); + + for (instruction_account_index, instruction_account) in instruction_accounts.iter().enumerate() + { + if instruction_account_index as IndexOfAccount != instruction_account.index_in_callee { + continue; // Skip duplicate account + } + let mut callee_account = instruction_context + .try_borrow_instruction_account( + transaction_context, + instruction_account.index_in_caller, + ) + .map_err(SyscallError::InstructionError)?; + let account_key = invoke_context + .transaction_context + .get_key_of_account_at_index(instruction_account.index_in_transaction) + .map_err(SyscallError::InstructionError)?; + if callee_account.is_executable() { + // Use the known account + invoke_context.get_compute_meter().consume( + (callee_account.get_data().len() as u64) + .saturating_div(invoke_context.get_compute_budget().cpi_bytes_per_unit), + )?; + + accounts.push((instruction_account.index_in_caller, None)); + } else if let Some(caller_account_index) = + account_info_keys.iter().position(|key| *key == account_key) + { + let mut caller_account = do_translate( + account_infos + .get(caller_account_index) + .ok_or(SyscallError::InvalidLength)?, + invoke_context, + )?; + { + if callee_account.get_lamports() != *caller_account.lamports { + callee_account + .set_lamports(*caller_account.lamports) + .map_err(SyscallError::InstructionError)?; + } + // The redundant check helps to avoid the expensive data comparison if we can + match callee_account + .can_data_be_resized(caller_account.data.len()) + .and_then(|_| callee_account.can_data_be_changed()) + { + Ok(()) => callee_account + .set_data(caller_account.data) + .map_err(SyscallError::InstructionError)?, + Err(err) if callee_account.get_data() != caller_account.data => { + return Err(EbpfError::UserError(BpfError::SyscallError( + SyscallError::InstructionError(err), + ))); + } + _ => {} + } + if !is_disable_cpi_setting_executable_and_rent_epoch_active + && callee_account.is_executable() != caller_account.executable + { + callee_account + .set_executable(caller_account.executable) + .map_err(SyscallError::InstructionError)?; + } + // Change the owner at the end so that we are allowed to change the lamports and data before + if callee_account.get_owner() != caller_account.owner { + callee_account + .set_owner(caller_account.owner.as_ref()) + .map_err(SyscallError::InstructionError)?; + } + drop(callee_account); + let callee_account = invoke_context + .transaction_context + .get_account_at_index(instruction_account.index_in_transaction) + .map_err(SyscallError::InstructionError)?; + if !is_disable_cpi_setting_executable_and_rent_epoch_active + && callee_account.borrow().rent_epoch() != caller_account.rent_epoch + { + if invoke_context + .feature_set + .is_active(&enable_early_verification_of_account_modifications::id()) + { + return Err(SyscallError::InstructionError( + InstructionError::RentEpochModified, + ) + .into()); + } else { + callee_account + .borrow_mut() + .set_rent_epoch(caller_account.rent_epoch); + } + } + } + let caller_account = if instruction_account.is_writable { + let orig_data_lens = invoke_context + .get_orig_account_lengths() + .map_err(SyscallError::InstructionError)?; + caller_account.original_data_len = *orig_data_lens + .get(instruction_account.index_in_caller as usize) + .ok_or_else(|| { + ic_msg!( + invoke_context, + "Internal error: index mismatch for account {}", + account_key + ); + SyscallError::InstructionError(InstructionError::MissingAccount) + })?; + Some(caller_account) + } else { + None + }; + accounts.push((instruction_account.index_in_caller, caller_account)); + } else { + ic_msg!( + invoke_context, + "Instruction references an unknown account {}", + account_key + ); + return Err(SyscallError::InstructionError(InstructionError::MissingAccount).into()); + } + } + + Ok(accounts) +} + +fn check_instruction_size( + num_accounts: usize, + data_len: usize, + invoke_context: &mut InvokeContext, +) -> Result<(), EbpfError> { + if invoke_context + .feature_set + .is_active(&feature_set::loosen_cpi_size_restriction::id()) + { + let data_len = data_len as u64; + let max_data_len = MAX_CPI_INSTRUCTION_DATA_LEN; + if data_len > max_data_len { + return Err(SyscallError::MaxInstructionDataLenExceeded { + data_len, + max_data_len, + } + .into()); + } + + let num_accounts = num_accounts as u64; + let max_accounts = MAX_CPI_INSTRUCTION_ACCOUNTS as u64; + if num_accounts > max_accounts { + return Err(SyscallError::MaxInstructionAccountsExceeded { + num_accounts, + max_accounts, + } + .into()); + } + } else { + let max_size = invoke_context.get_compute_budget().max_cpi_instruction_size; + let size = num_accounts + .saturating_mul(size_of::()) + .saturating_add(data_len); + if size > max_size { + return Err(SyscallError::InstructionTooLarge(size, max_size).into()); + } + } + Ok(()) +} + +fn check_account_infos( + num_account_infos: usize, + invoke_context: &mut InvokeContext, +) -> Result<(), EbpfError> { + if invoke_context + .feature_set + .is_active(&feature_set::loosen_cpi_size_restriction::id()) + { + let max_cpi_account_infos = if invoke_context + .feature_set + .is_active(&feature_set::increase_tx_account_lock_limit::id()) + { + MAX_CPI_ACCOUNT_INFOS + } else { + 64 + }; + let num_account_infos = num_account_infos as u64; + let max_account_infos = max_cpi_account_infos as u64; + if num_account_infos > max_account_infos { + return Err(SyscallError::MaxInstructionAccountInfosExceeded { + num_account_infos, + max_account_infos, + } + .into()); + } + } else { + let adjusted_len = if invoke_context + .feature_set + .is_active(&syscall_saturated_math::id()) + { + num_account_infos.saturating_mul(size_of::()) + } else { + #[allow(clippy::integer_arithmetic)] + { + num_account_infos * size_of::() + } + }; + if adjusted_len > invoke_context.get_compute_budget().max_cpi_instruction_size { + // Cap the number of account_infos a caller can pass to approximate + // maximum that accounts that could be passed in an instruction + return Err(SyscallError::TooManyAccounts.into()); + }; + } + Ok(()) +} + +fn check_authorized_program( + program_id: &Pubkey, + instruction_data: &[u8], + invoke_context: &InvokeContext, +) -> Result<(), EbpfError> { + if native_loader::check_id(program_id) + || bpf_loader::check_id(program_id) + || bpf_loader_deprecated::check_id(program_id) + || (bpf_loader_upgradeable::check_id(program_id) + && !(bpf_loader_upgradeable::is_upgrade_instruction(instruction_data) + || bpf_loader_upgradeable::is_set_authority_instruction(instruction_data) + || bpf_loader_upgradeable::is_close_instruction(instruction_data))) + || is_precompile(program_id, |feature_id: &Pubkey| { + invoke_context.feature_set.is_active(feature_id) + }) + { + return Err(SyscallError::ProgramNotSupported(*program_id).into()); + } + Ok(()) +} + +/// Call process instruction, common to both Rust and C +fn call<'a, 'b: 'a>( + syscall: &mut dyn SyscallInvokeSigned<'a, 'b>, + instruction_addr: u64, + account_infos_addr: u64, + account_infos_len: u64, + signers_seeds_addr: u64, + signers_seeds_len: u64, + memory_mapping: &mut MemoryMapping, +) -> Result> { + let mut invoke_context = syscall.get_context_mut()?; + invoke_context + .get_compute_meter() + .consume(invoke_context.get_compute_budget().invoke_units)?; + + // Translate and verify caller's data + let instruction = + syscall.translate_instruction(instruction_addr, memory_mapping, *invoke_context)?; + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context + .get_current_instruction_context() + .map_err(SyscallError::InstructionError)?; + let caller_program_id = instruction_context + .get_last_program_key(transaction_context) + .map_err(SyscallError::InstructionError)?; + let signers = syscall.translate_signers( + caller_program_id, + signers_seeds_addr, + signers_seeds_len, + memory_mapping, + *invoke_context, + )?; + let (instruction_accounts, program_indices) = invoke_context + .prepare_instruction(&instruction, &signers) + .map_err(SyscallError::InstructionError)?; + check_authorized_program(&instruction.program_id, &instruction.data, *invoke_context)?; + let mut accounts = syscall.translate_accounts( + &instruction_accounts, + &program_indices, + account_infos_addr, + account_infos_len, + memory_mapping, + *invoke_context, + )?; + + // Process instruction + let mut compute_units_consumed = 0; + invoke_context + .process_instruction( + &instruction.data, + &instruction_accounts, + &program_indices, + &mut compute_units_consumed, + &mut ExecuteTimings::default(), + ) + .map_err(SyscallError::InstructionError)?; + + // Copy results back to caller + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context + .get_current_instruction_context() + .map_err(SyscallError::InstructionError)?; + for (index_in_caller, caller_account) in accounts.iter_mut() { + if let Some(caller_account) = caller_account { + let callee_account = instruction_context + .try_borrow_instruction_account(transaction_context, *index_in_caller) + .map_err(SyscallError::InstructionError)?; + *caller_account.lamports = callee_account.get_lamports(); + *caller_account.owner = *callee_account.get_owner(); + let new_len = callee_account.get_data().len(); + if caller_account.data.len() != new_len { + let data_overflow = if invoke_context + .feature_set + .is_active(&syscall_saturated_math::id()) + { + new_len + > caller_account + .original_data_len + .saturating_add(MAX_PERMITTED_DATA_INCREASE) + } else { + #[allow(clippy::integer_arithmetic)] + { + new_len > caller_account.original_data_len + MAX_PERMITTED_DATA_INCREASE + } + }; + if data_overflow { + ic_msg!( + invoke_context, + "Account data size realloc limited to {} in inner instructions", + MAX_PERMITTED_DATA_INCREASE + ); + return Err( + SyscallError::InstructionError(InstructionError::InvalidRealloc).into(), + ); + } + if new_len < caller_account.data.len() { + caller_account + .data + .get_mut(new_len..) + .ok_or(SyscallError::InstructionError( + InstructionError::AccountDataTooSmall, + ))? + .fill(0); + } + caller_account.data = translate_slice_mut::( + memory_mapping, + caller_account.vm_data_addr, + new_len as u64, + false, // Don't care since it is byte aligned + invoke_context.get_check_size(), + )?; + *caller_account.ref_to_len_in_vm = new_len as u64; + let serialized_len_ptr = translate_type_mut::( + memory_mapping, + caller_account + .vm_data_addr + .saturating_sub(std::mem::size_of::() as u64), + invoke_context.get_check_aligned(), + )?; + *serialized_len_ptr = new_len as u64; + } + let to_slice = &mut caller_account.data; + let from_slice = callee_account + .get_data() + .get(0..new_len) + .ok_or(SyscallError::InvalidLength)?; + if to_slice.len() != from_slice.len() { + return Err( + SyscallError::InstructionError(InstructionError::AccountDataTooSmall).into(), + ); + } + to_slice.copy_from_slice(from_slice); + } + } + + Ok(SUCCESS) +} diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index e76bf7a8f1a5ca..4b80c4ca9d80aa 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -3507,7 +3507,34 @@ impl Bank { } pub fn is_block_boundary(&self, tick_height: u64) -> bool { +<<<<<<< HEAD tick_height % self.ticks_per_slot == 0 +======= + if self + .feature_set + .is_active(&feature_set::fix_recent_blockhashes::id()) + { + tick_height == self.max_tick_height + } else { + tick_height % self.ticks_per_slot == 0 + } + } + + /// Get the max number of accounts that a transaction may lock in this block + pub fn get_transaction_account_lock_limit(&self) -> usize { + if let Some(transaction_account_lock_limit) = + self.runtime_config.transaction_account_lock_limit + { + transaction_account_lock_limit + } else if self + .feature_set + .is_active(&feature_set::increase_tx_account_lock_limit::id()) + { + MAX_TX_ACCOUNT_LOCKS + } else { + 64 + } +>>>>>>> b9700244b (Increase transaction account lock limit from 64 to 128 (#27242)) } /// Prepare a transaction batch from a list of legacy transactions. Used for tests only. @@ -7007,8 +7034,12 @@ pub(crate) mod tests { system_program, sysvar::rewards::Rewards, timing::duration_as_s, +<<<<<<< HEAD transaction::MAX_TX_ACCOUNT_LOCKS, transaction_context::InstructionContext, +======= + transaction_context::IndexOfAccount, +>>>>>>> b9700244b (Increase transaction account lock limit from 64 to 128 (#27242)) }, solana_vote_program::{ vote_instruction, @@ -13060,7 +13091,8 @@ pub(crate) mod tests { bank.last_blockhash(), ); - while tx.message.account_keys.len() <= MAX_TX_ACCOUNT_LOCKS { + let transaction_account_lock_limit = bank.get_transaction_account_lock_limit(); + while tx.message.account_keys.len() <= transaction_account_lock_limit { tx.message.account_keys.push(solana_sdk::pubkey::new_rand()); } diff --git a/sdk/bpf/c/inc/sol/cpi.h b/sdk/bpf/c/inc/sol/cpi.h index 2cb11e9df9e51f..f54ec83a4f3922 100644 --- a/sdk/bpf/c/inc/sol/cpi.h +++ b/sdk/bpf/c/inc/sol/cpi.h @@ -12,6 +12,31 @@ extern "C" { #endif /** +<<<<<<< HEAD +======= + * Maximum CPI instruction data size. 10 KiB was chosen to ensure that CPI + * instructions are not more limited than transaction instructions if the size + * of transactions is doubled in the future. + */ +static const uint64_t MAX_CPI_INSTRUCTION_DATA_LEN = 10240; + +/** + * Maximum CPI instruction accounts. 255 was chosen to ensure that instruction + * accounts are always within the maximum instruction account limit for BPF + * program instructions. + */ +static const uint8_t MAX_CPI_INSTRUCTION_ACCOUNTS = 255; + +/** + * Maximum number of account info structs that can be used in a single CPI + * invocation. A limit on account info structs is effectively the same as + * limiting the number of unique accounts. 128 was chosen to match the max + * number of locked accounts per transaction (MAX_TX_ACCOUNT_LOCKS). + */ +static const uint16_t MAX_CPI_ACCOUNT_INFOS = 128; + +/** +>>>>>>> b9700244b (Increase transaction account lock limit from 64 to 128 (#27242)) * Account Meta */ typedef struct { diff --git a/sdk/bpf/c/inc/sol/inc/cpi.inc b/sdk/bpf/c/inc/sol/inc/cpi.inc new file mode 100644 index 00000000000000..41ce4fb01a691b --- /dev/null +++ b/sdk/bpf/c/inc/sol/inc/cpi.inc @@ -0,0 +1,117 @@ +#pragma once +/** + * @brief Solana Cross-Program Invocation + */ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Maximum CPI instruction data size. 10 KiB was chosen to ensure that CPI + * instructions are not more limited than transaction instructions if the size + * of transactions is doubled in the future. + */ +static const uint64_t MAX_CPI_INSTRUCTION_DATA_LEN = 10240; + +/** + * Maximum CPI instruction accounts. 255 was chosen to ensure that instruction + * accounts are always within the maximum instruction account limit for BPF + * program instructions. + */ +static const uint8_t MAX_CPI_INSTRUCTION_ACCOUNTS = 255; + +/** + * Maximum number of account info structs that can be used in a single CPI + * invocation. A limit on account info structs is effectively the same as + * limiting the number of unique accounts. 128 was chosen to match the max + * number of locked accounts per transaction (MAX_TX_ACCOUNT_LOCKS). + */ +static const uint16_t MAX_CPI_ACCOUNT_INFOS = 128; + +/** + * Account Meta + */ +typedef struct { + SolPubkey *pubkey; /** An account's public key */ + bool is_writable; /** True if the `pubkey` can be loaded as a read-write account */ + bool is_signer; /** True if an Instruction requires a Transaction signature matching `pubkey` */ +} SolAccountMeta; + +/** + * Instruction + */ +typedef struct { + SolPubkey *program_id; /** Pubkey of the instruction processor that executes this instruction */ + SolAccountMeta *accounts; /** Metadata for what accounts should be passed to the instruction processor */ + uint64_t account_len; /** Number of SolAccountMetas */ + uint8_t *data; /** Opaque data passed to the instruction processor */ + uint64_t data_len; /** Length of the data in bytes */ +} SolInstruction; + +/** + * Internal cross-program invocation function + */ +@SYSCALL uint64_t sol_invoke_signed_c( + const SolInstruction *, + const SolAccountInfo *, + int, + const SolSignerSeeds *, + int +); + +/** + * Invoke another program and sign for some of the keys + * + * @param instruction Instruction to process + * @param account_infos Accounts used by instruction + * @param account_infos_len Length of account_infos array + * @param seeds Seed bytes used to sign program accounts + * @param seeds_len Length of the seeds array + */ +static uint64_t sol_invoke_signed( + const SolInstruction *instruction, + const SolAccountInfo *account_infos, + int account_infos_len, + const SolSignerSeeds *signers_seeds, + int signers_seeds_len +) { + return sol_invoke_signed_c( + instruction, + account_infos, + account_infos_len, + signers_seeds, + signers_seeds_len + ); +} +/** + * Invoke another program + * + * @param instruction Instruction to process + * @param account_infos Accounts used by instruction + * @param account_infos_len Length of account_infos array +*/ +static uint64_t sol_invoke( + const SolInstruction *instruction, + const SolAccountInfo *account_infos, + int account_infos_len +) { + const SolSignerSeeds signers_seeds[] = {{}}; + return sol_invoke_signed( + instruction, + account_infos, + account_infos_len, + signers_seeds, + 0 + ); +} + +#ifdef __cplusplus +} +#endif + +/**@}*/ diff --git a/sdk/program/src/syscalls/mod.rs b/sdk/program/src/syscalls/mod.rs new file mode 100644 index 00000000000000..d66c9361e95792 --- /dev/null +++ b/sdk/program/src/syscalls/mod.rs @@ -0,0 +1,21 @@ +#[cfg(target_os = "solana")] +mod definitions; + +#[cfg(target_os = "solana")] +pub use definitions::*; + +/// Maximum CPI instruction data size. 10 KiB was chosen to ensure that CPI +/// instructions are not more limited than transaction instructions if the size +/// of transactions is doubled in the future. +pub const MAX_CPI_INSTRUCTION_DATA_LEN: u64 = 10 * 1024; + +/// Maximum CPI instruction accounts. 255 was chosen to ensure that instruction +/// accounts are always within the maximum instruction account limit for BPF +/// program instructions. +pub const MAX_CPI_INSTRUCTION_ACCOUNTS: u8 = u8::MAX; + +/// Maximum number of account info structs that can be used in a single CPI +/// invocation. A limit on account info structs is effectively the same as +/// limiting the number of unique accounts. 128 was chosen to match the max +/// number of locked accounts per transaction (MAX_TX_ACCOUNT_LOCKS). +pub const MAX_CPI_ACCOUNT_INFOS: usize = 128; diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index d6bced741648b4..e727be5ea74b14 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -419,6 +419,49 @@ pub mod check_ping_ancestor_requests { solana_sdk::declare_id!("AXLB87anNaUQtqBSsxkm4gvNzYY985aLtNtpJC94uWLJ"); } +<<<<<<< HEAD +======= +pub mod incremental_snapshot_only_incremental_hash_calculation { + solana_sdk::declare_id!("25vqsfjk7Nv1prsQJmA4Xu1bN61s8LXCBGUPp8Rfy1UF"); +} + +pub mod disable_cpi_setting_executable_and_rent_epoch { + solana_sdk::declare_id!("B9cdB55u4jQsDNsdTK525yE9dmSc5Ga7YBaBrDFvEhM9"); +} + +pub mod relax_authority_signer_check_for_lookup_table_creation { + solana_sdk::declare_id!("FKAcEvNgSY79RpqsPNUV5gDyumopH4cEHqUxyfm8b8Ap"); +} + +pub mod stop_sibling_instruction_search_at_parent { + solana_sdk::declare_id!("EYVpEP7uzH1CoXzbD6PubGhYmnxRXPeq3PPsm1ba3gpo"); +} + +pub mod vote_state_update_root_fix { + solana_sdk::declare_id!("G74BkWBzmsByZ1kxHy44H3wjwp5hp7JbrGRuDpco22tY"); +} + +pub mod cap_accounts_data_allocations_per_transaction { + solana_sdk::declare_id!("9gxu85LYRAcZL38We8MYJ4A9AwgBBPtVBAqebMcT1241"); +} + +pub mod return_none_for_zero_lamport_accounts { + solana_sdk::declare_id!("7K5HFrS1WAq6ND7RQbShXZXbtAookyTfaDQPTJNuZpze"); +} + +pub mod epoch_accounts_hash { + solana_sdk::declare_id!("5GpmAKxaGsWWbPp4bNXFLJxZVvG92ctxf7jQnzTQjF3n"); +} + +pub mod remove_deprecated_request_unit_ix { + solana_sdk::declare_id!("EfhYd3SafzGT472tYQDUc4dPd2xdEfKs5fwkowUgVt4W"); +} + +pub mod increase_tx_account_lock_limit { + solana_sdk::declare_id!("9LZdXeKGeBV6hRLdxS1rHbHoEUsKqesCC2ZAPTPKJAbK"); +} + +>>>>>>> b9700244b (Increase transaction account lock limit from 64 to 128 (#27242)) lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -518,6 +561,19 @@ lazy_static! { (prevent_crediting_accounts_that_end_rent_paying::id(), "prevent crediting rent paying accounts #26606"), (sign_repair_requests::id(), "sign repair requests #26834"), (check_ping_ancestor_requests::id(), "ancestor hash repair socket ping/pong support #26963"), +<<<<<<< HEAD +======= + (incremental_snapshot_only_incremental_hash_calculation::id(), "only hash accounts in incremental snapshot during incremental snapshot creation #26799"), + (disable_cpi_setting_executable_and_rent_epoch::id(), "disable setting is_executable and_rent_epoch in CPI #26987"), + (relax_authority_signer_check_for_lookup_table_creation::id(), "relax authority signer check for lookup table creation #27205"), + (stop_sibling_instruction_search_at_parent::id(), "stop the search in get_processed_sibling_instruction when the parent instruction is reached #27289"), + (vote_state_update_root_fix::id(), "fix root in vote state updates #27361"), + (cap_accounts_data_allocations_per_transaction::id(), "cap accounts data allocations per transaction #27375"), + (return_none_for_zero_lamport_accounts::id(), "return none for zero lamport accounts #27800"), + (epoch_accounts_hash::id(), "enable epoch accounts hash calculation #27539"), + (remove_deprecated_request_unit_ix::id(), "remove support for RequestUnitsDeprecated instruction #27500"), + (increase_tx_account_lock_limit::id(), "increase tx account lock limit to 128 #27241"), +>>>>>>> b9700244b (Increase transaction account lock limit from 64 to 128 (#27242)) /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/sdk/src/transaction/sanitized.rs b/sdk/src/transaction/sanitized.rs index 208dc03f7c841e..96de379a1fecb1 100644 --- a/sdk/src/transaction/sanitized.rs +++ b/sdk/src/transaction/sanitized.rs @@ -20,9 +20,9 @@ use { }; /// Maximum number of accounts that a transaction may lock. -/// 64 was chosen because it is roughly twice the previous -/// number of account keys that could fit in a legacy tx. -pub const MAX_TX_ACCOUNT_LOCKS: usize = 64; +/// 128 was chosen because it is the minimum number of accounts +/// needed for the Neon EVM implementation. +pub const MAX_TX_ACCOUNT_LOCKS: usize = 128; /// Sanitized transaction and the hash of its message #[derive(Debug, Clone)]