From 3ed5b92759911b4522842303affef29db65e77a4 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 15 May 2026 21:11:47 +0400 Subject: [PATCH] feat: add program runtime crate --- Cargo.toml | 50 +- solana/program-runtime/Cargo.toml | 96 + solana/program-runtime/README.md | 17 + .../program-runtime/fixtures/noop_aligned.so | Bin 0 -> 2024 bytes solana/program-runtime/src/cpi.rs | 2101 +++++++++++++++++ solana/program-runtime/src/deploy.rs | 113 + .../program-runtime/src/execution_budget.rs | 342 +++ solana/program-runtime/src/invoke_context.rs | 1909 +++++++++++++++ solana/program-runtime/src/lib.rs | 31 + solana/program-runtime/src/loaded_programs.rs | 338 +++ solana/program-runtime/src/mem_pool.rs | 150 ++ solana/program-runtime/src/memory.rs | 136 ++ solana/program-runtime/src/memory_context.rs | 87 + solana/program-runtime/src/serialization.rs | 1698 +++++++++++++ solana/program-runtime/src/stable_log.rs | 110 + solana/program-runtime/src/sysvar_cache.rs | 288 +++ solana/program-runtime/src/vm.rs | 391 +++ 17 files changed, 7851 insertions(+), 6 deletions(-) create mode 100644 solana/program-runtime/Cargo.toml create mode 100644 solana/program-runtime/README.md create mode 100644 solana/program-runtime/fixtures/noop_aligned.so create mode 100644 solana/program-runtime/src/cpi.rs create mode 100644 solana/program-runtime/src/deploy.rs create mode 100644 solana/program-runtime/src/execution_budget.rs create mode 100644 solana/program-runtime/src/invoke_context.rs create mode 100644 solana/program-runtime/src/lib.rs create mode 100644 solana/program-runtime/src/loaded_programs.rs create mode 100644 solana/program-runtime/src/mem_pool.rs create mode 100644 solana/program-runtime/src/memory.rs create mode 100644 solana/program-runtime/src/memory_context.rs create mode 100644 solana/program-runtime/src/serialization.rs create mode 100644 solana/program-runtime/src/stable_log.rs create mode 100644 solana/program-runtime/src/sysvar_cache.rs create mode 100644 solana/program-runtime/src/vm.rs diff --git a/Cargo.toml b/Cargo.toml index 119c732..e9374a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,9 @@ [workspace] -members = ["solana/account", "solana/transaction-context"] +members = [ + "solana/account", + "solana/program-runtime", + "solana/transaction-context", +] resolver = "3" [workspace.package] @@ -15,15 +19,23 @@ version = "0.1.0" magic-root-interface = { path = "programs/magic-root-interface" } magic-root-program = { path = "programs/magic-root-program" } solana-account = { path = "solana/account" } +solana-program-runtime = { path = "solana/program-runtime" } solana-transaction-context = { path = "solana/transaction-context" } ahash = "0.8.12" arc-swap = "1.9.1" +assert_matches = "1.5.0" +base64 = "0.22.1" bincode = "1.3.3" bitflags = "2.11.1" blake3 = "1.8.5" +cfg-if = "1.0.4" +criterion = "0.7.0" +derive_more = "2.1.1" +itertools = "0.14.0" +log = "0.4.29" qualifier_attr = "0.2.2" -rand = "0.8.5" +rand = "0.9.2" rustix = { version = "1.1.4" } serde = "1.0.228" serde_bytes = "0.11.19" @@ -42,21 +54,47 @@ solana-account-info = "3.1.1" solana-clock = "3.1.0" solana-compute-budget-instruction = "4.1.1" solana-cpi = "3.1.0" +solana-epoch-rewards = "3.0.1" +solana-epoch-schedule = "3.1.0" +solana-feature-gate-interface = { version = "4.0.0", features = ["bincode"] } +solana-fee-structure = "3.0.0" +solana-frozen-abi = "3.3.0" +solana-frozen-abi-macro = "3.3.0" solana-hash = "4.3.0" solana-instruction = "3.4.0" solana-instruction-error = "2.3.0" solana-instructions-sysvar = "4.0.0" +solana-keypair = "3.1.2" +solana-last-restart-slot = "3.0.0" +solana-loader-v3-interface = "6.1.0" solana-program-entrypoint = "3.1.1" solana-program-error = "3.0.1" solana-pubkey = "4.2.0" -solana-rent = "4.0.0-rc.1" -solana-sbpf = "0.13.1" +solana-rent = "4.2.0" +solana-sbpf = "=0.21.0" solana-sdk-ids = "3.1.0" solana-sha256-hasher = "3.1.0" solana-short-vec = "3.2.1" solana-signature = "3.4.0" -solana-sysvar = "4.0.0" -solana-system-interface = "3.2.0" +solana-signer = "3.0.1" +solana-slot-hashes = "3.0.1" +solana-stable-layout = "3.0.0" +solana-sysvar = "3.1.1" +solana-sysvar-id = "3.1.0" +solana-system-interface = ">=3.0.0, <3.2.0" +solana-svm-callback = "4.0.0-rc.1" +solana-svm-feature-set = "4.0.0-rc.1" +solana-svm-log-collector = "4.0.0-rc.1" +solana-svm-measure = "4.0.0-rc.1" +solana-svm-timings = "4.0.0-rc.1" +solana-svm-transaction = "4.0.0-rc.1" +solana-svm-type-overrides = "4.0.0-rc.1" +solana-system-interface = ">=3.0.0, <3.2.0" +solana-system-program = "4.0.0-rc.0" +solana-system-transaction = "3.0.0" +solana-sysvar = "3.1.1" +solana-sysvar-id = "3.1.0" +solana-transaction = "4.1.1" solana-transaction-error = "3.2.0" diff --git a/solana/program-runtime/Cargo.toml b/solana/program-runtime/Cargo.toml new file mode 100644 index 0000000..ab69205 --- /dev/null +++ b/solana/program-runtime/Cargo.toml @@ -0,0 +1,96 @@ +[package] +name = "solana-program-runtime" + +authors = { workspace = true } +description = "Solana program runtime" +documentation = "https://docs.rs/solana-program-runtime" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = "4.1.1" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[lib] +crate-type = ["lib"] +name = "solana_program_runtime" + +[features] +# No-op stub retained only so external (patched-in) crates that reference +# `solana-program-runtime/agave-unstable-api` still resolve; the lib is no +# longer gated on it, and the upstream svm-* deps enable their unstable API +# unconditionally below. +agave-unstable-api = [] +dev-context-only-utils = [] +dummy-for-ci-check = ["metrics"] +frozen-abi = ["dep:solana-frozen-abi", "dep:solana-frozen-abi-macro"] +metrics = [] +sbpf-debugger = ["solana-sbpf/debugger"] +shuttle-test = ["solana-sbpf/shuttle-test", "solana-svm-type-overrides/shuttle-test"] +svm-internal = ["dep:qualifier_attr"] + +[dependencies] +base64 = { workspace = true } +bincode = { workspace = true } +cfg-if = { workspace = true } +itertools = { workspace = true } +log = { workspace = true } +qualifier_attr = { workspace = true, optional = true } +rand = { workspace = true } +scc = { workspace = true } +serde = { workspace = true } +solana-account = { workspace = true, features = ["bincode"] } +solana-account-info = { workspace = true } +solana-clock = { workspace = true } +solana-epoch-rewards = { workspace = true } +solana-epoch-schedule = { workspace = true } +solana-fee-structure = { workspace = true } +solana-frozen-abi = { workspace = true, optional = true, features = ["frozen-abi"] } +solana-frozen-abi-macro = { workspace = true, optional = true, features = ["frozen-abi"] } +solana-hash = { workspace = true } +solana-instruction = { workspace = true } +solana-last-restart-slot = { workspace = true } +solana-loader-v3-interface = { workspace = true } +solana-program-entrypoint = { workspace = true } +solana-pubkey = { workspace = true } +solana-rent = { workspace = true } +solana-sbpf = { workspace = true, features = ["jit"] } +solana-sdk-ids = { workspace = true } +solana-slot-hashes = { workspace = true } +solana-stable-layout = { workspace = true } +solana-svm-callback = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-feature-set = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-log-collector = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-measure = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-timings = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-transaction = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-type-overrides = { workspace = true, features = ["agave-unstable-api"] } +solana-system-interface = { workspace = true } +solana-sysvar = { workspace = true, features = ["bincode"] } +solana-sysvar-id = { workspace = true } +solana-transaction-context = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +assert_matches = { workspace = true } +solana-account = { workspace = true, features = ["dev-context-only-utils"] } +solana-account-info = { workspace = true } +solana-instruction = { workspace = true, features = ["bincode"] } +solana-instruction-error = { workspace = true, features = ["serde"] } +solana-keypair = { workspace = true } +solana-program-runtime = { path = ".", features = ["dev-context-only-utils"] } +solana-pubkey = { workspace = true, features = ["rand"] } +solana-signer = { workspace = true } +solana-transaction = { workspace = true, features = ["dev-context-only-utils"] } +solana-transaction-context = { workspace = true, features = ["dev-context-only-utils"] } +test-case = "3.3.1" + +[lints.rust] +unexpected_cfgs = "allow" + +[lints.clippy] +too_many_arguments = "allow" diff --git a/solana/program-runtime/README.md b/solana/program-runtime/README.md new file mode 100644 index 0000000..5f501b6 --- /dev/null +++ b/solana/program-runtime/README.md @@ -0,0 +1,17 @@ +# `solana-program-runtime` + +A fork of Agave's `solana-program-runtime` (`anza-xyz/agave`), reshaped for the +engine and patched in workspace-wide through `[patch.crates-io]` so the whole +dependency tree resolves to this copy. + +This is the layer that runs a program once its accounts are loaded: invocation +state, CPI translation, SBF VM setup, sysvar access, logging, and the +program-cache primitives. Account loading and transaction-level policy are +deliberately *not* here — they live in `solana-svm`, above this crate. + +The engine's intentional divergences from upstream — direct account-region +mapping, the access-violation growth handler, CPI account sync — aren't re-explained +per crate; they're documented once for the whole fork in +[`../README.md`](../README.md). The one rule to carry while working in here: this +crate's `serialization` and `vm` error remapping must stay in lockstep with the +transaction-context access-violation handler, exactly as that document spells out. diff --git a/solana/program-runtime/fixtures/noop_aligned.so b/solana/program-runtime/fixtures/noop_aligned.so new file mode 100644 index 0000000000000000000000000000000000000000..f00ca366aa2903694310eee3168358982d6c49e3 GIT binary patch literal 2024 zcmbVMy>HV{5Wl2pOaZB=Y7qhnd8kAg>U_|Uq%ah`{U{XummL-!}kPOKyGAG|54mea-1TZVwvJn_>|~%2wr71C~6#~OXKr)j2%X? zC2@+!L_R9aB<`5dXo@q;Bv;L*C+GKHvEITy-V6qV9+E*dj>B1Fm`TsV0CyL#=n`W8 zrBNeu0MF5Xa-HR!bk=M--Y;^8opes*fyjB04M0RB;Octsj6F)*B7YT`JMVfQL}uc< z-bazoi2oOsVUFe04vgV40J8~jQu3S@{9mAUrtr(z(%10b z`$5=_TFrV8LZu#T`Rzr!QdzNcWhdtp?YUYQwmOTM%w|2TJ>GEKW+PJ#_Nw!{(OzbI z`$=QI)owoW-Oxb?J74y_vg^B-vwp5vDlg;<`D`J#P$;?CoLekja=l_c|In{q{Co9O zUJAY$j^keENJu7!{};8+x5-B_Z$~uOg&h+f9u-n&1J3P=t%w2BV#}OK7V{CIf8HZ!Fe>-6f3{I zAwT;6ZHcSSpT-k!veP6hS3){l<8^Kob#BqcEXomIHVUNE zbJP|}!CR^j)ObZ2B|SUt`bpx~MM-s2oul>B-(0y9R|89O5trji-0QijzJi;>;-5+U KtU3yg#{UNU{KrlJ literal 0 HcmV?d00001 diff --git a/solana/program-runtime/src/cpi.rs b/solana/program-runtime/src/cpi.rs new file mode 100644 index 0000000..dad6737 --- /dev/null +++ b/solana/program-runtime/src/cpi.rs @@ -0,0 +1,2101 @@ +//! Cross-program invocation translation and dispatch. + +use { + crate::{ + invoke_context::{InvokeContext, SerializedAccountMetadata}, + memory::{translate_slice, translate_type, translate_type_mut_for_cpi, translate_vm_slice}, + serialization::create_memory_region_of_account, + }, + solana_account_info::AccountInfo, + solana_instruction::{AccountMeta, Instruction, error::InstructionError}, + solana_loader_v3_interface::instruction as bpf_loader_upgradeable, + solana_program_entrypoint::MAX_PERMITTED_DATA_INCREASE, + solana_pubkey::{MAX_SEEDS, Pubkey, PubkeyError}, + solana_sbpf::{ebpf, memory_region::MemoryMapping}, + solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, native_loader}, + solana_stable_layout::stable_instruction::StableInstruction, + solana_svm_log_collector::ic_msg, + solana_svm_measure::measure::Measure, + solana_transaction_context::{ + IndexOfAccount, MAX_ACCOUNTS_PER_INSTRUCTION, MAX_INSTRUCTION_DATA_LEN, + instruction_accounts::BorrowedInstructionAccount, vm_slice::VmSlice, + }, + std::mem, + thiserror::Error, +}; + +/// Errors produced while translating or dispatching a CPI request. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum CpiError { + #[error("Invalid pointer")] + InvalidPointer, + #[error("Too many signers")] + TooManySigners, + #[error("Could not create program address with signer seeds: {0}")] + BadSeeds(PubkeyError), + #[error("InvalidLength")] + InvalidLength, + #[error("Invoked an instruction with too many accounts ({num_accounts} > {max_accounts})")] + MaxInstructionAccountsExceeded { num_accounts: u64, max_accounts: u64 }, + #[error("Invoked an instruction with data that is too large ({data_len} > {max_data_len})")] + MaxInstructionDataLenExceeded { data_len: u64, max_data_len: u64 }, + #[error( + "Invoked an instruction with too many account info's ({num_account_infos} > \ + {max_account_infos})" + )] + MaxInstructionAccountInfosExceeded { num_account_infos: u64, max_account_infos: u64 }, + #[error("Program {0} not supported by inner instructions")] + ProgramNotSupported(Pubkey), +} + +type Error = Box; + +const SUCCESS: u64 = 0; +/// Maximum signer seed groups accepted by CPI. +const MAX_SIGNERS: usize = 16; +/// SIMD-0339 `AccountInfo` translation byte size. +/// +/// Fixed size of **80 bytes** for each AccountInfo, broken down as: +/// - 32 bytes for account address +/// - 32 bytes for owner address +/// - 8 bytes for lamport balance +/// - 8 bytes for data length +const ACCOUNT_INFO_BYTE_SIZE: usize = 80; + +/// Rust representation of C's SolInstruction +#[derive(Debug)] +#[repr(C)] +struct SolInstruction { + pub program_id_addr: u64, + pub accounts_addr: u64, + pub accounts_len: u64, + pub data_addr: u64, + pub data_len: u64, +} + +/// Rust representation of C's SolAccountMeta +#[derive(Debug)] +#[repr(C)] +struct SolAccountMeta { + pub pubkey_addr: u64, + pub is_writable: bool, + pub is_signer: bool, +} + +/// Rust representation of C's SolAccountInfo +#[derive(Debug)] +#[repr(C)] +struct SolAccountInfo { + pub key_addr: u64, + pub lamports_addr: u64, + pub data_len: u64, + pub data_addr: u64, + pub owner_addr: u64, + pub rent_epoch: u64, + pub is_signer: bool, + pub is_writable: bool, + pub executable: bool, +} + +/// Rust representation of C's SolSignerSeed +#[derive(Debug)] +#[repr(C)] +struct SolSignerSeedC { + pub addr: u64, + pub len: u64, +} + +/// Rust representation of C's SolSignerSeeds +#[derive(Debug)] +#[repr(C)] +struct SolSignerSeedsC { + pub addr: u64, + pub len: u64, +} + +/// Maximum number of account info structs that can be used in a single CPI invocation +const MAX_CPI_ACCOUNT_INFOS: usize = 128; +/// Maximum number of account info structs that can be used in a single CPI invocation with SIMD-0339 active +const MAX_CPI_ACCOUNT_INFOS_SIMD_0339: usize = 255; + +/// Check that an account info pointer field points to the expected address +fn check_account_info_pointer( + invoke_context: &InvokeContext, + vm_addr: u64, + expected_vm_addr: u64, + field: &str, +) -> Result<(), Error> { + if vm_addr != expected_vm_addr { + ic_msg!( + invoke_context, + "Invalid account info pointer `{}': {:#x} != {:#x}", + field, + vm_addr, + expected_vm_addr + ); + return Err(Box::new(CpiError::InvalidPointer)); + } + Ok(()) +} + +/// Check that an instruction's account and data lengths are within limits +fn check_instruction_size(num_accounts: usize, data_len: usize) -> Result<(), Error> { + if num_accounts > MAX_ACCOUNTS_PER_INSTRUCTION { + return Err(Box::new(CpiError::MaxInstructionAccountsExceeded { + num_accounts: num_accounts as u64, + max_accounts: MAX_ACCOUNTS_PER_INSTRUCTION as u64, + })); + } + if data_len > MAX_INSTRUCTION_DATA_LEN { + return Err(Box::new(CpiError::MaxInstructionDataLenExceeded { + data_len: data_len as u64, + max_data_len: MAX_INSTRUCTION_DATA_LEN as u64, + })); + } + Ok(()) +} + +/// Check that the number of account infos is within the CPI limit +fn check_account_infos( + num_account_infos: usize, + invoke_context: &InvokeContext, +) -> Result<(), Error> { + let max_cpi_account_infos = if invoke_context.get_feature_set().increase_cpi_account_info_limit + { + MAX_CPI_ACCOUNT_INFOS_SIMD_0339 + } else if invoke_context.get_feature_set().increase_tx_account_lock_limit { + 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(Box::new(CpiError::MaxInstructionAccountInfosExceeded { + num_account_infos, + max_account_infos, + })); + } + Ok(()) +} + +/// Check whether a program is authorized for CPI +fn check_authorized_program( + program_id: &Pubkey, + instruction_data: &[u8], + invoke_context: &InvokeContext, +) -> Result<(), Error> { + if native_loader::check_id(program_id) + || bpf_loader::check_id(program_id) + || bpf_loader_deprecated::check_id(program_id) + || (solana_sdk_ids::bpf_loader_upgradeable::check_id(program_id) + && !(bpf_loader_upgradeable::is_upgrade_instruction(instruction_data) + || bpf_loader_upgradeable::is_set_authority_instruction(instruction_data) + || (invoke_context.get_feature_set().enable_bpf_loader_set_authority_checked_ix + && bpf_loader_upgradeable::is_set_authority_checked_instruction( + instruction_data, + )) + || bpf_loader_upgradeable::is_close_instruction(instruction_data))) + || invoke_context.is_precompile(program_id) + { + return Err(Box::new(CpiError::ProgramNotSupported(*program_id))); + } + Ok(()) +} + +/// Host side representation of AccountInfo or SolAccountInfo passed to the CPI syscall. +/// +/// At the start of a CPI, this can be different from the data stored in the +/// corresponding BorrowedAccount, and needs to be synched. +#[derive(Debug)] +pub struct CallerAccount<'a> { + pub lamports: &'a mut u64, + pub owner: &'a mut Pubkey, + // The original data length of the account at the start of the current + // instruction. We use this to determine whether an account was shrunk or + // grown before or after CPI, and to derive the vm address of the realloc + // region. + pub original_data_len: usize, + // This points to the data section for this account, as serialized and + // mapped inside the vm (see serialize_parameters() in + // BpfExecutor::execute). + // + // Empty when account data is directly mapped. + pub serialized_data: &'a mut [u8], + // Given the corresponding input AccountInfo::data, vm_data_addr points to + // the pointer field and ref_to_len_in_vm points to the length field. + pub vm_data_addr: u64, + pub ref_to_len_in_vm: &'a mut u64, +} + +impl<'a> CallerAccount<'a> { + pub fn get_serialized_data( + check_aligned: bool, + original_data_len: usize, + len: usize, + syscall_parameter_address_restrictions: bool, + ) -> Result<&'a mut [u8], Error> { + if syscall_parameter_address_restrictions { + let is_caller_loader_deprecated = !check_aligned; + let address_space_reserved_for_account = if is_caller_loader_deprecated { + original_data_len + } else { + original_data_len.saturating_add(MAX_PERMITTED_DATA_INCREASE) + }; + if len > address_space_reserved_for_account { + return Err(InstructionError::InvalidRealloc.into()); + } + } + Ok(&mut []) + } + + // Create a CallerAccount given an AccountInfo. + pub fn from_account_info( + invoke_context: &InvokeContext, + memory_mapping: &solana_sbpf::memory_region::MemoryMapping, + check_aligned: bool, + account_info: &solana_account_info::AccountInfo, + account_metadata: &crate::invoke_context::SerializedAccountMetadata, + ) -> Result, Error> { + use crate::memory::{translate_type, translate_type_mut_for_cpi}; + + let syscall_parameter_address_restrictions = + invoke_context.get_feature_set().syscall_parameter_address_restrictions; + if syscall_parameter_address_restrictions { + check_account_info_pointer( + invoke_context, + account_info.key as *const _ as u64, + account_metadata.vm_key_addr, + "key", + )?; + check_account_info_pointer( + invoke_context, + account_info.owner as *const _ as u64, + account_metadata.vm_owner_addr, + "owner", + )?; + } + + // account_info points to host memory. The addresses used internally are + // in vm space so they need to be translated. + let lamports = { + // Double translate lamports out of RefCell + let ptr = translate_type::( + memory_mapping, + account_info.lamports.as_ptr() as u64, + check_aligned, + )?; + if syscall_parameter_address_restrictions { + if account_info.lamports.as_ptr() as u64 >= solana_sbpf::ebpf::MM_INPUT_START { + return Err(Box::new(CpiError::InvalidPointer)); + } + + check_account_info_pointer( + invoke_context, + *ptr, + account_metadata.vm_lamports_addr, + "lamports", + )?; + } + translate_type_mut_for_cpi::(memory_mapping, *ptr, check_aligned)? + }; + + let owner = translate_type_mut_for_cpi::( + memory_mapping, + account_info.owner as *const _ as u64, + check_aligned, + )?; + + let (serialized_data, vm_data_addr, ref_to_len_in_vm) = { + if syscall_parameter_address_restrictions + && account_info.data.as_ptr() as u64 >= solana_sbpf::ebpf::MM_INPUT_START + { + return Err(Box::new(CpiError::InvalidPointer)); + } + + // Double translate data out of RefCell + let data = *translate_type::<&[u8]>( + memory_mapping, + account_info.data.as_ptr() as *const _ as u64, + check_aligned, + )?; + if syscall_parameter_address_restrictions { + check_account_info_pointer( + invoke_context, + data.as_ptr() as u64, + account_metadata.vm_data_addr, + "data", + )?; + } else { + // Moved to translate_accounts_common() via feature gate. + invoke_context.consume_checked( + (data.len() as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX), + )?; + } + + let vm_len_addr = (account_info.data.as_ptr() as *const u64 as u64) + .saturating_add(std::mem::size_of::() as u64); + if syscall_parameter_address_restrictions { + // In the same vein as the other check_account_info_pointer() checks, we don't lock + // this pointer to a specific address but we don't want it to be inside accounts, or + // callees might be able to write to the pointed memory. + if vm_len_addr >= solana_sbpf::ebpf::MM_INPUT_START { + return Err(Box::new(CpiError::InvalidPointer)); + } + } + let ref_to_len_in_vm = + translate_type_mut_for_cpi::(memory_mapping, vm_len_addr, false)?; + let vm_data_addr = data.as_ptr() as u64; + let serialized_data = CallerAccount::get_serialized_data( + check_aligned, + account_metadata.original_data_len, + if syscall_parameter_address_restrictions { + *ref_to_len_in_vm as usize + } else { + data.len() + }, + syscall_parameter_address_restrictions, + )?; + (serialized_data, vm_data_addr, ref_to_len_in_vm) + }; + + Ok(CallerAccount { + lamports, + owner, + original_data_len: account_metadata.original_data_len, + serialized_data, + vm_data_addr, + ref_to_len_in_vm, + }) + } + + // Create a CallerAccount given a SolAccountInfo. + fn from_sol_account_info( + invoke_context: &InvokeContext, + memory_mapping: &solana_sbpf::memory_region::MemoryMapping, + check_aligned: bool, + vm_addr: u64, + account_info: &SolAccountInfo, + account_metadata: &crate::invoke_context::SerializedAccountMetadata, + ) -> Result, Error> { + use crate::memory::translate_type_mut_for_cpi; + + let syscall_parameter_address_restrictions = + invoke_context.get_feature_set().syscall_parameter_address_restrictions; + if syscall_parameter_address_restrictions { + check_account_info_pointer( + invoke_context, + account_info.key_addr, + account_metadata.vm_key_addr, + "key", + )?; + + check_account_info_pointer( + invoke_context, + account_info.owner_addr, + account_metadata.vm_owner_addr, + "owner", + )?; + + check_account_info_pointer( + invoke_context, + account_info.lamports_addr, + account_metadata.vm_lamports_addr, + "lamports", + )?; + + check_account_info_pointer( + invoke_context, + account_info.data_addr, + account_metadata.vm_data_addr, + "data", + )?; + } + + // account_info points to host memory. The addresses used internally are + // in vm space so they need to be translated. + let lamports = translate_type_mut_for_cpi::( + memory_mapping, + account_info.lamports_addr, + check_aligned, + )?; + let owner = translate_type_mut_for_cpi::( + memory_mapping, + account_info.owner_addr, + check_aligned, + )?; + + if !syscall_parameter_address_restrictions { + // Moved to translate_accounts_common() via feature gate. + invoke_context.consume_checked( + account_info + .data_len + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX), + )?; + } + + // we already have the host addr we want: &mut account_info.data_len. + // The account info might be read only in the vm though, so we translate + // to ensure we can write. This is tested by programs/sbf/rust/ro_modify + // which puts SolAccountInfo in rodata. + let vm_len_addr = vm_addr + .saturating_add(&account_info.data_len as *const u64 as u64) + .saturating_sub(account_info as *const _ as *const u64 as u64); + if syscall_parameter_address_restrictions { + // In the same vein as the other check_account_info_pointer() checks, we don't lock + // this pointer to a specific address but we don't want it to be inside accounts, or + // callees might be able to write to the pointed memory. + if vm_len_addr >= solana_sbpf::ebpf::MM_INPUT_START { + return Err(Box::new(CpiError::InvalidPointer)); + } + } + let ref_to_len_in_vm = + translate_type_mut_for_cpi::(memory_mapping, vm_len_addr, false)?; + let serialized_data = CallerAccount::get_serialized_data( + check_aligned, + account_metadata.original_data_len, + if syscall_parameter_address_restrictions { + *ref_to_len_in_vm as usize + } else { + account_info.data_len as usize + }, + syscall_parameter_address_restrictions, + )?; + + Ok(CallerAccount { + lamports, + owner, + original_data_len: account_metadata.original_data_len, + serialized_data, + vm_data_addr: account_info.data_addr, + ref_to_len_in_vm, + }) + } +} + +/// Implemented by language specific data structure translators +pub trait SyscallInvokeSigned { + fn translate_instruction( + addr: u64, + invoke_context: &InvokeContext, + ) -> Result; + fn translate_accounts<'a>( + account_infos_addr: u64, + account_infos_len: u64, + invoke_context: &InvokeContext, + ) -> Result>, Error>; + fn translate_signers( + program_id: &Pubkey, + signers_seeds_addr: u64, + signers_seeds_len: u64, + invoke_context: &InvokeContext, + ) -> Result, Error>; +} + +pub fn translate_instruction_rust( + addr: u64, + invoke_context: &InvokeContext, +) -> Result { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + let ix = translate_type::(memory_mapping, addr, check_aligned)?; + let account_metas = translate_slice::( + memory_mapping, + ix.accounts.as_vaddr(), + ix.accounts.len(), + check_aligned, + )?; + let data = translate_slice::( + memory_mapping, + ix.data.as_vaddr(), + ix.data.len(), + check_aligned, + )?; + + check_instruction_size(account_metas.len(), data.len())?; + + let mut total_cu_translation_cost: u64 = (data.len() as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX); + + if invoke_context.get_feature_set().increase_cpi_account_info_limit { + // Each account meta is 34 bytes (32 for pubkey, 1 for is_signer, 1 for is_writable) + let account_meta_translation_cost = + (account_metas.len().saturating_mul(size_of::()) as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX); + + total_cu_translation_cost = + total_cu_translation_cost.saturating_add(account_meta_translation_cost); + } + + consume_compute_meter(invoke_context, total_cu_translation_cost)?; + + let mut accounts = Vec::with_capacity(account_metas.len()); + #[allow(clippy::needless_range_loop)] + for account_index in 0..account_metas.len() { + #[allow(clippy::indexing_slicing)] + let account_meta = &account_metas[account_index]; + if unsafe { + std::ptr::read_volatile(&account_meta.is_signer as *const _ as *const u8) > 1 + || std::ptr::read_volatile(&account_meta.is_writable as *const _ as *const u8) > 1 + } { + return Err(Box::new(InstructionError::InvalidArgument)); + } + accounts.push(account_meta.clone()); + } + + Ok(Instruction { + accounts, + data: data.to_vec(), + program_id: ix.program_id, + }) +} + +pub fn translate_accounts_rust<'a>( + account_infos_addr: u64, + account_infos_len: u64, + invoke_context: &InvokeContext, +) -> Result>, Error> { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + let (account_infos, account_info_keys) = translate_account_infos( + account_infos_addr, + account_infos_len, + |account_info: &AccountInfo| account_info.key as *const _ as u64, + memory_mapping, + invoke_context, + check_aligned, + )?; + + translate_accounts_common( + &account_info_keys, + account_infos, + account_infos_addr, + invoke_context, + memory_mapping, + check_aligned, + |invoke_context, memory_mapping, check_aligned, _, account_info, account_metadata| { + CallerAccount::from_account_info( + invoke_context, + memory_mapping, + check_aligned, + account_info, + account_metadata, + ) + }, + ) +} + +pub fn translate_signers_rust( + program_id: &Pubkey, + signers_seeds_addr: u64, + signers_seeds_len: u64, + invoke_context: &InvokeContext, +) -> Result, Error> { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + let mut signers = Vec::new(); + if signers_seeds_len > 0 { + let signers_seeds = translate_slice::>>( + memory_mapping, + signers_seeds_addr, + signers_seeds_len, + check_aligned, + )?; + if signers_seeds.len() > MAX_SIGNERS { + return Err(Box::new(CpiError::TooManySigners)); + } + for signer_seeds in signers_seeds.iter() { + let untranslated_seeds = translate_slice::>( + memory_mapping, + signer_seeds.ptr(), + signer_seeds.len(), + check_aligned, + )?; + if untranslated_seeds.len() > MAX_SEEDS { + return Err(Box::new(InstructionError::MaxSeedLengthExceeded)); + } + let seeds = untranslated_seeds + .iter() + .map(|untranslated_seed| { + translate_vm_slice(untranslated_seed, memory_mapping, check_aligned) + }) + .collect::, Error>>()?; + let signer = + Pubkey::create_program_address(&seeds, program_id).map_err(CpiError::BadSeeds)?; + signers.push(signer); + } + Ok(signers) + } else { + Ok(vec![]) + } +} + +pub fn translate_instruction_c( + addr: u64, + invoke_context: &InvokeContext, +) -> Result { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + let ix_c = translate_type::(memory_mapping, addr, check_aligned)?; + + let program_id = translate_type::(memory_mapping, ix_c.program_id_addr, check_aligned)?; + let account_metas = translate_slice::( + memory_mapping, + ix_c.accounts_addr, + ix_c.accounts_len, + check_aligned, + )?; + let data = translate_slice::(memory_mapping, ix_c.data_addr, ix_c.data_len, check_aligned)?; + + check_instruction_size(ix_c.accounts_len as usize, data.len())?; + + let mut total_cu_translation_cost: u64 = (data.len() as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX); + + if invoke_context.get_feature_set().increase_cpi_account_info_limit { + // Each account meta is 34 bytes (32 for pubkey, 1 for is_signer, 1 for is_writable) + let account_meta_translation_cost = + (ix_c.accounts_len.saturating_mul(size_of::() as u64)) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX); + + total_cu_translation_cost = + total_cu_translation_cost.saturating_add(account_meta_translation_cost); + } + + consume_compute_meter(invoke_context, total_cu_translation_cost)?; + + let mut accounts = Vec::with_capacity(ix_c.accounts_len as usize); + #[allow(clippy::needless_range_loop)] + for account_index in 0..ix_c.accounts_len as usize { + #[allow(clippy::indexing_slicing)] + let account_meta = &account_metas[account_index]; + if unsafe { + std::ptr::read_volatile(&account_meta.is_signer as *const _ as *const u8) > 1 + || std::ptr::read_volatile(&account_meta.is_writable as *const _ as *const u8) > 1 + } { + return Err(Box::new(InstructionError::InvalidArgument)); + } + let pubkey = + translate_type::(memory_mapping, account_meta.pubkey_addr, check_aligned)?; + accounts.push(AccountMeta { + pubkey: *pubkey, + is_signer: account_meta.is_signer, + is_writable: account_meta.is_writable, + }); + } + + Ok(Instruction { + accounts, + data: data.to_vec(), + program_id: *program_id, + }) +} + +pub fn translate_accounts_c<'a>( + account_infos_addr: u64, + account_infos_len: u64, + invoke_context: &InvokeContext, +) -> Result>, Error> { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + let (account_infos, account_info_keys) = translate_account_infos( + account_infos_addr, + account_infos_len, + |account_info: &SolAccountInfo| account_info.key_addr, + memory_mapping, + invoke_context, + check_aligned, + )?; + + translate_accounts_common( + &account_info_keys, + account_infos, + account_infos_addr, + invoke_context, + memory_mapping, + check_aligned, + CallerAccount::from_sol_account_info, + ) +} + +pub fn translate_signers_c( + program_id: &Pubkey, + signers_seeds_addr: u64, + signers_seeds_len: u64, + invoke_context: &InvokeContext, +) -> Result, Error> { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + if signers_seeds_len > 0 { + let signers_seeds = translate_slice::( + memory_mapping, + signers_seeds_addr, + signers_seeds_len, + check_aligned, + )?; + if signers_seeds.len() > MAX_SIGNERS { + return Err(Box::new(CpiError::TooManySigners)); + } + Ok(signers_seeds + .iter() + .map(|signer_seeds| { + let seeds = translate_slice::( + memory_mapping, + signer_seeds.addr, + signer_seeds.len, + check_aligned, + )?; + if seeds.len() > MAX_SEEDS { + return Err(Box::new(InstructionError::MaxSeedLengthExceeded) as Error); + } + let seeds_bytes = seeds + .iter() + .map(|seed| { + translate_slice::(memory_mapping, seed.addr, seed.len, check_aligned) + }) + .collect::, Error>>()?; + Pubkey::create_program_address(&seeds_bytes, program_id) + .map_err(|err| Box::new(CpiError::BadSeeds(err)) as Error) + }) + .collect::, Error>>()?) + } else { + Ok(vec![]) + } +} + +/// Call process instruction, common to both Rust and C +pub fn cpi_common( + invoke_context: &mut InvokeContext, + instruction_addr: u64, + account_infos_addr: u64, + account_infos_len: u64, + signers_seeds_addr: u64, + signers_seeds_len: u64, +) -> Result { + // CPI entry. + // + // Translate the inputs to the syscall and synchronize the caller's account + // changes so the callee can see them. + consume_compute_meter( + invoke_context, + invoke_context.get_execution_cost().invoke_units, + )?; + if let Some(execute_time) = invoke_context.execute_time.as_mut() { + execute_time.stop(); + invoke_context.timings.execute_us += execute_time.as_us(); + } + let syscall_parameter_address_restrictions = + invoke_context.get_feature_set().syscall_parameter_address_restrictions; + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping_mut()? as *mut MemoryMapping; + + let instruction = S::translate_instruction(instruction_addr, invoke_context)?; + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let caller_program_id = instruction_context.get_program_key()?; + let signers = S::translate_signers( + caller_program_id, + signers_seeds_addr, + signers_seeds_len, + invoke_context, + )?; + check_authorized_program(&instruction.program_id, &instruction.data, invoke_context)?; + invoke_context.prepare_next_cpi_instruction(instruction, &signers)?; + + let mut accounts = + S::translate_accounts(account_infos_addr, account_infos_len, invoke_context)?; + let memory_mapping = unsafe { &mut *memory_mapping }; + + if syscall_parameter_address_restrictions { + // before initiating CPI, the caller may have modified the + // account (caller_account). We need to update the corresponding + // BorrowedAccount (callee_account) so the callee can see the + // changes. + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + for translated_account in accounts.iter_mut() { + let callee_account = instruction_context + .try_borrow_instruction_account(translated_account.index_in_caller)?; + // update_callee_account() is moved from translate_accounts_common() + let update_caller = + update_callee_account(&translated_account.caller_account, callee_account)?; + translated_account.update_caller_account_region = + translated_account.update_caller_account_info || update_caller; + } + } + + // Process the callee instruction + let mut compute_units_consumed = 0; + invoke_context.process_instruction(&mut compute_units_consumed)?; + + // re-bind to please the borrow checker + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + + // CPI exit. + // + // Synchronize the callee's account changes so the caller can see them. + for translated_account in accounts.iter_mut() { + let mut callee_account = instruction_context + .try_borrow_instruction_account(translated_account.index_in_caller)?; + if translated_account.update_caller_account_info { + update_caller_account( + invoke_context, + memory_mapping, + check_aligned, + &mut translated_account.caller_account, + &mut callee_account, + syscall_parameter_address_restrictions, + )?; + } + } + + for translated_account in accounts.iter() { + let mut callee_account = instruction_context + .try_borrow_instruction_account(translated_account.index_in_caller)?; + if translated_account.update_caller_account_region { + update_caller_account_region( + memory_mapping, + check_aligned, + &translated_account.caller_account, + &mut callee_account, + )?; + } + } + + invoke_context.execute_time = Some(Measure::start("execute")); + Ok(SUCCESS) +} + +/// Account data and metadata that has been translated from caller space. +pub struct TranslatedAccount<'a> { + pub index_in_caller: IndexOfAccount, + pub caller_account: CallerAccount<'a>, + pub update_caller_account_region: bool, + pub update_caller_account_info: bool, +} + +fn translate_account_infos<'a, T, F>( + account_infos_addr: u64, + account_infos_len: u64, + key_addr: F, + memory_mapping: &'a MemoryMapping, + invoke_context: &InvokeContext, + check_aligned: bool, +) -> Result<(&'a [T], Vec<&'a Pubkey>), Error> +where + F: Fn(&T) -> u64, +{ + let syscall_parameter_address_restrictions = + invoke_context.get_feature_set().syscall_parameter_address_restrictions; + + // In the same vein as the other check_account_info_pointer() checks, we don't lock + // this pointer to a specific address but we don't want it to be inside accounts, or + // callees might be able to write to the pointed memory. + if syscall_parameter_address_restrictions + && account_infos_addr + .saturating_add(account_infos_len.saturating_mul(std::mem::size_of::() as u64)) + >= ebpf::MM_INPUT_START + { + return Err(CpiError::InvalidPointer.into()); + } + + let account_infos = translate_slice::( + memory_mapping, + account_infos_addr, + account_infos_len, + check_aligned, + )?; + check_account_infos(account_infos.len(), invoke_context)?; + + if invoke_context.get_feature_set().increase_cpi_account_info_limit { + let account_infos_bytes = account_infos.len().saturating_mul(ACCOUNT_INFO_BYTE_SIZE); + + consume_compute_meter( + invoke_context, + (account_infos_bytes as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX), + )?; + } + + let mut account_info_keys = Vec::with_capacity(account_infos_len as usize); + #[allow(clippy::needless_range_loop)] + for account_index in 0..account_infos_len as usize { + #[allow(clippy::indexing_slicing)] + let account_info = &account_infos[account_index]; + account_info_keys.push(translate_type::( + memory_mapping, + key_addr(account_info), + check_aligned, + )?); + } + Ok((account_infos, account_info_keys)) +} + +// Finish translating accounts and build TranslatedAccount from CallerAccount. +fn translate_accounts_common<'a, T, F>( + account_info_keys: &[&Pubkey], + account_infos: &[T], + account_infos_addr: u64, + invoke_context: &InvokeContext, + memory_mapping: &MemoryMapping, + check_aligned: bool, + do_translate: F, +) -> Result>, Error> +where + F: Fn( + &InvokeContext, + &MemoryMapping, + bool, + u64, + &T, + &SerializedAccountMetadata, + ) -> Result, Error>, +{ + let transaction_context = &invoke_context.transaction_context; + let next_instruction_context = transaction_context.get_next_instruction_context()?; + let next_instruction_accounts = next_instruction_context.instruction_accounts(); + let instruction_context = transaction_context.get_current_instruction_context()?; + let mut accounts = Vec::with_capacity(next_instruction_accounts.len()); + + // unwrapping here is fine: we're in a syscall and the method below fails + // only outside syscalls + let accounts_metadata = &invoke_context.get_syscall_context().unwrap().accounts_metadata; + + let syscall_parameter_address_restrictions = + invoke_context.get_feature_set().syscall_parameter_address_restrictions; + for (instruction_account_index, instruction_account) in + next_instruction_accounts.iter().enumerate() + { + if next_instruction_context + .is_instruction_account_duplicate(instruction_account_index as IndexOfAccount)? + .is_some() + { + continue; // Skip duplicate account + } + + let index_in_caller = instruction_context + .get_index_of_account_in_instruction(instruction_account.index_in_transaction)?; + let callee_account = instruction_context.try_borrow_instruction_account(index_in_caller)?; + let account_key = invoke_context + .transaction_context + .get_key_of_account_at_index(instruction_account.index_in_transaction)?; + + #[allow(deprecated)] + if callee_account.is_executable() { + // Use the known account + consume_compute_meter( + invoke_context, + (callee_account.get_data().len() as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX), + )?; + } else if let Some(caller_account_index) = + account_info_keys.iter().position(|key| *key == account_key) + { + let serialized_metadata = + accounts_metadata.get(index_in_caller as usize).ok_or_else(|| { + ic_msg!( + invoke_context, + "Internal error: index mismatch for account {}", + account_key + ); + Box::new(InstructionError::MissingAccount) as Error + })?; + + // build the CallerAccount corresponding to this account. + if caller_account_index >= account_infos.len() { + return Err(Box::new(CpiError::InvalidLength)); + } + #[allow(clippy::indexing_slicing)] + let caller_account = + do_translate( + invoke_context, + memory_mapping, + check_aligned, + account_infos_addr.saturating_add( + caller_account_index.saturating_mul(mem::size_of::()) as u64, + ), + &account_infos[caller_account_index], + serialized_metadata, + )?; + + if syscall_parameter_address_restrictions { + // Moved from do_translate() via feature gate. + consume_compute_meter( + invoke_context, + (*caller_account.ref_to_len_in_vm) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX), + )?; + } + let update_caller = if syscall_parameter_address_restrictions { + // update_callee_account() is moved to cpi_common() + true + } else { + // before initiating CPI, the caller may have modified the + // account (caller_account). We need to update the corresponding + // BorrowedAccount (callee_account) so the callee can see the + // changes. + update_callee_account(&caller_account, callee_account)? + }; + + accounts.push(TranslatedAccount { + index_in_caller, + caller_account, + update_caller_account_region: instruction_account.is_writable() || update_caller, + update_caller_account_info: instruction_account.is_writable(), + }); + } else { + ic_msg!( + invoke_context, + "Instruction references an unknown account {}", + account_key + ); + return Err(Box::new(InstructionError::MissingAccount)); + } + } + + Ok(accounts) +} + +fn consume_compute_meter(invoke_context: &InvokeContext, amount: u64) -> Result<(), Error> { + invoke_context.consume_checked(amount)?; + Ok(()) +} + +// Update the given account before executing CPI. +// +// caller_account and callee_account describe the same account. At CPI entry +// caller_account might include changes the caller has made to the account +// before executing CPI. +// +// This method updates callee_account so the CPI callee can see the caller's +// changes. +// +// When true is returned, the caller account's mapped data region must be +// updated after CPI because the pointer may have changed. +fn update_callee_account( + caller_account: &CallerAccount, + mut callee_account: BorrowedInstructionAccount<'_, '_>, +) -> Result { + let mut must_update_caller = false; + + if callee_account.get_lamports() != *caller_account.lamports { + callee_account.set_lamports(*caller_account.lamports)?; + } + + let prev_len = callee_account.get_data().len(); + let post_len = *caller_account.ref_to_len_in_vm as usize; + if prev_len != post_len { + callee_account.set_data_length(post_len)?; + // pointer to data may have changed, so caller must be updated + must_update_caller = true; + } + + // 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())?; + // caller gave ownership and thus write access away, so caller must be updated + must_update_caller = true; + } + + Ok(must_update_caller) +} + +fn update_caller_account_region( + memory_mapping: &mut MemoryMapping, + check_aligned: bool, + caller_account: &CallerAccount, + callee_account: &mut BorrowedInstructionAccount<'_, '_>, +) -> Result<(), Error> { + let is_caller_loader_deprecated = !check_aligned; + let address_space_reserved_for_account = if is_caller_loader_deprecated { + caller_account.original_data_len + } else { + caller_account.original_data_len.saturating_add(MAX_PERMITTED_DATA_INCREASE) + }; + + if address_space_reserved_for_account > 0 { + // We can trust vm_data_addr to point to the correct region because we + // enforce that in CallerAccount::from_(sol_)account_info. + let (region_index, region) = memory_mapping + .find_region(caller_account.vm_data_addr) + .ok_or_else(|| Box::new(InstructionError::MissingAccount) as Error)?; + // vm_data_addr must always point to the beginning of the region + debug_assert_eq!(region.vm_addr, caller_account.vm_data_addr); + let new_region = create_memory_region_of_account(callee_account, region.vm_addr)?; + unsafe { + memory_mapping.replace_region(region_index, new_region)?; + } + } + + Ok(()) +} + +// Update the given account after executing CPI. +// +// caller_account and callee_account describe to the same account. At CPI exit +// callee_account might include changes the callee has made to the account +// after executing. +// +// This method updates caller_account so the CPI caller can see the callee's +// changes. +// +// Safety: Once `syscall_parameter_address_restrictions` is enabled all fields of [CallerAccount] used +// in this function should never point inside the address space reserved for +// accounts (regardless of the current size of an account). +fn update_caller_account( + invoke_context: &InvokeContext, + memory_mapping: &MemoryMapping, + check_aligned: bool, + caller_account: &mut CallerAccount<'_>, + callee_account: &mut BorrowedInstructionAccount<'_, '_>, + syscall_parameter_address_restrictions: bool, +) -> Result<(), Error> { + *caller_account.lamports = callee_account.get_lamports(); + *caller_account.owner = *callee_account.get_owner(); + + let prev_len = *caller_account.ref_to_len_in_vm as usize; + let post_len = callee_account.get_data().len(); + let is_caller_loader_deprecated = !check_aligned; + let address_space_reserved_for_account = + if syscall_parameter_address_restrictions && is_caller_loader_deprecated { + caller_account.original_data_len + } else { + caller_account.original_data_len.saturating_add(MAX_PERMITTED_DATA_INCREASE) + }; + + if post_len > address_space_reserved_for_account + && (syscall_parameter_address_restrictions || prev_len != post_len) + { + let max_increase = + address_space_reserved_for_account.saturating_sub(caller_account.original_data_len); + ic_msg!( + invoke_context, + "Account data size realloc limited to {max_increase} in inner instructions", + ); + return Err(Box::new(InstructionError::InvalidRealloc)); + } + + if prev_len != post_len { + // this is the len field in the AccountInfo::data slice + *caller_account.ref_to_len_in_vm = post_len as u64; + + // this is the len field in the serialized parameters + let serialized_len_ptr = translate_type_mut_for_cpi::( + memory_mapping, + caller_account.vm_data_addr.saturating_sub(std::mem::size_of::() as u64), + check_aligned, + )?; + *serialized_len_ptr = post_len as u64; + } + + Ok(()) +} + +#[allow(clippy::indexing_slicing)] +#[allow(clippy::arithmetic_side_effects)] +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + invoke_context::{BpfAllocator, SerializedAccountMetadata, SyscallContext}, + memory::translate_type, + memory_context::MemoryContext, + with_mock_invoke_context_with_feature_set, + }, + assert_matches::assert_matches, + solana_account::{Account, AccountSharedData, ReadableAccount}, + solana_account_info::AccountInfo, + solana_sbpf::{ + ebpf::MM_INPUT_START, memory_region::MemoryRegion, program::SBPFVersion, vm::Config, + }, + solana_sdk_ids::{bpf_loader, system_program}, + solana_svm_feature_set::SVMFeatureSet, + solana_transaction_context::{ + IndexOfAccount, instruction_accounts::InstructionAccount, + transaction_accounts::KeyedAccountSharedData, + }, + std::{ + cell::{Cell, RefCell}, + mem, ptr, + rc::Rc, + slice, + }, + }; + + macro_rules! mock_invoke_context { + ($invoke_context:ident, + $transaction_context:ident, + $instruction_data:expr, + $transaction_accounts:expr, + $program_account:expr, + $instruction_accounts:expr) => { + let instruction_data = $instruction_data; + let instruction_accounts = $instruction_accounts + .iter() + .map(|index_in_transaction| { + InstructionAccount::new( + *index_in_transaction as IndexOfAccount, + false, + $transaction_accounts[*index_in_transaction as usize].2, + ) + }) + .collect::>(); + let transaction_accounts = $transaction_accounts + .into_iter() + .map(|a| (a.0, a.1)) + .collect::>(); + let mut feature_set = SVMFeatureSet::all_enabled(); + feature_set.syscall_parameter_address_restrictions = false; + let feature_set = &feature_set; + with_mock_invoke_context_with_feature_set!( + $invoke_context, + $transaction_context, + feature_set, + transaction_accounts + ); + $invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + $program_account, + instruction_accounts, + instruction_data.to_vec(), + ) + .unwrap(); + $invoke_context.push().unwrap(); + }; + } + + macro_rules! borrow_instruction_account { + ($borrowed_account:ident, $invoke_context:expr, $index:expr) => { + let instruction_context = + $invoke_context.transaction_context.get_current_instruction_context().unwrap(); + let $borrowed_account = + instruction_context.try_borrow_instruction_account($index).unwrap(); + }; + } + + struct MockCallerAccount { + lamports: u64, + owner: Pubkey, + vm_addr: u64, + data: Vec, + len: u64, + regions: Vec, + } + + impl MockCallerAccount { + fn new(lamports: u64, owner: Pubkey, data: &[u8]) -> MockCallerAccount { + let vm_addr = MM_INPUT_START; + let mut region_addr = vm_addr; + let region_len = mem::size_of::(); + let mut d = vec![0; region_len]; + let mut regions = vec![]; + + unsafe { ptr::write_unaligned::(d.as_mut_ptr().cast(), data.len() as u64) }; + + regions.push(MemoryRegion::new(&raw mut d[..], vm_addr)); + region_addr += region_len as u64; + + regions.push(MemoryRegion::new(&raw const data[..], region_addr)); + + MockCallerAccount { + lamports, + owner, + vm_addr, + data: d, + len: data.len() as u64, + regions, + } + } + + fn caller_account(&mut self) -> CallerAccount<'_> { + CallerAccount { + lamports: &mut self.lamports, + owner: &mut self.owner, + original_data_len: self.len as usize, + serialized_data: &mut [], + vm_data_addr: self.vm_addr + mem::size_of::() as u64, + ref_to_len_in_vm: &mut self.len, + } + } + } + + struct MockAccountInfo<'a> { + key: Pubkey, + is_signer: bool, + is_writable: bool, + lamports: u64, + data: &'a [u8], + owner: Pubkey, + executable: bool, + _unused: u64, + } + + impl MockAccountInfo<'_> { + fn new(key: Pubkey, account: &AccountSharedData) -> MockAccountInfo<'_> { + MockAccountInfo { + key, + is_signer: false, + is_writable: false, + lamports: account.lamports(), + data: account.data(), + owner: *account.owner(), + executable: account.executable(), + _unused: account.rent_epoch(), + } + } + + fn into_region(self, vm_addr: u64) -> (Vec, MemoryRegion, SerializedAccountMetadata) { + let size = mem::size_of::() + + mem::size_of::() * 2 + + mem::size_of::>>() + + mem::size_of::() + + mem::size_of::>>() + + self.data.len(); + let mut data = vec![0; size]; + + let vm_addr = vm_addr as usize; + let key_addr = vm_addr + mem::size_of::(); + let lamports_cell_addr = key_addr + mem::size_of::(); + let lamports_addr = lamports_cell_addr + mem::size_of::>>(); + let owner_addr = lamports_addr + mem::size_of::(); + let data_cell_addr = owner_addr + mem::size_of::(); + let data_addr = data_cell_addr + mem::size_of::>>(); + + #[allow(deprecated)] + #[allow(clippy::used_underscore_binding)] + let info = AccountInfo { + key: unsafe { (key_addr as *const Pubkey).as_ref() }.unwrap(), + is_signer: self.is_signer, + is_writable: self.is_writable, + lamports: unsafe { + Rc::from_raw((lamports_cell_addr + RcBox::<&mut u64>::VALUE_OFFSET) as *const _) + }, + data: unsafe { + Rc::from_raw((data_cell_addr + RcBox::<&mut [u8]>::VALUE_OFFSET) as *const _) + }, + owner: unsafe { (owner_addr as *const Pubkey).as_ref() }.unwrap(), + executable: self.executable, + _unused: self._unused, + }; + + unsafe { + ptr::write_unaligned(data.as_mut_ptr().cast(), info); + ptr::write_unaligned( + (data.as_mut_ptr() as usize + key_addr - vm_addr) as *mut _, + self.key, + ); + ptr::write_unaligned( + (data.as_mut_ptr() as usize + lamports_cell_addr - vm_addr) as *mut _, + RcBox::new(RefCell::new((lamports_addr as *mut u64).as_mut().unwrap())), + ); + ptr::write_unaligned( + (data.as_mut_ptr() as usize + lamports_addr - vm_addr) as *mut _, + self.lamports, + ); + ptr::write_unaligned( + (data.as_mut_ptr() as usize + owner_addr - vm_addr) as *mut _, + self.owner, + ); + ptr::write_unaligned( + (data.as_mut_ptr() as usize + data_cell_addr - vm_addr) as *mut _, + RcBox::new(RefCell::new(slice::from_raw_parts_mut( + data_addr as *mut u8, + self.data.len(), + ))), + ); + data[data_addr - vm_addr..].copy_from_slice(self.data); + } + + let region = MemoryRegion::new(&raw mut data[..], vm_addr as u64); + ( + data, + region, + SerializedAccountMetadata { + vm_addr: vm_addr as u64, + original_data_len: self.data.len(), + vm_key_addr: key_addr as u64, + vm_lamports_addr: lamports_addr as u64, + vm_owner_addr: owner_addr as u64, + vm_data_addr: data_addr as u64, + }, + ) + } + } + + struct MockInstruction { + program_id: Pubkey, + accounts: Vec, + data: Vec, + } + + impl MockInstruction { + 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 mut data = vec![0; size]; + + let vm_addr = vm_addr as usize; + let accounts_addr = vm_addr + mem::size_of::(); + let data_addr = accounts_addr + accounts_len; + + let ins = Instruction { + program_id: self.program_id, + accounts: unsafe { + Vec::from_raw_parts( + accounts_addr as *mut _, + self.accounts.len(), + self.accounts.len(), + ) + }, + data: unsafe { + 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); + data[accounts_addr - vm_addr..][..accounts_len].copy_from_slice( + slice::from_raw_parts(self.accounts.as_ptr().cast(), accounts_len), + ); + data[data_addr - vm_addr..].copy_from_slice(&self.data); + } + + let region = MemoryRegion::new(&raw mut data[..], vm_addr as u64); + (data, region) + } + } + + #[repr(C)] + struct RcBox { + strong: Cell, + weak: Cell, + value: T, + } + + impl RcBox { + const VALUE_OFFSET: usize = mem::size_of::>() * 2; + fn new(value: T) -> RcBox { + RcBox { + strong: Cell::new(0), + weak: Cell::new(0), + value, + } + } + } + + type TestTransactionAccount = (Pubkey, AccountSharedData, bool); + + fn transaction_with_one_writable_instruction_account( + data: Vec, + ) -> Vec { + let program_id = Pubkey::new_unique(); + let account = AccountSharedData::from(Account { + lamports: 1, + data, + owner: program_id, + executable: false, + rent_epoch: 100, + }); + vec![ + ( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + false, + ), + (Pubkey::new_unique(), account, true), + ] + } + + fn transaction_with_one_readonly_instruction_account( + data: Vec, + ) -> Vec { + let program_id = Pubkey::new_unique(); + let account_owner = Pubkey::new_unique(); + let account = AccountSharedData::from(Account { + lamports: 1, + data, + owner: account_owner, + executable: false, + rent_epoch: 100, + }); + vec![ + ( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + false, + ), + (Pubkey::new_unique(), account, true), + ] + } + + fn mock_signers(signers: &[&[u8]], vm_addr: u64) -> (Vec, MemoryRegion) { + let vm_addr = vm_addr as usize; + + // calculate size + let fat_ptr_size_of_slice = mem::size_of::<&[()]>(); // pointer size + length size + let singers_length = signers.len(); + let sum_signers_data_length: usize = signers.iter().map(|s| s.len()).sum(); + + // init data vec + let total_size = fat_ptr_size_of_slice + + singers_length * fat_ptr_size_of_slice + + sum_signers_data_length; + let mut data = vec![0; total_size]; + + // data is composed by 3 parts + // A. + // [ singers address, singers length, ..., + // B. | + // signer1 address, signer1 length, signer2 address ..., + // ^ p1 ---> + // C. | + // signer1 data, signer2 data, ... ] + // ^ p2 ---> + + // A. + data[..fat_ptr_size_of_slice / 2] + .clone_from_slice(&(fat_ptr_size_of_slice + vm_addr).to_le_bytes()); + data[fat_ptr_size_of_slice / 2..fat_ptr_size_of_slice] + .clone_from_slice(&(singers_length).to_le_bytes()); + + // B. + C. + let (mut p1, mut p2) = ( + fat_ptr_size_of_slice, + fat_ptr_size_of_slice + singers_length * fat_ptr_size_of_slice, + ); + for signer in signers.iter() { + let signer_length = signer.len(); + + // B. + data[p1..p1 + fat_ptr_size_of_slice / 2] + .clone_from_slice(&(p2 + vm_addr).to_le_bytes()); + data[p1 + fat_ptr_size_of_slice / 2..p1 + fat_ptr_size_of_slice] + .clone_from_slice(&(signer_length).to_le_bytes()); + p1 += fat_ptr_size_of_slice; + + // C. + data[p2..p2 + signer_length].clone_from_slice(signer); + p2 += signer_length; + } + + let region = MemoryRegion::new(&raw mut data[..], vm_addr as u64); + (data, region) + } + + #[test] + fn test_translate_instruction() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foo".to_vec()); + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let program_id = Pubkey::new_unique(); + let accounts = vec![AccountMeta { + pubkey: Pubkey::new_unique(), + is_signer: true, + is_writable: false, + }]; + let data = b"ins data".to_vec(); + let vm_addr = MM_INPUT_START; + let (_mem, region) = MockInstruction { + program_id, + accounts: accounts.clone(), + data: data.clone(), + } + .into_region(vm_addr); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = + unsafe { MemoryMapping::new(vec![region], &config, SBPFVersion::V3) }.unwrap(); + invoke_context + .memory_contexts + .set_memory_context_abi_v1(MemoryContext::new( + BpfAllocator::new(solana_program_entrypoint::HEAP_LENGTH as u64), + Vec::new(), + memory_mapping, + )) + .unwrap(); + + let ins = translate_instruction_rust(vm_addr, &invoke_context).unwrap(); + assert_eq!(ins.program_id, program_id); + assert_eq!(ins.accounts, accounts); + assert_eq!(ins.data, data); + } + + #[test] + fn test_translate_signers() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foo".to_vec()); + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let program_id = Pubkey::new_unique(); + let (derived_key, bump_seed) = Pubkey::find_program_address(&[b"foo"], &program_id); + + let vm_addr = MM_INPUT_START; + let (_mem, region) = mock_signers(&[b"foo", &[bump_seed]], vm_addr); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = + unsafe { MemoryMapping::new(vec![region], &config, SBPFVersion::V3) }.unwrap(); + invoke_context + .memory_contexts + .set_memory_context_abi_v1(MemoryContext::new( + BpfAllocator::new(solana_program_entrypoint::HEAP_LENGTH as u64), + Vec::new(), + memory_mapping, + )) + .unwrap(); + + let signers = translate_signers_rust(&program_id, vm_addr, 1, &invoke_context).unwrap(); + assert_eq!(signers[0], derived_key); + } + + #[test] + fn test_translate_accounts_rust() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foobar".to_vec()); + let account = transaction_accounts[1].1.clone(); + let key = transaction_accounts[1].0; + let original_data_len = account.data().len(); + + let vm_addr = MM_INPUT_START; + let (_mem, region, account_metadata) = + MockAccountInfo::new(key, &account).into_region(vm_addr); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = + unsafe { MemoryMapping::new(vec![region], &config, SBPFVersion::V3) }.unwrap(); + + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1, 1] + ); + + invoke_context + .set_syscall_context(SyscallContext { + allocator: BpfAllocator::new(solana_program_entrypoint::HEAP_LENGTH as u64), + accounts_metadata: vec![account_metadata], + }) + .unwrap(); + invoke_context + .memory_contexts + .set_memory_context_abi_v1(MemoryContext::new( + BpfAllocator::new(solana_program_entrypoint::HEAP_LENGTH as u64), + Vec::new(), + memory_mapping, + )) + .unwrap(); + + invoke_context + .transaction_context + .configure_next_cpi_for_tests( + 0, + vec![ + InstructionAccount::new(1, false, true), + InstructionAccount::new(1, false, true), + ], + vec![], + ) + .unwrap(); + let accounts = translate_accounts_rust(vm_addr, 1, &invoke_context).unwrap(); + assert_eq!(accounts.len(), 1); + let caller_account = &accounts[0].caller_account; + assert!(caller_account.serialized_data.is_empty()); + assert_eq!(caller_account.original_data_len, original_data_len); + } + + #[test] + fn test_get_serialized_data() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foo".to_vec()); + let account = transaction_accounts[1].1.clone(); + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + assert_matches!( + CallerAccount::get_serialized_data( + true, // check_aligned + account.data().len(), + account.data().len().saturating_add(MAX_PERMITTED_DATA_INCREASE).saturating_add(1), + true, // syscall_parameter_address_restrictions + ), + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::InvalidRealloc + ); + } + + #[test] + fn test_caller_account_from_account_info() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foo".to_vec()); + let account = transaction_accounts[1].1.clone(); + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let key = Pubkey::new_unique(); + let vm_addr = MM_INPUT_START; + let (_mem, region, account_metadata) = + MockAccountInfo::new(key, &account).into_region(vm_addr); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = + unsafe { MemoryMapping::new(vec![region], &config, SBPFVersion::V3) }.unwrap(); + + let account_info = translate_type::(&memory_mapping, vm_addr, false).unwrap(); + + let caller_account = CallerAccount::from_account_info( + &invoke_context, + &memory_mapping, + true, // check_aligned + account_info, + &account_metadata, + ) + .unwrap(); + assert_eq!(*caller_account.lamports, account.lamports()); + assert_eq!(caller_account.owner, account.owner()); + assert_eq!(caller_account.original_data_len, account.data().len()); + assert_eq!( + *caller_account.ref_to_len_in_vm as usize, + account.data().len() + ); + assert!(caller_account.serialized_data.is_empty()); + } + + #[test] + fn test_update_caller_account_lamports_owner() { + let transaction_accounts = transaction_with_one_writable_instruction_account(vec![]); + let account = transaction_accounts[1].1.clone(); + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let mut mock_caller_account = + MockCallerAccount::new(1234, *account.owner(), account.data()); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = unsafe { + MemoryMapping::new( + mock_caller_account.regions.split_off(0), + &config, + SBPFVersion::V3, + ) + } + .unwrap(); + + let mut caller_account = mock_caller_account.caller_account(); + let instruction_context = + invoke_context.transaction_context.get_current_instruction_context().unwrap(); + let mut callee_account = instruction_context.try_borrow_instruction_account(0).unwrap(); + callee_account.set_lamports(42).unwrap(); + callee_account.set_owner(Pubkey::new_unique().as_ref()).unwrap(); + + update_caller_account( + &invoke_context, + &memory_mapping, + true, // check_aligned + &mut caller_account, + &mut callee_account, + false, // syscall_parameter_address_restrictions + ) + .unwrap(); + + assert_eq!(*caller_account.lamports, 42); + assert_eq!(caller_account.owner, callee_account.get_owner()); + } + + #[test] + fn test_update_caller_account_data() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foobar".to_vec()); + let account = transaction_accounts[1].1.clone(); + let original_data_len = account.data().len(); + + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let mut mock_caller_account = + MockCallerAccount::new(account.lamports(), *account.owner(), account.data()); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = unsafe { + MemoryMapping::new( + mock_caller_account.regions.clone(), + &config, + SBPFVersion::V3, + ) + } + .unwrap(); + + let len_ptr = mock_caller_account.data.as_ptr(); + let serialized_len = || unsafe { *len_ptr.cast::() as usize }; + let mut caller_account = mock_caller_account.caller_account(); + let instruction_context = + invoke_context.transaction_context.get_current_instruction_context().unwrap(); + let mut callee_account = instruction_context.try_borrow_instruction_account(0).unwrap(); + + for new_value in [b"foo".to_vec(), b"foobaz".to_vec(), b"foobazbad".to_vec()] { + assert!(caller_account.serialized_data.is_empty()); + callee_account.set_data_from_slice(&new_value).unwrap(); + + update_caller_account( + &invoke_context, + &memory_mapping, + true, // check_aligned + &mut caller_account, + &mut callee_account, + false, // syscall_parameter_address_restrictions + ) + .unwrap(); + + let data_len = callee_account.get_data().len(); + assert_eq!(data_len, *caller_account.ref_to_len_in_vm as usize); + assert_eq!(data_len, serialized_len()); + assert!(caller_account.serialized_data.is_empty()); + } + + callee_account + .set_data_length(original_data_len + MAX_PERMITTED_DATA_INCREASE) + .unwrap(); + update_caller_account( + &invoke_context, + &memory_mapping, + true, // check_aligned + &mut caller_account, + &mut callee_account, + false, // syscall_parameter_address_restrictions + ) + .unwrap(); + let data_len = callee_account.get_data().len(); + assert_eq!(data_len, serialized_len()); + + callee_account + .set_data_length(original_data_len + MAX_PERMITTED_DATA_INCREASE + 1) + .unwrap(); + assert_matches!( + update_caller_account( + &invoke_context, + &memory_mapping, + true, // check_aligned + &mut caller_account, + &mut callee_account, + false, // syscall_parameter_address_restrictions + ), + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::InvalidRealloc + ); + + // close the account + callee_account.set_data_length(0).unwrap(); + callee_account.set_owner(system_program::id().as_ref()).unwrap(); + update_caller_account( + &invoke_context, + &memory_mapping, + true, // check_aligned + &mut caller_account, + &mut callee_account, + false, // syscall_parameter_address_restrictions + ) + .unwrap(); + let data_len = callee_account.get_data().len(); + assert_eq!(data_len, 0); + } + + #[test] + fn test_update_callee_account_lamports_owner() { + let transaction_accounts = transaction_with_one_writable_instruction_account(vec![]); + let account = transaction_accounts[1].1.clone(); + + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let mut mock_caller_account = + MockCallerAccount::new(1234, *account.owner(), account.data()); + let caller_account = mock_caller_account.caller_account(); + + borrow_instruction_account!(callee_account, invoke_context, 0); + + *caller_account.lamports = 42; + *caller_account.owner = Pubkey::new_unique(); + + update_callee_account(&caller_account, callee_account).unwrap(); + + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_eq!(callee_account.get_lamports(), 42); + assert_eq!(caller_account.owner, callee_account.get_owner()); + } + + #[test] + fn test_update_callee_account_data_writable() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foobar".to_vec()); + let account = transaction_accounts[1].1.clone(); + + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let mut mock_caller_account = + MockCallerAccount::new(1234, *account.owner(), account.data()); + let mut caller_account = mock_caller_account.caller_account(); + borrow_instruction_account!(callee_account, invoke_context, 0); + + assert!(caller_account.serialized_data.is_empty()); + assert!(!update_callee_account(&caller_account, callee_account).unwrap()); + + // growing resize + *caller_account.ref_to_len_in_vm = b"foobarbaz".len() as u64; + borrow_instruction_account!(callee_account, invoke_context, 0); + assert!(update_callee_account(&caller_account, callee_account).unwrap(),); + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_eq!(callee_account.get_data().len(), b"foobarbaz".len()); + drop(callee_account); + + // truncating resize + *caller_account.ref_to_len_in_vm = b"baz".len() as u64; + borrow_instruction_account!(callee_account, invoke_context, 0); + assert!(update_callee_account(&caller_account, callee_account).unwrap(),); + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_eq!(callee_account.get_data().len(), b"baz".len()); + drop(callee_account); + + // close the account + *caller_account.ref_to_len_in_vm = 0; + let mut owner = system_program::id(); + caller_account.owner = &mut owner; + borrow_instruction_account!(callee_account, invoke_context, 0); + update_callee_account(&caller_account, callee_account).unwrap(); + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_eq!(callee_account.get_data(), b""); + } + + #[test] + fn test_update_callee_account_data_readonly() { + let transaction_accounts = + transaction_with_one_readonly_instruction_account(b"foobar".to_vec()); + let account = transaction_accounts[1].1.clone(); + + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let mut mock_caller_account = + MockCallerAccount::new(1234, *account.owner(), account.data()); + let caller_account = mock_caller_account.caller_account(); + + // growing resize + *caller_account.ref_to_len_in_vm = b"foobarbaz".len() as u64; + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_matches!( + update_callee_account(&caller_account, callee_account), + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::AccountDataSizeChanged + ); + + // truncating resize + *caller_account.ref_to_len_in_vm = b"baz".len() as u64; + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_matches!( + update_callee_account(&caller_account, callee_account), + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::AccountDataSizeChanged + ); + } +} diff --git a/solana/program-runtime/src/deploy.rs b/solana/program-runtime/src/deploy.rs new file mode 100644 index 0000000..58d522f --- /dev/null +++ b/solana/program-runtime/src/deploy.rs @@ -0,0 +1,113 @@ +//! Program deployment functionality. + +use { + crate::{ + invoke_context::InvokeContext, + loaded_programs::{ProgramCacheEntry, ProgramCacheForTxBatch, ProgramRuntimeEnvironment}, + }, + solana_instruction::error::InstructionError, + solana_pubkey::Pubkey, + solana_sbpf::{ + elf::{ElfError, Executable}, + program::BuiltinProgram, + verifier::RequisiteVerifier, + }, + solana_svm_log_collector::{LogCollector, ic_logger_msg}, + solana_svm_type_overrides::sync::Arc, + std::{cell::RefCell, rc::Rc}, +}; + +fn morph_into_deployment_environment_v1<'a>( + from: Arc>>, +) -> Result>, ElfError> { + let mut config = from.get_config().clone(); + config.reject_broken_elfs = true; + // Once the tests are being build using a toolchain which supports the newer SBPF versions, + // the deployment of older versions will be disabled: + // config.enabled_sbpf_versions = + // *config.enabled_sbpf_versions.end()..=*config.enabled_sbpf_versions.end(); + + let mut result = BuiltinProgram::new_loader(config); + + for (_key, (name, value)) in from.get_function_registry().iter() { + // Deployment of programs with sol_alloc_free is disabled. So do not register the syscall. + if name != *b"sol_alloc_free_" { + result.register_function(unsafe { std::str::from_utf8_unchecked(name) }, value)?; + } + } + + Ok(result) +} + +/// Directly deploy a program using a provided invoke context. +/// This function should only be invoked from the runtime, since it does not +/// provide any account loads or checks. +#[allow(clippy::too_many_arguments)] +pub fn deploy_program( + log_collector: Option>>, + program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, + program_runtime_environment: ProgramRuntimeEnvironment, + program_id: &Pubkey, + programdata: &[u8], +) -> Result<(), InstructionError> { + let deployment_program_runtime_environment = morph_into_deployment_environment_v1(Arc::clone( + &*program_runtime_environment, + )) + .map_err(|e| { + ic_logger_msg!(log_collector, "Failed to register syscalls: {}", e); + InstructionError::ProgramEnvironmentSetupFailure + })?; + // Verify using stricter deployment_program_runtime_environment + let executable = Executable::::load( + programdata, + Arc::new(deployment_program_runtime_environment), + ) + .map_err(|err| { + ic_logger_msg!(log_collector, "{}", err); + InstructionError::InvalidAccountData + })?; + executable.verify::().map_err(|err| { + ic_logger_msg!(log_collector, "{}", err); + InstructionError::InvalidAccountData + })?; + // Reload but with program_runtime_environment + let executor = unsafe { + // SAFETY: The executable has been verified just above. + ProgramCacheEntry::reload(program_runtime_environment, programdata) + } + .map_err(|err| { + ic_logger_msg!(log_collector, "{}", err); + InstructionError::InvalidAccountData + })?; + program_cache_for_tx_batch.store_modified_entry(*program_id, Arc::new(executor)); + Ok(()) +} + +#[macro_export] +macro_rules! deploy_program { + ($invoke_context:expr, $program_id:expr, $loader_key:expr, $account_size:expr, $programdata:expr, $deployment_slot:expr $(,)?) => { + assert_eq!( + $deployment_slot, + $invoke_context.program_cache_for_tx_batch.slot() + ); + #[cfg(feature = "metrics")] + let mut load_program_metrics = $crate::loaded_programs::LoadProgramMetrics::default(); + $crate::deploy::deploy_program( + $invoke_context.get_log_collector(), + #[cfg(feature = "metrics")] + &mut load_program_metrics, + $invoke_context.program_cache_for_tx_batch, + $invoke_context + .get_program_runtime_environments_for_deployment() + .get_env_for_deployment() + .clone(), + $program_id, + $loader_key, + $account_size, + $programdata, + $deployment_slot, + )?; + #[cfg(feature = "metrics")] + load_program_metrics.submit_datapoint(&mut $invoke_context.timings); + }; +} diff --git a/solana/program-runtime/src/execution_budget.rs b/solana/program-runtime/src/execution_budget.rs new file mode 100644 index 0000000..3a4b944 --- /dev/null +++ b/solana/program-runtime/src/execution_budget.rs @@ -0,0 +1,342 @@ +use { + solana_fee_structure::FeeDetails, solana_program_entrypoint::HEAP_LENGTH, + solana_transaction_context::MAX_INSTRUCTION_TRACE_LENGTH, std::num::NonZeroU32, +}; + +/// Max instruction stack depth. This is the maximum nesting of instructions that can happen during +/// a transaction. +pub const MAX_INSTRUCTION_STACK_DEPTH: usize = 5; +/// Max instruction stack depth with SIMD-0268 enabled. Allows 8 nested CPIs. +pub const MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268: usize = 9; + +fn get_max_instruction_stack_depth(simd_0268_active: bool) -> usize { + if simd_0268_active { + MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268 + } else { + MAX_INSTRUCTION_STACK_DEPTH + } +} + +/// Default CPI invocation cost. +pub const DEFAULT_INVOCATION_COST: u64 = 1000; +/// CPI invocation cost with SIMD-0339 active. +pub const INVOKE_UNITS_COST_SIMD_0339: u64 = 946; + +fn get_invoke_unit_cost(simd_0339_active: bool) -> u64 { + if simd_0339_active { + INVOKE_UNITS_COST_SIMD_0339 + } else { + DEFAULT_INVOCATION_COST + } +} + +/// Max call depth. This is the maximum nesting of SBF to SBF call that can happen within a program. +pub const MAX_CALL_DEPTH: usize = 64; + +/// The size of one SBF stack frame. +pub const STACK_FRAME_SIZE: usize = 4096; + +/// Maximum compute units a transaction may request. +pub const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000; + +/// Roughly 0.5us/page, where page is 32K; given roughly 15CU/us, the +/// default heap page cost = 0.5 * 15 ~= 8CU/page +pub const DEFAULT_HEAP_COST: u64 = 8; +/// Default compute-unit limit for an instruction. +pub const DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT: u32 = 200_000; +/// Maximum compute units allocated to native builtins that have not moved to SBF. +pub const MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT: u32 = 3_000; +/// Maximum heap frame size a transaction may request. +pub const MAX_HEAP_FRAME_BYTES: u32 = 256 * 1024; +/// Minimum heap frame size. +pub const MIN_HEAP_FRAME_BYTES: u32 = HEAP_LENGTH as u32; + +/// Default loaded account data limit for one transaction. +pub const MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES: NonZeroU32 = + NonZeroU32::new(64 * 1024 * 1024).unwrap(); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SVMTransactionExecutionBudget { + /// Number of compute units that a transaction or individual instruction is + /// allowed to consume. Compute units are consumed by program execution, + /// resources they use, etc... + pub compute_unit_limit: u64, + /// Maximum program instruction invocation stack depth. Invocation stack + /// depth starts at 1 for transaction instructions and the stack depth is + /// incremented each time a program invokes an instruction and decremented + /// when a program returns. + pub max_instruction_stack_depth: usize, + /// Maximum cross-program invocation and instructions per transaction + pub max_instruction_trace_length: usize, + /// Maximum number of slices hashed per syscall + pub sha256_max_slices: u64, + /// Maximum SBF to BPF call depth + pub max_call_depth: usize, + /// Size of a stack frame in bytes, must match the size specified in the LLVM SBF backend + pub stack_frame_size: usize, + /// program heap region size, default: solana_program_entrypoint::HEAP_LENGTH + pub heap_size: u32, +} + +impl Default for SVMTransactionExecutionBudget { + fn default() -> Self { + Self::new_with_defaults(/* simd_0268_active */ false) + } +} + +impl SVMTransactionExecutionBudget { + /// Creates the default execution budget for the selected feature state. + pub fn new_with_defaults(simd_0268_active: bool) -> Self { + SVMTransactionExecutionBudget { + compute_unit_limit: u64::from(MAX_COMPUTE_UNIT_LIMIT), + max_instruction_stack_depth: get_max_instruction_stack_depth(simd_0268_active), + max_instruction_trace_length: MAX_INSTRUCTION_TRACE_LENGTH, + sha256_max_slices: 20_000, + max_call_depth: MAX_CALL_DEPTH, + stack_frame_size: STACK_FRAME_SIZE, + heap_size: u32::try_from(solana_program_entrypoint::HEAP_LENGTH).unwrap(), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SVMTransactionExecutionCost { + /// Number of compute units consumed by a log_u64 call + pub log_64_units: u64, + /// Number of compute units consumed by a create_program_address call + pub create_program_address_units: u64, + /// Number of compute units consumed by an invoke call (not including the cost incurred by + /// the called program) + pub invoke_units: u64, + /// Base number of compute units consumed to call SHA256 + pub sha256_base_cost: u64, + /// Incremental number of units consumed by SHA256 (based on bytes) + pub sha256_byte_cost: u64, + /// Number of compute units consumed by logging a `Pubkey` + pub log_pubkey_units: u64, + /// Number of account data bytes per compute unit charged during a cross-program invocation + pub cpi_bytes_per_unit: u64, + /// Base number of compute units consumed to get a sysvar + pub sysvar_base_cost: u64, + /// Number of compute units consumed to call secp256k1_recover + pub secp256k1_recover_cost: u64, + /// Number of compute units consumed to do a syscall without any work + pub syscall_base_cost: u64, + /// Number of compute units consumed to validate a curve25519 edwards point + pub curve25519_edwards_validate_point_cost: u64, + /// Number of compute units consumed to add two curve25519 edwards points + pub curve25519_edwards_add_cost: u64, + /// Number of compute units consumed to subtract two curve25519 edwards points + pub curve25519_edwards_subtract_cost: u64, + /// Number of compute units consumed to multiply a curve25519 edwards point + pub curve25519_edwards_multiply_cost: u64, + /// Number of compute units consumed for a multiscalar multiplication (msm) of edwards points. + /// The total cost is calculated as `msm_base_cost + (length - 1) * msm_incremental_cost`. + pub curve25519_edwards_msm_base_cost: u64, + /// Number of compute units consumed for a multiscalar multiplication (msm) of edwards points. + /// The total cost is calculated as `msm_base_cost + (length - 1) * msm_incremental_cost`. + pub curve25519_edwards_msm_incremental_cost: u64, + /// Number of compute units consumed to validate a curve25519 ristretto point + pub curve25519_ristretto_validate_point_cost: u64, + /// Number of compute units consumed to add two curve25519 ristretto points + pub curve25519_ristretto_add_cost: u64, + /// Number of compute units consumed to subtract two curve25519 ristretto points + pub curve25519_ristretto_subtract_cost: u64, + /// Number of compute units consumed to multiply a curve25519 ristretto point + pub curve25519_ristretto_multiply_cost: u64, + /// Number of compute units consumed for a multiscalar multiplication (msm) of ristretto points. + /// The total cost is calculated as `msm_base_cost + (length - 1) * msm_incremental_cost`. + pub curve25519_ristretto_msm_base_cost: u64, + /// Number of compute units consumed for a multiscalar multiplication (msm) of ristretto points. + /// The total cost is calculated as `msm_base_cost + (length - 1) * msm_incremental_cost`. + pub curve25519_ristretto_msm_incremental_cost: u64, + /// Number of compute units per additional 32k heap above the default (~.5 + /// us per 32k at 15 units/us rounded up) + pub heap_cost: u64, + /// Memory operation syscall base cost + pub mem_op_base_cost: u64, + /// Number of compute units consumed to call alt_bn128_g1_addition + pub alt_bn128_g1_addition_cost: u64, + /// Number of compute units consumed to call alt_bn128_g2_addition + pub alt_bn128_g2_addition_cost: u64, + /// Number of compute units consumed to call alt_bn128_g1_multiplication. + pub alt_bn128_g1_multiplication_cost: u64, + /// Number of compute units consumed to call alt_bn128_g2_multiplication. + pub alt_bn128_g2_multiplication_cost: u64, + /// Total cost will be alt_bn128_pairing_one_pair_cost_first + /// + alt_bn128_pairing_one_pair_cost_other * (num_elems - 1) + pub alt_bn128_pairing_one_pair_cost_first: u64, + pub alt_bn128_pairing_one_pair_cost_other: u64, + /// Big integer modular exponentiation base cost + pub big_modular_exponentiation_base_cost: u64, + /// Big integer moduler exponentiation cost divisor + /// The modular exponentiation cost is computed as + /// `input_length`/`big_modular_exponentiation_cost_divisor` + `big_modular_exponentiation_base_cost` + pub big_modular_exponentiation_cost_divisor: u64, + /// Coefficient `a` of the quadratic function which determines the number + /// of compute units consumed to call poseidon syscall for a given number + /// of inputs. + pub poseidon_cost_coefficient_a: u64, + /// Coefficient `c` of the quadratic function which determines the number + /// of compute units consumed to call poseidon syscall for a given number + /// of inputs. + pub poseidon_cost_coefficient_c: u64, + /// Number of compute units consumed for accessing the remaining compute units. + pub get_remaining_compute_units_cost: u64, + /// Number of compute units consumed to call alt_bn128_g1_compress. + pub alt_bn128_g1_compress: u64, + /// Number of compute units consumed to call alt_bn128_g1_decompress. + pub alt_bn128_g1_decompress: u64, + /// Number of compute units consumed to call alt_bn128_g2_compress. + pub alt_bn128_g2_compress: u64, + /// Number of compute units consumed to call alt_bn128_g2_decompress. + pub alt_bn128_g2_decompress: u64, + /// Number of compute units consumed to add two bls12_381 g1 points. + pub bls12_381_g1_add_cost: u64, + /// Number of compute units consumed to add two bls12_381 g2 points. + pub bls12_381_g2_add_cost: u64, + /// Number of compute units consumed to subtract two bls12_381 g1 points. + pub bls12_381_g1_subtract_cost: u64, + /// Number of compute units consumed to subtract two bls12_381 g2 points. + pub bls12_381_g2_subtract_cost: u64, + /// Number of compute units consumed to multiply a bls12_381 g1 point. + pub bls12_381_g1_multiply_cost: u64, + /// Number of compute units consumed to multiply a bls12_381 g2 point. + pub bls12_381_g2_multiply_cost: u64, + /// Number of compute units consumed to decompress a bls12_381 g1 point. + pub bls12_381_g1_decompress_cost: u64, + /// Number of compute units consumed to decompress a bls12_381 g2 point. + pub bls12_381_g2_decompress_cost: u64, + /// Number of compute units consumed to validate a bls12_381 g1 point. + pub bls12_381_g1_validate_cost: u64, + /// Number of compute units consumed to validate a bls12_381 g2 point. + pub bls12_381_g2_validate_cost: u64, + /// Base number of compute units consumed to perform a bls12_381 pairing. + pub bls12_381_one_pair_cost: u64, + /// Incremental number of compute units consumed per pair in a bls12_381 pairing. + pub bls12_381_additional_pair_cost: u64, +} + +impl Default for SVMTransactionExecutionCost { + fn default() -> Self { + Self::new_with_defaults(/* simd_0339_active */ false) + } +} + +impl SVMTransactionExecutionCost { + pub fn new_with_defaults(simd_0339_active: bool) -> Self { + SVMTransactionExecutionCost { + log_64_units: 100, + create_program_address_units: 1500, + invoke_units: get_invoke_unit_cost(simd_0339_active), + sha256_base_cost: 85, + sha256_byte_cost: 1, + log_pubkey_units: 100, + cpi_bytes_per_unit: 250, // ~50MB at 200,000 units + sysvar_base_cost: 100, + secp256k1_recover_cost: 25_000, + syscall_base_cost: 100, + curve25519_edwards_validate_point_cost: 159, + curve25519_edwards_add_cost: 473, + curve25519_edwards_subtract_cost: 475, + curve25519_edwards_multiply_cost: 2_177, + curve25519_edwards_msm_base_cost: 2_273, + curve25519_edwards_msm_incremental_cost: 758, + curve25519_ristretto_validate_point_cost: 169, + curve25519_ristretto_add_cost: 521, + curve25519_ristretto_subtract_cost: 519, + curve25519_ristretto_multiply_cost: 2_208, + curve25519_ristretto_msm_base_cost: 2303, + curve25519_ristretto_msm_incremental_cost: 788, + heap_cost: DEFAULT_HEAP_COST, + mem_op_base_cost: 10, + alt_bn128_g1_addition_cost: 334, + alt_bn128_g2_addition_cost: 535, + alt_bn128_g1_multiplication_cost: 3_840, + alt_bn128_g2_multiplication_cost: 15_670, + alt_bn128_pairing_one_pair_cost_first: 36_364, + alt_bn128_pairing_one_pair_cost_other: 12_121, + big_modular_exponentiation_base_cost: 190, + big_modular_exponentiation_cost_divisor: 2, + poseidon_cost_coefficient_a: 61, + poseidon_cost_coefficient_c: 542, + get_remaining_compute_units_cost: 100, + alt_bn128_g1_compress: 30, + alt_bn128_g1_decompress: 398, + alt_bn128_g2_compress: 86, + alt_bn128_g2_decompress: 13610, + bls12_381_g1_add_cost: 128, + bls12_381_g2_add_cost: 203, + bls12_381_g1_subtract_cost: 129, + bls12_381_g2_subtract_cost: 204, + bls12_381_g1_multiply_cost: 4_627, + bls12_381_g2_multiply_cost: 8_255, + bls12_381_g1_decompress_cost: 2_100, + bls12_381_g2_decompress_cost: 3_050, + bls12_381_g1_validate_cost: 1_565, + bls12_381_g2_validate_cost: 1_968, + bls12_381_one_pair_cost: 25_445, + bls12_381_additional_pair_cost: 13_023, + } + } + + /// Returns cost of the Poseidon hash function for the given number of + /// inputs is determined by the following quadratic function: + /// + /// 61*n^2 + 542 + /// + /// Which approximates the results of benchmarks of light-posiedon + /// library[0]. These results assume 1 CU per 33 ns. Examples: + /// + /// * 1 input + /// * light-poseidon benchmark: `18,303 / 33 ≈ 555` + /// * function: `61*1^2 + 542 = 603` + /// * 2 inputs + /// * light-poseidon benchmark: `25,866 / 33 ≈ 784` + /// * function: `61*2^2 + 542 = 786` + /// * 3 inputs + /// * light-poseidon benchmark: `37,549 / 33 ≈ 1,138` + /// * function; `61*3^2 + 542 = 1091` + /// + /// [0] https://github.com/Lightprotocol/light-poseidon#performance + pub fn poseidon_cost(&self, nr_inputs: u64) -> Option { + let squared_inputs = nr_inputs.checked_pow(2)?; + let mul_result = self.poseidon_cost_coefficient_a.checked_mul(squared_inputs)?; + let final_result = mul_result.checked_add(self.poseidon_cost_coefficient_c)?; + + Some(final_result) + } +} + +/// Execution budget, loaded-data limit, and fee metadata for one transaction. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SVMTransactionExecutionAndFeeBudgetLimits { + /// Compute and invocation limits. + pub budget: SVMTransactionExecutionBudget, + /// Maximum loaded account data size. + pub loaded_accounts_data_size_limit: u32, + /// Fee metadata carried with the transaction. + pub fee_details: FeeDetails, +} + +#[cfg(feature = "dev-context-only-utils")] +impl Default for SVMTransactionExecutionAndFeeBudgetLimits { + fn default() -> Self { + Self { + budget: SVMTransactionExecutionBudget::default(), + loaded_accounts_data_size_limit: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + fee_details: FeeDetails::default(), + } + } +} + +#[cfg(feature = "dev-context-only-utils")] +impl SVMTransactionExecutionAndFeeBudgetLimits { + /// Creates default limits with explicit fee metadata. + pub fn with_fee(fee_details: FeeDetails) -> Self { + Self { + fee_details, + ..SVMTransactionExecutionAndFeeBudgetLimits::default() + } + } +} diff --git a/solana/program-runtime/src/invoke_context.rs b/solana/program-runtime/src/invoke_context.rs new file mode 100644 index 0000000..39c82e1 --- /dev/null +++ b/solana/program-runtime/src/invoke_context.rs @@ -0,0 +1,1909 @@ +use { + crate::{ + execution_budget::{SVMTransactionExecutionBudget, SVMTransactionExecutionCost}, + loaded_programs::{ + ProgramCacheEntry, ProgramCacheEntryType, ProgramCacheForTxBatch, + ProgramRuntimeEnvironments, + }, + memory_context::{MemoryContext, MemoryContexts}, + stable_log, + sysvar_cache::SysvarCache, + }, + solana_account::{AccountSharedData, create_account_shared_data_for_test}, + solana_epoch_schedule::EpochSchedule, + solana_hash::Hash, + solana_instruction::{AccountMeta, Instruction, error::InstructionError}, + solana_pubkey::Pubkey, + solana_sbpf::{ + ebpf::MM_HEAP_START, + elf::Executable as GenericExecutable, + error::{EbpfError, ProgramResult}, + memory_region::MemoryMapping, + program::{BuiltinCodegen, BuiltinFunction, SBPFVersion}, + vm::{Config, ContextObject, EbpfVm}, + }, + solana_sdk_ids::{ + bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4, native_loader, sysvar, + }, + solana_svm_callback::InvokeContextCallback, + solana_svm_feature_set::SVMFeatureSet, + solana_svm_log_collector::{LogCollector, ic_msg}, + solana_svm_measure::measure::Measure, + solana_svm_timings::ExecuteDetailsTimings, + solana_svm_transaction::{instruction::SVMInstruction, svm_message::SVMMessage}, + solana_svm_type_overrides::sync::Arc, + solana_transaction_context::{ + IndexOfAccount, MAX_ACCOUNTS_PER_TRANSACTION, instruction::InstructionContext, + instruction_accounts::InstructionAccount, transaction::TransactionContext, + transaction_accounts::KeyedAccountSharedData, + }, + std::{ + alloc::Layout, + borrow::Cow, + cell::{Cell, RefCell}, + fmt::{self, Debug}, + ptr::NonNull, + rc::Rc, + }, +}; + +pub use crate::memory_context::SerializedAccountMetadata; + +pub type BuiltinFunctionWithContext = ( + BuiltinFunction>, + BuiltinCodegen>, +); +pub type Executable = GenericExecutable>; +pub type RegisterTrace<'a> = &'a [[u64; 12]]; + +/// Adapter so we can unify the interfaces of built-in programs and syscalls +#[macro_export] +macro_rules! declare_process_instruction { + ($process_instruction:ident, $cu_to_consume:expr, |$invoke_context:ident| $inner:tt) => { + $crate::solana_sbpf::declare_builtin_function!( + $process_instruction, + fn rust( + invoke_context: &mut $crate::invoke_context::InvokeContext<'_, '_>, + _arg0: u64, + _arg1: u64, + _arg2: u64, + _arg3: u64, + _arg4: u64, + ) -> Result> { + fn process_instruction_inner( + $invoke_context: &mut $crate::invoke_context::InvokeContext<'_, '_>, + ) -> std::result::Result<(), $crate::__private::InstructionError> + $inner + + let consumption_result = if $cu_to_consume > 0 + { + invoke_context.consume_checked($cu_to_consume) + } else { + Ok(()) + }; + consumption_result + .and_then(|_| { + process_instruction_inner(invoke_context) + .map(|_| 0) + .map_err(|err| Box::new(err) as Box) + }) + .into() + } + ); + }; +} + +impl ContextObject for InvokeContext<'_, '_> { + fn consume(&mut self, amount: u64) { + // 1 to 1 instruction to compute unit mapping + // ignore overflow, Ebpf will bail if exceeded + let compute_meter = self.compute_meter.0.get(); + self.compute_meter.0.set(compute_meter.saturating_sub(amount)); + } + + fn get_remaining(&self) -> u64 { + self.compute_meter.0.get() + } + + fn active_mapping_ptr(&mut self) -> NonNull { + let memory_mapping = self + .memory_contexts + .memory_mapping_mut() + .expect("memory context must be set for the current instruction"); + NonNull::from(memory_mapping) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct AllocErr; +impl fmt::Display for AllocErr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("Error: Memory allocation failed") + } +} + +pub struct BpfAllocator { + len: u64, + pos: u64, +} + +impl BpfAllocator { + pub fn new(len: u64) -> Self { + Self { len, pos: 0 } + } + + pub fn alloc(&mut self, layout: Layout) -> Result { + let bytes_to_align = (self.pos as *const u8).align_offset(layout.align()) as u64; + if self.pos.saturating_add(bytes_to_align).saturating_add(layout.size() as u64) <= self.len + { + self.pos = self.pos.saturating_add(bytes_to_align); + let addr = MM_HEAP_START.saturating_add(self.pos); + self.pos = self.pos.saturating_add(layout.size() as u64); + Ok(addr) + } else { + Err(AllocErr) + } + } +} + +pub struct EnvironmentConfig<'a> { + pub blockhash: Hash, + pub blockhash_lamports_per_signature: u64, + epoch_stake_callback: &'a dyn InvokeContextCallback, + feature_set: &'a SVMFeatureSet, + pub program_runtime_environments_for_execution: &'a ProgramRuntimeEnvironments, + sysvar_cache: &'a SysvarCache, +} +impl<'a> EnvironmentConfig<'a> { + pub fn new( + blockhash: Hash, + blockhash_lamports_per_signature: u64, + epoch_stake_callback: &'a dyn InvokeContextCallback, + feature_set: &'a SVMFeatureSet, + program_runtime_environments_for_execution: &'a ProgramRuntimeEnvironments, + sysvar_cache: &'a SysvarCache, + ) -> Self { + Self { + blockhash, + blockhash_lamports_per_signature, + epoch_stake_callback, + feature_set, + program_runtime_environments_for_execution, + sysvar_cache, + } + } + + /// Get cached sysvars. + pub fn sysvar_cache(&self) -> &SysvarCache { + self.sysvar_cache + } +} + +pub struct SyscallContext { + pub allocator: BpfAllocator, + pub accounts_metadata: Vec, +} + +pub struct ComputeMeter(Cell); + +impl ComputeMeter { + /// Consume compute units. + pub fn consume_checked(&self, amount: u64) -> Result<(), Box> { + let compute_meter = self.0.get(); + let exceeded = compute_meter < amount; + self.0.set(compute_meter.saturating_sub(amount)); + if exceeded { + return Err(Box::new(InstructionError::ComputationalBudgetExceeded)); + } + Ok(()) + } + + /// Set compute units. Only use for tests and benchmarks. + pub fn mock_set_remaining(&self, remaining: u64) { + self.0.set(remaining); + } +} + +/// Main pipeline from runtime to program execution. +pub struct InvokeContext<'a, 'ix_data> { + /// Information about the currently executing transaction. + pub transaction_context: &'a mut TransactionContext<'ix_data>, + /// The local program cache for the transaction batch. + pub program_cache_for_tx_batch: &'a mut ProgramCacheForTxBatch, + /// Runtime configurations used to provision the invocation environment. + pub environment_config: EnvironmentConfig<'a>, + /// The compute budget for the current invocation. + compute_budget: SVMTransactionExecutionBudget, + /// The compute cost for the current invocation. + execution_cost: SVMTransactionExecutionCost, + /// Instruction compute meter, for tracking compute units consumed against + /// the designated compute budget during program execution. + pub compute_meter: ComputeMeter, + log_collector: Option>>, + /// Latest measurement not yet accumulated in [ExecuteDetailsTimings::execute_us] + pub execute_time: Option, + pub timings: ExecuteDetailsTimings, + pub syscall_context: Vec>, + pub memory_contexts: MemoryContexts, + /// Pairs of index in TX instruction trace and VM register trace + register_traces: Vec<(usize, Vec<[u64; 12]>)>, +} + +impl<'a, 'ix_data> InvokeContext<'a, 'ix_data> { + #[allow(clippy::too_many_arguments)] + pub fn new( + transaction_context: &'a mut TransactionContext<'ix_data>, + program_cache_for_tx_batch: &'a mut ProgramCacheForTxBatch, + environment_config: EnvironmentConfig<'a>, + log_collector: Option>>, + compute_budget: SVMTransactionExecutionBudget, + execution_cost: SVMTransactionExecutionCost, + ) -> Self { + Self { + transaction_context, + program_cache_for_tx_batch, + environment_config, + log_collector, + compute_budget, + execution_cost, + compute_meter: ComputeMeter(Cell::new(compute_budget.compute_unit_limit)), + execute_time: None, + timings: ExecuteDetailsTimings::default(), + syscall_context: Vec::new(), + memory_contexts: MemoryContexts::new(), + register_traces: Vec::new(), + } + } + + /// Push a stack frame onto the invocation stack + pub fn push(&mut self) -> Result<(), InstructionError> { + let instruction_context = self.transaction_context.get_next_instruction_context()?; + let program_id = instruction_context + .get_program_key() + .map_err(|_| InstructionError::UnsupportedProgramId)?; + if self.transaction_context.get_instruction_stack_height() != 0 { + let contains = + (0..self.transaction_context.get_instruction_stack_height()).any(|level| { + self.transaction_context + .get_instruction_context_at_nesting_level(level) + .and_then(|instruction_context| instruction_context.get_program_key()) + .map(|program_key| program_key == program_id) + .unwrap_or(false) + }); + let is_last = self + .transaction_context + .get_current_instruction_context() + .and_then(|instruction_context| instruction_context.get_program_key()) + .map(|program_key| program_key == program_id) + .unwrap_or(false); + if contains && !is_last { + // Reentrancy not allowed unless caller is calling itself + return Err(InstructionError::ReentrancyNotAllowed); + } + } + + self.syscall_context.push(None); + self.memory_contexts.push_placeholder(); + self.transaction_context.push() + } + + /// Pop a stack frame from the invocation stack + pub(crate) fn pop(&mut self) -> Result<(), InstructionError> { + self.syscall_context.pop(); + self.memory_contexts.pop(); + self.transaction_context.pop() + } + + /// Current height of the invocation stack, top level instructions are height + /// `solana_instruction::TRANSACTION_LEVEL_STACK_HEIGHT` + pub fn get_stack_height(&self) -> usize { + self.transaction_context.get_instruction_stack_height() + } + + /// Entrypoint for a cross-program invocation from a builtin program. + /// + /// Takes signer seeds and derives PDAs internally via + /// `create_program_address`, mirroring the SBF CPI path. This makes + /// it structurally impossible for a builtin to vouch for a non-PDA + /// address (e.g. a user wallet) as a signer. + pub fn native_invoke_signed( + &mut self, + instruction: Instruction, + signer_seeds: &[&[&[u8]]], + ) -> Result<(), InstructionError> { + let caller_program_id = + *self.transaction_context.get_current_instruction_context()?.get_program_key()?; + // The conversion from `PubkeyError` to `InstructionError` through + // num-traits is incorrect, but it's the existing behavior. + let signers = signer_seeds + .iter() + .map(|seeds| Pubkey::create_program_address(seeds, &caller_program_id)) + .collect::, solana_pubkey::PubkeyError>>() + .map_err(|e| e as u64)?; + self.prepare_next_cpi_instruction(instruction, &signers)?; + self.process_instruction(&mut 0)?; + Ok(()) + } + + /// Deprecated entrypoint for a cross-program invocation from a builtin program + // NOTE: + // we only keep it around for one special case of CPI with post-delegation actions + pub fn native_invoke( + &mut self, + instruction: Instruction, + signers: &[Pubkey], + ) -> Result<(), InstructionError> { + self.prepare_next_cpi_instruction(instruction, signers)?; + self.process_instruction(&mut 0)?; + Ok(()) + } + + /// Helper to prepare for process_instruction() when the instruction is not a top level one, + /// and depends on `AccountMeta`s + pub fn prepare_next_cpi_instruction( + &mut self, + instruction: Instruction, + signers: &[Pubkey], + ) -> Result<(), InstructionError> { + // We reference accounts by an u8 index, so we have a total of 256 accounts. + let mut transaction_callee_map: Vec = vec![u16::MAX; MAX_ACCOUNTS_PER_TRANSACTION]; + let mut instruction_accounts: Vec = + Vec::with_capacity(instruction.accounts.len()); + + // This code block is necessary to restrict the scope of the immutable borrow of + // transaction context (the `instruction_context` variable). At the end of this + // function, we must borrow it again as mutable. + let program_account_index = { + let instruction_context = self.transaction_context.get_current_instruction_context()?; + + for account_meta in instruction.accounts.iter() { + let index_in_transaction = self + .transaction_context + .find_index_of_account(&account_meta.pubkey) + .ok_or_else(|| { + ic_msg!( + self, + "Instruction references an unknown account {}", + account_meta.pubkey, + ); + InstructionError::MissingAccount + })?; + + debug_assert!((index_in_transaction as usize) < transaction_callee_map.len()); + let index_in_callee = + transaction_callee_map.get_mut(index_in_transaction as usize).unwrap(); + + if (*index_in_callee as usize) < instruction_accounts.len() { + let cloned_account = { + let instruction_account = instruction_accounts + .get_mut(*index_in_callee as usize) + .ok_or(InstructionError::MissingAccount)?; + instruction_account.set_is_signer( + instruction_account.is_signer() || account_meta.is_signer, + ); + instruction_account.set_is_writable( + instruction_account.is_writable() || account_meta.is_writable, + ); + *instruction_account + }; + instruction_accounts.push(cloned_account); + } else { + *index_in_callee = instruction_accounts.len() as u16; + instruction_accounts.push(InstructionAccount::new( + index_in_transaction, + account_meta.is_signer, + account_meta.is_writable, + )); + } + } + + for current_index in 0..instruction_accounts.len() { + let instruction_account = instruction_accounts.get(current_index).unwrap(); + let index_in_callee = *transaction_callee_map + .get(instruction_account.index_in_transaction as usize) + .unwrap() as usize; + + if current_index != index_in_callee { + let (is_signer, is_writable) = { + let reference_account = instruction_accounts + .get(index_in_callee) + .ok_or(InstructionError::MissingAccount)?; + ( + reference_account.is_signer(), + reference_account.is_writable(), + ) + }; + + let current_account = instruction_accounts.get_mut(current_index).unwrap(); + current_account.set_is_signer(current_account.is_signer() || is_signer); + current_account.set_is_writable(current_account.is_writable() || is_writable); + // This account is repeated, so there is no need to check for permissions + continue; + } + + let index_in_caller = instruction_context.get_index_of_account_in_instruction( + instruction_account.index_in_transaction, + )?; + + // This unwrap is safe because instruction.accounts.len() == instruction_accounts.len() + let account_key = &instruction.accounts.get(current_index).unwrap().pubkey; + // get_index_of_account_in_instruction has already checked if the index is valid. + let caller_instruction_account = instruction_context + .instruction_accounts() + .get(index_in_caller as usize) + .unwrap(); + + // Readonly in caller cannot become writable in callee + if instruction_account.is_writable() && !caller_instruction_account.is_writable() { + ic_msg!(self, "{}'s writable privilege escalated", account_key,); + return Err(InstructionError::PrivilegeEscalation); + } + + // To be signed in the callee, + // it must be either signed in the caller or by the program + if instruction_account.is_signer() + && !(caller_instruction_account.is_signer() || signers.contains(account_key)) + { + ic_msg!(self, "{}'s signer privilege escalated", account_key,); + return Err(InstructionError::PrivilegeEscalation); + } + } + + // Find and validate executables / program accounts + let callee_program_id = &instruction.program_id; + let program_account_index_in_transaction = + self.transaction_context.find_index_of_account(callee_program_id); + let program_account_index_in_instruction = program_account_index_in_transaction + .map(|index| instruction_context.get_index_of_account_in_instruction(index)); + + // We first check if the account exists in the transaction, and then see if it is part + // of the instruction. + if program_account_index_in_instruction.is_none() + || program_account_index_in_instruction.unwrap().is_err() + { + ic_msg!(self, "Unknown program {}", callee_program_id); + return Err(InstructionError::MissingAccount); + } + + // SAFETY: This unwrap is safe, because we checked the index in instruction in the + // previous if-condition. + program_account_index_in_transaction.unwrap() + }; + + // This ? operator should not error out because `fn get_current_instruction_index` is also called + // in `get_current_instruction_context` + let caller_index = self.transaction_context.get_current_instruction_index()?; + self.transaction_context.configure_instruction_at_index( + self.transaction_context.get_instruction_trace_length(), + program_account_index, + instruction_accounts, + transaction_callee_map, + Cow::Owned(instruction.data), + Some(caller_index as u16), + )?; + Ok(()) + } + + /// Helper to prepare for process_instruction()/process_precompile() when the instruction is + /// a top level one + pub fn prepare_next_top_level_instruction( + &mut self, + message: &impl SVMMessage, + instruction: &SVMInstruction, + program_account_index: IndexOfAccount, + data: &'ix_data [u8], + ) -> Result<(), InstructionError> { + // We reference accounts by an u8 index, so we have a total of 256 accounts. + let mut transaction_callee_map: Vec = vec![u16::MAX; MAX_ACCOUNTS_PER_TRANSACTION]; + + let mut instruction_accounts: Vec = + Vec::with_capacity(instruction.accounts.len()); + for index_in_transaction in instruction.accounts.iter() { + debug_assert!((*index_in_transaction as usize) < transaction_callee_map.len()); + + let index_in_callee = + transaction_callee_map.get_mut(*index_in_transaction as usize).unwrap(); + + if (*index_in_callee as usize) > instruction_accounts.len() { + *index_in_callee = instruction_accounts.len() as u16; + } + + let index_in_transaction = *index_in_transaction as usize; + instruction_accounts.push(InstructionAccount::new( + index_in_transaction as IndexOfAccount, + message.is_signer(index_in_transaction), + message.is_writable(index_in_transaction), + )); + } + + self.transaction_context.configure_instruction_at_index( + self.transaction_context.get_instruction_trace_length(), + program_account_index, + instruction_accounts, + transaction_callee_map, + Cow::Borrowed(data), + None, + )?; + Ok(()) + } + + /// Processes an instruction and returns how many compute units were used + pub fn process_instruction( + &mut self, + compute_units_consumed: &mut u64, + ) -> Result<(), InstructionError> { + *compute_units_consumed = 0; + self.push()?; + self.process_executable_chain(compute_units_consumed) + // MUST pop if and only if `push` succeeded, independent of `result`. + // Thus, the `.and()` instead of an `.and_then()`. + .and(self.pop()) + } + + /// Processes a precompile instruction + pub fn process_precompile( + &mut self, + program_id: &Pubkey, + instruction_data: &[u8], + message_instruction_datas_iter: impl Iterator, + ) -> Result<(), InstructionError> { + self.push()?; + let instruction_datas: Vec<_> = message_instruction_datas_iter.collect(); + self.environment_config + .epoch_stake_callback + .process_precompile(program_id, instruction_data, instruction_datas) + .map_err(InstructionError::from) + .and(self.pop()) + } + + /// Calls the instruction's program entrypoint method + fn process_executable_chain( + &mut self, + compute_units_consumed: &mut u64, + ) -> Result<(), InstructionError> { + let instruction_context = self.transaction_context.get_current_instruction_context()?; + let program_id = *instruction_context.get_program_key()?; + let owner_id = instruction_context.get_program_owner()?; + let process_executable_chain_time = Measure::start("process_executable_chain_time"); + + let cache_id = if native_loader::check_id(&owner_id) + || bpf_loader_deprecated::check_id(&owner_id) + || bpf_loader::check_id(&owner_id) + || bpf_loader_upgradeable::check_id(&owner_id) + || loader_v4::check_id(&owner_id) + { + program_id + } else { + return Err(InstructionError::UnsupportedProgramId); + }; + + let pre_remaining_units = self.get_remaining(); + let entry = self + .program_cache_for_tx_batch + .find(&cache_id) + .ok_or(InstructionError::UnsupportedProgramId)?; + let result = match &entry.program { + ProgramCacheEntryType::Builtin(program) => { + // The Murmur3 hash value (used by RBPF) of the string "entrypoint". + const ENTRYPOINT_KEY: u32 = 0x71E3CF81; + let function = program + .get_function_registry() + .lookup_by_key(ENTRYPOINT_KEY) + .map(|(_name, (function, _codegen))| function) + .ok_or(InstructionError::UnsupportedProgramId)?; + + self.transaction_context.set_return_data(program_id, Vec::new())?; + let logger = self.get_log_collector(); + stable_log::program_invoke(&logger, &program_id, self.get_stack_height()); + self.set_syscall_context(SyscallContext { + allocator: BpfAllocator::new(0), + accounts_metadata: Vec::new(), + })?; + self.memory_contexts.set_memory_context_abi_v1(MemoryContext::new( + BpfAllocator::new(0), + Vec::new(), + // Built-ins do not dereference VM memory, but the invoke + // context still requires an active mapping. + unsafe { + MemoryMapping::new(Vec::new(), &Config::default(), SBPFVersion::Reserved) + .unwrap() + }, + ))?; + let mut vm = EbpfVm::new( + Arc::clone( + &**self + .environment_config + .program_runtime_environments_for_execution + .get_env_for_execution(), + ), + SBPFVersion::V0, + // Removes lifetime tracking. + unsafe { std::mem::transmute::<&mut InvokeContext, &mut InvokeContext>(self) }, + 0, + ); + vm.invoke_function(function); + match vm.program_result { + ProgramResult::Ok(_) => { + stable_log::program_success(&logger, &program_id); + Ok(()) + } + ProgramResult::Err(ref err) => { + if let EbpfError::SyscallError(syscall_error) = err { + if let Some(instruction_err) = + syscall_error.downcast_ref::() + { + stable_log::program_failure(&logger, &program_id, instruction_err); + Err(instruction_err.clone()) + } else { + stable_log::program_failure(&logger, &program_id, syscall_error); + Err(InstructionError::ProgramFailedToComplete) + } + } else { + stable_log::program_failure(&logger, &program_id, err); + Err(InstructionError::ProgramFailedToComplete) + } + } + } + } + ProgramCacheEntryType::Loaded(program) => { + self.transaction_context.set_return_data(program_id, Vec::new())?; + let logger = self.get_log_collector(); + stable_log::program_invoke(&logger, &program_id, self.get_stack_height()); + let result = crate::vm::execute(program, self).map_err(|error| { + error + .downcast_ref::() + .cloned() + .unwrap_or(InstructionError::ProgramFailedToComplete) + }); + match &result { + Ok(()) => stable_log::program_success(&logger, &program_id), + Err(error) => stable_log::program_failure(&logger, &program_id, error), + } + result + } + _ => Err(InstructionError::UnsupportedProgramId), + }; + let post_remaining_units = self.get_remaining(); + *compute_units_consumed = pre_remaining_units.saturating_sub(post_remaining_units); + + if matches!(&entry.program, ProgramCacheEntryType::Builtin(_)) + && result.is_ok() + && *compute_units_consumed == 0 + { + return Err(InstructionError::BuiltinProgramsMustConsumeComputeUnits); + } + + process_executable_chain_time.end_as_us(); + result + } + + /// Get this invocation's LogCollector + pub fn get_log_collector(&self) -> Option>> { + self.log_collector.clone() + } + + /// Consume compute units + pub fn consume_checked(&self, amount: u64) -> Result<(), Box> { + self.compute_meter.consume_checked(amount) + } + + /// Set compute units + /// + /// Only use for tests and benchmarks + pub fn mock_set_remaining(&self, remaining: u64) { + self.compute_meter.mock_set_remaining(remaining); + } + + /// Get this invocation's compute budget + pub fn get_compute_budget(&self) -> &SVMTransactionExecutionBudget { + &self.compute_budget + } + + /// Get this invocation's compute budget + pub fn get_execution_cost(&self) -> &SVMTransactionExecutionCost { + &self.execution_cost + } + + /// Get the current feature set. + pub fn get_feature_set(&self) -> &SVMFeatureSet { + self.environment_config.feature_set + } + + pub fn is_deprecate_legacy_vote_ixs_active(&self) -> bool { + self.environment_config.feature_set.deprecate_legacy_vote_ixs + } + + /// Get cached sysvars + pub fn get_sysvar_cache(&self) -> &SysvarCache { + self.environment_config.sysvar_cache + } + + /// Get cached epoch total stake. + pub fn get_epoch_stake(&self) -> u64 { + self.environment_config.epoch_stake_callback.get_epoch_stake() + } + + /// Get cached stake for the epoch vote account. + pub fn get_epoch_stake_for_vote_account(&self, pubkey: &'a Pubkey) -> u64 { + self.environment_config + .epoch_stake_callback + .get_epoch_stake_for_vote_account(pubkey) + } + + pub fn is_precompile(&self, pubkey: &Pubkey) -> bool { + self.environment_config.epoch_stake_callback.is_precompile(pubkey) + } + + // Should alignment be enforced during user pointer translation + pub fn get_check_aligned(&self) -> bool { + self.transaction_context + .get_current_instruction_context() + .and_then(|instruction_context| { + let owner_id = instruction_context.get_program_owner(); + debug_assert!(owner_id.is_ok()); + owner_id + }) + .map(|owner_key| owner_key != bpf_loader_deprecated::id()) + .unwrap_or(true) + } + + // Set this instruction syscall context + pub fn set_syscall_context( + &mut self, + syscall_context: SyscallContext, + ) -> Result<(), InstructionError> { + *self.syscall_context.last_mut().ok_or(InstructionError::CallDepth)? = + Some(syscall_context); + Ok(()) + } + + // Get this instruction's SyscallContext + pub fn get_syscall_context(&self) -> Result<&SyscallContext, InstructionError> { + self.syscall_context + .last() + .and_then(std::option::Option::as_ref) + .ok_or(InstructionError::CallDepth) + } + + // Get this instruction's SyscallContext + pub fn get_syscall_context_mut(&mut self) -> Result<&mut SyscallContext, InstructionError> { + self.syscall_context + .last_mut() + .and_then(|syscall_context| syscall_context.as_mut()) + .ok_or(InstructionError::CallDepth) + } + + /// Insert a VM register trace + pub fn insert_register_trace(&mut self, register_trace: Vec<[u64; 12]>) { + if register_trace.is_empty() { + return; + } + let Ok(instruction_context) = self.transaction_context.get_current_instruction_context() + else { + return; + }; + self.register_traces + .push((instruction_context.get_index_in_trace(), register_trace)); + } + + /// Iterates over all VM register traces (including CPI) + pub fn iterate_vm_traces( + &self, + callback: &dyn Fn(InstructionContext, &Executable, RegisterTrace), + ) { + for (index_in_trace, register_trace) in &self.register_traces { + let Ok(instruction_context) = self + .transaction_context + .get_instruction_context_at_index_in_trace(*index_in_trace) + else { + continue; + }; + let Ok(program_id) = instruction_context.get_program_key() else { + continue; + }; + let Some(entry) = self.program_cache_for_tx_batch.find(program_id) else { + continue; + }; + let ProgramCacheEntryType::Loaded(ref executable) = entry.program else { + continue; + }; + callback(instruction_context, executable, register_trace.as_slice()); + } + } +} + +#[macro_export] +macro_rules! with_mock_invoke_context_with_feature_set { + ( + $invoke_context:ident, + $transaction_context:ident, + $feature_set:ident, + $top_level_instructions:literal, + $transaction_accounts:expr $(,)? + ) => { + use { + solana_svm_callback::InvokeContextCallback, + solana_svm_log_collector::LogCollector, + $crate::{ + __private::{Hash, ReadableAccount, Rent, TransactionContext}, + execution_budget::{SVMTransactionExecutionBudget, SVMTransactionExecutionCost}, + invoke_context::{EnvironmentConfig, InvokeContext}, + loaded_programs::{ProgramCacheForTxBatch, ProgramRuntimeEnvironments}, + sysvar_cache::SysvarCache, + }, + }; + + struct MockInvokeContextCallback {} + impl InvokeContextCallback for MockInvokeContextCallback {} + + let compute_budget = SVMTransactionExecutionBudget::new_with_defaults( + $feature_set.raise_cpi_nesting_limit_to_8, + ); + let mut $transaction_context = TransactionContext::new( + $transaction_accounts, + Rent::default(), + compute_budget.max_instruction_stack_depth, + compute_budget.max_instruction_trace_length, + $top_level_instructions, + ); + let mut sysvar_cache = SysvarCache::default(); + sysvar_cache.fill_missing_entries(|pubkey, callback| { + for index in 0..$transaction_context.get_number_of_accounts() { + if $transaction_context.get_key_of_account_at_index(index).unwrap() == pubkey { + callback($transaction_context.accounts().try_borrow(index).unwrap().data()); + } + } + }); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + &MockInvokeContextCallback {}, + $feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + let mut $invoke_context = InvokeContext::new( + &mut $transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + Some(LogCollector::new_ref()), + compute_budget, + SVMTransactionExecutionCost::new_with_defaults( + $feature_set.increase_cpi_account_info_limit, + ), + ); + }; + ( + $invoke_context:ident, + $transaction_context:ident, + $feature_set:ident, + $transaction_accounts:expr $(,)? + ) => { + with_mock_invoke_context_with_feature_set!( + $invoke_context, + $transaction_context, + $feature_set, + 1, + $transaction_accounts + ); + }; +} + +#[macro_export] +macro_rules! with_mock_invoke_context { + ( + $invoke_context:ident, + $transaction_context:ident, + $top_level_instructions:literal, + $transaction_accounts:expr $(,)? + ) => { + let feature_set = &solana_svm_feature_set::SVMFeatureSet::default(); + $crate::with_mock_invoke_context_with_feature_set!( + $invoke_context, + $transaction_context, + feature_set, + $top_level_instructions, + $transaction_accounts + ) + }; + ( + $invoke_context:ident, + $transaction_context:ident, + $transaction_accounts:expr $(,)? + ) => { + with_mock_invoke_context!( + $invoke_context, + $transaction_context, + 1, + $transaction_accounts + ); + }; +} + +#[allow(clippy::too_many_arguments)] +pub fn mock_process_instruction_with_feature_set< + F: FnMut(&mut InvokeContext), + G: FnMut(&mut InvokeContext), +>( + loader_id: &Pubkey, + program_index: Option, + instruction_data: &[u8], + mut transaction_accounts: Vec, + instruction_account_metas: Vec, + expected_result: Result<(), InstructionError>, + builtin_function: BuiltinFunctionWithContext, + mut pre_adjustments: F, + mut post_adjustments: G, + feature_set: &SVMFeatureSet, +) -> Vec { + let mut instruction_accounts: Vec = + Vec::with_capacity(instruction_account_metas.len()); + for account_meta in instruction_account_metas.iter() { + let index_in_transaction = transaction_accounts + .iter() + .position(|(key, _account)| *key == account_meta.pubkey) + .unwrap_or(transaction_accounts.len()) + as IndexOfAccount; + instruction_accounts.push(InstructionAccount::new( + index_in_transaction, + account_meta.is_signer, + account_meta.is_writable, + )); + } + + let program_index = if let Some(index) = program_index { + index + } else { + let processor_account = AccountSharedData::new(0, 0, &native_loader::id()); + transaction_accounts.push((*loader_id, processor_account)); + transaction_accounts.len().saturating_sub(1) as IndexOfAccount + }; + let pop_epoch_schedule_account = + if !transaction_accounts.iter().any(|(key, _)| *key == sysvar::epoch_schedule::id()) { + transaction_accounts.push(( + sysvar::epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + )); + true + } else { + false + }; + with_mock_invoke_context_with_feature_set!( + invoke_context, + transaction_context, + feature_set, + transaction_accounts + ); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + *loader_id, + Arc::new(ProgramCacheEntry::new_builtin(builtin_function)), + ); + program_cache_for_tx_batch.set_slot_for_tests( + invoke_context + .get_sysvar_cache() + .get_clock() + .map(|clock| clock.slot) + .unwrap_or(1), + ); + invoke_context.program_cache_for_tx_batch = &mut program_cache_for_tx_batch; + pre_adjustments(&mut invoke_context); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + program_index, + instruction_accounts, + instruction_data.to_vec(), + ) + .unwrap(); + let result = invoke_context.process_instruction(&mut 0); + assert_eq!(result, expected_result); + post_adjustments(&mut invoke_context); + let mut transaction_accounts = transaction_context.deconstruct_without_keys().unwrap(); + if pop_epoch_schedule_account { + transaction_accounts.pop(); + } + transaction_accounts.pop(); + transaction_accounts +} + +pub fn mock_process_instruction( + loader_id: &Pubkey, + program_index: Option, + instruction_data: &[u8], + transaction_accounts: Vec, + instruction_account_metas: Vec, + expected_result: Result<(), InstructionError>, + builtin_function: BuiltinFunctionWithContext, + pre_adjustments: F, + post_adjustments: G, +) -> Vec { + mock_process_instruction_with_feature_set( + loader_id, + program_index, + instruction_data, + transaction_accounts, + instruction_account_metas, + expected_result, + builtin_function, + pre_adjustments, + post_adjustments, + &SVMFeatureSet::all_enabled(), + ) +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::execution_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + serde::{Deserialize, Serialize}, + solana_account::WritableAccount, + solana_instruction::Instruction, + solana_keypair::Keypair, + solana_rent::Rent, + solana_sbpf::program::BuiltinFunctionDefinition, + solana_sdk_ids::system_program, + solana_signer::Signer, + solana_transaction::{Transaction, sanitized::SanitizedTransaction}, + solana_transaction_context::MAX_ACCOUNTS_PER_INSTRUCTION, + std::collections::HashSet, + test_case::test_case, + }; + + #[derive(Debug, Serialize, Deserialize)] + enum MockInstruction { + NoopSuccess, + NoopFail, + ModifyOwned, + ModifyNotOwned, + ModifyReadonly, + UnbalancedPush, + UnbalancedPop, + ConsumeComputeUnits { + compute_units_to_consume: u64, + desired_result: Result<(), InstructionError>, + }, + Resize { + new_len: u64, + }, + } + + const MOCK_BUILTIN_COMPUTE_UNIT_COST: u64 = 1; + + declare_process_instruction!( + MockBuiltin, + MOCK_BUILTIN_COMPUTE_UNIT_COST, + |invoke_context| { + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let instruction_data = instruction_context.get_instruction_data(); + let program_id = instruction_context.get_program_key()?; + let instruction_accounts = (0..4) + .map(|instruction_account_index| { + InstructionAccount::new(instruction_account_index, false, false) + }) + .collect::>(); + assert_eq!( + program_id, + instruction_context.try_borrow_instruction_account(0)?.get_owner() + ); + assert_ne!( + instruction_context.try_borrow_instruction_account(1)?.get_owner(), + instruction_context.get_key_of_instruction_account(0)? + ); + + if let Ok(instruction) = bincode::deserialize(instruction_data) { + match instruction { + MockInstruction::NoopSuccess => (), + MockInstruction::NoopFail => return Err(InstructionError::GenericError), + MockInstruction::ModifyOwned => instruction_context + .try_borrow_instruction_account(0)? + .set_data_from_slice(&[1])?, + MockInstruction::ModifyNotOwned => instruction_context + .try_borrow_instruction_account(1)? + .set_data_from_slice(&[1])?, + MockInstruction::ModifyReadonly => instruction_context + .try_borrow_instruction_account(2)? + .set_data_from_slice(&[1])?, + MockInstruction::UnbalancedPush => { + instruction_context + .try_borrow_instruction_account(0)? + .checked_add_lamports(1)?; + let program_id = *transaction_context.get_key_of_account_at_index(3)?; + let metas = vec![ + AccountMeta::new_readonly( + *transaction_context.get_key_of_account_at_index(0)?, + false, + ), + AccountMeta::new_readonly( + *transaction_context.get_key_of_account_at_index(1)?, + false, + ), + ]; + let inner_instruction = Instruction::new_with_bincode( + program_id, + &MockInstruction::NoopSuccess, + metas, + ); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + 3, + instruction_accounts, + vec![], + ) + .unwrap(); + let result = invoke_context.push(); + assert_eq!(result, Err(InstructionError::UnbalancedInstruction)); + result?; + invoke_context + .native_invoke_signed(inner_instruction, &[]) + .and(invoke_context.pop())?; + } + MockInstruction::UnbalancedPop => instruction_context + .try_borrow_instruction_account(0)? + .checked_add_lamports(1)?, + MockInstruction::ConsumeComputeUnits { + compute_units_to_consume, + desired_result, + } => { + invoke_context + .consume_checked(compute_units_to_consume) + .map_err(|_| InstructionError::ComputationalBudgetExceeded)?; + return desired_result; + } + MockInstruction::Resize { new_len } => instruction_context + .try_borrow_instruction_account(0)? + .set_data_from_slice(&vec![0; new_len as usize])?, + } + } else { + return Err(InstructionError::InvalidInstructionData); + } + Ok(()) + } + ); + + #[test_case(false; "SIMD-0268 disabled")] + #[test_case(true; "SIMD-0268 enabled")] + fn test_instruction_stack_height(simd_0268_active: bool) { + let one_more_than_max_depth = + SVMTransactionExecutionBudget::new_with_defaults(simd_0268_active) + .max_instruction_stack_depth + .saturating_add(1); + let mut invoke_stack = vec![]; + let mut transaction_accounts = vec![]; + let mut instruction_accounts = vec![]; + for index in 0..one_more_than_max_depth { + invoke_stack.push(solana_pubkey::new_rand()); + transaction_accounts.push(( + solana_pubkey::new_rand(), + AccountSharedData::new(index as u64, 1, invoke_stack.get(index).unwrap()), + )); + instruction_accounts.push(InstructionAccount::new( + index as IndexOfAccount, + false, + true, + )); + } + for (index, program_id) in invoke_stack.iter().enumerate() { + transaction_accounts.push(( + *program_id, + AccountSharedData::new(1, 1, &solana_pubkey::Pubkey::default()), + )); + instruction_accounts.push(InstructionAccount::new( + index as IndexOfAccount, + false, + false, + )); + } + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + + // Check call depth increases and has a limit + let mut depth_reached: usize = 0; + for _ in 0..invoke_stack.len() { + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + one_more_than_max_depth.saturating_add(depth_reached) as IndexOfAccount, + instruction_accounts.clone(), + vec![], + ) + .unwrap(); + if Err(InstructionError::CallDepth) == invoke_context.push() { + break; + } + depth_reached = depth_reached.saturating_add(1); + } + assert_ne!(depth_reached, 0); + assert!(depth_reached < one_more_than_max_depth); + } + + #[test] + fn test_max_instruction_trace_length_top_level() { + const MAX_INSTRUCTIONS: usize = 8; + let mut transaction_context = TransactionContext::new( + vec![( + Pubkey::new_unique(), + AccountSharedData::new(1, 1, &Pubkey::new_unique()), + )], + Rent::default(), + 1, + MAX_INSTRUCTIONS, + MAX_INSTRUCTIONS, + ); + for _ in 0..MAX_INSTRUCTIONS { + transaction_context.push().unwrap(); + transaction_context + .configure_top_level_instruction_for_tests( + 0, + vec![InstructionAccount::new(0, false, false)], + vec![], + ) + .unwrap(); + transaction_context.pop().unwrap(); + } + assert_eq!( + transaction_context.push(), + Err(InstructionError::MaxInstructionTraceLengthExceeded) + ); + } + + #[test] + fn test_max_instruction_trace_length_cpi() { + // Hitting the limit with CPIs + const MAX_INSTRUCTIONS: usize = 8; + let mut transaction_context = TransactionContext::new( + vec![( + Pubkey::new_unique(), + AccountSharedData::new(1, 1, &Pubkey::new_unique()), + )], + Rent::default(), + 256, + MAX_INSTRUCTIONS, + 2, + ); + + // To be uncommented when we reorder the trace + // transaction_context + // .configure_instruction_at_index( + // 0, + // 0, + // vec![InstructionAccount::new(0, false, false)], + // vec![u16::MAX; 256], + // Cow::Owned(Vec::new()), + // None, + // ) + // .unwrap(); + // + // transaction_context + // .configure_instruction_at_index( + // 1, + // 0, + // vec![InstructionAccount::new(0, false, false)], + // vec![u16::MAX; 256], + // Cow::Owned(Vec::new()), + // None, + // ) + // .unwrap(); + + for _ in 0..MAX_INSTRUCTIONS { + transaction_context.push().unwrap(); + transaction_context + .configure_next_cpi_for_tests( + 0, + vec![InstructionAccount::new(0, false, false)], + Vec::new(), + ) + .unwrap(); + } + + assert_eq!( + transaction_context.push(), + Err(InstructionError::MaxInstructionTraceLengthExceeded) + ); + } + + #[test_case(MockInstruction::NoopSuccess, Ok(()); "NoopSuccess")] + #[test_case(MockInstruction::NoopFail, Err(InstructionError::GenericError); "NoopFail")] + #[test_case(MockInstruction::ModifyOwned, Ok(()); "ModifyOwned")] + #[test_case(MockInstruction::ModifyNotOwned, Err(InstructionError::ExternalAccountDataModified); "ModifyNotOwned")] + #[test_case(MockInstruction::ModifyReadonly, Err(InstructionError::ReadonlyDataModified); "ModifyReadonly")] + #[test_case(MockInstruction::UnbalancedPush, Err(InstructionError::UnbalancedInstruction); "UnbalancedPush")] + #[test_case(MockInstruction::UnbalancedPop, Err(InstructionError::UnbalancedInstruction); "UnbalancedPop")] + fn test_process_instruction_account_modifications( + instruction: MockInstruction, + expected_result: Result<(), InstructionError>, + ) { + let callee_program_id = solana_pubkey::new_rand(); + let owned_account = AccountSharedData::new(42, 1, &callee_program_id); + let not_owned_account = AccountSharedData::new(84, 1, &solana_pubkey::new_rand()); + let readonly_account = AccountSharedData::new(168, 1, &solana_pubkey::new_rand()); + let loader_account = AccountSharedData::new(0, 1, &native_loader::id()); + let mut program_account = AccountSharedData::new(1, 1, &native_loader::id()); + program_account.set_executable(true); + let transaction_accounts = vec![ + (solana_pubkey::new_rand(), owned_account), + (solana_pubkey::new_rand(), not_owned_account), + (solana_pubkey::new_rand(), readonly_account), + (callee_program_id, program_account), + (solana_pubkey::new_rand(), loader_account), + ]; + let metas = vec![ + AccountMeta::new(transaction_accounts.first().unwrap().0, false), + AccountMeta::new(transaction_accounts.get(1).unwrap().0, false), + AccountMeta::new_readonly(transaction_accounts.get(2).unwrap().0, false), + ]; + let instruction_accounts = (0..4) + .map(|instruction_account_index| { + InstructionAccount::new( + instruction_account_index, + false, + instruction_account_index < 2, + ) + }) + .collect::>(); + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + callee_program_id, + Arc::new(ProgramCacheEntry::new_builtin(( + MockBuiltin::vm, + MockBuiltin::codegen, + ))), + ); + invoke_context.program_cache_for_tx_batch = &mut program_cache_for_tx_batch; + + // Account modification tests + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(4, instruction_accounts, vec![]) + .unwrap(); + invoke_context.push().unwrap(); + let inner_instruction = + Instruction::new_with_bincode(callee_program_id, &instruction, metas); + let result = invoke_context + .native_invoke_signed(inner_instruction, &[]) + .and(invoke_context.pop()); + assert_eq!(result, expected_result); + } + + #[test_case(Ok(()); "Ok")] + #[test_case(Err(InstructionError::GenericError); "GenericError")] + fn test_process_instruction_compute_unit_consumption( + expected_result: Result<(), InstructionError>, + ) { + let callee_program_id = solana_pubkey::new_rand(); + let owned_account = AccountSharedData::new(42, 1, &callee_program_id); + let not_owned_account = AccountSharedData::new(84, 1, &solana_pubkey::new_rand()); + let readonly_account = AccountSharedData::new(168, 1, &solana_pubkey::new_rand()); + let loader_account = AccountSharedData::new(0, 1, &native_loader::id()); + let mut program_account = AccountSharedData::new(1, 1, &native_loader::id()); + program_account.set_executable(true); + let transaction_accounts = vec![ + (solana_pubkey::new_rand(), owned_account), + (solana_pubkey::new_rand(), not_owned_account), + (solana_pubkey::new_rand(), readonly_account), + (callee_program_id, program_account), + (solana_pubkey::new_rand(), loader_account), + ]; + let metas = vec![ + AccountMeta::new(transaction_accounts.first().unwrap().0, false), + AccountMeta::new(transaction_accounts.get(1).unwrap().0, false), + AccountMeta::new_readonly(transaction_accounts.get(2).unwrap().0, false), + ]; + let instruction_accounts = (0..4) + .map(|instruction_account_index| { + InstructionAccount::new( + instruction_account_index, + false, + instruction_account_index < 2, + ) + }) + .collect::>(); + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + callee_program_id, + Arc::new(ProgramCacheEntry::new_builtin(( + MockBuiltin::vm, + MockBuiltin::codegen, + ))), + ); + invoke_context.program_cache_for_tx_batch = &mut program_cache_for_tx_batch; + + // Compute unit consumption tests + let compute_units_to_consume = 10; + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(4, instruction_accounts, vec![]) + .unwrap(); + invoke_context.push().unwrap(); + let inner_instruction = Instruction::new_with_bincode( + callee_program_id, + &MockInstruction::ConsumeComputeUnits { + compute_units_to_consume, + desired_result: expected_result.clone(), + }, + metas, + ); + invoke_context.prepare_next_cpi_instruction(inner_instruction, &[]).unwrap(); + + let mut compute_units_consumed = 0; + let result = invoke_context.process_instruction(&mut compute_units_consumed); + + // Because the instruction had compute cost > 0, then regardless of the execution result, + // the number of compute units consumed should be a non-default which is something greater + // than zero. + assert!(compute_units_consumed > 0); + assert_eq!( + compute_units_consumed, + compute_units_to_consume.saturating_add(MOCK_BUILTIN_COMPUTE_UNIT_COST), + ); + assert_eq!(result, expected_result); + + invoke_context.pop().unwrap(); + } + + #[test] + fn test_invoke_context_compute_budget() { + let transaction_accounts = vec![(solana_pubkey::new_rand(), AccountSharedData::default())]; + let execution_budget = SVMTransactionExecutionBudget { + compute_unit_limit: u64::from(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT), + ..SVMTransactionExecutionBudget::default() + }; + + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + invoke_context.compute_budget = execution_budget; + + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(0, vec![], vec![]) + .unwrap(); + invoke_context.push().unwrap(); + assert_eq!(*invoke_context.get_compute_budget(), execution_budget); + invoke_context.pop().unwrap(); + } + + #[test_case(0; "Resize the account to *the same size*, so not consuming any additional size")] + #[test_case(1; "Resize the account larger")] + #[test_case(-1; "Resize the account smaller")] + fn test_process_instruction_accounts_resize_delta(resize_delta: i64) { + let program_key = Pubkey::new_unique(); + let user_account_data_len = 123u64; + let user_account = + AccountSharedData::new(100, user_account_data_len as usize, &program_key); + let dummy_account = AccountSharedData::new(10, 0, &program_key); + let mut program_account = AccountSharedData::new(500, 500, &native_loader::id()); + program_account.set_executable(true); + let transaction_accounts = vec![ + (Pubkey::new_unique(), user_account), + (Pubkey::new_unique(), dummy_account), + (program_key, program_account), + ]; + let instruction_accounts = vec![ + InstructionAccount::new(0, false, true), + InstructionAccount::new(1, false, false), + ]; + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + program_key, + Arc::new(ProgramCacheEntry::new_builtin(( + MockBuiltin::vm, + MockBuiltin::codegen, + ))), + ); + invoke_context.program_cache_for_tx_batch = &mut program_cache_for_tx_batch; + + let new_len = (user_account_data_len as i64).saturating_add(resize_delta) as u64; + let instruction_data = bincode::serialize(&MockInstruction::Resize { new_len }).unwrap(); + + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(2, instruction_accounts, instruction_data) + .unwrap(); + let result = invoke_context.process_instruction(&mut 0); + + assert!(result.is_ok()); + assert_eq!( + invoke_context.transaction_context.accounts().resize_delta(), + resize_delta + ); + } + + #[test] + fn test_prepare_instruction_maximum_accounts() { + const MAX_ACCOUNTS_REFERENCED: usize = u16::MAX as usize; + let mut transaction_accounts: Vec = + Vec::with_capacity(MAX_ACCOUNTS_PER_TRANSACTION); + let mut account_metas: Vec = Vec::with_capacity(MAX_ACCOUNTS_REFERENCED); + + // Fee-payer + let fee_payer = Keypair::new(); + transaction_accounts.push(( + fee_payer.pubkey(), + AccountSharedData::new(1, 1, &Pubkey::new_unique()), + )); + account_metas.push(AccountMeta::new(fee_payer.pubkey(), true)); + + let program_id = Pubkey::new_unique(); + let mut program_account = AccountSharedData::new(1, 1, &Pubkey::new_unique()); + program_account.set_executable(true); + transaction_accounts.push((program_id, program_account)); + account_metas.push(AccountMeta::new_readonly(program_id, false)); + + for i in 2..MAX_ACCOUNTS_REFERENCED { + // Let's reference 256 unique accounts, and the rest is repeated. + if i < MAX_ACCOUNTS_PER_TRANSACTION { + let key = Pubkey::new_unique(); + transaction_accounts + .push((key, AccountSharedData::new(1, 1, &Pubkey::new_unique()))); + account_metas.push(AccountMeta::new_readonly(key, false)); + } else { + let repeated_key = + transaction_accounts.get(i % MAX_ACCOUNTS_PER_TRANSACTION).unwrap().0; + account_metas.push(AccountMeta::new_readonly(repeated_key, false)); + } + } + + with_mock_invoke_context!(invoke_context, transaction_context, 2, transaction_accounts); + + let instruction_1 = Instruction::new_with_bytes(program_id, &[20], account_metas.clone()); + + let instruction_2 = Instruction::new_with_bytes( + program_id, + &[20], + account_metas.iter().rev().cloned().collect(), + ); + + let transaction = Transaction::new_with_payer( + &[instruction_1.clone(), instruction_2.clone()], + Some(&fee_payer.pubkey()), + ); + + let sanitized = + SanitizedTransaction::try_from_legacy_transaction(transaction, &HashSet::new()) + .unwrap(); + + fn test_case_1(invoke_context: &InvokeContext) { + let instruction_context = + invoke_context.transaction_context.get_next_instruction_context().unwrap(); + for index_in_instruction in 0..MAX_ACCOUNTS_REFERENCED as IndexOfAccount { + let index_in_transaction = instruction_context + .get_index_of_instruction_account_in_transaction(index_in_instruction) + .unwrap(); + let other_ix_index = instruction_context + .get_index_of_account_in_instruction(index_in_transaction) + .unwrap(); + if (index_in_instruction as usize) < MAX_ACCOUNTS_PER_TRANSACTION { + assert_eq!(index_in_instruction, index_in_transaction); + assert_eq!(index_in_instruction, other_ix_index); + } else { + assert_eq!( + index_in_instruction as usize % MAX_ACCOUNTS_PER_TRANSACTION, + index_in_transaction as usize + ); + assert_eq!( + index_in_instruction as usize % MAX_ACCOUNTS_PER_TRANSACTION, + other_ix_index as usize + ); + } + } + } + + fn test_case_2(invoke_context: &InvokeContext) { + let instruction_context = + invoke_context.transaction_context.get_next_instruction_context().unwrap(); + for index_in_instruction in 0..MAX_ACCOUNTS_REFERENCED as IndexOfAccount { + let index_in_transaction = instruction_context + .get_index_of_instruction_account_in_transaction(index_in_instruction) + .unwrap(); + let other_ix_index = instruction_context + .get_index_of_account_in_instruction(index_in_transaction) + .unwrap(); + assert_eq!( + index_in_transaction, + (MAX_ACCOUNTS_REFERENCED as u16) + .saturating_sub(index_in_instruction) + .saturating_sub(1) + .overflowing_rem(MAX_ACCOUNTS_PER_TRANSACTION as u16) + .0 + ); + if (index_in_instruction as usize) < MAX_ACCOUNTS_PER_TRANSACTION { + assert_eq!(index_in_instruction, other_ix_index); + } else { + assert_eq!( + index_in_instruction as usize % MAX_ACCOUNTS_PER_TRANSACTION, + other_ix_index as usize + ); + } + } + } + + let svm_instruction = + SVMInstruction::from(sanitized.message().instructions().first().unwrap()); + invoke_context + .prepare_next_top_level_instruction( + &sanitized, + &svm_instruction, + 90, + svm_instruction.data, + ) + .unwrap(); + + test_case_1(&invoke_context); + + invoke_context.transaction_context.push().unwrap(); + let svm_instruction = + SVMInstruction::from(sanitized.message().instructions().get(1).unwrap()); + invoke_context + .prepare_next_top_level_instruction( + &sanitized, + &svm_instruction, + 90, + svm_instruction.data, + ) + .unwrap(); + + test_case_2(&invoke_context); + + invoke_context.transaction_context.push().unwrap(); + invoke_context + .prepare_next_cpi_instruction(instruction_1, &[fee_payer.pubkey()]) + .unwrap(); + test_case_1(&invoke_context); + + invoke_context.transaction_context.push().unwrap(); + invoke_context + .prepare_next_cpi_instruction(instruction_2, &[fee_payer.pubkey()]) + .unwrap(); + test_case_2(&invoke_context); + } + + #[test] + fn test_duplicated_accounts() { + let mut transaction_accounts: Vec = + Vec::with_capacity(MAX_ACCOUNTS_PER_TRANSACTION); + let mut account_metas: Vec = + Vec::with_capacity(MAX_ACCOUNTS_PER_INSTRUCTION.saturating_sub(1)); + + // Fee-payer + let fee_payer = Keypair::new(); + transaction_accounts.push(( + fee_payer.pubkey(), + AccountSharedData::new(1, 1, &Pubkey::new_unique()), + )); + account_metas.push(AccountMeta::new(fee_payer.pubkey(), true)); + + let program_id = Pubkey::new_unique(); + let mut program_account = AccountSharedData::new(1, 1, &Pubkey::new_unique()); + program_account.set_executable(true); + transaction_accounts.push((program_id, program_account)); + account_metas.push(AccountMeta::new_readonly(program_id, false)); + + for i in 2..account_metas.capacity() { + if i % 2 == 0 { + let key = Pubkey::new_unique(); + transaction_accounts + .push((key, AccountSharedData::new(1, 1, &Pubkey::new_unique()))); + account_metas.push(AccountMeta::new_readonly(key, false)); + } else { + let last_key = transaction_accounts.last().unwrap().0; + account_metas.push(AccountMeta::new_readonly(last_key, false)); + } + } + + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + + let instruction = Instruction::new_with_bytes(program_id, &[20], account_metas.clone()); + + let transaction = Transaction::new_with_payer(&[instruction], Some(&fee_payer.pubkey())); + + let sanitized = + SanitizedTransaction::try_from_legacy_transaction(transaction, &HashSet::new()) + .unwrap(); + let svm_instruction = + SVMInstruction::from(sanitized.message().instructions().first().unwrap()); + + invoke_context + .prepare_next_top_level_instruction( + &sanitized, + &svm_instruction, + 90, + svm_instruction.data, + ) + .unwrap(); + + { + let instruction_context = + invoke_context.transaction_context.get_next_instruction_context().unwrap(); + for index_in_instruction in 2..account_metas.len() as IndexOfAccount { + let is_duplicate = instruction_context + .is_instruction_account_duplicate(index_in_instruction) + .unwrap(); + if index_in_instruction % 2 == 0 { + assert!(is_duplicate.is_none()); + } else { + assert_eq!(is_duplicate, Some(index_in_instruction.saturating_sub(1))); + } + } + } + + invoke_context.transaction_context.push().unwrap(); + + let instruction = Instruction::new_with_bytes( + program_id, + &[20], + account_metas.iter().cloned().rev().collect(), + ); + + invoke_context + .prepare_next_cpi_instruction(instruction, &[fee_payer.pubkey()]) + .unwrap(); + let instruction_context = + invoke_context.transaction_context.get_next_instruction_context().unwrap(); + for index_in_instruction in 2..account_metas.len().saturating_sub(1) as u16 { + let is_duplicate = instruction_context + .is_instruction_account_duplicate(index_in_instruction) + .unwrap(); + if index_in_instruction % 2 == 0 { + assert!(is_duplicate.is_none()); + } else { + assert_eq!(is_duplicate, Some(index_in_instruction.saturating_sub(1))); + } + } + } + + // Used for native_invoke_signed tests below. + const TEST_CALLER_PROGRAM_ID: Pubkey = Pubkey::new_from_array([1u8; 32]); + const TEST_CALLEE_PROGRAM_ID: Pubkey = Pubkey::new_from_array([2u8; 32]); + const TEST_WRONG_PROGRAM_ID: Pubkey = Pubkey::new_from_array([3u8; 32]); + const TEST_MOCK_EXTRA_KEY: Pubkey = Pubkey::new_from_array([4u8; 32]); + const TEST_ACCOUNT_KEY: Pubkey = Pubkey::new_from_array([5u8; 32]); + + /// Runs a `native_invoke_signed` call with the standard test setup and returns + /// the result. + /// + /// Same layout for all tests: + /// 0: target account (writable, signer iff `target_is_signer`) + /// 1: caller program (executable) + /// 2: mock extra (satisfies MockBuiltin's 2-account requirement) + /// 3: callee program (executable) + fn run_native_invoke_signed_test( + target_key: Pubkey, + target_is_signer: bool, + inner_instruction: Instruction, + signer_seeds: &[&[&[u8]]], + ) -> Result<(), InstructionError> { + let target_account = AccountSharedData::new(100, 0, &TEST_CALLEE_PROGRAM_ID); + let mock_extra_account = AccountSharedData::new(0, 1, &system_program::id()); + let mut caller_program_account = AccountSharedData::new(1, 1, &native_loader::id()); + caller_program_account.set_executable(true); + let mut callee_program_account = AccountSharedData::new(1, 1, &native_loader::id()); + callee_program_account.set_executable(true); + let transaction_accounts = vec![ + (target_key, target_account), + (TEST_CALLER_PROGRAM_ID, caller_program_account), + (TEST_MOCK_EXTRA_KEY, mock_extra_account), + (TEST_CALLEE_PROGRAM_ID, callee_program_account), + ]; + + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + TEST_CALLEE_PROGRAM_ID, + Arc::new(ProgramCacheEntry::new_builtin(( + MockBuiltin::vm, + MockBuiltin::codegen, + ))), + ); + invoke_context.program_cache_for_tx_batch = &mut program_cache_for_tx_batch; + + let instruction_accounts = (0..4) + .map(|i| InstructionAccount::new(i, i == 0 && target_is_signer, i < 2)) + .collect::>(); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(1, instruction_accounts, vec![]) + .unwrap(); + invoke_context.push().unwrap(); + + let result = invoke_context.native_invoke_signed(inner_instruction, signer_seeds); + invoke_context.pop().unwrap(); + result + } + + // Valid PDA seeds grant signer privilege to the derived address. + #[test] + fn test_native_invoke_signed_with_valid_pda_signer() { + let (pda_key, bump_seed) = + Pubkey::find_program_address(&[b"seed"], &TEST_CALLER_PROGRAM_ID); + let instruction = Instruction::new_with_bincode( + TEST_CALLEE_PROGRAM_ID, + &MockInstruction::NoopSuccess, + vec![ + AccountMeta::new(pda_key, true), + AccountMeta::new_readonly(TEST_MOCK_EXTRA_KEY, false), + ], + ); + let result = + run_native_invoke_signed_test(pda_key, false, instruction, &[&[b"seed", &[bump_seed]]]); + assert!( + result.is_ok(), + "valid PDA signer should succeed: {result:?}" + ); + } + + // Oversized seeds (>MAX_SEED_LEN) hit `MaxSeedLengthExceeded` + // (discriminant 0) which the broken `as u64` num-traits conversion + // maps to `Custom(0)`. + #[test] + fn test_native_invoke_signed_with_invalid_seeds() { + let instruction = Instruction::new_with_bincode( + TEST_CALLEE_PROGRAM_ID, + &MockInstruction::NoopSuccess, + vec![AccountMeta::new(TEST_ACCOUNT_KEY, true)], + ); + let oversized_seed = [0u8; 33]; + let result = run_native_invoke_signed_test( + TEST_ACCOUNT_KEY, + false, + instruction, + &[&[&oversized_seed]], + ); + assert_eq!(result, Err(InstructionError::Custom(0))); + } + + // CPI marks an account as signer but caller provides no seeds — + // signer privilege escalation. + #[test] + fn test_native_invoke_signed_pda_privilege_escalation_without_seeds() { + let (pda_key, _bump_seed) = + Pubkey::find_program_address(&[b"seed"], &TEST_CALLER_PROGRAM_ID); + let instruction = Instruction::new_with_bincode( + TEST_CALLEE_PROGRAM_ID, + &MockInstruction::NoopSuccess, + vec![AccountMeta::new(pda_key, true)], + ); + let result = run_native_invoke_signed_test(pda_key, false, instruction, &[]); + assert_eq!(result, Err(InstructionError::PrivilegeEscalation)); + } + + // Seeds valid for a different program ID don't grant signer privilege + // because native_invoke_signed derives against the caller's own program ID. + #[test] + fn test_native_invoke_signed_uses_caller_program_id_for_pda() { + let (pda_key, bump_seed) = Pubkey::find_program_address(&[b"seed"], &TEST_WRONG_PROGRAM_ID); + let instruction = Instruction::new_with_bincode( + TEST_CALLEE_PROGRAM_ID, + &MockInstruction::NoopSuccess, + vec![AccountMeta::new(pda_key, true)], + ); + let result = + run_native_invoke_signed_test(pda_key, false, instruction, &[&[b"seed", &[bump_seed]]]); + assert_eq!(result, Err(InstructionError::PrivilegeEscalation)); + } + + // Top-level signer privilege carries through CPI without needing seeds. + #[test] + fn test_native_invoke_signed_top_level_signer_needs_no_seeds() { + let (pda_key, _bump_seed) = + Pubkey::find_program_address(&[b"seed"], &TEST_CALLER_PROGRAM_ID); + let instruction = Instruction::new_with_bincode( + TEST_CALLEE_PROGRAM_ID, + &MockInstruction::NoopSuccess, + vec![ + AccountMeta::new(pda_key, true), + AccountMeta::new_readonly(TEST_MOCK_EXTRA_KEY, false), + ], + ); + let result = run_native_invoke_signed_test(pda_key, true, instruction, &[]); + assert!( + result.is_ok(), + "top-level signer should not need seeds: {result:?}" + ); + } +} diff --git a/solana/program-runtime/src/lib.rs b/solana/program-runtime/src/lib.rs new file mode 100644 index 0000000..d2642e3 --- /dev/null +++ b/solana/program-runtime/src/lib.rs @@ -0,0 +1,31 @@ +#![cfg_attr(feature = "frozen-abi", feature(min_specialization))] +#![allow(clippy::disallowed_methods)] +#![deny(clippy::arithmetic_side_effects)] +#![deny(clippy::indexing_slicing)] +#![doc = include_str!("../README.md")] + +pub use solana_sbpf; +pub mod cpi; +pub mod deploy; +pub mod execution_budget; +pub mod invoke_context; +pub mod loaded_programs; +pub mod mem_pool; +pub mod memory; +pub mod memory_context; +pub mod serialization; +pub mod stable_log; +pub mod sysvar_cache; +pub mod vm; + +// re-exports for macros +pub mod __private { + pub use { + crate::vm::{MEMORY_POOL, calculate_heap_cost, create_vm}, + solana_account::ReadableAccount, + solana_hash::Hash, + solana_instruction::error::InstructionError, + solana_rent::Rent, + solana_transaction_context::transaction::TransactionContext, + }; +} diff --git a/solana/program-runtime/src/loaded_programs.rs b/solana/program-runtime/src/loaded_programs.rs new file mode 100644 index 0000000..38e0939 --- /dev/null +++ b/solana/program-runtime/src/loaded_programs.rs @@ -0,0 +1,338 @@ +use { + crate::invoke_context::{BuiltinFunctionWithContext, InvokeContext}, + solana_clock::Slot, + solana_pubkey::Pubkey, + solana_sbpf::{ + elf::Executable, program::BuiltinProgram, verifier::RequisiteVerifier, vm::Config, + }, + solana_svm_type_overrides::sync::Arc, + std::{ + collections::HashMap, + fmt::{Debug, Formatter}, + hash::{Hash, Hasher}, + ops::Deref, + }, +}; + +#[repr(transparent)] +pub struct ProgramRuntimeEnvironment(Arc>>); + +impl ProgramRuntimeEnvironment { + pub fn from(program: BuiltinProgram>) -> Self { + Self(Arc::new(program)) + } + + /// Converts a loader reference into its transparent runtime-environment wrapper. + /// + /// # Safety + /// + /// `ProgramRuntimeEnvironment` is `repr(transparent)` over the same `Arc` + /// type, so the reference layout is identical. + pub unsafe fn from_ref<'a>( + program: &'a Arc>>, + ) -> &'a Self { + unsafe { &*(program as *const Arc<_> as *const Self) } + } +} + +impl Clone for ProgramRuntimeEnvironment { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Debug for ProgramRuntimeEnvironment { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("ProgramRuntimeEnvironment").field(&Arc::as_ptr(&self.0)).finish() + } +} + +impl Deref for ProgramRuntimeEnvironment { + type Target = Arc>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Hash for ProgramRuntimeEnvironment { + fn hash(&self, state: &mut H) { + std::ptr::hash(Arc::as_ptr(&self.0), state); + } +} + +impl PartialEq for ProgramRuntimeEnvironment { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for ProgramRuntimeEnvironment {} + +pub const MAX_LOADED_ENTRY_COUNT: usize = 512; + +/// Actual payload of [ProgramCacheEntry]. +#[derive(Default)] +pub enum ProgramCacheEntryType { + /// Program failed verification for the current runtime environment. + FailedVerification(ProgramRuntimeEnvironment), + /// Program is unavailable or intentionally closed. + #[default] + Closed, + /// Retained for API compatibility with older delayed-visibility flows. + DelayVisibility, + /// Program was verified but is not currently compiled. + Unloaded(ProgramRuntimeEnvironment), + /// Verified and compiled program. + Loaded(Executable>), + /// Builtin program shipped with the runtime. + Builtin(BuiltinProgram>), +} + +impl Debug for ProgramCacheEntryType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ProgramCacheEntryType::FailedVerification(_) => { + write!(f, "ProgramCacheEntryType::FailedVerification") + } + ProgramCacheEntryType::Closed => write!(f, "ProgramCacheEntryType::Closed"), + ProgramCacheEntryType::DelayVisibility => { + write!(f, "ProgramCacheEntryType::DelayVisibility") + } + ProgramCacheEntryType::Unloaded(_) => write!(f, "ProgramCacheEntryType::Unloaded"), + ProgramCacheEntryType::Loaded(_) => write!(f, "ProgramCacheEntryType::Loaded"), + ProgramCacheEntryType::Builtin(_) => write!(f, "ProgramCacheEntryType::Builtin"), + } + } +} + +impl ProgramCacheEntryType { + /// Returns the runtime environment when this entry keeps one. + pub fn get_environment(&self) -> Option<&ProgramRuntimeEnvironment> { + match self { + ProgramCacheEntryType::Loaded(program) => { + // SAFETY: `ProgramRuntimeEnvironment` is transparent over the loader Arc. + Some(unsafe { ProgramRuntimeEnvironment::from_ref(program.get_loader()) }) + } + ProgramCacheEntryType::FailedVerification(env) + | ProgramCacheEntryType::Unloaded(env) => Some(env), + _ => None, + } + } +} + +/// Single cache entry for a program address. +#[derive(Debug, Default)] +pub struct ProgramCacheEntry { + pub program: ProgramCacheEntryType, +} + +impl ProgramCacheEntry { + /// Creates a new user program. + pub fn new( + program_runtime_environment: ProgramRuntimeEnvironment, + elf_bytes: &[u8], + ) -> Result> { + Self::new_internal(program_runtime_environment, elf_bytes, false) + } + + /// Reloads a previously verified user program without re-running the verifier. + /// + /// # Safety + /// + /// Callers must ensure `elf_bytes` were already verified for the provided + /// runtime environment. + pub unsafe fn reload( + program_runtime_environment: ProgramRuntimeEnvironment, + elf_bytes: &[u8], + ) -> Result> { + Self::new_internal(program_runtime_environment, elf_bytes, true) + } + + fn new_internal( + program_runtime_environment: ProgramRuntimeEnvironment, + elf_bytes: &[u8], + reloading: bool, + ) -> Result> { + // Some architectures build without JIT support. + #[allow(unused_mut)] + let mut executable = + Executable::load(elf_bytes, Arc::clone(&*program_runtime_environment))?; + if !reloading { + executable.verify::()?; + } + + #[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))] + executable.jit_compile()?; + + Ok(Self { + program: ProgramCacheEntryType::Loaded(executable), + }) + } + + /// Creates a new built-in program. + pub fn new_builtin(builtin_function: BuiltinFunctionWithContext) -> Self { + let mut program = BuiltinProgram::new_builtin(); + program.register_function("entrypoint", builtin_function).unwrap(); + Self { + program: ProgramCacheEntryType::Builtin(program), + } + } +} + +/// Shared runtime environments keyed by feature configuration. +#[derive(Clone, Debug)] +pub struct ProgramRuntimeEnvironments { + execution: ProgramRuntimeEnvironment, +} + +impl ProgramRuntimeEnvironments { + pub fn new(execution: ProgramRuntimeEnvironment) -> Self { + Self { execution } + } + + pub fn get_env_for_execution(&self) -> &ProgramRuntimeEnvironment { + &self.execution + } +} + +impl Default for ProgramRuntimeEnvironments { + fn default() -> Self { + let empty_loader = + ProgramRuntimeEnvironment::from(BuiltinProgram::new_loader(Config::default())); + Self::new(empty_loader.clone()) + } +} + +/// Global program cache shared across transaction batches. +#[derive(Default)] +pub struct ProgramCache { + index: scc::HashMap>, +} + +impl Debug for ProgramCache { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ProgramCache").field("index_len", &self.index.len()).finish() + } +} + +/// Local view into [ProgramCache] used by a transaction batch. +#[derive(Clone, Debug, Default)] +pub struct ProgramCacheForTxBatch { + entries: HashMap>, + modified_entries: HashMap>, + slot: Slot, + pub hit_max_limit: bool, + pub loaded_missing: bool, + pub merged_modified: bool, +} + +impl ProgramCacheForTxBatch { + pub fn new(slot: Slot) -> Self { + Self { + entries: HashMap::new(), + modified_entries: HashMap::new(), + slot, + hit_max_limit: false, + loaded_missing: false, + merged_modified: false, + } + } + + pub fn replenish(&mut self, key: Pubkey, entry: Arc) { + self.entries.insert(key, entry); + } + + pub fn store_modified_entry(&mut self, key: Pubkey, entry: Arc) { + self.modified_entries.insert(key, entry); + } + + pub fn drain_modified_entries(&mut self) -> HashMap> { + std::mem::take(&mut self.modified_entries) + } + + pub fn find(&self, key: &Pubkey) -> Option> { + self.modified_entries.get(key).or_else(|| self.entries.get(key)).cloned() + } + + pub fn slot(&self) -> Slot { + self.slot + } + + pub fn set_slot_for_tests(&mut self, slot: Slot) { + self.slot = slot; + } + + pub fn merge(&mut self, modified_entries: &HashMap>) { + for (key, entry) in modified_entries { + self.merged_modified = true; + self.replenish(*key, entry.clone()); + } + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} + +impl ProgramCache { + pub fn assign_program(&self, key: Pubkey, entry: Arc) { + self.index.upsert_sync(key, entry); + } + + pub fn get(&self, key: &Pubkey) -> Option> { + self.index.get_sync(key).map(|e| e.get().clone()) + } + + pub fn merge(&self, modified_entries: &HashMap>) { + for (key, entry) in modified_entries { + self.assign_program(*key, entry.clone()); + } + } +} + +#[cfg(test)] +mod tests { + use { + super::{ + ProgramCache, ProgramCacheEntry, ProgramCacheEntryType, ProgramRuntimeEnvironment, + }, + solana_pubkey::Pubkey, + solana_sbpf::{elf::Executable, program::BuiltinProgram}, + solana_svm_type_overrides::sync::Arc, + }; + + static MOCK_ENVIRONMENT: std::sync::OnceLock = + std::sync::OnceLock::new(); + + fn mock_env() -> ProgramRuntimeEnvironment { + MOCK_ENVIRONMENT + .get_or_init(|| ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock())) + .clone() + } + + fn test_entry() -> Arc { + let elf = std::fs::read(concat!( + env!("CARGO_MANIFEST_DIR"), + "/fixtures/noop_aligned.so" + )) + .unwrap(); + let environment = mock_env(); + let executable = Executable::load(&elf, Arc::clone(&*environment)).unwrap(); + Arc::new(ProgramCacheEntry { + program: ProgramCacheEntryType::Loaded(executable), + }) + } + + #[test] + fn assign_program_makes_entry_retrievable() { + let cache = ProgramCache::default(); + let key = Pubkey::new_unique(); + let entry = test_entry(); + cache.assign_program(key, entry.clone()); + + let fetched = cache.get(&key); + assert!(fetched.is_some()); + assert!(Arc::ptr_eq(&fetched.unwrap(), &entry)); + } +} diff --git a/solana/program-runtime/src/mem_pool.rs b/solana/program-runtime/src/mem_pool.rs new file mode 100644 index 0000000..519d610 --- /dev/null +++ b/solana/program-runtime/src/mem_pool.rs @@ -0,0 +1,150 @@ +use { + crate::execution_budget::{ + MAX_CALL_DEPTH, MAX_HEAP_FRAME_BYTES, MAX_INSTRUCTION_STACK_DEPTH, MIN_HEAP_FRAME_BYTES, + STACK_FRAME_SIZE, + }, + solana_sbpf::{aligned_memory::AlignedMemory, ebpf::HOST_ALIGN}, + std::array, +}; + +trait Reset { + fn reset(&mut self); +} + +struct Pool { + items: [Option; SIZE], + next_empty: usize, +} + +impl Pool { + fn new(items: [T; SIZE]) -> Self { + Self { + items: items.map(|i| Some(i)), + next_empty: SIZE, + } + } + + fn len(&self) -> usize { + SIZE + } + + fn get(&mut self) -> Option { + if self.next_empty == 0 { + return None; + } + self.next_empty = self.next_empty.saturating_sub(1); + self.items.get_mut(self.next_empty).and_then(|item| item.take()) + } + + fn put(&mut self, mut value: T) -> bool { + self.items + .get_mut(self.next_empty) + .map(|item| { + value.reset(); + item.replace(value); + self.next_empty = self.next_empty.saturating_add(1); + true + }) + .unwrap_or(false) + } +} + +impl Reset for AlignedMemory<{ HOST_ALIGN }> { + fn reset(&mut self) { + self.as_slice_mut().fill(0) + } +} + +/// Fixed-size reusable stack and heap buffers for SBF VMs. +pub struct VmMemoryPool { + stack: Pool, MAX_INSTRUCTION_STACK_DEPTH>, + heap: Pool, MAX_INSTRUCTION_STACK_DEPTH>, +} + +impl VmMemoryPool { + /// Allocates a pool sized for the maximum instruction stack depth. + pub fn new() -> Self { + Self { + stack: Pool::new(array::from_fn(|_| { + AlignedMemory::zero_filled(STACK_FRAME_SIZE * MAX_CALL_DEPTH) + })), + heap: Pool::new(array::from_fn(|_| { + AlignedMemory::zero_filled(MAX_HEAP_FRAME_BYTES as usize) + })), + } + } + + /// Number of stack buffers managed by the pool. + pub fn stack_len(&self) -> usize { + self.stack.len() + } + + /// Number of heap buffers managed by the pool. + pub fn heap_len(&self) -> usize { + self.heap.len() + } + + /// Returns a zeroed stack buffer, allocating one if the pool is empty. + pub fn get_stack(&mut self, size: usize) -> AlignedMemory<{ HOST_ALIGN }> { + debug_assert!(size == STACK_FRAME_SIZE * MAX_CALL_DEPTH); + self.stack.get().unwrap_or_else(|| AlignedMemory::zero_filled(size)) + } + + /// Returns a stack buffer to the pool after clearing it. + pub fn put_stack(&mut self, stack: AlignedMemory<{ HOST_ALIGN }>) -> bool { + self.stack.put(stack) + } + + /// Returns a zeroed heap buffer, allocating one if the pool is empty. + pub fn get_heap(&mut self, heap_size: u32) -> AlignedMemory<{ HOST_ALIGN }> { + debug_assert!((MIN_HEAP_FRAME_BYTES..=MAX_HEAP_FRAME_BYTES).contains(&heap_size)); + self.heap + .get() + .unwrap_or_else(|| AlignedMemory::zero_filled(MAX_HEAP_FRAME_BYTES as usize)) + } + + /// Returns a heap buffer to the pool after clearing it. + pub fn put_heap(&mut self, heap: AlignedMemory<{ HOST_ALIGN }>) -> bool { + let heap_size = heap.len(); + debug_assert!( + heap_size >= MIN_HEAP_FRAME_BYTES as usize + && heap_size <= MAX_HEAP_FRAME_BYTES as usize + ); + self.heap.put(heap) + } +} + +impl Default for VmMemoryPool { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[derive(Debug, Eq, PartialEq)] + struct Item(u8, u8); + impl Reset for Item { + fn reset(&mut self) { + self.1 = 0; + } + } + + #[test] + fn test_pool() { + let mut pool = Pool::::new([Item(0, 1), Item(1, 1)]); + assert_eq!(pool.get(), Some(Item(1, 1))); + assert_eq!(pool.get(), Some(Item(0, 1))); + assert_eq!(pool.get(), None); + pool.put(Item(1, 1)); + assert_eq!(pool.get(), Some(Item(1, 0))); + pool.put(Item(2, 2)); + pool.put(Item(3, 3)); + assert!(!pool.put(Item(4, 4))); + assert_eq!(pool.get(), Some(Item(3, 0))); + assert_eq!(pool.get(), Some(Item(2, 0))); + assert_eq!(pool.get(), None); + } +} diff --git a/solana/program-runtime/src/memory.rs b/solana/program-runtime/src/memory.rs new file mode 100644 index 0000000..725ccd5 --- /dev/null +++ b/solana/program-runtime/src/memory.rs @@ -0,0 +1,136 @@ +//! Memory translation utilities. + +use { + solana_sbpf::memory_region::{AccessType, MemoryMapping}, + solana_transaction_context::vm_slice::VmSlice, + std::{mem::align_of, slice::from_raw_parts_mut}, +}; + +/// Error types for memory translation operations. +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum MemoryTranslationError { + #[error("Unaligned pointer")] + UnalignedPointer, + #[error("Invalid length")] + InvalidLength, +} + +pub fn address_is_aligned(address: u64) -> bool { + (address as *mut T as usize) + .checked_rem(align_of::()) + .map(|rem| rem == 0) + .expect("T to be non-zero aligned") +} + +// Do not use this directly +#[macro_export] +macro_rules! translate_inner { + ($memory_mapping:expr, $map:ident, $access_type:expr, $vm_addr:expr, $len:expr $(,)?) => { + Result::>::from( + $memory_mapping.$map($access_type, $vm_addr, $len).map_err(|err| err.into()), + ) + }; +} + +// Do not use this directly +#[macro_export] +macro_rules! translate_type_inner { + ($memory_mapping:expr, $access_type:expr, $vm_addr:expr, $T:ty, $check_aligned:expr $(,)?) => {{ + let host_addr = $crate::translate_inner!( + $memory_mapping, + map, + $access_type, + $vm_addr, + size_of::<$T>() as u64 + )?; + if !$check_aligned { + Ok(unsafe { std::mem::transmute::(host_addr) }) + } else if !$crate::memory::address_is_aligned::<$T>(host_addr) { + Err($crate::memory::MemoryTranslationError::UnalignedPointer.into()) + } else { + Ok(unsafe { &mut *(host_addr as *mut $T) }) + } + }}; +} + +// Do not use this directly +#[macro_export] +macro_rules! translate_slice_inner { + ($memory_mapping:expr, $access_type:expr, $vm_addr:expr, $len:expr, $T:ty, $check_aligned:expr $(,)?) => {{ + if $len == 0 { + return Ok(&mut []); + } + let total_size = $len.saturating_mul(size_of::<$T>() as u64); + if isize::try_from(total_size).is_err() { + return Err($crate::memory::MemoryTranslationError::InvalidLength.into()); + } + let host_addr = + $crate::translate_inner!($memory_mapping, map, $access_type, $vm_addr, total_size)?; + if $check_aligned && !$crate::memory::address_is_aligned::<$T>(host_addr) { + return Err($crate::memory::MemoryTranslationError::UnalignedPointer.into()); + } + Ok(unsafe { from_raw_parts_mut(host_addr as *mut $T, $len as usize) }) + }}; +} + +pub fn translate_type<'a, T>( + memory_mapping: &MemoryMapping, + vm_addr: u64, + check_aligned: bool, +) -> Result<&'a T, Box> { + translate_type_inner!(memory_mapping, AccessType::Load, vm_addr, T, check_aligned) + .map(|value| &*value) +} + +pub fn translate_slice( + memory_mapping: &MemoryMapping, + vm_addr: u64, + len: u64, + check_aligned: bool, +) -> Result<&[T], Box> { + translate_slice_inner!( + memory_mapping, + AccessType::Load, + vm_addr, + len, + T, + check_aligned, + ) + .map(|value| &*value) +} + +/// CPI-specific version with intentionally different lifetime signature. +/// This version is missing lifetime 'a of the return type in the parameter &MemoryMapping. +pub fn translate_type_mut_for_cpi<'a, T>( + memory_mapping: &MemoryMapping, + vm_addr: u64, + check_aligned: bool, +) -> Result<&'a mut T, Box> { + translate_type_inner!(memory_mapping, AccessType::Store, vm_addr, T, check_aligned) +} + +/// CPI-specific version with intentionally different lifetime signature. +/// This version is missing lifetime 'a of the return type in the parameter &MemoryMapping. +pub fn translate_slice_mut_for_cpi<'a, T>( + memory_mapping: &MemoryMapping, + vm_addr: u64, + len: u64, + check_aligned: bool, +) -> Result<&'a mut [T], Box> { + translate_slice_inner!( + memory_mapping, + AccessType::Store, + vm_addr, + len, + T, + check_aligned, + ) +} + +pub fn translate_vm_slice<'a, T>( + slice: &VmSlice, + memory_mapping: &'a MemoryMapping, + check_aligned: bool, +) -> Result<&'a [T], Box> { + translate_slice::(memory_mapping, slice.ptr(), slice.len(), check_aligned) +} diff --git a/solana/program-runtime/src/memory_context.rs b/solana/program-runtime/src/memory_context.rs new file mode 100644 index 0000000..b82d556 --- /dev/null +++ b/solana/program-runtime/src/memory_context.rs @@ -0,0 +1,87 @@ +use { + crate::invoke_context::BpfAllocator, solana_instruction::error::InstructionError, + solana_sbpf::memory_region::MemoryMapping, +}; + +enum MemoryContextType { + ABIv1(MemoryContext), + Placeholder, +} + +pub struct MemoryContexts { + contexts: Vec, +} + +impl MemoryContexts { + pub(crate) fn new() -> Self { + Self { contexts: Vec::new() } + } + + pub fn set_memory_context_abi_v1( + &mut self, + memory_context: MemoryContext, + ) -> Result<(), InstructionError> { + *self.contexts.last_mut().ok_or(InstructionError::CallDepth)? = + MemoryContextType::ABIv1(memory_context); + Ok(()) + } + + pub fn memory_context_mut_abi_v1(&mut self) -> Result<&mut MemoryContext, InstructionError> { + match self.contexts.last_mut().ok_or(InstructionError::CallDepth)? { + MemoryContextType::ABIv1(context) => Ok(context), + MemoryContextType::Placeholder => Err(InstructionError::ProgramEnvironmentSetupFailure), + } + } + + pub fn memory_mapping(&self) -> Result<&MemoryMapping, InstructionError> { + match self.contexts.last().ok_or(InstructionError::CallDepth)? { + MemoryContextType::ABIv1(context) => Ok(&context.memory_mapping), + MemoryContextType::Placeholder => Err(InstructionError::ProgramEnvironmentSetupFailure), + } + } + + pub fn memory_mapping_mut(&mut self) -> Result<&mut MemoryMapping, InstructionError> { + match self.contexts.last_mut().ok_or(InstructionError::CallDepth)? { + MemoryContextType::ABIv1(context) => Ok(&mut context.memory_mapping), + MemoryContextType::Placeholder => Err(InstructionError::ProgramEnvironmentSetupFailure), + } + } + + pub fn push_placeholder(&mut self) { + self.contexts.push(MemoryContextType::Placeholder); + } + + pub fn pop(&mut self) { + self.contexts.pop(); + } +} + +pub struct MemoryContext { + pub allocator: BpfAllocator, + pub accounts_metadata: Vec, + memory_mapping: Box, +} + +impl MemoryContext { + pub fn new( + allocator: BpfAllocator, + accounts_metadata: Vec, + memory_mapping: MemoryMapping, + ) -> Self { + Self { + allocator, + accounts_metadata, + memory_mapping: Box::new(memory_mapping), + } + } +} + +#[derive(Debug, Clone)] +pub struct SerializedAccountMetadata { + pub vm_addr: u64, + pub original_data_len: usize, + pub vm_data_addr: u64, + pub vm_key_addr: u64, + pub vm_lamports_addr: u64, + pub vm_owner_addr: u64, +} diff --git a/solana/program-runtime/src/serialization.rs b/solana/program-runtime/src/serialization.rs new file mode 100644 index 0000000..6e7263e --- /dev/null +++ b/solana/program-runtime/src/serialization.rs @@ -0,0 +1,1698 @@ +#![allow(clippy::arithmetic_side_effects)] + +use { + crate::invoke_context::SerializedAccountMetadata, + solana_instruction::error::InstructionError, + solana_program_entrypoint::{BPF_ALIGN_OF_U128, MAX_PERMITTED_DATA_INCREASE, NON_DUP_MARKER}, + solana_pubkey::Pubkey, + solana_sbpf::{ + aligned_memory::{AlignedMemory, Pod}, + ebpf::{HOST_ALIGN, MM_INPUT_START}, + memory_region::MemoryRegion, + }, + solana_sdk_ids::bpf_loader_deprecated, + solana_system_interface::MAX_PERMITTED_DATA_LENGTH, + solana_transaction_context::{ + IndexOfAccount, MAX_ACCOUNTS_PER_INSTRUCTION, instruction::InstructionContext, + instruction_accounts::BorrowedInstructionAccount, + }, + std::mem::{self, size_of}, +}; + +/// Modifies an existing memory mapping region to point at account data. +pub fn modify_memory_region_of_account( + account: &mut BorrowedInstructionAccount<'_, '_>, + region: &mut MemoryRegion, +) { + region.len = account.get_data().len() as u64; + if account.can_data_be_changed().is_ok() { + region.writable = true; + region.access_violation_handler_payload = Some(account.get_index_in_transaction()); + } else { + region.writable = false; + region.access_violation_handler_payload = None; + } +} + +/// Creates the memory mapping in serialization and CPI return for direct account data. +pub fn create_memory_region_of_account( + account: &mut BorrowedInstructionAccount<'_, '_>, + vaddr: u64, +) -> Result { + let can_data_be_changed = account.can_data_be_changed().is_ok(); + let mut memory_region = if can_data_be_changed && !account.is_shared() { + MemoryRegion::new(&raw mut account.get_data_mut()?[..], vaddr) + } else { + MemoryRegion::new(&raw const account.get_data()[..], vaddr) + }; + if can_data_be_changed { + memory_region.access_violation_handler_payload = Some(account.get_index_in_transaction()); + } + Ok(memory_region) +} + +#[allow(dead_code)] +enum SerializeAccount<'a, 'ix_data> { + Account(IndexOfAccount, BorrowedInstructionAccount<'a, 'ix_data>), + Duplicate(IndexOfAccount), +} + +struct Serializer { + buffer: AlignedMemory, + regions: Vec, + vaddr: u64, + region_start: usize, + is_loader_v1: bool, +} + +impl Serializer { + fn new(size: usize, start_addr: u64, is_loader_v1: bool) -> Serializer { + Serializer { + buffer: AlignedMemory::with_capacity(size), + regions: Vec::new(), + region_start: 0, + vaddr: start_addr, + is_loader_v1, + } + } + + fn fill_write(&mut self, num: usize, value: u8) -> std::io::Result<()> { + self.buffer.fill_write(num, value) + } + + fn write(&mut self, value: T) -> u64 { + self.debug_assert_alignment::(); + let vaddr = self + .vaddr + .saturating_add(self.buffer.len() as u64) + .saturating_sub(self.region_start as u64); + // Safety: + // in serialize_parameters_(aligned|unaligned) first we compute the + // required size then we write into the newly allocated buffer. There's + // no need to check bounds at every write. + // + // AlignedMemory::write_unchecked _does_ debug_assert!() that the capacity + // is enough, so in the unlikely case we introduce a bug in the size + // computation, tests will abort. + unsafe { + self.buffer.write_unchecked(value); + } + + vaddr + } + + fn write_all(&mut self, value: &[u8]) -> u64 { + let vaddr = self + .vaddr + .saturating_add(self.buffer.len() as u64) + .saturating_sub(self.region_start as u64); + // Safety: + // see write() - the buffer is guaranteed to be large enough + unsafe { + self.buffer.write_all_unchecked(value); + } + + vaddr + } + + fn write_account( + &mut self, + account: &mut BorrowedInstructionAccount<'_, '_>, + ) -> Result { + self.push_region(); + let vm_data_addr = self.vaddr; + let address_space_reserved_for_account = if !self.is_loader_v1 { + account.get_data().len().saturating_add(MAX_PERMITTED_DATA_INCREASE) + } else { + account.get_data().len() + }; + if address_space_reserved_for_account > 0 { + let new_region = create_memory_region_of_account(account, self.vaddr)?; + self.vaddr += address_space_reserved_for_account as u64; + self.regions.push(new_region); + } + if !self.is_loader_v1 { + let align_offset = + (account.get_data().len() as *const u8).align_offset(BPF_ALIGN_OF_U128); + // The deserialization code is going to align the vm_addr to + // BPF_ALIGN_OF_U128. Always add one BPF_ALIGN_OF_U128 worth of + // padding and shift the start of the next region, so that once + // vm_addr is aligned, the corresponding host_addr is aligned too. + self.fill_write(BPF_ALIGN_OF_U128, 0) + .map_err(|_| InstructionError::InvalidArgument)?; + self.region_start += BPF_ALIGN_OF_U128.saturating_sub(align_offset); + } + Ok(vm_data_addr) + } + + fn push_region(&mut self) { + let range = self.region_start..self.buffer.len(); + self.regions.push(MemoryRegion::new( + &raw mut self.buffer.as_slice_mut().get_mut(range.clone()).unwrap()[..], + self.vaddr, + )); + self.region_start = range.end; + self.vaddr += range.len() as u64; + } + + fn finish(mut self) -> (AlignedMemory, Vec) { + self.push_region(); + debug_assert_eq!(self.region_start, self.buffer.len()); + (self.buffer, self.regions) + } + + fn debug_assert_alignment(&self) { + debug_assert!( + self.is_loader_v1 + || self.buffer.as_slice().as_ptr_range().end.align_offset(mem::align_of::()) + == 0 + ); + } +} + +pub fn serialize_parameters( + instruction_context: &InstructionContext, + direct_account_pointers_in_program_input: bool, +) -> Result< + ( + AlignedMemory, + Vec, + Vec, + usize, + ), + InstructionError, +> { + let num_ix_accounts = instruction_context.get_number_of_instruction_accounts(); + if num_ix_accounts > MAX_ACCOUNTS_PER_INSTRUCTION as IndexOfAccount { + return Err(InstructionError::MaxAccountsExceeded); + } + + let program_id = *instruction_context.get_program_key()?; + let is_loader_deprecated = + instruction_context.get_program_owner()? == bpf_loader_deprecated::id(); + + let accounts = (0..instruction_context.get_number_of_instruction_accounts()) + .map(|instruction_account_index| { + if let Some(index) = instruction_context + .is_instruction_account_duplicate(instruction_account_index) + .unwrap() + { + SerializeAccount::Duplicate(index) + } else { + let account = instruction_context + .try_borrow_instruction_account(instruction_account_index) + .unwrap(); + SerializeAccount::Account(instruction_account_index, account) + } + }) + // fun fact: jemalloc is good at caching tiny allocations like this one, + // so collecting here is actually faster than passing the iterator + // around, since the iterator does the work to produce its items each + // time it's iterated on. + .collect::>(); + + if is_loader_deprecated { + // Used by loader-v1 (bpf_loader_deprecated) + serialize_parameters_for_abiv0( + accounts, + instruction_context.get_instruction_data(), + &program_id, + ) + } else { + // Used by loader-v2 (bpf_loader) and loader-v3 (bpf_loader_upgradeable) + serialize_parameters_for_abiv1( + accounts, + instruction_context.get_instruction_data(), + &program_id, + // SIMD-0449: only available on ABIv1 + direct_account_pointers_in_program_input, + ) + } +} + +pub fn deserialize_parameters( + instruction_context: &InstructionContext, + buffer: &[u8], + accounts_metadata: &[SerializedAccountMetadata], +) -> Result<(), InstructionError> { + let is_loader_deprecated = + instruction_context.get_program_owner()? == bpf_loader_deprecated::id(); + let account_lengths = accounts_metadata.iter().map(|a| a.original_data_len); + if is_loader_deprecated { + // Used by loader-v1 (bpf_loader_deprecated) + deserialize_parameters_for_abiv0(instruction_context, buffer, account_lengths) + } else { + // Used by loader-v2 (bpf_loader) and loader-v3 (bpf_loader_upgradeable) + deserialize_parameters_for_abiv1(instruction_context, buffer, account_lengths) + } +} + +fn serialize_parameters_for_abiv0( + accounts: Vec, + instruction_data: &[u8], + program_id: &Pubkey, +) -> Result< + ( + AlignedMemory, + Vec, + Vec, + usize, + ), + InstructionError, +> { + // Calculate size in order to alloc once + let mut size = size_of::(); + for account in &accounts { + size += 1; // dup + match account { + SerializeAccount::Duplicate(_) => {} + SerializeAccount::Account(_, _) => { + size += size_of::() // is_signer + + size_of::() // is_writable + + size_of::() // key + + size_of::() // lamports + + size_of::() // data len + + size_of::() // owner + + size_of::() // executable + + size_of::(); // rent_epoch + } + } + } + size += size_of::() // instruction data len + + instruction_data.len() // instruction data + + size_of::(); // program id + + let mut s = Serializer::new(size, MM_INPUT_START, true); + + let mut accounts_metadata: Vec = Vec::with_capacity(accounts.len()); + s.write::((accounts.len() as u64).to_le()); + for account in accounts { + match account { + SerializeAccount::Duplicate(position) => { + accounts_metadata.push(accounts_metadata.get(position as usize).unwrap().clone()); + s.write(position as u8); + } + SerializeAccount::Account(_, mut account) => { + let vm_addr = s.write::(NON_DUP_MARKER); + s.write::(account.is_signer() as u8); + s.write::(account.is_writable() as u8); + let vm_key_addr = s.write_all(account.get_key().as_ref()); + let vm_lamports_addr = s.write::(account.get_lamports().to_le()); + s.write::((account.get_data().len() as u64).to_le()); + let vm_data_addr = s.write_account(&mut account)?; + let vm_owner_addr = s.write_all(account.get_owner().as_ref()); + #[allow(deprecated)] + s.write::(account.is_executable() as u8); + let rent_epoch = u64::MAX; + s.write::(rent_epoch.to_le()); + accounts_metadata.push(SerializedAccountMetadata { + vm_addr, + original_data_len: account.get_data().len(), + vm_key_addr, + vm_lamports_addr, + vm_owner_addr, + vm_data_addr, + }); + } + }; + } + s.write::((instruction_data.len() as u64).to_le()); + let instruction_data_offset = s.write_all(instruction_data); + s.write_all(program_id.as_ref()); + + let (mem, regions) = s.finish(); + Ok(( + mem, + regions, + accounts_metadata, + instruction_data_offset as usize, + )) +} + +fn deserialize_parameters_for_abiv0>( + instruction_context: &InstructionContext, + buffer: &[u8], + account_lengths: I, +) -> Result<(), InstructionError> { + let mut start = size_of::(); // number of accounts + for (instruction_account_index, pre_len) in (0..instruction_context + .get_number_of_instruction_accounts()) + .zip(account_lengths.into_iter()) + { + let duplicate = + instruction_context.is_instruction_account_duplicate(instruction_account_index)?; + start += 1; // is_dup + if duplicate.is_none() { + let mut borrowed_account = + instruction_context.try_borrow_instruction_account(instruction_account_index)?; + start += size_of::(); // is_signer + start += size_of::(); // is_writable + start += size_of::(); // key + let lamports = buffer + .get(start..start.saturating_add(8)) + .map(<[u8; 8]>::try_from) + .and_then(Result::ok) + .map(u64::from_le_bytes) + .ok_or(InstructionError::InvalidArgument)?; + if borrowed_account.get_lamports() != lamports { + borrowed_account.set_lamports(lamports)?; + } + start += size_of::() // lamports + + size_of::(); // data length + if borrowed_account.get_data().len() != pre_len { + borrowed_account.set_data_length(pre_len)?; + } + start += size_of::() // owner + + size_of::() // executable + + size_of::(); // rent_epoch + } + } + Ok(()) +} + +fn serialize_parameters_for_abiv1( + accounts: Vec, + instruction_data: &[u8], + program_id: &Pubkey, + direct_account_pointers_program_input: bool, +) -> Result< + ( + AlignedMemory, + Vec, + Vec, + usize, + ), + InstructionError, +> { + let mut accounts_metadata = Vec::with_capacity(accounts.len()); + // Calculate size in order to alloc once + let mut size = size_of::(); + for account in &accounts { + size += 1; // dup + match account { + SerializeAccount::Duplicate(_) => size += 7, // padding to 64-bit aligned + SerializeAccount::Account(_, _) => { + size += size_of::() // is_signer + + size_of::() // is_writable + + size_of::() // executable + + size_of::() // original_data_len + + size_of::() // key + + size_of::() // owner + + size_of::() // lamports + + size_of::() // data len + + size_of::(); // rent epoch + size += BPF_ALIGN_OF_U128; + } + } + } + size += size_of::() // data len + + instruction_data.len() + + size_of::(); // program id; + + // reserve space for account pointer array if SIMD-0449 is enabled + let account_pointers_offset = if direct_account_pointers_program_input { + let offset = (size as *const u8).align_offset(BPF_ALIGN_OF_U128); + size += offset + accounts.len() * size_of::(); + Some(offset) + } else { + None + }; + + let mut s = Serializer::new(size, MM_INPUT_START, false); + + // Serialize into the buffer + s.write::((accounts.len() as u64).to_le()); + for account in accounts { + match account { + SerializeAccount::Account(_, mut borrowed_account) => { + let vm_addr = s.write::(NON_DUP_MARKER); + s.write::(borrowed_account.is_signer() as u8); + s.write::(borrowed_account.is_writable() as u8); + #[allow(deprecated)] + s.write::(borrowed_account.is_executable() as u8); + s.write_all(&[0u8, 0, 0, 0]); + let vm_key_addr = s.write_all(borrowed_account.get_key().as_ref()); + let vm_owner_addr = s.write_all(borrowed_account.get_owner().as_ref()); + let vm_lamports_addr = s.write::(borrowed_account.get_lamports().to_le()); + s.write::((borrowed_account.get_data().len() as u64).to_le()); + let vm_data_addr = s.write_account(&mut borrowed_account)?; + let rent_epoch = u64::MAX; + s.write::(rent_epoch.to_le()); + accounts_metadata.push(SerializedAccountMetadata { + vm_addr, + original_data_len: borrowed_account.get_data().len(), + vm_key_addr, + vm_owner_addr, + vm_lamports_addr, + vm_data_addr, + }); + } + SerializeAccount::Duplicate(position) => { + accounts_metadata.push(accounts_metadata.get(position as usize).unwrap().clone()); + s.write::(position as u8); + s.write_all(&[0u8, 0, 0, 0, 0, 0, 0]); + } + }; + } + s.write::((instruction_data.len() as u64).to_le()); + let instruction_data_offset = s.write_all(instruction_data); + s.write_all(program_id.as_ref()); + + if let Some(offset) = account_pointers_offset { + // Add padding before the account pointer array to reach 8-byte alignment + // (BPF_ALIGN_OF_U128). + s.fill_write(offset, 0).map_err(|_| InstructionError::InvalidArgument)?; + for entry in accounts_metadata.iter() { + s.write::(entry.vm_addr.to_le()); + } + } + + let (mem, regions) = s.finish(); + Ok(( + mem, + regions, + accounts_metadata, + instruction_data_offset as usize, + )) +} + +fn deserialize_parameters_for_abiv1>( + instruction_context: &InstructionContext, + buffer: &[u8], + account_lengths: I, +) -> Result<(), InstructionError> { + let mut start = size_of::(); // number of accounts + for (instruction_account_index, pre_len) in (0..instruction_context + .get_number_of_instruction_accounts()) + .zip(account_lengths.into_iter()) + { + let duplicate = + instruction_context.is_instruction_account_duplicate(instruction_account_index)?; + start += size_of::(); // position + if duplicate.is_some() { + start += 7; // padding to 64-bit aligned + } else { + let mut borrowed_account = + instruction_context.try_borrow_instruction_account(instruction_account_index)?; + start += size_of::() // is_signer + + size_of::() // is_writable + + size_of::() // executable + + size_of::() // original_data_len + + size_of::(); // key + let owner = buffer + .get(start..start + size_of::()) + .ok_or(InstructionError::InvalidArgument)?; + start += size_of::(); // owner + let lamports = buffer + .get(start..start.saturating_add(8)) + .map(<[u8; 8]>::try_from) + .and_then(Result::ok) + .map(u64::from_le_bytes) + .ok_or(InstructionError::InvalidArgument)?; + if borrowed_account.get_lamports() != lamports { + borrowed_account.set_lamports(lamports)?; + } + start += size_of::(); // lamports + let post_len = buffer + .get(start..start.saturating_add(8)) + .map(<[u8; 8]>::try_from) + .and_then(Result::ok) + .map(u64::from_le_bytes) + .ok_or(InstructionError::InvalidArgument)? as usize; + start += size_of::(); // data length + if post_len.saturating_sub(pre_len) > MAX_PERMITTED_DATA_INCREASE + || post_len > MAX_PERMITTED_DATA_LENGTH as usize + { + return Err(InstructionError::InvalidRealloc); + } + if borrowed_account.get_data().len() != post_len { + borrowed_account.set_data_length(post_len)?; + } + // See Serializer::write_account() as to why we have this padding. + start += BPF_ALIGN_OF_U128; + start += size_of::(); // rent_epoch + if borrowed_account.get_owner().to_bytes() != owner { + // Change the owner at the end so that we are allowed to change the lamports and data before + borrowed_account.set_owner(owner)?; + } + } + } + Ok(()) +} + +#[cfg(test)] +#[allow(clippy::indexing_slicing)] +mod tests { + use { + super::*, + crate::with_mock_invoke_context, + solana_account::{ + Account, AccountSharedData, CoWAccount, ReadableAccount, + test_utils::{active_borrowed_data, borrowed_account_buffer, borrowed_shared_data}, + }, + solana_account_info::AccountInfo, + solana_program_entrypoint::deserialize, + solana_rent::Rent, + solana_sbpf::{memory_region::MemoryMapping, program::SBPFVersion, vm::Config}, + solana_sdk_ids::bpf_loader, + solana_system_interface::MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION, + solana_transaction_context::{ + MAX_ACCOUNTS_PER_TRANSACTION, instruction_accounts::InstructionAccount, + transaction::TransactionContext, + }, + std::{ + borrow::Cow, + cell::RefCell, + mem::transmute, + rc::Rc, + slice::{self, from_raw_parts, from_raw_parts_mut}, + }, + test_case::test_case, + }; + + fn deduplicated_instruction_accounts( + transaction_indexes: &[IndexOfAccount], + is_writable: fn(usize) -> bool, + ) -> Vec { + transaction_indexes + .iter() + .enumerate() + .map(|(index_in_instruction, index_in_transaction)| { + InstructionAccount::new( + *index_in_transaction, + false, + is_writable(index_in_instruction), + ) + }) + .collect() + } + + #[test_case(false; "direct_account_pointers_in_program_input disabled")] + #[test_case(true; "direct_account_pointers_in_program_input enabled")] + fn test_serialize_parameters_with_many_accounts( + direct_account_pointers_in_program_input: bool, + ) { + struct TestCase { + num_ix_accounts: usize, + append_dup_account: bool, + expected_err: Option, + name: &'static str, + } + + for TestCase { + num_ix_accounts, + append_dup_account, + expected_err, + name, + } in [ + TestCase { + name: "serialize max accounts with cap", + num_ix_accounts: MAX_ACCOUNTS_PER_INSTRUCTION, + append_dup_account: false, + expected_err: None, + }, + TestCase { + name: "serialize too many accounts with cap", + num_ix_accounts: MAX_ACCOUNTS_PER_INSTRUCTION + 1, + append_dup_account: false, + expected_err: Some(InstructionError::MaxAccountsExceeded), + }, + TestCase { + name: "serialize too many accounts and append dup with cap", + num_ix_accounts: MAX_ACCOUNTS_PER_INSTRUCTION, + append_dup_account: true, + expected_err: Some(InstructionError::MaxAccountsExceeded), + }, + ] { + let program_id = solana_pubkey::new_rand(); + let mut transaction_accounts = vec![( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + )]; + for _ in 0..num_ix_accounts { + transaction_accounts.push(( + Pubkey::new_unique(), + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: program_id, + executable: false, + rent_epoch: 0, + }), + )); + } + + let transaction_accounts_indexes: Vec = + (0..num_ix_accounts as u16).collect(); + let mut instruction_accounts = + deduplicated_instruction_accounts(&transaction_accounts_indexes, |_| false); + if append_dup_account { + instruction_accounts.push(instruction_accounts.last().cloned().unwrap()); + } + let instruction_data = vec![]; + + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + if instruction_accounts.len() > MAX_ACCOUNTS_PER_INSTRUCTION { + // Special case implementation of configure_next_instruction_for_tests() + // which avoids the overflow when constructing the dedup_map + // by simply not filling it. + let dedup_map = vec![u16::MAX; MAX_ACCOUNTS_PER_TRANSACTION]; + invoke_context + .transaction_context + .configure_instruction_at_index( + 0, + 0, + instruction_accounts, + dedup_map, + Cow::Owned(instruction_data.clone()), + Some(0), + ) + .unwrap(); + } else { + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + 0, + instruction_accounts, + instruction_data.clone(), + ) + .unwrap(); + } + invoke_context.push().unwrap(); + let instruction_context = + invoke_context.transaction_context.get_current_instruction_context().unwrap(); + + let serialization_result = serialize_parameters( + &instruction_context, + direct_account_pointers_in_program_input, + ); + assert_eq!( + serialization_result.as_ref().err(), + expected_err.as_ref(), + "{name} test case failed", + ); + if expected_err.is_some() { + continue; + } + + let (_serialized, regions, _account_lengths, _instruction_data_offset) = + serialization_result.unwrap(); + let mut serialized_regions = concat_regions(®ions); + let (de_program_id, de_accounts, de_instruction_data) = unsafe { + deserialize(serialized_regions.as_slice_mut().first_mut().unwrap() as *mut u8) + }; + assert_eq!(de_program_id, &program_id); + assert_eq!(de_instruction_data, &instruction_data); + for account_info in de_accounts { + let index_in_transaction = invoke_context + .transaction_context + .find_index_of_account(account_info.key) + .unwrap(); + let account = invoke_context + .transaction_context + .accounts() + .try_borrow(index_in_transaction) + .unwrap(); + assert_eq!(account.lamports(), account_info.lamports()); + assert_eq!(account.data(), &account_info.data.borrow()[..]); + assert_eq!(account.owner(), account_info.owner); + assert_eq!(account.executable(), account_info.executable); + #[allow(deprecated)] + { + // Using the sdk entrypoint, the rent-epoch is skipped + assert_eq!(0, account_info._unused); + } + } + } + } + + #[test_case(false; "direct_account_pointers_in_program_input disabled")] + #[test_case(true; "direct_account_pointers_in_program_input enabled")] + fn test_serialize_parameters(direct_account_pointers_in_program_input: bool) { + let program_id = solana_pubkey::new_rand(); + let transaction_accounts = vec![ + ( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 1, + data: vec![1u8, 2, 3, 4, 5], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 2, + data: vec![11u8, 12, 13, 14, 15, 16, 17, 18, 19], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 200, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 3, + data: vec![], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 3100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 4, + data: vec![1u8, 2, 3, 4, 5], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 5, + data: vec![11u8, 12, 13, 14, 15, 16, 17, 18, 19], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 200, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 6, + data: vec![], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 3100, + }), + ), + ( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader_deprecated::id(), + executable: true, + rent_epoch: 0, + }), + ), + ]; + let instruction_accounts = + deduplicated_instruction_accounts(&[1, 1, 2, 3, 4, 4, 5, 6], |index| index >= 4); + let instruction_data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + let original_accounts = transaction_accounts.clone(); + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + 0, + instruction_accounts.clone(), + instruction_data.clone(), + ) + .unwrap(); + invoke_context.push().unwrap(); + let instruction_context = + invoke_context.transaction_context.get_current_instruction_context().unwrap(); + + // check serialize_parameters_for_abiv1 + let (serialized, regions, accounts_metadata, _instruction_data_offset) = + serialize_parameters( + &instruction_context, + direct_account_pointers_in_program_input, + ) + .unwrap(); + + let mut serialized_regions = concat_regions(®ions); + let (de_program_id, de_accounts, de_instruction_data) = unsafe { + deserialize(serialized_regions.as_slice_mut().first_mut().unwrap() as *mut u8) + }; + + assert_eq!(&program_id, de_program_id); + assert_eq!(instruction_data, de_instruction_data); + assert_eq!( + (de_instruction_data.first().unwrap() as *const u8).align_offset(BPF_ALIGN_OF_U128), + 0 + ); + for account_info in de_accounts { + let index_in_transaction = invoke_context + .transaction_context + .find_index_of_account(account_info.key) + .unwrap(); + let account = invoke_context + .transaction_context + .accounts() + .try_borrow(index_in_transaction) + .unwrap(); + assert_eq!(account.lamports(), account_info.lamports()); + assert_eq!(account.data(), &account_info.data.borrow()[..]); + assert_eq!(account.owner(), account_info.owner); + assert_eq!(account.executable(), account_info.executable); + #[allow(deprecated)] + { + // Using the sdk entrypoint, the rent-epoch is skipped + assert_eq!(0, account_info._unused); + } + + assert_eq!( + (*account_info.lamports.borrow() as *const u64).align_offset(BPF_ALIGN_OF_U128), + 0 + ); + assert_eq!( + account_info.data.borrow().as_ptr().align_offset(BPF_ALIGN_OF_U128), + 0 + ); + } + + deserialize_parameters( + &instruction_context, + serialized.as_slice(), + &accounts_metadata, + ) + .unwrap(); + for (index_in_transaction, (_key, original_account)) in original_accounts.iter().enumerate() + { + let account = invoke_context + .transaction_context + .accounts() + .try_borrow(index_in_transaction as IndexOfAccount) + .unwrap(); + assert_eq!(&*account, original_account); + } + + invoke_context.pop().unwrap(); + // check serialize_parameters_for_abiv0 + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + 7, + instruction_accounts, + instruction_data.clone(), + ) + .unwrap(); + invoke_context.push().unwrap(); + let instruction_context = + invoke_context.transaction_context.get_current_instruction_context().unwrap(); + + let (serialized, regions, account_lengths, _instruction_data_offset) = + serialize_parameters( + &instruction_context, + direct_account_pointers_in_program_input, + ) + .unwrap(); + let mut serialized_regions = concat_regions(®ions); + + let (de_program_id, de_accounts, de_instruction_data) = unsafe { + deserialize_for_abiv0(serialized_regions.as_slice_mut().first_mut().unwrap() as *mut u8) + }; + assert_eq!(&program_id, de_program_id); + assert_eq!(instruction_data, de_instruction_data); + for account_info in de_accounts { + let index_in_transaction = invoke_context + .transaction_context + .find_index_of_account(account_info.key) + .unwrap(); + let account = invoke_context + .transaction_context + .accounts() + .try_borrow(index_in_transaction) + .unwrap(); + assert_eq!(account.lamports(), account_info.lamports()); + assert_eq!(account.data(), &account_info.data.borrow()[..]); + assert_eq!(account.owner(), account_info.owner); + assert_eq!(account.executable(), account_info.executable); + #[allow(deprecated)] + { + assert_eq!(u64::MAX, account_info._unused); + } + } + + deserialize_parameters( + &instruction_context, + serialized.as_slice(), + &account_lengths, + ) + .unwrap(); + for (index_in_transaction, (_key, original_account)) in original_accounts.iter().enumerate() + { + let account = invoke_context + .transaction_context + .accounts() + .try_borrow(index_in_transaction as IndexOfAccount) + .unwrap(); + assert_eq!(&*account, original_account); + } + } + + #[test_case(false; "direct_account_pointers_in_program_input disabled")] + #[test_case(true; "direct_account_pointers_in_program_input enabled")] + fn test_serialize_parameters_mask_out_rent_epoch_in_vm_serialization( + direct_account_pointers_in_program_input: bool, + ) { + let transaction_accounts = vec![ + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 1, + data: vec![1u8, 2, 3, 4, 5], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 2, + data: vec![11u8, 12, 13, 14, 15, 16, 17, 18, 19], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 200, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 3, + data: vec![], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 300, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 4, + data: vec![1u8, 2, 3, 4, 5], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 5, + data: vec![11u8, 12, 13, 14, 15, 16, 17, 18, 19], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 200, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 6, + data: vec![], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 3100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader_deprecated::id(), + executable: true, + rent_epoch: 0, + }), + ), + ]; + let instruction_accounts = + deduplicated_instruction_accounts(&[1, 1, 2, 3, 4, 4, 5, 6], |index| index >= 4); + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(0, instruction_accounts.clone(), vec![]) + .unwrap(); + invoke_context.push().unwrap(); + let instruction_context = + invoke_context.transaction_context.get_current_instruction_context().unwrap(); + + // check serialize_parameters_for_abiv1 + let (_serialized, regions, _accounts_metadata, _instruction_data_offset) = + serialize_parameters( + &instruction_context, + direct_account_pointers_in_program_input, + ) + .unwrap(); + + let mut serialized_regions = concat_regions(®ions); + let (_de_program_id, de_accounts, _de_instruction_data) = unsafe { + deserialize(serialized_regions.as_slice_mut().first_mut().unwrap() as *mut u8) + }; + + for account_info in de_accounts { + // Using program-entrypoint, the rent-epoch will always be 0 + #[allow(deprecated)] + { + assert_eq!(0, account_info._unused); + } + } + + // check serialize_parameters_for_abiv0 + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(7, instruction_accounts, vec![]) + .unwrap(); + invoke_context.push().unwrap(); + let instruction_context = + invoke_context.transaction_context.get_current_instruction_context().unwrap(); + + let (_serialized, regions, _account_lengths, _instruction_data_offset) = + serialize_parameters( + &instruction_context, + direct_account_pointers_in_program_input, + ) + .unwrap(); + let mut serialized_regions = concat_regions(®ions); + + let (_de_program_id, de_accounts, _de_instruction_data) = unsafe { + deserialize_for_abiv0(serialized_regions.as_slice_mut().first_mut().unwrap() as *mut u8) + }; + for account_info in de_accounts { + #[allow(deprecated)] + { + assert_eq!(account_info._unused, u64::MAX); + } + } + } + + // the old bpf_loader in-program deserializer bpf_loader::id() + /// + /// # Safety + /// + /// `input` must point to a valid ABI-v0 serialized instruction buffer laid + /// out exactly as the legacy loader expects for the duration of the returned + /// borrows. + #[deny(unsafe_op_in_unsafe_fn)] + unsafe fn deserialize_for_abiv0<'a>( + input: *mut u8, + ) -> (&'a Pubkey, Vec>, &'a [u8]) { + // this boring boilerplate struct is needed until inline const... + struct Ptr(std::marker::PhantomData); + impl Ptr { + const COULD_BE_UNALIGNED: bool = std::mem::align_of::() > 1; + + #[inline(always)] + fn read_possibly_unaligned(input: *mut u8, offset: usize) -> T { + unsafe { + let src = input.add(offset) as *const T; + if Self::COULD_BE_UNALIGNED { src.read_unaligned() } else { src.read() } + } + } + + // rustc inserts debug_assert! for misaligned pointer dereferences when + // deserializing, starting from [1]. so, use std::mem::transmute as the last resort + // while preventing clippy from complaining to suggest not to use it. + // [1]: https://github.com/rust-lang/rust/commit/22a7a19f9333bc1fcba97ce444a3515cb5fb33e6 + // as for the ub nature of the misaligned pointer dereference, this is + // acceptable in this code, given that this is cfg(test) and it's cared only with + // x86-64 and the target only incurs some performance penalty, not like segfaults + // in other targets. + #[inline(always)] + fn ref_possibly_unaligned<'a>(input: *mut u8, offset: usize) -> &'a T { + #[allow(clippy::transmute_ptr_to_ref)] + unsafe { + transmute(input.add(offset) as *const T) + } + } + + // See ref_possibly_unaligned's comment + #[inline(always)] + fn mut_possibly_unaligned<'a>(input: *mut u8, offset: usize) -> &'a mut T { + #[allow(clippy::transmute_ptr_to_ref)] + unsafe { + transmute(input.add(offset) as *mut T) + } + } + } + + let mut offset: usize = 0; + + // number of accounts present + + let num_accounts = Ptr::::read_possibly_unaligned(input, offset) as usize; + offset += size_of::(); + + // account Infos + + let mut accounts = Vec::with_capacity(num_accounts); + for _ in 0..num_accounts { + let dup_info = Ptr::::read_possibly_unaligned(input, offset); + offset += size_of::(); + if dup_info == NON_DUP_MARKER { + let is_signer = Ptr::::read_possibly_unaligned(input, offset) != 0; + offset += size_of::(); + + let is_writable = Ptr::::read_possibly_unaligned(input, offset) != 0; + offset += size_of::(); + + let key = Ptr::::ref_possibly_unaligned(input, offset); + offset += size_of::(); + + let lamports = Rc::new(RefCell::new(Ptr::mut_possibly_unaligned(input, offset))); + offset += size_of::(); + + let data_len = Ptr::::read_possibly_unaligned(input, offset) as usize; + offset += size_of::(); + + let data = Rc::new(RefCell::new(unsafe { + from_raw_parts_mut(input.add(offset), data_len) + })); + offset += data_len; + + let owner: &Pubkey = Ptr::::ref_possibly_unaligned(input, offset); + offset += size_of::(); + + let executable = Ptr::::read_possibly_unaligned(input, offset) != 0; + offset += size_of::(); + + let unused = Ptr::::read_possibly_unaligned(input, offset); + offset += size_of::(); + + #[allow(deprecated)] + accounts.push(AccountInfo { + key, + is_signer, + is_writable, + lamports, + data, + owner, + executable, + _unused: unused, + }); + } else { + // duplicate account, clone the original + accounts.push(accounts.get(dup_info as usize).unwrap().clone()); + } + } + + // instruction data + + let instruction_data_len = Ptr::::read_possibly_unaligned(input, offset) as usize; + offset += size_of::(); + + let instruction_data = unsafe { from_raw_parts(input.add(offset), instruction_data_len) }; + offset += instruction_data_len; + + // program Id + + let program_id = Ptr::::ref_possibly_unaligned(input, offset); + + (program_id, accounts, instruction_data) + } + + fn concat_regions(regions: &[MemoryRegion]) -> AlignedMemory { + let last_region = regions.last().unwrap(); + let mut mem = AlignedMemory::zero_filled( + (last_region.vm_addr - MM_INPUT_START + last_region.len) as usize, + ); + for region in regions { + let host_slice = unsafe { + slice::from_raw_parts(region.host_addr as *const u8, region.len as usize) + }; + mem.as_slice_mut()[(region.vm_addr - MM_INPUT_START) as usize..][..region.len as usize] + .copy_from_slice(host_slice) + } + mem + } + + fn write_vm_data_len( + serialized: &mut AlignedMemory, + account_metadata: &SerializedAccountMetadata, + len: usize, + ) { + let offset = account_metadata + .vm_data_addr + .saturating_sub(MM_INPUT_START) + .saturating_sub(size_of::() as u64) as usize; + serialized.as_slice_mut()[offset..offset + size_of::()] + .copy_from_slice(&(len as u64).to_le_bytes()); + } + + #[test] + fn test_vas_serialization_direct_maps_account_data_only() { + let program_id = Pubkey::new_unique(); + let account_data = b"direct-account-data-is-not-copied".to_vec(); + let transaction_accounts = vec![ + ( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + ), + ( + Pubkey::new_unique(), + AccountSharedData::from(Account { + lamports: 1, + data: account_data.clone(), + owner: program_id, + executable: false, + rent_epoch: 0, + }), + ), + ]; + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + 0, + deduplicated_instruction_accounts(&[1], |_| true), + vec![], + ) + .unwrap(); + invoke_context.push().unwrap(); + let instruction_context = + invoke_context.transaction_context.get_current_instruction_context().unwrap(); + + let (serialized, regions, accounts_metadata, _instruction_data_offset) = + serialize_parameters(&instruction_context, false).unwrap(); + + assert!( + !serialized + .as_slice() + .windows(account_data.len()) + .any(|window| window == account_data) + ); + let data_region = regions + .iter() + .find(|region| region.vm_addr == accounts_metadata[0].vm_data_addr) + .unwrap(); + assert_eq!(data_region.len, account_data.len() as u64); + assert!(data_region.writable); + let mapped_data = unsafe { + slice::from_raw_parts(data_region.host_addr as *const u8, data_region.len as usize) + }; + assert_eq!(mapped_data, account_data); + } + + #[test_case(4, 8, Ok(4); "unchanged vm length restores original after transient growth")] + #[test_case(7, 8, Ok(7); "vm length grow wins over larger transient backing")] + #[test_case(2, 4, Ok(2); "vm length shrink truncates")] + #[test_case( + 4 + MAX_PERMITTED_DATA_INCREASE + 1, + 4, + Err(InstructionError::InvalidRealloc); + "invalid realloc limit errors" + )] + fn test_vas_deserialize_reconciles_direct_mapped_length( + vm_len: usize, + transient_len: usize, + expected: Result, + ) { + let program_id = Pubkey::new_unique(); + let transaction_accounts = vec![ + ( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + ), + ( + Pubkey::new_unique(), + AccountSharedData::from(Account { + lamports: 1, + data: vec![1, 2, 3, 4], + owner: program_id, + executable: false, + rent_epoch: 0, + }), + ), + ]; + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + 0, + deduplicated_instruction_accounts(&[1], |_| true), + vec![], + ) + .unwrap(); + invoke_context.push().unwrap(); + let instruction_context = + invoke_context.transaction_context.get_current_instruction_context().unwrap(); + + let (mut serialized, _regions, accounts_metadata, _instruction_data_offset) = + serialize_parameters(&instruction_context, false).unwrap(); + write_vm_data_len(&mut serialized, &accounts_metadata[0], vm_len); + { + let mut account = instruction_context.try_borrow_instruction_account(0).unwrap(); + account.set_data_length(transient_len).unwrap(); + } + + let result = deserialize_parameters( + &instruction_context, + serialized.as_slice(), + &accounts_metadata, + ); + assert_eq!(result, expected.clone().map(|_| ())); + if let Ok(expected_len) = expected { + let account = instruction_context.try_borrow_instruction_account(0).unwrap(); + assert_eq!(account.get_data().len(), expected_len); + } + } + + #[test] + fn test_vas_borrowed_writable_account_store_uses_shadow_image() { + let program_id = Pubkey::new_unique(); + let initial_data = vec![1, 2, 3]; + let mut borrowed_buf = borrowed_account_buffer(initial_data.clone(), program_id); + let borrowed_account = borrowed_shared_data(&mut borrowed_buf); + let mut transaction_context = TransactionContext::new( + vec![ + (Pubkey::new_unique(), borrowed_account), + (program_id, AccountSharedData::default()), + ], + Rent::default(), + /* max_instruction_stack_depth */ 1, + /* max_instruction_trace_length */ 1, + /* number_of_top_level_instructions */ 1, + ); + transaction_context + .configure_top_level_instruction_for_tests( + 1, + vec![InstructionAccount::new(0, false, true)], + vec![], + ) + .unwrap(); + transaction_context.push().unwrap(); + let instruction_context = transaction_context.get_current_instruction_context().unwrap(); + let account_start_offset = MM_INPUT_START; + let region = create_memory_region_of_account( + &mut instruction_context.try_borrow_instruction_account(0).unwrap(), + account_start_offset, + ) + .unwrap(); + + assert_eq!(region.len, initial_data.len() as u64); + assert!(!region.writable); + assert_eq!(region.access_violation_handler_payload, Some(0)); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let mut memory_mapping = unsafe { + MemoryMapping::new_with_access_violation_handler( + vec![region], + &config, + SBPFVersion::V3, + transaction_context.access_violation_handler(), + ) + } + .unwrap(); + + assert_eq!(memory_mapping.load::(account_start_offset).unwrap(), 1); + assert_eq!(active_borrowed_data(&mut borrowed_buf), initial_data); + + memory_mapping.store::(9, account_start_offset).unwrap(); + assert_eq!( + transaction_context.accounts().try_borrow(0).unwrap().data(), + &[9, 2, 3], + ); + assert_eq!(active_borrowed_data(&mut borrowed_buf), initial_data); + + { + let account = transaction_context.accounts().try_borrow(0).unwrap(); + match account.cow() { + CoWAccount::Borrowed(account) => account.commit(), + CoWAccount::Owned(_) => panic!("borrowed account should stay borrowed"), + } + } + assert_eq!(active_borrowed_data(&mut borrowed_buf), vec![9, 2, 3]); + } + + #[test] + fn test_vas_borrowed_writable_account_store_without_commit_keeps_active_image() { + let program_id = Pubkey::new_unique(); + let initial_data = vec![4, 5, 6]; + let mut borrowed_buf = borrowed_account_buffer(initial_data.clone(), program_id); + let borrowed_account = borrowed_shared_data(&mut borrowed_buf); + let mut transaction_context = TransactionContext::new( + vec![ + (Pubkey::new_unique(), borrowed_account), + (program_id, AccountSharedData::default()), + ], + Rent::default(), + /* max_instruction_stack_depth */ 1, + /* max_instruction_trace_length */ 1, + /* number_of_top_level_instructions */ 1, + ); + transaction_context + .configure_top_level_instruction_for_tests( + 1, + vec![InstructionAccount::new(0, false, true)], + vec![], + ) + .unwrap(); + transaction_context.push().unwrap(); + let instruction_context = transaction_context.get_current_instruction_context().unwrap(); + let account_start_offset = MM_INPUT_START; + let region = create_memory_region_of_account( + &mut instruction_context.try_borrow_instruction_account(0).unwrap(), + account_start_offset, + ) + .unwrap(); + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let mut memory_mapping = unsafe { + MemoryMapping::new_with_access_violation_handler( + vec![region], + &config, + SBPFVersion::V3, + transaction_context.access_violation_handler(), + ) + } + .unwrap(); + + memory_mapping.store::(7, account_start_offset).unwrap(); + + assert_eq!( + transaction_context.accounts().try_borrow(0).unwrap().data(), + &[7, 5, 6], + ); + assert_eq!(active_borrowed_data(&mut borrowed_buf), initial_data); + } + + #[test] + fn test_access_violation_handler() { + let program_id = Pubkey::new_unique(); + let shared_account = AccountSharedData::new(0, 4, &program_id); + let mut transaction_context = TransactionContext::new( + vec![ + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 4, &program_id), + ), // readonly + (Pubkey::new_unique(), shared_account.clone()), // writable shared + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &program_id), + ), // another writable account + ( + Pubkey::new_unique(), + AccountSharedData::new( + 0, + MAX_PERMITTED_DATA_LENGTH as usize - 0x100, + &program_id, + ), + ), // almost max sized writable account + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &program_id), + ), // writable dummy to burn accounts_resize_delta + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0x3000, &program_id), + ), // writable dummy to burn accounts_resize_delta + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &program_id), + ), // writable dummy to burn accounts_resize_delta + (program_id, AccountSharedData::default()), // program + ], + Rent::default(), + /* max_instruction_stack_depth */ 1, + /* max_instruction_trace_length */ 1, + /* number_of_top_level_instructions */ 1, + ); + let transaction_accounts_indexes = [0, 1, 2, 3, 4, 5, 6]; + let instruction_accounts = + deduplicated_instruction_accounts(&transaction_accounts_indexes, |index| index > 0); + transaction_context + .configure_top_level_instruction_for_tests(7, instruction_accounts, vec![]) + .unwrap(); + transaction_context.push().unwrap(); + let instruction_context = transaction_context.get_current_instruction_context().unwrap(); + let account_start_offsets = [ + MM_INPUT_START, + MM_INPUT_START + 4 + MAX_PERMITTED_DATA_INCREASE as u64, + MM_INPUT_START + (4 + MAX_PERMITTED_DATA_INCREASE as u64) * 2, + MM_INPUT_START + (4 + MAX_PERMITTED_DATA_INCREASE as u64) * 3, + ]; + let regions = account_start_offsets + .iter() + .enumerate() + .map(|(index_in_instruction, account_start_offset)| { + create_memory_region_of_account( + &mut instruction_context + .try_borrow_instruction_account(index_in_instruction as IndexOfAccount) + .unwrap(), + *account_start_offset, + ) + .unwrap() + }) + .collect::>(); + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let mut memory_mapping = unsafe { + MemoryMapping::new_with_access_violation_handler( + regions, + &config, + SBPFVersion::V3, + transaction_context.access_violation_handler(), + ) + } + .unwrap(); + + // Reading readonly account is allowed + memory_mapping.load::(account_start_offsets[0]).unwrap(); + + // Reading writable account is allowed + memory_mapping.load::(account_start_offsets[1]).unwrap(); + + // Reading beyond readonly accounts current size is denied + memory_mapping.load::(account_start_offsets[0] + 4).unwrap_err(); + + // Writing to readonly account is denied + memory_mapping.store::(0, account_start_offsets[0]).unwrap_err(); + + // Writing to shared writable account makes it unique (CoW logic.) + // It has been previously been made non-unique at the beginning of + // the test through a clone. + let _shared_account_ref = shared_account; + assert!(transaction_context.accounts().try_borrow_mut(1).unwrap().is_shared()); + memory_mapping.store::(0, account_start_offsets[1]).unwrap(); + assert!(!transaction_context.accounts().try_borrow_mut(1).unwrap().is_shared()); + assert_eq!( + transaction_context.accounts().try_borrow(1).unwrap().data().len(), + 4, + ); + + // Reading beyond writable accounts current size grows is denied + memory_mapping.load::(account_start_offsets[1] + 4).unwrap_err(); + + // Writing beyond writable accounts current size grows it only to the + // requested access length. + memory_mapping.store::(0, account_start_offsets[1] + 4).unwrap(); + assert_eq!( + transaction_context.accounts().try_borrow(1).unwrap().data().len(), + 8, + ); + assert!(transaction_context.accounts().try_borrow(1).unwrap().data().len() < 0x3000); + + // Writing beyond almost max sized writable accounts current size only grows it + // to MAX_PERMITTED_DATA_LENGTH + memory_mapping + .store::(0, account_start_offsets[3] + MAX_PERMITTED_DATA_LENGTH - 4) + .unwrap(); + assert_eq!( + transaction_context.accounts().try_borrow(3).unwrap().data().len(), + MAX_PERMITTED_DATA_LENGTH as usize, + ); + + // Accessing the rest of the address space reserved for + // the almost max sized writable account is denied + memory_mapping + .load::(account_start_offsets[3] + MAX_PERMITTED_DATA_LENGTH) + .unwrap_err(); + memory_mapping + .store::(0, account_start_offsets[3] + MAX_PERMITTED_DATA_LENGTH) + .unwrap_err(); + + // Burn through most of the accounts_resize_delta budget + let remaining_allowed_growth: usize = 0x700; + let target_resize_delta = MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION + - remaining_allowed_growth as i64; + for index_in_instruction in 4..7 { + let burn = target_resize_delta + .saturating_sub(transaction_context.accounts().resize_delta()) + as usize; + if burn == 0 { + break; + } + let mut borrowed_account = instruction_context + .try_borrow_instruction_account(index_in_instruction) + .unwrap(); + let old_len = borrowed_account.get_data().len(); + let new_len = old_len.saturating_add(burn).min(MAX_PERMITTED_DATA_LENGTH as usize); + borrowed_account.set_data_length(new_len).unwrap(); + } + assert_eq!( + transaction_context.accounts().resize_delta(), + target_resize_delta, + ); + + // Writing beyond empty writable accounts current size grows to the + // requested access length while it fits in the remaining transaction budget. + memory_mapping.store::(0, account_start_offsets[2] + 0x500).unwrap(); + assert_eq!( + transaction_context.accounts().try_borrow(2).unwrap().data().len(), + 0x504, + ); + + // A write that would need more than the remaining transaction budget is denied. + memory_mapping + .store::( + 0, + account_start_offsets[2] + remaining_allowed_growth as u64, + ) + .unwrap_err(); + } +} diff --git a/solana/program-runtime/src/stable_log.rs b/solana/program-runtime/src/stable_log.rs new file mode 100644 index 0000000..bf31fea --- /dev/null +++ b/solana/program-runtime/src/stable_log.rs @@ -0,0 +1,110 @@ +//! Stable program log messages +//! +//! The format of these log messages should not be modified to avoid breaking downstream consumers +//! of program logging +use { + base64::{Engine, prelude::BASE64_STANDARD}, + itertools::Itertools, + solana_pubkey::Pubkey, + solana_svm_log_collector::{LogCollector, ic_logger_msg}, + std::{cell::RefCell, rc::Rc}, +}; + +/// Log a program invoke. +/// +/// The general form is: +/// +/// ```notrust +/// "Program
invoke []" +/// ``` +pub fn program_invoke( + log_collector: &Option>>, + program_id: &Pubkey, + invoke_depth: usize, +) { + ic_logger_msg!( + log_collector, + "Program {} invoke [{}]", + program_id, + invoke_depth + ); +} + +/// Log a message from the program itself. +/// +/// The general form is: +/// +/// ```notrust +/// "Program log: " +/// ``` +/// +/// That is, any program-generated output is guaranteed to be prefixed by "Program log: " +pub fn program_log(log_collector: &Option>>, message: &str) { + ic_logger_msg!(log_collector, "Program log: {}", message); +} + +/// Emit a program data. +/// +/// The general form is: +/// +/// ```notrust +/// "Program data: *" +/// ``` +/// +/// That is, any program-generated output is guaranteed to be prefixed by "Program data: " +pub fn program_data(log_collector: &Option>>, data: &[&[u8]]) { + ic_logger_msg!( + log_collector, + "Program data: {}", + data.iter().map(|v| BASE64_STANDARD.encode(v)).join(" ") + ); +} + +/// Log return data as from the program itself. This line will not be present if no return +/// data was set, or if the return data was set to zero length. +/// +/// The general form is: +/// +/// ```notrust +/// "Program return: " +/// ``` +/// +/// That is, any program-generated output is guaranteed to be prefixed by "Program return: " +pub fn program_return( + log_collector: &Option>>, + program_id: &Pubkey, + data: &[u8], +) { + ic_logger_msg!( + log_collector, + "Program return: {} {}", + program_id, + BASE64_STANDARD.encode(data) + ); +} + +/// Log successful program execution. +/// +/// The general form is: +/// +/// ```notrust +/// "Program
success" +/// ``` +pub fn program_success(log_collector: &Option>>, program_id: &Pubkey) { + ic_logger_msg!(log_collector, "Program {} success", program_id); +} + +/// Log program execution failure +/// +/// The general form is: +/// +/// ```notrust +/// "Program
failed: " +/// ``` +pub fn program_failure( + log_collector: &Option>>, + program_id: &Pubkey, + err: &E, +) { + ic_logger_msg!(log_collector, "Program {} failed: {}", program_id, err); +} diff --git a/solana/program-runtime/src/sysvar_cache.rs b/solana/program-runtime/src/sysvar_cache.rs new file mode 100644 index 0000000..2277c68 --- /dev/null +++ b/solana/program-runtime/src/sysvar_cache.rs @@ -0,0 +1,288 @@ +use solana_epoch_rewards::EpochRewards; +#[allow(deprecated)] +use solana_sysvar::fees::Fees; +#[allow(deprecated)] +use solana_sysvar::recent_blockhashes::RecentBlockhashes; +use { + crate::invoke_context::InvokeContext, + serde::de::DeserializeOwned, + solana_clock::Clock, + solana_epoch_schedule::EpochSchedule, + solana_instruction::error::InstructionError, + solana_last_restart_slot::LastRestartSlot, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_slot_hashes::SlotHashes, + solana_svm_type_overrides::sync::Arc, + solana_sysvar_id::SysvarId, + solana_transaction_context::{IndexOfAccount, instruction::InstructionContext}, +}; + +/// Serialized sysvars exposed to programs during execution. +#[derive(Default, Debug)] +pub struct SysvarCache { + // Full account data, including any trailing zero bytes. + clock: Option>, + epoch_schedule: Option>, + rent: Option>, + slot_hashes: Option>, + last_restart_slot: Option>, + + // Object representations of large sysvars used by native builtins. + slot_hashes_obj: Option, + + #[allow(deprecated)] + recent_blockhashes: Option, +} + +impl SysvarCache { + /// Returns the serialized sysvar buffer for `SyscallGetSysvar`. + pub fn sysvar_id_to_buffer(&self, sysvar_id: &Pubkey) -> &Option> { + if Clock::check_id(sysvar_id) { + &self.clock + } else if EpochSchedule::check_id(sysvar_id) { + &self.epoch_schedule + } else if Rent::check_id(sysvar_id) { + &self.rent + } else if SlotHashes::check_id(sysvar_id) { + &self.slot_hashes + } else if LastRestartSlot::check_id(sysvar_id) { + &self.last_restart_slot + } else { + &None + } + } + + fn get_sysvar_obj( + &self, + sysvar_id: &Pubkey, + ) -> Result, InstructionError> { + if let Some(sysvar_buf) = self.sysvar_id_to_buffer(sysvar_id) { + bincode::deserialize(sysvar_buf) + .map(Arc::new) + .map_err(|_| InstructionError::UnsupportedSysvar) + } else { + Err(InstructionError::UnsupportedSysvar) + } + } + + /// Stores a serialized clock sysvar. + pub fn set_clock(&mut self, clock: &Clock) { + let buffer = self.clock.get_or_insert_default(); + buffer.clear(); + // bincode doesn't fail when writing to a correctly sized sysvar buffer. + let _ = bincode::serialize_into(buffer, clock); + } + + /// Returns the cached clock sysvar. + pub fn get_clock(&self) -> Result, InstructionError> { + self.get_sysvar_obj(&Clock::id()) + } + + /// Returns the cached rent sysvar. + pub fn get_rent(&self) -> Result, InstructionError> { + self.get_sysvar_obj(&Rent::id()) + } + + /// Returns the cached last-restart-slot sysvar. + pub fn get_last_restart_slot(&self) -> Result, InstructionError> { + self.get_sysvar_obj(&LastRestartSlot::id()) + } + + /// Returns the cached slot hashes sysvar. + pub fn get_slot_hashes(&self) -> Result, InstructionError> { + self.slot_hashes_obj + .as_ref() + .map(|s| Arc::new(SlotHashes::new(s.slot_hashes()))) + .ok_or(InstructionError::UnsupportedSysvar) + } + + #[deprecated] + #[allow(deprecated)] + /// Returns the cached recent-blockhashes sysvar. + pub fn get_recent_blockhashes(&self) -> Result, InstructionError> { + self.recent_blockhashes + .clone() + .ok_or(InstructionError::UnsupportedSysvar) + .map(Arc::new) + } + + /// Returns the (deprecated) fees sysvar; this engine always reports defaults. + #[allow(deprecated)] + pub fn get_fees(&self) -> Result, InstructionError> { + Ok(Arc::new(Default::default())) + } + + /// Returns the epoch-schedule sysvar; this engine always reports defaults. + pub fn get_epoch_schedule(&self) -> Result, InstructionError> { + Ok(Arc::new(Default::default())) + } + + /// Returns the epoch-rewards sysvar; this engine always reports defaults. + pub fn get_epoch_rewards(&self) -> Result, InstructionError> { + Ok(Arc::new(Default::default())) + } + + /// Fills missing sysvars by asking the caller for serialized account data. + pub fn fill_missing_entries( + &mut self, + mut get_account_data: F, + ) { + if self.clock.is_none() { + get_account_data(&Clock::id(), &mut |data: &[u8]| { + if bincode::deserialize::(data).is_ok() { + self.clock = Some(data.to_vec()); + } + }); + } + + if self.epoch_schedule.is_none() { + get_account_data(&EpochSchedule::id(), &mut |data: &[u8]| { + if bincode::deserialize::(data).is_ok() { + self.epoch_schedule = Some(data.to_vec()); + } + }); + } + + if self.rent.is_none() { + get_account_data(&Rent::id(), &mut |data: &[u8]| { + if bincode::deserialize::(data).is_ok() { + self.rent = Some(data.to_vec()); + } + }); + } + + if self.slot_hashes.is_none() { + get_account_data(&SlotHashes::id(), &mut |data: &[u8]| { + if let Ok(obj) = bincode::deserialize::(data) { + self.slot_hashes = Some(data.to_vec()); + self.slot_hashes_obj = Some(obj); + } + }); + } + + if self.last_restart_slot.is_none() { + get_account_data(&LastRestartSlot::id(), &mut |data: &[u8]| { + if bincode::deserialize::(data).is_ok() { + self.last_restart_slot = Some(data.to_vec()); + } + }); + } + + #[allow(deprecated)] + if self.recent_blockhashes.is_none() { + get_account_data(&RecentBlockhashes::id(), &mut |data: &[u8]| { + if let Ok(recent_blockhashes) = bincode::deserialize(data) { + self.recent_blockhashes = Some(recent_blockhashes); + } + }); + } + } + + /// Clears all cached sysvars. + pub fn reset(&mut self) { + *self = Self::default(); + } +} + +/// Sysvar accessors that also verify the instruction account matches the +/// requested sysvar id. +pub mod get_sysvar_with_account_check { + use super::*; + + fn check_sysvar_account( + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result<(), InstructionError> { + if !S::check_id( + instruction_context.get_key_of_instruction_account(instruction_account_index)?, + ) { + return Err(InstructionError::InvalidArgument); + } + Ok(()) + } + + /// Returns the clock sysvar after checking the provided instruction account. + pub fn clock( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + check_sysvar_account::(instruction_context, instruction_account_index)?; + invoke_context.get_sysvar_cache().get_clock() + } + + /// Returns the rent sysvar after checking the provided instruction account. + pub fn rent( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + check_sysvar_account::(instruction_context, instruction_account_index)?; + invoke_context.get_sysvar_cache().get_rent() + } + + /// Returns slot hashes after checking the provided instruction account. + pub fn slot_hashes( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + check_sysvar_account::(instruction_context, instruction_account_index)?; + invoke_context.get_sysvar_cache().get_slot_hashes() + } + + #[allow(deprecated)] + /// Returns recent blockhashes after checking the provided instruction account. + pub fn recent_blockhashes( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + check_sysvar_account::(instruction_context, instruction_account_index)?; + invoke_context.get_sysvar_cache().get_recent_blockhashes() + } + + pub fn last_restart_slot( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + check_sysvar_account::(instruction_context, instruction_account_index)?; + invoke_context.get_sysvar_cache().get_last_restart_slot() + } +} + +#[cfg(test)] +mod tests { + use {super::*, solana_sysvar::SysvarSerialize, test_case::test_case}; + + // sysvar cache provides the full account data of a sysvar + // the setters MUST NOT be changed to serialize an object representation + // it is required that the syscall be able to access the full buffer as it exists onchain + // this is meant to cover the cases: + // * account data is larger than struct sysvar + // * vector sysvar has fewer than its maximum entries + // if at any point the data is roundtripped through bincode, the vector will shrink + #[test_case(Clock::default(); "clock")] + #[test_case(Rent::default(); "rent")] + #[test_case(SlotHashes::default(); "slot_hashes")] + #[test_case(LastRestartSlot::default(); "last_restart_slot")] + fn test_sysvar_cache_preserves_bytes(_: T) { + let id = T::id(); + let size = T::size_of().saturating_mul(2); + let in_buf = vec![0; size]; + + let mut sysvar_cache = SysvarCache::default(); + sysvar_cache.fill_missing_entries(|pubkey, callback| { + if *pubkey == id { + callback(&in_buf) + } + }); + let sysvar_cache = sysvar_cache; + + let out_buf = sysvar_cache.sysvar_id_to_buffer(&id).clone().unwrap(); + + assert_eq!(out_buf, in_buf); + } +} diff --git a/solana/program-runtime/src/vm.rs b/solana/program-runtime/src/vm.rs new file mode 100644 index 0000000..5ee9790 --- /dev/null +++ b/solana/program-runtime/src/vm.rs @@ -0,0 +1,391 @@ +//! SBF virtual machine provisioning and execution. + +#[cfg(feature = "svm-internal")] +use qualifier_attr::qualifiers; +use { + crate::{ + execution_budget::MAX_INSTRUCTION_STACK_DEPTH, + invoke_context::{BpfAllocator, InvokeContext, SerializedAccountMetadata, SyscallContext}, + mem_pool::VmMemoryPool, + memory_context::MemoryContext, + serialization, stable_log, + }, + cfg_if::cfg_if, + solana_instruction::error::InstructionError, + solana_program_entrypoint::{MAX_PERMITTED_DATA_INCREASE, SUCCESS}, + solana_sbpf::{ + ebpf::{self, MM_HEAP_START}, + elf::Executable, + error::{EbpfError, ProgramResult}, + memory_region::{AccessType, MemoryMapping, MemoryRegion}, + vm::{CallFrame, ContextObject, EbpfVm, ExecutionMode}, + }, + solana_sdk_ids::bpf_loader_deprecated, + solana_svm_log_collector::ic_logger_msg, + solana_svm_measure::measure::Measure, + solana_transaction_context::{IndexOfAccount, transaction::TransactionContext}, + std::{cell::RefCell, mem}, +}; + +thread_local! { + pub static MEMORY_POOL: RefCell = RefCell::new(VmMemoryPool::new()); +} + +/// Calculates extra compute units charged for a requested heap size. +pub fn calculate_heap_cost(heap_size: u32, heap_cost: u64) -> u64 { + const KIBIBYTE: u64 = 1024; + const PAGE_SIZE_KB: u64 = 32; + let mut rounded_heap_size = u64::from(heap_size); + rounded_heap_size = + rounded_heap_size.saturating_add(PAGE_SIZE_KB.saturating_mul(KIBIBYTE).saturating_sub(1)); + rounded_heap_size + .checked_div(PAGE_SIZE_KB.saturating_mul(KIBIBYTE)) + .expect("PAGE_SIZE_KB * KIBIBYTE > 0") + .saturating_sub(1) + .saturating_mul(heap_cost) +} + +/// Creates an SBF VM bound to the current invocation context. +#[cfg_attr(feature = "svm-internal", qualifiers(pub))] +pub fn create_vm<'a, 'b, 'c>( + program: &'a Executable>, + regions: Vec, + accounts_metadata: Vec, + invoke_context: &'a mut InvokeContext<'b, 'c>, + stack: &mut [u8], + heap: &mut [u8], +) -> Result>, Box> { + let stack_size = stack.len(); + let heap_size = heap.len(); + let memory_mapping = create_memory_mapping( + program, + stack, + heap, + regions, + invoke_context.transaction_context, + )?; + invoke_context.set_syscall_context(SyscallContext { + allocator: BpfAllocator::new(heap_size as u64), + accounts_metadata: accounts_metadata.clone(), + })?; + invoke_context.memory_contexts.set_memory_context_abi_v1(MemoryContext::new( + BpfAllocator::new(heap_size as u64), + accounts_metadata, + memory_mapping, + ))?; + Ok(EbpfVm::new( + program.get_loader().clone(), + program.get_sbpf_version(), + invoke_context, + stack_size, + )) +} + +fn create_memory_mapping<'a, C: ContextObject>( + executable: &Executable, + stack: &'a mut [u8], + heap: &'a mut [u8], + additional_regions: Vec, + transaction_context: &TransactionContext, +) -> Result> { + let config = executable.get_config(); + let sbpf_version = executable.get_sbpf_version(); + let regions: Vec = vec![ + executable.get_ro_region(), + MemoryRegion::new_gapped( + &raw mut stack[..], + ebpf::MM_STACK_START, + if sbpf_version.stack_frame_gaps() && config.enable_stack_frame_gaps { + config.stack_frame_size as u64 + } else { + 0 + }, + ), + MemoryRegion::new(&raw mut heap[..], MM_HEAP_START), + ] + .into_iter() + .chain(additional_regions) + .collect(); + + Ok(unsafe { + MemoryMapping::new_with_access_violation_handler( + regions, + config, + sbpf_version, + transaction_context.access_violation_handler(), + )? + }) +} + +/// Create the SBF virtual machine +#[macro_export] +macro_rules! create_vm { + ($vm:ident, $program:expr, $regions:expr, $accounts_metadata:expr, $invoke_context:expr $(,)?) => { + let invoke_context = &*$invoke_context; + let stack_size = $program.get_config().stack_size(); + let heap_size = invoke_context.get_compute_budget().heap_size; + let heap_cost_result = + invoke_context.consume_checked($crate::__private::calculate_heap_cost( + heap_size, + invoke_context.get_execution_cost().heap_cost, + )); + let $vm = heap_cost_result.and_then(|_| { + let (mut stack, mut heap) = $crate::__private::MEMORY_POOL + .with_borrow_mut(|pool| (pool.get_stack(stack_size), pool.get_heap(heap_size))); + let vm = $crate::__private::create_vm( + $program, + $regions, + $accounts_metadata, + $invoke_context, + stack.as_slice_mut().get_mut(..stack_size).expect("invalid stack size"), + heap.as_slice_mut().get_mut(..heap_size as usize).expect("invalid heap size"), + ); + vm.map(|vm| (vm, stack, heap)) + }); + }; +} + +#[cfg_attr(feature = "svm-internal", qualifiers(pub))] +pub fn execute<'a, 'b, 'c>( + executable: &'a Executable>, + invoke_context: &'a mut InvokeContext<'b, 'c>, +) -> Result<(), Box> { + // We dropped the lifetime tracking in the Executor by setting it to 'static, + // thus we need to reintroduce the correct lifetime of InvokeContext here again. + let executable = unsafe { + mem::transmute::< + &'a Executable>, + &'a Executable>, + >(executable) + }; + 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 program_id = *instruction_context.get_program_key()?; + let is_loader_deprecated = + instruction_context.get_program_owner()? == bpf_loader_deprecated::id(); + cfg_if! { + if #[cfg(any( + target_os = "windows", + not(target_arch = "x86_64"), + feature = "sbpf-debugger" + ))] { + let use_jit = false; + #[cfg(feature = "sbpf-debugger")] + let (debug_port, debug_metadata) = ( + invoke_context.debug_port, + format!( + "program_id={};cpi_level={};caller={}", + program_id, + instruction_context.get_stack_height().saturating_sub(1), + invoke_context + .get_stack_height() + .checked_sub(2) + .and_then(|nesting_level| { + transaction_context + .get_instruction_context_at_nesting_level(nesting_level) + .ok() + }) + .and_then(|ctx| ctx.get_program_key().ok()) + .map(|key| key.to_string()) + .unwrap_or_else(|| "none".into()) + ), + ); + } else { + let use_jit = executable.get_compiled_program().is_some(); + } + } + let direct_account_pointers_in_program_input = + invoke_context.get_feature_set().direct_account_pointers_in_program_input; + + let mut serialize_time = Measure::start("serialize"); + let (parameter_bytes, regions, accounts_metadata, instruction_data_offset) = + serialization::serialize_parameters( + &instruction_context, + direct_account_pointers_in_program_input, + )?; + serialize_time.stop(); + + // save the account addresses so in case we hit an AccessViolation error we + // can map to a more specific error + let account_region_addrs = accounts_metadata + .iter() + .map(|m| { + let vm_end = m.vm_data_addr.saturating_add(m.original_data_len as u64).saturating_add( + if !is_loader_deprecated { MAX_PERMITTED_DATA_INCREASE as u64 } else { 0 }, + ); + m.vm_data_addr..vm_end + }) + .collect::>(); + + let mut create_vm_time = Measure::start("create_vm"); + let execution_result = { + let compute_meter_prev = invoke_context.get_remaining(); + create_vm!(vm, executable, regions, accounts_metadata, invoke_context); + let (mut vm, stack, heap) = match vm { + Ok(info) => info, + Err(e) => { + ic_logger_msg!(log_collector, "Failed to create SBF VM: {}", e); + return Err(Box::new(InstructionError::ProgramEnvironmentSetupFailure)); + } + }; + create_vm_time.stop(); + + #[cfg(feature = "sbpf-debugger")] + { + vm.debug_port = debug_port; + vm.debug_metadata = Some(debug_metadata); + } + let mut execute_time = Measure::start("execute"); + vm.registers[1] = ebpf::MM_INPUT_START; + vm.registers[2] = instruction_data_offset as u64; + let mut execution_mode = + if use_jit { ExecutionMode::PreferJit } else { ExecutionMode::Interpreted }; + let mut call_frames = std::iter::repeat_with(|| CallFrame { + caller_saved_registers: [0; ebpf::SCRATCH_REGS], + frame_pointer: 0, + target_pc: 0, + }) + .take(executable.get_config().max_call_depth) + .collect::>(); + let (compute_units_consumed, result) = + vm.execute_program(executable, &mut execution_mode, &mut call_frames); + let register_trace = std::mem::take(&mut vm.register_trace); + MEMORY_POOL.with_borrow_mut(|memory_pool| { + memory_pool.put_stack(stack); + memory_pool.put_heap(heap); + debug_assert!(memory_pool.stack_len() <= MAX_INSTRUCTION_STACK_DEPTH); + debug_assert!(memory_pool.heap_len() <= MAX_INSTRUCTION_STACK_DEPTH); + }); + drop(vm); + invoke_context.insert_register_trace(register_trace); + execute_time.stop(); + invoke_context.timings.execute_us += execute_time.as_us(); + + ic_logger_msg!( + log_collector, + "Program {} consumed {} of {} compute units", + &program_id, + compute_units_consumed, + compute_meter_prev + ); + let (_returned_from_program_id, return_data) = + invoke_context.transaction_context.get_return_data(); + if !return_data.is_empty() { + stable_log::program_return(&log_collector, &program_id, return_data); + } + match result { + ProgramResult::Ok(status) if status != SUCCESS => { + let error: InstructionError = status.into(); + Err(Box::new(error) as Box) + } + ProgramResult::Err(mut error) => { + // Don't clean me up!! + // This feature is active on all networks, but we still toggle + // it off during fuzzing. + if invoke_context.get_feature_set().deplete_cu_meter_on_vm_failure + && !matches!(error, EbpfError::SyscallError(_)) + { + // when an exception is thrown during the execution of a + // Basic Block (e.g., a null memory dereference or other + // faults), determining the exact number of CUs consumed + // up to the point of failure requires additional effort + // and is unnecessary since these cases are rare. + // + // In order to simplify CU tracking, simply consume all + // remaining compute units so that the block cost + // tracker uses the full requested compute unit cost for + // this failed transaction. + invoke_context.consume(invoke_context.get_remaining()); + } + + if let EbpfError::SyscallError(err) = error { + error = err + .downcast::() + .map(|err| *err) + .unwrap_or_else(EbpfError::SyscallError); + } + if let EbpfError::AccessViolation(access_type, vm_addr, len, _section_name) = error + { + // Account data is directly mapped. A write to readonly data + // or an access into the reserved growth range appears as a + // memory violation; map it to the account-specific error. + if let Some((instruction_account_index, vm_addr_range)) = account_region_addrs + .iter() + .enumerate() + .find(|(_, vm_addr_range)| vm_addr_range.contains(&vm_addr)) + { + let transaction_context = &invoke_context.transaction_context; + let instruction_context = + transaction_context.get_current_instruction_context()?; + let account = instruction_context.try_borrow_instruction_account( + instruction_account_index as IndexOfAccount, + )?; + if vm_addr.saturating_add(len) <= vm_addr_range.end { + // The access was within the range of the account address space, + // but it might not be within the range of the actual data. + let is_access_outside_of_data = + vm_addr.saturating_add(len).saturating_sub(vm_addr_range.start) + as usize + > account.get_data().len(); + error = EbpfError::SyscallError(Box::new( + #[allow(deprecated)] + match access_type { + AccessType::Store => { + if let Err(err) = account.can_data_be_changed() { + err + } else { + // The store was allowed but failed, + // thus it must have been an attempt to grow the account. + debug_assert!(is_access_outside_of_data); + InstructionError::InvalidRealloc + } + } + AccessType::Load => { + // Loads should only fail when they are outside of the account data. + debug_assert!(is_access_outside_of_data); + if account.can_data_be_changed().is_err() { + // Load beyond readonly account data happened because the program + // expected more data than there actually is. + InstructionError::AccountDataTooSmall + } else { + // Load beyond writable account data also attempted to grow. + InstructionError::InvalidRealloc + } + } + }, + )); + } + } + } + Err(if let EbpfError::SyscallError(err) = error { err } else { error.into() }) + } + _ => Ok(()), + } + }; + + fn deserialize_parameters( + invoke_context: &mut InvokeContext, + parameter_bytes: &[u8], + ) -> Result<(), InstructionError> { + serialization::deserialize_parameters( + &invoke_context.transaction_context.get_current_instruction_context()?, + parameter_bytes, + &invoke_context.get_syscall_context()?.accounts_metadata, + ) + } + + let mut deserialize_time = Measure::start("deserialize"); + let execute_or_deserialize_result = execution_result.and_then(|_| { + deserialize_parameters(invoke_context, parameter_bytes.as_slice()) + .map_err(|error| Box::new(error) as Box) + }); + deserialize_time.stop(); + + // Update the timings + invoke_context.timings.serialize_us += serialize_time.as_us(); + invoke_context.timings.create_vm_us += create_vm_time.as_us(); + invoke_context.timings.deserialize_us += deserialize_time.as_us(); + + execute_or_deserialize_result +}