From 0c36e4c82db4fc3bcc86e251d07c29ba24f6eb1d Mon Sep 17 00:00:00 2001 From: Brooks Date: Thu, 16 Feb 2023 08:16:25 -0500 Subject: [PATCH] Adds stable layout types to pass to the runtime (#30192) --- Cargo.lock | 1 + program-runtime/src/invoke_context.rs | 14 +- program-test/src/lib.rs | 4 +- .../address-lookup-table/src/processor.rs | 8 +- programs/bpf_loader/Cargo.toml | 3 + programs/bpf_loader/src/lib.rs | 5 +- programs/bpf_loader/src/syscalls/cpi.rs | 28 +-- programs/bpf_loader/src/syscalls/mod.rs | 11 +- sdk/program/src/lib.rs | 1 + sdk/program/src/program.rs | 11 +- sdk/program/src/stable_layout.rs | 10 + .../src/stable_layout/stable_instruction.rs | 96 ++++++++++ sdk/program/src/stable_layout/stable_rc.rs | 29 +++ .../src/stable_layout/stable_ref_cell.rs | 25 +++ sdk/program/src/stable_layout/stable_slice.rs | 27 +++ sdk/program/src/stable_layout/stable_vec.rs | 181 ++++++++++++++++++ sdk/src/lib.rs | 4 +- 17 files changed, 423 insertions(+), 35 deletions(-) create mode 100644 sdk/program/src/stable_layout.rs create mode 100644 sdk/program/src/stable_layout/stable_instruction.rs create mode 100644 sdk/program/src/stable_layout/stable_rc.rs create mode 100644 sdk/program/src/stable_layout/stable_ref_cell.rs create mode 100644 sdk/program/src/stable_layout/stable_slice.rs create mode 100644 sdk/program/src/stable_layout/stable_vec.rs diff --git a/Cargo.lock b/Cargo.lock index d69dc9208..9b5dbc491 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4954,6 +4954,7 @@ dependencies = [ "byteorder", "libsecp256k1", "log", + "memoffset 0.8.0", "rand 0.7.3", "solana-measure", "solana-program-runtime", diff --git a/program-runtime/src/invoke_context.rs b/program-runtime/src/invoke_context.rs index d3674bbfe..5e74f0eaf 100644 --- a/program-runtime/src/invoke_context.rs +++ b/program-runtime/src/invoke_context.rs @@ -17,11 +17,12 @@ use { bpf_loader_upgradeable::{self, UpgradeableLoaderState}, feature_set::{enable_early_verification_of_account_modifications, FeatureSet}, hash::Hash, - instruction::{AccountMeta, Instruction, InstructionError}, + instruction::{AccountMeta, InstructionError}, native_loader, pubkey::Pubkey, rent::Rent, saturating_add_assign, + stable_layout::stable_instruction::StableInstruction, transaction_context::{ IndexOfAccount, InstructionAccount, TransactionAccount, TransactionContext, }, @@ -489,7 +490,7 @@ impl<'a> InvokeContext<'a> { /// Entrypoint for a cross-program invocation from a builtin program pub fn native_invoke( &mut self, - instruction: Instruction, + instruction: StableInstruction, signers: &[Pubkey], ) -> Result<(), InstructionError> { let (instruction_accounts, program_indices) = @@ -509,7 +510,7 @@ impl<'a> InvokeContext<'a> { #[allow(clippy::type_complexity)] pub fn prepare_instruction( &mut self, - instruction: &Instruction, + instruction: &StableInstruction, signers: &[Pubkey], ) -> Result<(Vec, Vec), InstructionError> { // Finds the index of each account in the instruction by its pubkey. @@ -1036,7 +1037,7 @@ mod tests { super::*, crate::compute_budget, serde::{Deserialize, Serialize}, - solana_sdk::account::WritableAccount, + solana_sdk::{account::WritableAccount, instruction::Instruction}, }; #[derive(Debug, Serialize, Deserialize)] @@ -1161,7 +1162,7 @@ mod tests { assert_eq!(result, Err(InstructionError::UnbalancedInstruction)); result?; invoke_context - .native_invoke(inner_instruction, &[]) + .native_invoke(inner_instruction.into(), &[]) .and(invoke_context.pop())?; } MockInstruction::UnbalancedPop => instruction_context @@ -1336,7 +1337,7 @@ mod tests { let inner_instruction = Instruction::new_with_bincode(callee_program_id, &case.0, metas.clone()); let result = invoke_context - .native_invoke(inner_instruction, &[]) + .native_invoke(inner_instruction.into(), &[]) .and(invoke_context.pop()); assert_eq!(result, case.1); } @@ -1359,6 +1360,7 @@ mod tests { }, metas.clone(), ); + let inner_instruction = StableInstruction::from(inner_instruction); let (inner_instruction_accounts, program_indices) = invoke_context .prepare_instruction(&inner_instruction, &[]) .unwrap(); diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs index 105cba8a4..99c32decb 100644 --- a/program-test/src/lib.rs +++ b/program-test/src/lib.rs @@ -40,6 +40,7 @@ use { pubkey::Pubkey, rent::Rent, signature::{Keypair, Signer}, + stable_layout::stable_instruction::StableInstruction, sysvar::{Sysvar, SysvarId}, }, solana_vote_program::vote_state::{self, VoteState, VoteStateVersions}, @@ -227,6 +228,7 @@ impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs { account_infos: &[AccountInfo], signers_seeds: &[&[&[u8]]], ) -> ProgramResult { + let instruction = StableInstruction::from(instruction.clone()); let invoke_context = get_invoke_context(); let log_collector = invoke_context.get_log_collector(); let transaction_context = &invoke_context.transaction_context; @@ -249,7 +251,7 @@ impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs { .collect::>(); let (instruction_accounts, program_indices) = invoke_context - .prepare_instruction(instruction, &signers) + .prepare_instruction(&instruction, &signers) .unwrap(); // Copy caller's account_info modifications into invoke_context accounts diff --git a/programs/address-lookup-table/src/processor.rs b/programs/address-lookup-table/src/processor.rs index 3aeb2c37d..221c3d2b3 100644 --- a/programs/address-lookup-table/src/processor.rs +++ b/programs/address-lookup-table/src/processor.rs @@ -144,18 +144,18 @@ impl Processor { if required_lamports > 0 { invoke_context.native_invoke( - system_instruction::transfer(&payer_key, &table_key, required_lamports), + system_instruction::transfer(&payer_key, &table_key, required_lamports).into(), &[payer_key], )?; } invoke_context.native_invoke( - system_instruction::allocate(&table_key, table_account_data_len as u64), + system_instruction::allocate(&table_key, table_account_data_len as u64).into(), &[table_key], )?; invoke_context.native_invoke( - system_instruction::assign(&table_key, &crate::id()), + system_instruction::assign(&table_key, &crate::id()).into(), &[table_key], )?; @@ -332,7 +332,7 @@ impl Processor { drop(payer_account); invoke_context.native_invoke( - system_instruction::transfer(&payer_key, &table_key, required_lamports), + system_instruction::transfer(&payer_key, &table_key, required_lamports).into(), &[payer_key], )?; } diff --git a/programs/bpf_loader/Cargo.toml b/programs/bpf_loader/Cargo.toml index 029feab40..60f5a43ae 100644 --- a/programs/bpf_loader/Cargo.toml +++ b/programs/bpf_loader/Cargo.toml @@ -22,6 +22,9 @@ solana-zk-token-sdk = { path = "../../zk-token-sdk", version = "=1.16.0" } solana_rbpf = "=0.2.38" thiserror = "1.0" +[dev-dependencies] +memoffset = "0.8" + [lib] crate-type = ["lib"] name = "solana_bpf_loader_program" diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index 70a484990..19a4a9f0f 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -736,7 +736,7 @@ fn process_loader_upgradeable_instruction( .iter() .map(|seeds| Pubkey::create_program_address(seeds, caller_program_id)) .collect::, solana_sdk::pubkey::PubkeyError>>()?; - invoke_context.native_invoke(instruction, signers.as_slice())?; + invoke_context.native_invoke(instruction.into(), signers.as_slice())?; // Load and verify the program bits let transaction_context = &invoke_context.transaction_context; @@ -1360,7 +1360,8 @@ fn process_loader_upgradeable_instruction( )?; invoke_context.native_invoke( - system_instruction::transfer(&payer_key, &programdata_key, required_payment), + system_instruction::transfer(&payer_key, &programdata_key, required_payment) + .into(), &[], )?; } diff --git a/programs/bpf_loader/src/syscalls/cpi.rs b/programs/bpf_loader/src/syscalls/cpi.rs index a8baa3833..4e99f30a0 100644 --- a/programs/bpf_loader/src/syscalls/cpi.rs +++ b/programs/bpf_loader/src/syscalls/cpi.rs @@ -3,6 +3,7 @@ use { crate::declare_syscall, solana_sdk::{ feature_set::enable_bpf_loader_set_authority_checked_ix, + stable_layout::stable_instruction::StableInstruction, syscalls::{ MAX_CPI_ACCOUNT_INFOS, MAX_CPI_INSTRUCTION_ACCOUNTS, MAX_CPI_INSTRUCTION_DATA_LEN, }, @@ -211,7 +212,7 @@ trait SyscallInvokeSigned { addr: u64, memory_mapping: &mut MemoryMapping, invoke_context: &mut InvokeContext, - ) -> Result; + ) -> Result; fn translate_accounts<'a>( instruction_accounts: &[InstructionAccount], program_indices: &[IndexOfAccount], @@ -258,8 +259,8 @@ impl SyscallInvokeSigned for SyscallInvokeSignedRust { addr: u64, memory_mapping: &mut MemoryMapping, invoke_context: &mut InvokeContext, - ) -> Result { - let ix = translate_type::( + ) -> Result { + let ix = translate_type::( memory_mapping, addr, invoke_context.get_check_aligned(), @@ -296,10 +297,11 @@ impl SyscallInvokeSigned for SyscallInvokeSignedRust { invoke_context.get_check_size(), )? .to_vec(); - Ok(Instruction { + + Ok(StableInstruction { + accounts: accounts.into(), + data: data.into(), program_id: ix.program_id, - accounts, - data, }) } @@ -469,7 +471,7 @@ impl SyscallInvokeSigned for SyscallInvokeSignedC { addr: u64, memory_mapping: &mut MemoryMapping, invoke_context: &mut InvokeContext, - ) -> Result { + ) -> Result { let ix_c = translate_type::( memory_mapping, addr, @@ -530,10 +532,10 @@ impl SyscallInvokeSigned for SyscallInvokeSignedC { }) .collect::, EbpfError>>()?; - Ok(Instruction { + Ok(StableInstruction { + accounts: accounts.into(), + data: data.into(), program_id: *program_id, - accounts, - data, }) } @@ -1128,6 +1130,7 @@ mod tests { solana_sdk::{ account::{Account, AccountSharedData}, clock::Epoch, + instruction::Instruction, rent::Rent, transaction_context::{TransactionAccount, TransactionContext}, }, @@ -1693,12 +1696,12 @@ mod tests { fn into_region(self, vm_addr: u64) -> (Vec, MemoryRegion) { let accounts_len = mem::size_of::() * self.accounts.len(); - let size = mem::size_of::() + accounts_len + self.data.len(); + let size = mem::size_of::() + accounts_len + self.data.len(); let mut data = vec![0; size]; let vm_addr = vm_addr as usize; - let accounts_addr = vm_addr + mem::size_of::(); + let accounts_addr = vm_addr + mem::size_of::(); let data_addr = accounts_addr + accounts_len; let ins = Instruction { @@ -1714,6 +1717,7 @@ mod tests { Vec::from_raw_parts(data_addr as *mut _, self.data.len(), self.data.len()) }, }; + let ins = StableInstruction::from(ins); unsafe { ptr::write_unaligned(data.as_mut_ptr().cast(), ins); diff --git a/programs/bpf_loader/src/syscalls/mod.rs b/programs/bpf_loader/src/syscalls/mod.rs index c16308979..7b58f2c17 100644 --- a/programs/bpf_loader/src/syscalls/mod.rs +++ b/programs/bpf_loader/src/syscalls/mod.rs @@ -44,7 +44,7 @@ use { }, hash::{Hasher, HASH_BYTES}, instruction::{ - AccountMeta, Instruction, InstructionError, ProcessedSiblingInstruction, + AccountMeta, InstructionError, ProcessedSiblingInstruction, TRANSACTION_LEVEL_STACK_HEIGHT, }, keccak, native_loader, @@ -1821,7 +1821,9 @@ mod tests { bpf_loader, fee_calculator::FeeCalculator, hash::hashv, + instruction::Instruction, program::check_type_assumptions, + stable_layout::stable_instruction::StableInstruction, sysvar::{clock::Clock, epoch_schedule::EpochSchedule, rent::Rent}, transaction_context::TransactionContext, }, @@ -1936,17 +1938,18 @@ mod tests { &"foobar", vec![AccountMeta::new(solana_sdk::pubkey::new_rand(), false)], ); + let instruction = StableInstruction::from(instruction); let addr = &instruction as *const _ as u64; let mut memory_region = MemoryRegion { host_addr: addr, vm_addr: 0x100000000, - len: std::mem::size_of::() as u64, + len: std::mem::size_of::() as u64, vm_gap_shift: 63, is_writable: false, }; let mut memory_mapping = MemoryMapping::new(vec![memory_region.clone()], &config).unwrap(); let translated_instruction = - translate_type::(&memory_mapping, 0x100000000, true).unwrap(); + translate_type::(&memory_mapping, 0x100000000, true).unwrap(); assert_eq!(instruction, *translated_instruction); memory_region.len = 1; let memory_region_index = memory_mapping @@ -1957,7 +1960,7 @@ mod tests { memory_mapping .replace_region(memory_region_index, memory_region) .unwrap(); - assert!(translate_type::(&memory_mapping, 0x100000000, true).is_err()); + assert!(translate_type::(&memory_mapping, 0x100000000, true).is_err()); } #[test] diff --git a/sdk/program/src/lib.rs b/sdk/program/src/lib.rs index dab056c29..bff9179fb 100644 --- a/sdk/program/src/lib.rs +++ b/sdk/program/src/lib.rs @@ -521,6 +521,7 @@ pub mod serialize_utils; pub mod short_vec; pub mod slot_hashes; pub mod slot_history; +pub mod stable_layout; pub mod stake; pub mod stake_history; pub mod syscalls; diff --git a/sdk/program/src/program.rs b/sdk/program/src/program.rs index eb6fd3fb1..a41f7b526 100644 --- a/sdk/program/src/program.rs +++ b/sdk/program/src/program.rs @@ -10,6 +10,7 @@ use crate::{ account_info::AccountInfo, entrypoint::ProgramResult, instruction::Instruction, pubkey::Pubkey, + stable_layout::stable_instruction::StableInstruction, }; /// Invoke a cross-program instruction. @@ -292,9 +293,10 @@ pub fn invoke_signed_unchecked( ) -> ProgramResult { #[cfg(target_os = "solana")] { + let instruction = StableInstruction::from(instruction.clone()); let result = unsafe { crate::syscalls::sol_invoke_signed_rust( - instruction as *const _ as *const u8, + &instruction as *const _ as *const u8, account_infos as *const _ as *const u8, account_infos.len() as u64, signers_seeds as *const _ as *const u8, @@ -432,17 +434,18 @@ pub fn check_type_assumptions() { accounts: vec![account_meta1.clone(), account_meta2.clone()], data: data.clone(), }; + let instruction = StableInstruction::from(instruction); let instruction_addr = &instruction as *const _ as u64; // program id - assert_eq!(offset_of!(Instruction, program_id), 48); + assert_eq!(offset_of!(StableInstruction, program_id), 48); let pubkey_ptr = (instruction_addr + 48) as *const Pubkey; unsafe { assert_eq!(*pubkey_ptr, pubkey1); } // accounts - assert_eq!(offset_of!(Instruction, accounts), 0); + assert_eq!(offset_of!(StableInstruction, accounts), 0); let accounts_ptr = (instruction_addr) as *const *const AccountMeta; let accounts_cap = (instruction_addr + 8) as *const usize; let accounts_len = (instruction_addr + 16) as *const usize; @@ -455,7 +458,7 @@ pub fn check_type_assumptions() { } // data - assert_eq!(offset_of!(Instruction, data), 24); + assert_eq!(offset_of!(StableInstruction, data), 24); let data_ptr = (instruction_addr + 24) as *const *const [u8; 5]; let data_cap = (instruction_addr + 24 + 8) as *const usize; let data_len = (instruction_addr + 24 + 16) as *const usize; diff --git a/sdk/program/src/stable_layout.rs b/sdk/program/src/stable_layout.rs new file mode 100644 index 000000000..37d3d8bf2 --- /dev/null +++ b/sdk/program/src/stable_layout.rs @@ -0,0 +1,10 @@ +#![doc(hidden)] +//! Types with stable memory layouts +//! +//! Internal use only; here be dragons! + +pub mod stable_instruction; +pub mod stable_rc; +pub mod stable_ref_cell; +pub mod stable_slice; +pub mod stable_vec; diff --git a/sdk/program/src/stable_layout/stable_instruction.rs b/sdk/program/src/stable_layout/stable_instruction.rs new file mode 100644 index 000000000..e01bd3747 --- /dev/null +++ b/sdk/program/src/stable_layout/stable_instruction.rs @@ -0,0 +1,96 @@ +//! `Instruction`, with a stable memory layout + +use { + crate::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + stable_layout::stable_vec::StableVec, + }, + std::fmt::Debug, +}; + +/// `Instruction`, with a stable memory layout +/// +/// This is used within the runtime to ensure memory mapping and memory accesses are valid. We +/// rely on known addresses and offsets within the runtime, and since `Instruction`'s layout is +/// allowed to change, we must provide a way to lock down the memory layout. `StableInstruction` +/// reimplements the bare minimum of `Instruction`'s API sufficient only for the runtime's needs. +/// +/// # Examples +/// +/// Creating a `StableInstruction` from an `Instruction` +/// +/// ``` +/// # use solana_program::{instruction::Instruction, pubkey::Pubkey, stable_layout::stable_instruction::StableInstruction}; +/// # let program_id = Pubkey::default(); +/// # let accounts = Vec::default(); +/// # let data = Vec::default(); +/// let instruction = Instruction { program_id, accounts, data }; +/// let instruction = StableInstruction::from(instruction); +/// ``` +#[derive(Debug, PartialEq)] +#[repr(C)] +pub struct StableInstruction { + pub accounts: StableVec, + pub data: StableVec, + pub program_id: Pubkey, +} + +impl From for StableInstruction { + fn from(other: Instruction) -> Self { + Self { + accounts: other.accounts.into(), + data: other.data.into(), + program_id: other.program_id, + } + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + memoffset::offset_of, + std::mem::{align_of, size_of}, + }; + + #[test] + fn test_memory_layout() { + assert_eq!(offset_of!(StableInstruction, accounts), 0); + assert_eq!(offset_of!(StableInstruction, data), 24); + assert_eq!(offset_of!(StableInstruction, program_id), 48); + assert_eq!(align_of::(), 8); + assert_eq!(size_of::(), 24 + 24 + 32); + + let program_id = Pubkey::new_unique(); + let account_meta1 = AccountMeta { + pubkey: Pubkey::new_unique(), + is_signer: true, + is_writable: false, + }; + let account_meta2 = AccountMeta { + pubkey: Pubkey::new_unique(), + is_signer: false, + is_writable: true, + }; + let accounts = vec![account_meta1, account_meta2]; + let data = vec![1, 2, 3, 4, 5]; + let instruction = Instruction { + program_id, + accounts: accounts.clone(), + data: data.clone(), + }; + let instruction = StableInstruction::from(instruction); + + let instruction_addr = &instruction as *const _ as u64; + + let accounts_ptr = instruction_addr as *const StableVec; + assert_eq!(unsafe { &*accounts_ptr }, &accounts); + + let data_ptr = (instruction_addr + 24) as *const StableVec; + assert_eq!(unsafe { &*data_ptr }, &data); + + let pubkey_ptr = (instruction_addr + 48) as *const Pubkey; + assert_eq!(unsafe { *pubkey_ptr }, program_id); + } +} diff --git a/sdk/program/src/stable_layout/stable_rc.rs b/sdk/program/src/stable_layout/stable_rc.rs new file mode 100644 index 000000000..5865eda3e --- /dev/null +++ b/sdk/program/src/stable_layout/stable_rc.rs @@ -0,0 +1,29 @@ +//! Ensure Rc has a stable memory layout + +#[cfg(test)] +mod tests { + use std::{ + mem::{align_of, size_of}, + rc::Rc, + }; + + #[test] + fn test_memory_layout() { + assert_eq!(align_of::>(), 8); + assert_eq!(size_of::>(), 8); + + let value = 42; + let rc = Rc::new(value); + let _rc2 = Rc::clone(&rc); // used to increment strong count + + let addr_rc = &rc as *const _ as usize; + let addr_ptr = addr_rc; + let addr_rcbox = unsafe { *(addr_ptr as *const *const i32) } as usize; + let addr_strong = addr_rcbox; + let addr_weak = addr_rcbox + 8; + let addr_value = addr_rcbox + 16; + assert_eq!(unsafe { *(addr_strong as *const usize) }, 2); + assert_eq!(unsafe { *(addr_weak as *const usize) }, 1); + assert_eq!(unsafe { *(addr_value as *const i32) }, 42); + } +} diff --git a/sdk/program/src/stable_layout/stable_ref_cell.rs b/sdk/program/src/stable_layout/stable_ref_cell.rs new file mode 100644 index 000000000..c5741acac --- /dev/null +++ b/sdk/program/src/stable_layout/stable_ref_cell.rs @@ -0,0 +1,25 @@ +//! Ensure RefCell has a stable layout + +#[cfg(test)] +mod tests { + use std::{ + cell::RefCell, + mem::{align_of, size_of}, + }; + + #[test] + fn test_memory_layout() { + assert_eq!(align_of::>(), 8); + assert_eq!(size_of::>(), 8 + 4 + /* padding */4); + + let value = 42; + let refcell = RefCell::new(value); + let _borrow = refcell.borrow(); // used to increment borrow count + + let addr_refcell = &refcell as *const _ as usize; + let addr_borrow = addr_refcell; + let addr_value = addr_refcell + 8; + assert_eq!(unsafe { *(addr_borrow as *const isize) }, 1); + assert_eq!(unsafe { *(addr_value as *const i32) }, 42); + } +} diff --git a/sdk/program/src/stable_layout/stable_slice.rs b/sdk/program/src/stable_layout/stable_slice.rs new file mode 100644 index 000000000..55b9dca15 --- /dev/null +++ b/sdk/program/src/stable_layout/stable_slice.rs @@ -0,0 +1,27 @@ +//! Ensure slice has a stable memory layout + +#[cfg(test)] +mod tests { + use std::mem::{align_of, size_of}; + + #[test] + fn test_memory_layout() { + assert_eq!(align_of::<&[i32]>(), 8); + assert_eq!(size_of::<&[i32]>(), /*ptr*/ 8 + /*len*/8); + + let array = [11, 22, 33, 44, 55]; + let slice = array.as_slice(); + + let addr_slice = &slice as *const _ as usize; + let addr_ptr = addr_slice; + let addr_len = addr_slice + 8; + assert_eq!(unsafe { *(addr_len as *const usize) }, 5); + + let ptr_data = addr_ptr as *const *const i32; + assert_eq!(unsafe { *((*ptr_data).offset(0)) }, 11); + assert_eq!(unsafe { *((*ptr_data).offset(1)) }, 22); + assert_eq!(unsafe { *((*ptr_data).offset(2)) }, 33); + assert_eq!(unsafe { *((*ptr_data).offset(3)) }, 44); + assert_eq!(unsafe { *((*ptr_data).offset(4)) }, 55); + } +} diff --git a/sdk/program/src/stable_layout/stable_vec.rs b/sdk/program/src/stable_layout/stable_vec.rs new file mode 100644 index 000000000..532252085 --- /dev/null +++ b/sdk/program/src/stable_layout/stable_vec.rs @@ -0,0 +1,181 @@ +//! `Vec`, with a stable memory layout + +use std::{marker::PhantomData, mem::ManuallyDrop, ptr::NonNull}; + +/// `Vec`, with a stable memory layout +/// +/// This container is used within the runtime to ensure memory mapping and memory accesses are +/// valid. We rely on known addresses and offsets within the runtime, and since `Vec`'s layout +/// is allowed to change, we must provide a way to lock down the memory layout. `StableVec` +/// reimplements the bare minimum of `Vec`'s API sufficient only for the runtime's needs. +/// +/// To ensure memory allocation and deallocation is handled correctly, it is only possible to +/// create a new `StableVec` from an existing `Vec`. This way we ensure all Rust invariants are +/// upheld. +/// +/// # Examples +/// +/// Creating a `StableVec` from a `Vec` +/// +/// ``` +/// # use solana_program::stable_layout::stable_vec::StableVec; +/// let vec = vec!["meow", "woof", "moo"]; +/// let vec = StableVec::from(vec); +/// ``` +#[repr(C)] +pub struct StableVec { + pub ptr: NonNull, + pub cap: usize, + pub len: usize, + _marker: PhantomData, +} + +impl StableVec { + #[inline] + pub fn as_ptr(&self) -> *const T { + // We shadow the slice method of the same name to avoid going through + // `deref`, which creates an intermediate reference. + self.ptr.as_ptr() + } + + #[inline] + pub fn as_mut_ptr(&mut self) -> *mut T { + // We shadow the slice method of the same name to avoid going through + // `deref_mut`, which creates an intermediate reference. + self.ptr.as_ptr() + } +} + +impl AsRef<[T]> for StableVec { + fn as_ref(&self) -> &[T] { + self + } +} + +impl AsMut<[T]> for StableVec { + fn as_mut(&mut self) -> &mut [T] { + self + } +} + +impl std::ops::Deref for StableVec { + type Target = [T]; + + #[inline] + fn deref(&self) -> &[T] { + unsafe { core::slice::from_raw_parts(self.as_ptr(), self.len) } + } +} + +impl std::ops::DerefMut for StableVec { + #[inline] + fn deref_mut(&mut self) -> &mut [T] { + unsafe { core::slice::from_raw_parts_mut(self.as_mut_ptr(), self.len) } + } +} + +impl std::fmt::Debug for StableVec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&**self, f) + } +} + +macro_rules! impl_partial_eq { + ([$($vars:tt)*] $lhs:ty, $rhs:ty) => { + impl PartialEq<$rhs> for $lhs + where + T: PartialEq, + { + #[inline] + fn eq(&self, other: &$rhs) -> bool { self[..] == other[..] } + } + } +} +impl_partial_eq! { [] StableVec, StableVec } +impl_partial_eq! { [] StableVec, Vec } +impl_partial_eq! { [] Vec, StableVec } +impl_partial_eq! { [] StableVec, &[U] } +impl_partial_eq! { [] StableVec, &mut [U] } +impl_partial_eq! { [] &[T], StableVec } +impl_partial_eq! { [] &mut [T], StableVec } +impl_partial_eq! { [] StableVec, [U] } +impl_partial_eq! { [] [T], StableVec } +impl_partial_eq! { [const N: usize] StableVec, [U; N] } +impl_partial_eq! { [const N: usize] StableVec, &[U; N] } + +impl From> for StableVec { + fn from(other: Vec) -> Self { + // NOTE: This impl is basically copied from `Vec::into_raw_parts()`. Once that fn is + // stabilized, use it here. + // + // We are going to pilfer `other`'s guts, and we don't want it to be dropped when it goes + // out of scope. + let mut other = ManuallyDrop::new(other); + Self { + // SAFETY: We have a valid Vec, so its ptr is non-null. + ptr: unsafe { NonNull::new_unchecked(other.as_mut_ptr()) }, + cap: other.capacity(), + len: other.len(), + _marker: PhantomData, + } + } +} + +impl From> for Vec { + fn from(other: StableVec) -> Self { + // We are going to pilfer `other`'s guts, and we don't want it to be dropped when it goes + // out of scope. + let other = ManuallyDrop::new(other); + // SAFETY: We have a valid StableVec, which we can only get from a Vec. Therefore it is + // safe to convert back to Vec. + unsafe { Vec::from_raw_parts(other.ptr.as_ptr(), other.len, other.cap) } + } +} + +impl Drop for StableVec { + fn drop(&mut self) { + // We only allow creating a StableVec through creating a Vec. To ensure we are dropped + // correctly, convert ourselves back to a Vec and let Vec's drop handling take over. + // + // SAFETY: We have a valid StableVec, which we can only get from a Vec. Therefore it is + // safe to convert back to Vec. + let _vec = unsafe { Vec::from_raw_parts(self.ptr.as_ptr(), self.len, self.cap) }; + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + memoffset::offset_of, + std::mem::{align_of, size_of}, + }; + + #[test] + fn test_memory_layout() { + assert_eq!(offset_of!(StableVec, ptr), 0); + assert_eq!(offset_of!(StableVec, cap), 8); + assert_eq!(offset_of!(StableVec, len), 16); + assert_eq!(align_of::>(), 8); + assert_eq!(size_of::>(), 8 + 8 + 8); + + // create a vec with different values for cap and len + let vec = { + let mut vec = Vec::with_capacity(3); + vec.push(11); + vec.push(22); + vec + }; + let vec = StableVec::from(vec); + + let addr_vec = &vec as *const _ as usize; + let addr_ptr = addr_vec; + let addr_cap = addr_vec + 8; + let addr_len = addr_vec + 16; + assert_eq!(unsafe { *(addr_cap as *const usize) }, 3); + assert_eq!(unsafe { *(addr_len as *const usize) }, 2); + + let ptr_data = addr_ptr as *const &[i32; 2]; + assert_eq!(unsafe { *ptr_data }, &[11, 22]); + } +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index f6c312664..d60881cac 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -50,8 +50,8 @@ pub use solana_program::{ instruction, keccak, lamports, loader_instruction, loader_upgradeable_instruction, message, msg, native_token, nonce, program, program_error, program_memory, program_option, program_pack, rent, sanitize, sdk_ids, secp256k1_program, secp256k1_recover, serde_varint, serialize_utils, - short_vec, slot_hashes, slot_history, stake, stake_history, syscalls, system_instruction, - system_program, sysvar, unchecked_div_by_const, vote, wasm_bindgen, + short_vec, slot_hashes, slot_history, stable_layout, stake, stake_history, syscalls, + system_instruction, system_program, sysvar, unchecked_div_by_const, vote, wasm_bindgen, }; pub mod account;