diff --git a/Cargo.lock b/Cargo.lock index 2acf4de21df..a71c023a94e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8091,6 +8091,7 @@ dependencies = [ "solana-clock", "solana-compute-budget", "solana-instruction", + "solana-loader-v3-interface", "solana-loader-v4-interface", "solana-log-collector", "solana-measure", diff --git a/programs/loader-v4/Cargo.toml b/programs/loader-v4/Cargo.toml index 640c6fe5751..3b91def6e35 100644 --- a/programs/loader-v4/Cargo.toml +++ b/programs/loader-v4/Cargo.toml @@ -16,6 +16,7 @@ solana-bincode = { workspace = true } solana-bpf-loader-program = { workspace = true, features = ["svm-internal"] } solana-compute-budget = { workspace = true } solana-instruction = { workspace = true } +solana-loader-v3-interface = { workspace = true } solana-loader-v4-interface = { workspace = true } solana-log-collector = { workspace = true } solana-measure = { workspace = true } diff --git a/programs/loader-v4/src/lib.rs b/programs/loader-v4/src/lib.rs index e87f8d24bad..78ed1477ddd 100644 --- a/programs/loader-v4/src/lib.rs +++ b/programs/loader-v4/src/lib.rs @@ -4,6 +4,7 @@ use { solana_bincode::limited_deserialize, solana_bpf_loader_program::{deploy_program, execute}, solana_instruction::error::InstructionError, + solana_loader_v3_interface::state::UpgradeableLoaderState, solana_loader_v4_interface::{ instruction::LoaderV4Instruction, state::{LoaderV4State, LoaderV4Status}, @@ -17,7 +18,7 @@ use { }, solana_pubkey::Pubkey, solana_sbpf::{declare_builtin_function, memory_region::MemoryMapping}, - solana_sdk_ids::loader_v4, + solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4}, solana_transaction_context::{BorrowedAccount, InstructionContext}, solana_type_overrides::sync::{atomic::Ordering, Arc}, std::{cell::RefCell, rc::Rc}, @@ -106,13 +107,10 @@ fn process_instruction_write( ic_logger_msg!(log_collector, "Program is not retracted"); return Err(InstructionError::InvalidArgument); } - let end_offset = (offset as usize).saturating_add(bytes.len()); + let destination_offset = (offset as usize).saturating_add(LoaderV4State::program_data_offset()); program .get_data_mut()? - .get_mut( - LoaderV4State::program_data_offset().saturating_add(offset as usize) - ..LoaderV4State::program_data_offset().saturating_add(end_offset), - ) + .get_mut(destination_offset..destination_offset.saturating_add(bytes.len())) .ok_or_else(|| { ic_logger_msg!(log_collector, "Write out of bounds"); InstructionError::AccountDataTooSmall @@ -121,6 +119,65 @@ fn process_instruction_write( Ok(()) } +fn process_instruction_copy( + invoke_context: &mut InvokeContext, + destination_offset: u32, + source_offset: u32, + length: u32, +) -> Result<(), InstructionError> { + let log_collector = invoke_context.get_log_collector(); + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let mut program = instruction_context.try_borrow_instruction_account(transaction_context, 0)?; + let authority_address = instruction_context + .get_index_of_instruction_account_in_transaction(1) + .and_then(|index| transaction_context.get_key_of_account_at_index(index))?; + let source_program = + instruction_context.try_borrow_instruction_account(transaction_context, 2)?; + let state = check_program_account( + &log_collector, + instruction_context, + &program, + authority_address, + )?; + if !matches!(state.status, LoaderV4Status::Retracted) { + ic_logger_msg!(log_collector, "Program is not retracted"); + return Err(InstructionError::InvalidArgument); + } + let source_owner = &source_program.get_owner(); + let source_offset = + (source_offset as usize).saturating_add(if loader_v4::check_id(source_owner) { + LoaderV4State::program_data_offset() + } else if bpf_loader_upgradeable::check_id(source_owner) { + UpgradeableLoaderState::size_of_programdata_metadata() + } else if bpf_loader_deprecated::check_id(source_owner) + || bpf_loader::check_id(source_owner) + { + 0 + } else { + ic_logger_msg!(log_collector, "Source is not a program"); + return Err(InstructionError::InvalidArgument); + }); + let data = source_program + .get_data() + .get(source_offset..source_offset.saturating_add(length as usize)) + .ok_or_else(|| { + ic_logger_msg!(log_collector, "Read out of bounds"); + InstructionError::AccountDataTooSmall + })?; + let destination_offset = + (destination_offset as usize).saturating_add(LoaderV4State::program_data_offset()); + program + .get_data_mut()? + .get_mut(destination_offset..destination_offset.saturating_add(length as usize)) + .ok_or_else(|| { + ic_logger_msg!(log_collector, "Write out of bounds"); + InstructionError::AccountDataTooSmall + })? + .copy_from_slice(data); + Ok(()) +} + fn process_instruction_truncate( invoke_context: &mut InvokeContext, new_size: u32, @@ -434,6 +491,13 @@ fn process_instruction_inner( LoaderV4Instruction::Write { offset, bytes } => { process_instruction_write(invoke_context, offset, bytes) } + LoaderV4Instruction::Copy { + destination_offset, + source_offset, + length, + } => { + process_instruction_copy(invoke_context, destination_offset, source_offset, length) + } LoaderV4Instruction::Truncate { new_size } => { process_instruction_truncate(invoke_context, new_size) } @@ -756,6 +820,141 @@ mod tests { }); } + #[test] + fn test_loader_instruction_copy() { + let authority_address = Pubkey::new_unique(); + let transaction_accounts = vec![ + ( + Pubkey::new_unique(), + load_program_account_from_elf( + authority_address, + LoaderV4Status::Retracted, + "sbpfv3_return_err", + ), + ), + ( + authority_address, + AccountSharedData::new(0, 0, &Pubkey::new_unique()), + ), + ( + Pubkey::new_unique(), + load_program_account_from_elf( + authority_address, + LoaderV4Status::Deployed, + "sbpfv3_return_err", + ), + ), + ( + clock::id(), + create_account_shared_data_for_test(&clock::Clock::default()), + ), + ( + rent::id(), + create_account_shared_data_for_test(&rent::Rent::default()), + ), + ]; + + // Overwrite existing data + process_instruction( + vec![], + &bincode::serialize(&LoaderV4Instruction::Copy { + destination_offset: 1, + source_offset: 2, + length: 3, + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, false, false)], + Ok(()), + ); + + // Empty copy + process_instruction( + vec![], + &bincode::serialize(&LoaderV4Instruction::Copy { + destination_offset: 1, + source_offset: 2, + length: 0, + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, false, false)], + Ok(()), + ); + + // Error: Program is not retracted + process_instruction( + vec![], + &bincode::serialize(&LoaderV4Instruction::Copy { + destination_offset: 1, + source_offset: 2, + length: 3, + }) + .unwrap(), + transaction_accounts.clone(), + &[(2, false, true), (1, true, false), (0, false, false)], + Err(InstructionError::InvalidArgument), + ); + + // Error: Destination and source collide + process_instruction( + vec![], + &bincode::serialize(&LoaderV4Instruction::Copy { + destination_offset: 1, + source_offset: 2, + length: 3, + }) + .unwrap(), + transaction_accounts.clone(), + &[(2, false, true), (1, true, false), (2, false, false)], + Err(InstructionError::AccountBorrowFailed), + ); + + // Error: Read out of bounds + process_instruction( + vec![], + &bincode::serialize(&LoaderV4Instruction::Copy { + destination_offset: 1, + source_offset: transaction_accounts[2] + .1 + .data() + .len() + .saturating_sub(LoaderV4State::program_data_offset()) + .saturating_sub(3) as u32, + length: 4, + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, false, false)], + Err(InstructionError::AccountDataTooSmall), + ); + + // Error: Write out of bounds + process_instruction( + vec![], + &bincode::serialize(&LoaderV4Instruction::Copy { + destination_offset: transaction_accounts[0] + .1 + .data() + .len() + .saturating_sub(LoaderV4State::program_data_offset()) + .saturating_sub(3) as u32, + source_offset: 2, + length: 4, + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, false, false)], + Err(InstructionError::AccountDataTooSmall), + ); + + test_loader_instruction_general_errors(LoaderV4Instruction::Copy { + destination_offset: 1, + source_offset: 2, + length: 3, + }); + } + #[test] fn test_loader_instruction_truncate() { let authority_address = Pubkey::new_unique(); diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 7c00e8cc0eb..85d16abe950 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6302,6 +6302,7 @@ dependencies = [ "solana-bpf-loader-program", "solana-compute-budget", "solana-instruction", + "solana-loader-v3-interface", "solana-loader-v4-interface", "solana-log-collector", "solana-measure", diff --git a/sdk/loader-v4-interface/src/instruction.rs b/sdk/loader-v4-interface/src/instruction.rs index b3d54d766b5..c63d639fefb 100644 --- a/sdk/loader-v4-interface/src/instruction.rs +++ b/sdk/loader-v4-interface/src/instruction.rs @@ -26,6 +26,21 @@ pub enum LoaderV4Instruction { bytes: Vec, }, + /// Copy ELF data into an undeployed program account. + /// + /// # Account references + /// 0. `[writable]` The program account to write to. + /// 1. `[signer]` The authority of the program. + /// 2. `[]` The program account to copy from. + Copy { + /// Offset at which to write. + destination_offset: u32, + /// Offset at which to read. + source_offset: u32, + /// Amount of bytes to copy. + length: u32, + }, + /// Changes the size of an undeployed program account. /// /// A program account is automatically initialized when its size is first increased. @@ -90,26 +105,30 @@ pub fn is_write_instruction(instruction_data: &[u8]) -> bool { !instruction_data.is_empty() && 0 == instruction_data[0] } -pub fn is_truncate_instruction(instruction_data: &[u8]) -> bool { +pub fn is_copy_instruction(instruction_data: &[u8]) -> bool { !instruction_data.is_empty() && 1 == instruction_data[0] } -pub fn is_deploy_instruction(instruction_data: &[u8]) -> bool { +pub fn is_truncate_instruction(instruction_data: &[u8]) -> bool { !instruction_data.is_empty() && 2 == instruction_data[0] } -pub fn is_retract_instruction(instruction_data: &[u8]) -> bool { +pub fn is_deploy_instruction(instruction_data: &[u8]) -> bool { !instruction_data.is_empty() && 3 == instruction_data[0] } -pub fn is_transfer_authority_instruction(instruction_data: &[u8]) -> bool { +pub fn is_retract_instruction(instruction_data: &[u8]) -> bool { !instruction_data.is_empty() && 4 == instruction_data[0] } -pub fn is_finalize_instruction(instruction_data: &[u8]) -> bool { +pub fn is_transfer_authority_instruction(instruction_data: &[u8]) -> bool { !instruction_data.is_empty() && 5 == instruction_data[0] } +pub fn is_finalize_instruction(instruction_data: &[u8]) -> bool { + !instruction_data.is_empty() && 6 == instruction_data[0] +} + /// Returns the instructions required to initialize a program/buffer account. #[cfg(feature = "bincode")] pub fn create_buffer( @@ -190,6 +209,31 @@ pub fn write( ) } +/// Returns the instructions required to copy a chunk of program data. +#[cfg(feature = "bincode")] +pub fn copy( + program_address: &Pubkey, + authority: &Pubkey, + source_address: &Pubkey, + destination_offset: u32, + source_offset: u32, + length: u32, +) -> Instruction { + Instruction::new_with_bincode( + id(), + &LoaderV4Instruction::Copy { + destination_offset, + source_offset, + length, + }, + vec![ + AccountMeta::new(*program_address, false), + AccountMeta::new_readonly(*authority, true), + AccountMeta::new_readonly(*source_address, false), + ], + ) +} + /// Returns the instructions required to deploy a program. #[cfg(feature = "bincode")] pub fn deploy(program_address: &Pubkey, authority: &Pubkey) -> Instruction { @@ -319,6 +363,26 @@ mod tests { assert!(instruction.accounts[1].is_signer); } + #[test] + fn test_copy_instruction() { + let program = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let source = Pubkey::new_unique(); + let instruction = copy(&program, &authority, &source, 1, 2, 3); + assert!(is_copy_instruction(&instruction.data)); + assert_eq!(instruction.program_id, id()); + assert_eq!(instruction.accounts.len(), 3); + assert_eq!(instruction.accounts[0].pubkey, program); + assert!(instruction.accounts[0].is_writable); + assert!(!instruction.accounts[0].is_signer); + assert_eq!(instruction.accounts[1].pubkey, authority); + assert!(!instruction.accounts[1].is_writable); + assert!(instruction.accounts[1].is_signer); + assert_eq!(instruction.accounts[2].pubkey, source); + assert!(!instruction.accounts[2].is_writable); + assert!(!instruction.accounts[2].is_signer); + } + #[test] fn test_truncate_instruction() { let program = Pubkey::new_unique(); diff --git a/svm/examples/Cargo.lock b/svm/examples/Cargo.lock index 8ccb91329f5..e233910c155 100644 --- a/svm/examples/Cargo.lock +++ b/svm/examples/Cargo.lock @@ -6132,6 +6132,7 @@ dependencies = [ "solana-bpf-loader-program", "solana-compute-budget", "solana-instruction", + "solana-loader-v3-interface", "solana-loader-v4-interface", "solana-log-collector", "solana-measure",