From aab4e6e79aee1a4bb02aac4637c48ee9460e0432 Mon Sep 17 00:00:00 2001 From: hanako mumei <81144685+2501babe@users.noreply.github.com> Date: Mon, 13 May 2024 01:49:17 -0700 Subject: [PATCH 1/3] impl SIMD0127: sol_get_sysvar --- program-runtime/src/sysvar_cache.rs | 37 ++ programs/bpf_loader/src/syscalls/mod.rs | 565 +++++++++++++++++++-- programs/bpf_loader/src/syscalls/sysvar.rs | 71 +++ sdk/program/src/program_stubs.rs | 22 + sdk/program/src/syscalls/definitions.rs | 17 +- sdk/src/feature_set.rs | 7 +- 6 files changed, 674 insertions(+), 45 deletions(-) diff --git a/program-runtime/src/sysvar_cache.rs b/program-runtime/src/sysvar_cache.rs index bbb89f05032a31..952f5726eb83f7 100644 --- a/program-runtime/src/sysvar_cache.rs +++ b/program-runtime/src/sysvar_cache.rs @@ -321,3 +321,40 @@ pub mod get_sysvar_with_account_check { invoke_context.get_sysvar_cache().get_last_restart_slot() } } + +#[cfg(test)] +mod tests { + use {super::*, 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(EpochSchedule::default(); "epoch_schedule")] + #[test_case(EpochRewards::default(); "epoch_rewards")] + #[test_case(Rent::default(); "rent")] + #[test_case(SlotHashes::default(); "slot_hashes")] + #[test_case(StakeHistory::default(); "stake_history")] + #[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/programs/bpf_loader/src/syscalls/mod.rs b/programs/bpf_loader/src/syscalls/mod.rs index 71824d43f76f8a..d13634ddcf00d7 100644 --- a/programs/bpf_loader/src/syscalls/mod.rs +++ b/programs/bpf_loader/src/syscalls/mod.rs @@ -7,6 +7,7 @@ pub use self::{ sysvar::{ SyscallGetClockSysvar, SyscallGetEpochRewardsSysvar, SyscallGetEpochScheduleSysvar, SyscallGetFeesSysvar, SyscallGetLastRestartSlotSysvar, SyscallGetRentSysvar, + SyscallGetSysvar, }, }; #[allow(deprecated)] @@ -38,8 +39,9 @@ use { disable_deploy_of_alloc_free_syscall, disable_fees_sysvar, enable_alt_bn128_compression_syscall, enable_alt_bn128_syscall, enable_big_mod_exp_syscall, enable_partitioned_epoch_reward, enable_poseidon_syscall, - error_on_syscall_bpf_function_hash_collisions, last_restart_slot_sysvar, - reject_callx_r10, remaining_compute_units_syscall_enabled, switch_to_new_elf_parser, + error_on_syscall_bpf_function_hash_collisions, get_sysvar_syscall_enabled, + last_restart_slot_sysvar, reject_callx_r10, remaining_compute_units_syscall_enabled, + switch_to_new_elf_parser, }, hash::{Hash, Hasher}, instruction::{AccountMeta, InstructionError, ProcessedSiblingInstruction}, @@ -278,6 +280,7 @@ pub fn create_program_runtime_environment_v1<'a>( let enable_poseidon_syscall = feature_set.is_active(&enable_poseidon_syscall::id()); let remaining_compute_units_syscall_enabled = feature_set.is_active(&remaining_compute_units_syscall_enabled::id()); + let get_sysvar_syscall_enabled = feature_set.is_active(&get_sysvar_syscall_enabled::id()); // !!! ATTENTION !!! // When adding new features for RBPF here, // also add them to `Bank::apply_builtin_program_feature_transitions()`. @@ -464,6 +467,14 @@ pub fn create_program_runtime_environment_v1<'a>( SyscallAltBn128Compression::vm, )?; + // Sysvar getter + register_feature_gated_function!( + result, + get_sysvar_syscall_enabled, + *b"sol_get_sysvar", + SyscallGetSysvar::vm, + )?; + // Log data result.register_function_hashed(*b"sol_log_data", SyscallLogData::vm)?; @@ -2019,12 +2030,16 @@ mod tests { hash::{hashv, HASH_BYTES}, instruction::Instruction, program::check_type_assumptions, + slot_hashes::{self, SlotHashes}, stable_layout::stable_instruction::StableInstruction, + stake_history::{self, StakeHistory, StakeHistoryEntry}, sysvar::{ self, clock::Clock, epoch_rewards::EpochRewards, epoch_schedule::EpochSchedule, + last_restart_slot::LastRestartSlot, }, }, std::{mem, str::FromStr}, + test_case::test_case, }; macro_rules! assert_access_violation { @@ -3358,6 +3373,7 @@ mod tests { fn are_bytes_equal(first: &T, second: &T) -> bool { let p_first = first as *const _ as *const u8; let p_second = second as *const _ as *const u8; + for i in 0..(size_of::() as isize) { unsafe { if *p_first.offset(i) != *p_second.offset(i) { @@ -3379,16 +3395,19 @@ mod tests { src_clock.epoch = 3; src_clock.leader_schedule_epoch = 4; src_clock.unix_timestamp = 5; + let mut src_epochschedule = create_filled_type::(false); src_epochschedule.slots_per_epoch = 1; src_epochschedule.leader_schedule_slot_offset = 2; src_epochschedule.warmup = false; src_epochschedule.first_normal_epoch = 3; src_epochschedule.first_normal_slot = 4; + let mut src_fees = create_filled_type::(false); src_fees.fee_calculator = FeeCalculator { lamports_per_signature: 1, }; + let mut src_rent = create_filled_type::(false); src_rent.lamports_per_byte_year = 1; src_rent.exemption_threshold = 2.0; @@ -3403,6 +3422,9 @@ mod tests { src_rewards.distributed_rewards = 10; src_rewards.active = true; + let mut src_restart = create_filled_type::(false); + src_restart.last_restart_slot = 1; + let transaction_accounts = vec![ ( sysvar::clock::id(), @@ -3424,19 +3446,28 @@ mod tests { sysvar::epoch_rewards::id(), create_account_shared_data_for_test(&src_rewards), ), + ( + sysvar::last_restart_slot::id(), + create_account_shared_data_for_test(&src_restart), + ), ]; with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); // Test clock sysvar { - let mut got_clock = Clock::default(); - let got_clock_va = 0x100000000; + let mut got_clock_obj = Clock::default(); + let got_clock_obj_va = 0x100000000; + + let mut got_clock_buf = vec![0; Clock::size_of()]; + let got_clock_buf_va = 0x200000000; + let clock_id_va = 0x300000000; let mut memory_mapping = MemoryMapping::new( - vec![MemoryRegion::new_writable( - bytes_of_mut(&mut got_clock), - got_clock_va, - )], + vec![ + MemoryRegion::new_writable(bytes_of_mut(&mut got_clock_obj), got_clock_obj_va), + MemoryRegion::new_writable(&mut got_clock_buf, got_clock_buf_va), + MemoryRegion::new_readonly(&Clock::id().to_bytes(), clock_id_va), + ], &config, &SBPFVersion::V2, ) @@ -3444,7 +3475,7 @@ mod tests { let result = SyscallGetClockSysvar::rust( &mut invoke_context, - got_clock_va, + got_clock_obj_va, 0, 0, 0, @@ -3452,7 +3483,7 @@ mod tests { &mut memory_mapping, ); result.unwrap(); - assert_eq!(got_clock, src_clock); + assert_eq!(got_clock_obj, src_clock); let mut clean_clock = create_filled_type::(true); clean_clock.slot = src_clock.slot; @@ -3460,19 +3491,49 @@ mod tests { clean_clock.epoch = src_clock.epoch; clean_clock.leader_schedule_epoch = src_clock.leader_schedule_epoch; clean_clock.unix_timestamp = src_clock.unix_timestamp; - assert!(are_bytes_equal(&got_clock, &clean_clock)); + assert!(are_bytes_equal(&got_clock_obj, &clean_clock)); + + let result = SyscallGetSysvar::rust( + &mut invoke_context, + clock_id_va, + got_clock_buf_va, + 0, + Clock::size_of() as u64, + 0, + &mut memory_mapping, + ); + result.unwrap(); + + let clock_from_buf = bincode::deserialize::(&got_clock_buf).unwrap(); + + assert_eq!(clock_from_buf, src_clock); + assert!(are_bytes_equal(&clock_from_buf, &clean_clock)); } // Test epoch_schedule sysvar { - let mut got_epochschedule = EpochSchedule::default(); - let got_epochschedule_va = 0x100000000; + let mut got_epochschedule_obj = EpochSchedule::default(); + let got_epochschedule_obj_va = 0x100000000; + + let mut got_epochschedule_buf = vec![0; EpochSchedule::size_of()]; + let got_epochschedule_buf_va = 0x200000000; + let epochschedule_id_va = 0x300000000; let mut memory_mapping = MemoryMapping::new( - vec![MemoryRegion::new_writable( - bytes_of_mut(&mut got_epochschedule), - got_epochschedule_va, - )], + vec![ + MemoryRegion::new_writable( + bytes_of_mut(&mut got_epochschedule_obj), + got_epochschedule_obj_va, + ), + MemoryRegion::new_writable( + &mut got_epochschedule_buf, + got_epochschedule_buf_va, + ), + MemoryRegion::new_readonly( + &EpochSchedule::id().to_bytes(), + epochschedule_id_va, + ), + ], &config, &SBPFVersion::V2, ) @@ -3480,7 +3541,7 @@ mod tests { let result = SyscallGetEpochScheduleSysvar::rust( &mut invoke_context, - got_epochschedule_va, + got_epochschedule_obj_va, 0, 0, 0, @@ -3488,7 +3549,7 @@ mod tests { &mut memory_mapping, ); result.unwrap(); - assert_eq!(got_epochschedule, src_epochschedule); + assert_eq!(got_epochschedule_obj, src_epochschedule); let mut clean_epochschedule = create_filled_type::(true); clean_epochschedule.slots_per_epoch = src_epochschedule.slots_per_epoch; @@ -3497,7 +3558,33 @@ mod tests { clean_epochschedule.warmup = src_epochschedule.warmup; clean_epochschedule.first_normal_epoch = src_epochschedule.first_normal_epoch; clean_epochschedule.first_normal_slot = src_epochschedule.first_normal_slot; - assert!(are_bytes_equal(&got_epochschedule, &clean_epochschedule)); + assert!(are_bytes_equal( + &got_epochschedule_obj, + &clean_epochschedule + )); + + let result = SyscallGetSysvar::rust( + &mut invoke_context, + epochschedule_id_va, + got_epochschedule_buf_va, + 0, + EpochSchedule::size_of() as u64, + 0, + &mut memory_mapping, + ); + result.unwrap(); + + // clone is to zero the alignment padding + let epochschedule_from_buf = + bincode::deserialize::(&got_epochschedule_buf) + .unwrap() + .clone(); + + assert_eq!(epochschedule_from_buf, src_epochschedule); + assert!(are_bytes_equal( + &epochschedule_from_buf, + &clean_epochschedule + )); } // Test fees sysvar @@ -3530,18 +3617,25 @@ mod tests { let mut clean_fees = create_filled_type::(true); clean_fees.fee_calculator = src_fees.fee_calculator; assert!(are_bytes_equal(&got_fees, &clean_fees)); + + // fees sysvar is not accessible via sol_get_sysvar so nothing further to test } // Test rent sysvar { - let mut got_rent = create_filled_type::(true); - let got_rent_va = 0x100000000; + let mut got_rent_obj = create_filled_type::(true); + let got_rent_obj_va = 0x100000000; + + let mut got_rent_buf = vec![0; Rent::size_of()]; + let got_rent_buf_va = 0x200000000; + let rent_id_va = 0x300000000; let mut memory_mapping = MemoryMapping::new( - vec![MemoryRegion::new_writable( - bytes_of_mut(&mut got_rent), - got_rent_va, - )], + vec![ + MemoryRegion::new_writable(bytes_of_mut(&mut got_rent_obj), got_rent_obj_va), + MemoryRegion::new_writable(&mut got_rent_buf, got_rent_buf_va), + MemoryRegion::new_readonly(&Rent::id().to_bytes(), rent_id_va), + ], &config, &SBPFVersion::V2, ) @@ -3549,7 +3643,7 @@ mod tests { let result = SyscallGetRentSysvar::rust( &mut invoke_context, - got_rent_va, + got_rent_obj_va, 0, 0, 0, @@ -3557,25 +3651,50 @@ mod tests { &mut memory_mapping, ); result.unwrap(); - assert_eq!(got_rent, src_rent); + assert_eq!(got_rent_obj, src_rent); let mut clean_rent = create_filled_type::(true); clean_rent.lamports_per_byte_year = src_rent.lamports_per_byte_year; clean_rent.exemption_threshold = src_rent.exemption_threshold; clean_rent.burn_percent = src_rent.burn_percent; - assert!(are_bytes_equal(&got_rent, &clean_rent)); + assert!(are_bytes_equal(&got_rent_obj, &clean_rent)); + + let result = SyscallGetSysvar::rust( + &mut invoke_context, + rent_id_va, + got_rent_buf_va, + 0, + Rent::size_of() as u64, + 0, + &mut memory_mapping, + ); + result.unwrap(); + + // clone is to zero the alignment padding + let rent_from_buf = bincode::deserialize::(&got_rent_buf).unwrap().clone(); + + assert_eq!(rent_from_buf, src_rent); + assert!(are_bytes_equal(&rent_from_buf, &clean_rent)); } // Test epoch rewards sysvar { - let mut got_rewards = create_filled_type::(true); - let got_rewards_va = 0x100000000; + let mut got_rewards_obj = create_filled_type::(true); + let got_rewards_obj_va = 0x100000000; + + let mut got_rewards_buf = vec![0; EpochRewards::size_of()]; + let got_rewards_buf_va = 0x200000000; + let rewards_id_va = 0x300000000; let mut memory_mapping = MemoryMapping::new( - vec![MemoryRegion::new_writable( - bytes_of_mut(&mut got_rewards), - got_rewards_va, - )], + vec![ + MemoryRegion::new_writable( + bytes_of_mut(&mut got_rewards_obj), + got_rewards_obj_va, + ), + MemoryRegion::new_writable(&mut got_rewards_buf, got_rewards_buf_va), + MemoryRegion::new_readonly(&EpochRewards::id().to_bytes(), rewards_id_va), + ], &config, &SBPFVersion::V2, ) @@ -3583,7 +3702,7 @@ mod tests { let result = SyscallGetEpochRewardsSysvar::rust( &mut invoke_context, - got_rewards_va, + got_rewards_obj_va, 0, 0, 0, @@ -3591,7 +3710,7 @@ mod tests { &mut memory_mapping, ); result.unwrap(); - assert_eq!(got_rewards, src_rewards); + assert_eq!(got_rewards_obj, src_rewards); let mut clean_rewards = create_filled_type::(true); clean_rewards.distribution_starting_block_height = @@ -3602,7 +3721,377 @@ mod tests { clean_rewards.total_rewards = src_rewards.total_rewards; clean_rewards.distributed_rewards = src_rewards.distributed_rewards; clean_rewards.active = src_rewards.active; - assert!(are_bytes_equal(&got_rewards, &clean_rewards)); + assert!(are_bytes_equal(&got_rewards_obj, &clean_rewards)); + + let result = SyscallGetSysvar::rust( + &mut invoke_context, + rewards_id_va, + got_rewards_buf_va, + 0, + EpochRewards::size_of() as u64, + 0, + &mut memory_mapping, + ); + result.unwrap(); + + // clone is to zero the alignment padding + let rewards_from_buf = bincode::deserialize::(&got_rewards_buf) + .unwrap() + .clone(); + + assert_eq!(rewards_from_buf, src_rewards); + assert!(are_bytes_equal(&rewards_from_buf, &clean_rewards)); + } + + // Test last restart slot sysvar + { + let mut got_restart_obj = LastRestartSlot::default(); + let got_restart_obj_va = 0x100000000; + + let mut got_restart_buf = vec![0; LastRestartSlot::size_of()]; + let got_restart_buf_va = 0x200000000; + let restart_id_va = 0x300000000; + + let mut memory_mapping = MemoryMapping::new( + vec![ + MemoryRegion::new_writable( + bytes_of_mut(&mut got_restart_obj), + got_restart_obj_va, + ), + MemoryRegion::new_writable(&mut got_restart_buf, got_restart_buf_va), + MemoryRegion::new_readonly(&LastRestartSlot::id().to_bytes(), restart_id_va), + ], + &config, + &SBPFVersion::V2, + ) + .unwrap(); + + let result = SyscallGetLastRestartSlotSysvar::rust( + &mut invoke_context, + got_restart_obj_va, + 0, + 0, + 0, + 0, + &mut memory_mapping, + ); + result.unwrap(); + assert_eq!(got_restart_obj, src_restart); + + let mut clean_restart = create_filled_type::(true); + clean_restart.last_restart_slot = src_restart.last_restart_slot; + assert!(are_bytes_equal(&got_restart_obj, &clean_restart)); + + let result = SyscallGetSysvar::rust( + &mut invoke_context, + restart_id_va, + got_restart_buf_va, + 0, + LastRestartSlot::size_of() as u64, + 0, + &mut memory_mapping, + ); + result.unwrap(); + + let restart_from_buf = + bincode::deserialize::(&got_restart_buf).unwrap(); + + assert_eq!(restart_from_buf, src_restart); + assert!(are_bytes_equal(&restart_from_buf, &clean_restart)); + } + } + + #[test_case(false; "partial")] + #[test_case(true; "full")] + fn test_syscall_get_stake_history(filled: bool) { + let config = Config::default(); + + let mut src_history = StakeHistory::default(); + + let epochs = if filled { + stake_history::MAX_ENTRIES + 1 + } else { + stake_history::MAX_ENTRIES / 2 + } as u64; + + for epoch in 1..epochs { + src_history.add( + epoch, + StakeHistoryEntry { + effective: epoch * 2, + activating: epoch * 3, + deactivating: epoch * 5, + }, + ); + } + + let src_history = src_history; + + let mut src_history_buf = vec![0; StakeHistory::size_of()]; + bincode::serialize_into(&mut src_history_buf, &src_history).unwrap(); + + let transaction_accounts = vec![( + sysvar::stake_history::id(), + create_account_shared_data_for_test(&src_history), + )]; + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + + { + let mut got_history_buf = vec![0; StakeHistory::size_of()]; + let got_history_buf_va = 0x100000000; + let history_id_va = 0x200000000; + + let mut memory_mapping = MemoryMapping::new( + vec![ + MemoryRegion::new_writable(&mut got_history_buf, got_history_buf_va), + MemoryRegion::new_readonly(&StakeHistory::id().to_bytes(), history_id_va), + ], + &config, + &SBPFVersion::V2, + ) + .unwrap(); + + let result = SyscallGetSysvar::rust( + &mut invoke_context, + history_id_va, + got_history_buf_va, + 0, + StakeHistory::size_of() as u64, + 0, + &mut memory_mapping, + ); + result.unwrap(); + + let history_from_buf = bincode::deserialize::(&got_history_buf).unwrap(); + assert_eq!(history_from_buf, src_history); + } + } + + #[test_case(false; "partial")] + #[test_case(true; "full")] + fn test_syscall_get_slot_hashes(filled: bool) { + let config = Config::default(); + + let mut src_hashes = SlotHashes::default(); + + let slots = if filled { + slot_hashes::MAX_ENTRIES + 1 + } else { + slot_hashes::MAX_ENTRIES / 2 + } as u64; + + for slot in 1..slots { + src_hashes.add(slot, hashv(&[&slot.to_le_bytes()])); + } + + let src_hashes = src_hashes; + + let mut src_hashes_buf = vec![0; SlotHashes::size_of()]; + bincode::serialize_into(&mut src_hashes_buf, &src_hashes).unwrap(); + + let transaction_accounts = vec![( + sysvar::slot_hashes::id(), + create_account_shared_data_for_test(&src_hashes), + )]; + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + + { + let mut got_hashes_buf = vec![0; SlotHashes::size_of()]; + let got_hashes_buf_va = 0x100000000; + let hashes_id_va = 0x200000000; + + let mut memory_mapping = MemoryMapping::new( + vec![ + MemoryRegion::new_writable(&mut got_hashes_buf, got_hashes_buf_va), + MemoryRegion::new_readonly(&SlotHashes::id().to_bytes(), hashes_id_va), + ], + &config, + &SBPFVersion::V2, + ) + .unwrap(); + + let result = SyscallGetSysvar::rust( + &mut invoke_context, + hashes_id_va, + got_hashes_buf_va, + 0, + SlotHashes::size_of() as u64, + 0, + &mut memory_mapping, + ); + result.unwrap(); + + let hashes_from_buf = bincode::deserialize::(&got_hashes_buf).unwrap(); + assert_eq!(hashes_from_buf, src_hashes); + } + } + + #[test] + fn test_syscall_get_sysvar_errors() { + let config = Config::default(); + + let mut src_clock = create_filled_type::(false); + src_clock.slot = 1; + src_clock.epoch_start_timestamp = 2; + src_clock.epoch = 3; + src_clock.leader_schedule_epoch = 4; + src_clock.unix_timestamp = 5; + + let clock_id_va = 0x100000000; + + let mut got_clock_buf_rw = vec![0; Clock::size_of()]; + let got_clock_buf_rw_va = 0x200000000; + + let got_clock_buf_ro = vec![0; Clock::size_of()]; + let got_clock_buf_ro_va = 0x300000000; + + let mut memory_mapping = MemoryMapping::new( + vec![ + MemoryRegion::new_readonly(&Clock::id().to_bytes(), clock_id_va), + MemoryRegion::new_writable(&mut got_clock_buf_rw, got_clock_buf_rw_va), + MemoryRegion::new_readonly(&got_clock_buf_ro, got_clock_buf_ro_va), + ], + &config, + &SBPFVersion::V2, + ) + .unwrap(); + + let access_violation_err = + std::mem::discriminant(&EbpfError::AccessViolation(AccessType::Load, 0, 0, "")); + + let got_clock_empty = vec![0; Clock::size_of()]; + + { + // start without the clock sysvar because we expect to hit specific errors before loading it + with_mock_invoke_context!(invoke_context, transaction_context, vec![]); + + // Abort: "Not all bytes in VM memory range `[sysvar_id, sysvar_id + 32)` are readable." + let e = SyscallGetSysvar::rust( + &mut invoke_context, + clock_id_va + 1, + got_clock_buf_rw_va, + 0, + Clock::size_of() as u64, + 0, + &mut memory_mapping, + ) + .unwrap_err(); + + assert_eq!( + std::mem::discriminant(e.downcast_ref::().unwrap()), + access_violation_err, + ); + assert_eq!(got_clock_buf_rw, got_clock_empty); + + // Abort: "Not all bytes in VM memory range `[var_addr, var_addr + length)` are writable." + let e = SyscallGetSysvar::rust( + &mut invoke_context, + clock_id_va, + got_clock_buf_rw_va + 1, + 0, + Clock::size_of() as u64, + 0, + &mut memory_mapping, + ) + .unwrap_err(); + + assert_eq!( + std::mem::discriminant(e.downcast_ref::().unwrap()), + access_violation_err, + ); + assert_eq!(got_clock_buf_rw, got_clock_empty); + + let e = SyscallGetSysvar::rust( + &mut invoke_context, + clock_id_va, + got_clock_buf_ro_va, + 0, + Clock::size_of() as u64, + 0, + &mut memory_mapping, + ) + .unwrap_err(); + + assert_eq!( + std::mem::discriminant(e.downcast_ref::().unwrap()), + access_violation_err, + ); + assert_eq!(got_clock_buf_rw, got_clock_empty); + + // Abort: "`offset + length` is not in `[0, 2^64)`." + let e = SyscallGetSysvar::rust( + &mut invoke_context, + clock_id_va, + got_clock_buf_rw_va, + u64::MAX - Clock::size_of() as u64 / 2, + Clock::size_of() as u64, + 0, + &mut memory_mapping, + ) + .unwrap_err(); + + assert_eq!( + *e.downcast_ref::().unwrap(), + InstructionError::ArithmeticOverflow, + ); + assert_eq!(got_clock_buf_rw, got_clock_empty); + + // "`var_addr + length` is not in `[0, 2^64)`" is theoretically impossible to trigger + // because if the sum extended outside u64::MAX then it would not be writable and translate would fail + + // "`2` if the sysvar data is not present in the Sysvar Cache." + let result = SyscallGetSysvar::rust( + &mut invoke_context, + clock_id_va, + got_clock_buf_rw_va, + 0, + Clock::size_of() as u64, + 0, + &mut memory_mapping, + ) + .unwrap(); + + assert_eq!(result, 2); + assert_eq!(got_clock_buf_rw, got_clock_empty); + } + + { + let transaction_accounts = vec![( + sysvar::clock::id(), + create_account_shared_data_for_test(&src_clock), + )]; + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + + // "`1` if `offset + length` is greater than the length of the sysvar data." + let result = SyscallGetSysvar::rust( + &mut invoke_context, + clock_id_va, + got_clock_buf_rw_va, + 1, + Clock::size_of() as u64, + 0, + &mut memory_mapping, + ) + .unwrap(); + + assert_eq!(result, 1); + assert_eq!(got_clock_buf_rw, got_clock_empty); + + // and now lets succeed + SyscallGetSysvar::rust( + &mut invoke_context, + clock_id_va, + got_clock_buf_rw_va, + 0, + Clock::size_of() as u64, + 0, + &mut memory_mapping, + ) + .unwrap(); + + let clock_from_buf = bincode::deserialize::(&got_clock_buf_rw).unwrap(); + + assert_eq!(clock_from_buf, src_clock); } } diff --git a/programs/bpf_loader/src/syscalls/sysvar.rs b/programs/bpf_loader/src/syscalls/sysvar.rs index c5a99d5547bb2c..8d8fa667830e4d 100644 --- a/programs/bpf_loader/src/syscalls/sysvar.rs +++ b/programs/bpf_loader/src/syscalls/sysvar.rs @@ -160,3 +160,74 @@ declare_builtin_function!( ) } ); + +const SYSVAR_NOT_FOUND: u64 = 2; +const OFFSET_LENGTH_EXCEEDS_SYSVAR: u64 = 1; + +// quoted language from SIMD0127 +// because this syscall can both return error codes and abort, well-ordered error checking is crucial +declare_builtin_function!( + /// Get a slice of a Sysvar in-memory representation + SyscallGetSysvar, + fn rust( + invoke_context: &mut InvokeContext, + sysvar_id_addr: u64, + var_addr: u64, + offset: u64, + length: u64, + _arg5: u64, + memory_mapping: &mut MemoryMapping, + ) -> Result { + let check_aligned = invoke_context.get_check_aligned(); + let ComputeBudget { + sysvar_base_cost, + cpi_bytes_per_unit, + mem_op_base_cost, + .. + } = *invoke_context.get_compute_budget(); + + // Abort: "Compute budget is exceeded." + consume_compute_meter( + invoke_context, + sysvar_base_cost + .saturating_add(32_u64.div_ceil(cpi_bytes_per_unit)) + .saturating_add(std::cmp::max( + length.div_ceil(cpi_bytes_per_unit), + mem_op_base_cost, + )), + )?; + + // Abort: "Not all bytes in VM memory range `[sysvar_id, sysvar_id + 32)` are readable." + let sysvar_id = translate_type::(memory_mapping, sysvar_id_addr, check_aligned)?; + + // Abort: "Not all bytes in VM memory range `[var_addr, var_addr + length)` are writable." + let var = translate_slice_mut::(memory_mapping, var_addr, length, check_aligned)?; + + // Abort: "`offset + length` is not in `[0, 2^64)`." + let offset_length = offset + .checked_add(length) + .ok_or(InstructionError::ArithmeticOverflow)?; + + // Abort: "`var_addr + length` is not in `[0, 2^64)`." + let _ = var_addr + .checked_add(length) + .ok_or(InstructionError::ArithmeticOverflow)?; + + let cache = invoke_context.get_sysvar_cache(); + + // "`2` if the sysvar data is not present in the Sysvar Cache." + let sysvar_buf = match cache.sysvar_id_to_buffer(sysvar_id) { + None => return Ok(SYSVAR_NOT_FOUND), + Some(ref sysvar_buf) => sysvar_buf, + }; + + // "`1` if `offset + length` is greater than the length of the sysvar data." + if let Some(sysvar_slice) = sysvar_buf.get(offset as usize..offset_length as usize) { + var.copy_from_slice(sysvar_slice); + } else { + return Ok(OFFSET_LENGTH_EXCEEDS_SYSVAR); + } + + Ok(SUCCESS) + } +); diff --git a/sdk/program/src/program_stubs.rs b/sdk/program/src/program_stubs.rs index cf890659fa68a1..2b06ecbee8646c 100644 --- a/sdk/program/src/program_stubs.rs +++ b/sdk/program/src/program_stubs.rs @@ -43,6 +43,15 @@ pub trait SyscallStubs: Sync + Send { sol_log("SyscallStubs: sol_invoke_signed() not available"); Ok(()) } + fn sol_get_sysvar( + &self, + _sysvar_id_addr: *const u8, + _var_addr: *mut u8, + _offset: u64, + _length: u64, + ) -> u64 { + UNSUPPORTED_SYSVAR + } fn sol_get_clock_sysvar(&self, _var_addr: *mut u8) -> u64 { UNSUPPORTED_SYSVAR } @@ -145,6 +154,19 @@ pub(crate) fn sol_invoke_signed( .sol_invoke_signed(instruction, account_infos, signers_seeds) } +#[allow(dead_code)] +pub(crate) fn sol_get_sysvar( + sysvar_id_addr: *const u8, + var_addr: *mut u8, + offset: u64, + length: u64, +) -> u64 { + SYSCALL_STUBS + .read() + .unwrap() + .sol_get_sysvar(sysvar_id_addr, var_addr, offset, length) +} + pub(crate) fn sol_get_clock_sysvar(var_addr: *mut u8) -> u64 { SYSCALL_STUBS.read().unwrap().sol_get_clock_sysvar(var_addr) } diff --git a/sdk/program/src/syscalls/definitions.rs b/sdk/program/src/syscalls/definitions.rs index b2dedceba953a0..dbcf6c2240adba 100644 --- a/sdk/program/src/syscalls/definitions.rs +++ b/sdk/program/src/syscalls/definitions.rs @@ -46,11 +46,6 @@ define_syscall!(fn sol_sha256(vals: *const u8, val_len: u64, hash_result: *mut u define_syscall!(fn sol_keccak256(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64); define_syscall!(fn sol_secp256k1_recover(hash: *const u8, recovery_id: u64, signature: *const u8, result: *mut u8) -> u64); define_syscall!(fn sol_blake3(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64); -define_syscall!(fn sol_get_clock_sysvar(addr: *mut u8) -> u64); -define_syscall!(fn sol_get_epoch_schedule_sysvar(addr: *mut u8) -> u64); -define_syscall!(fn sol_get_fees_sysvar(addr: *mut u8) -> u64); -define_syscall!(fn sol_get_rent_sysvar(addr: *mut u8) -> u64); -define_syscall!(fn sol_get_last_restart_slot(addr: *mut u8) -> u64); define_syscall!(fn sol_memcpy_(dst: *mut u8, src: *const u8, n: u64)); define_syscall!(fn sol_memmove_(dst: *mut u8, src: *const u8, n: u64)); define_syscall!(fn sol_memcmp_(s1: *const u8, s2: *const u8, n: u64, result: *mut i32)); @@ -68,10 +63,20 @@ define_syscall!(fn sol_curve_multiscalar_mul(curve_id: u64, scalars_addr: *const define_syscall!(fn sol_curve_pairing_map(curve_id: u64, point: *const u8, result: *mut u8) -> u64); define_syscall!(fn sol_alt_bn128_group_op(group_op: u64, input: *const u8, input_size: u64, result: *mut u8) -> u64); define_syscall!(fn sol_big_mod_exp(params: *const u8, result: *mut u8) -> u64); -define_syscall!(fn sol_get_epoch_rewards_sysvar(addr: *mut u8) -> u64); define_syscall!(fn sol_poseidon(parameters: u64, endianness: u64, vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64); define_syscall!(fn sol_remaining_compute_units() -> u64); define_syscall!(fn sol_alt_bn128_compression(op: u64, input: *const u8, input_size: u64, result: *mut u8) -> u64); +define_syscall!(fn sol_get_sysvar(sysvar_id_addr: *const u8, result: *mut u8, offset: u64, length: u64) -> u64); + +// these are to be deprecated once they are superceded by sol_get_sysvar +define_syscall!(fn sol_get_clock_sysvar(addr: *mut u8) -> u64); +define_syscall!(fn sol_get_epoch_schedule_sysvar(addr: *mut u8) -> u64); +define_syscall!(fn sol_get_rent_sysvar(addr: *mut u8) -> u64); +define_syscall!(fn sol_get_last_restart_slot(addr: *mut u8) -> u64); +define_syscall!(fn sol_get_epoch_rewards_sysvar(addr: *mut u8) -> u64); + +// this cannot go through sol_get_sysvar but can be removed once no longer in use +define_syscall!(fn sol_get_fees_sysvar(addr: *mut u8) -> u64); #[cfg(target_feature = "static-syscalls")] pub const fn sys_hash(name: &str) -> usize { diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 9fff562fcf3d81..2e0b286806afaf 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -797,6 +797,10 @@ pub mod reward_full_priority_fee { solana_sdk::declare_id!("3opE3EzAKnUftUDURkzMgwpNgimBAypW1mNDYH4x4Zg7"); } +pub mod get_sysvar_syscall_enabled { + solana_sdk::declare_id!("CLCoTADvV64PSrnR6QXty6Fwrt9Xc6EdxSJE4wLRePjq"); +} + pub mod abort_on_invalid_curve { solana_sdk::declare_id!("FuS3FPfJDKSNot99ECLXtp3rueq36hMNStJkPJwWodLh"); } @@ -995,7 +999,8 @@ lazy_static! { (enable_tower_sync_ix::id(), "Enable tower sync vote instruction"), (chained_merkle_conflict_duplicate_proofs::id(), "generate duplicate proofs for chained merkle root conflicts"), (reward_full_priority_fee::id(), "Reward full priority fee to validators #34731"), - (abort_on_invalid_curve::id(), "Abort when elliptic curve syscalls invoked on invalid curve id SIMD-0137") + (abort_on_invalid_curve::id(), "Abort when elliptic curve syscalls invoked on invalid curve id SIMD-0137"), + (get_sysvar_syscall_enabled::id(), "Enable syscall for fetching Sysvar bytes #615"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() From 15d46c2cdec6e0b7bfcbcce5de3b477bb7e696e3 Mon Sep 17 00:00:00 2001 From: hanako mumei <81144685+2501babe@users.noreply.github.com> Date: Wed, 15 May 2024 14:21:37 -0700 Subject: [PATCH 2/3] use floored checked div --- programs/bpf_loader/src/syscalls/sysvar.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/programs/bpf_loader/src/syscalls/sysvar.rs b/programs/bpf_loader/src/syscalls/sysvar.rs index 8d8fa667830e4d..a61b304feb208a 100644 --- a/programs/bpf_loader/src/syscalls/sysvar.rs +++ b/programs/bpf_loader/src/syscalls/sysvar.rs @@ -187,14 +187,13 @@ declare_builtin_function!( } = *invoke_context.get_compute_budget(); // Abort: "Compute budget is exceeded." + let sysvar_id_cost = 32_u64.checked_div(cpi_bytes_per_unit).unwrap_or(0); + let sysvar_buf_cost = length.checked_div(cpi_bytes_per_unit).unwrap_or(0); consume_compute_meter( invoke_context, sysvar_base_cost - .saturating_add(32_u64.div_ceil(cpi_bytes_per_unit)) - .saturating_add(std::cmp::max( - length.div_ceil(cpi_bytes_per_unit), - mem_op_base_cost, - )), + .saturating_add(sysvar_id_cost) + .saturating_add(std::cmp::max(sysvar_buf_cost, mem_op_base_cost)), )?; // Abort: "Not all bytes in VM memory range `[sysvar_id, sysvar_id + 32)` are readable." From beed36f319b80abc5f81a698152964216105f73a Mon Sep 17 00:00:00 2001 From: hanako mumei <81144685+2501babe@users.noreply.github.com> Date: Thu, 16 May 2024 00:09:51 -0700 Subject: [PATCH 3/3] make it clear that objects are equal before clone --- programs/bpf_loader/src/syscalls/mod.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/programs/bpf_loader/src/syscalls/mod.rs b/programs/bpf_loader/src/syscalls/mod.rs index d13634ddcf00d7..1745368c2ebee5 100644 --- a/programs/bpf_loader/src/syscalls/mod.rs +++ b/programs/bpf_loader/src/syscalls/mod.rs @@ -3574,15 +3574,14 @@ mod tests { ); result.unwrap(); - // clone is to zero the alignment padding let epochschedule_from_buf = - bincode::deserialize::(&got_epochschedule_buf) - .unwrap() - .clone(); + bincode::deserialize::(&got_epochschedule_buf).unwrap(); assert_eq!(epochschedule_from_buf, src_epochschedule); + + // clone is to zero the alignment padding assert!(are_bytes_equal( - &epochschedule_from_buf, + &epochschedule_from_buf.clone(), &clean_epochschedule )); } @@ -3670,11 +3669,12 @@ mod tests { ); result.unwrap(); - // clone is to zero the alignment padding - let rent_from_buf = bincode::deserialize::(&got_rent_buf).unwrap().clone(); + let rent_from_buf = bincode::deserialize::(&got_rent_buf).unwrap(); assert_eq!(rent_from_buf, src_rent); - assert!(are_bytes_equal(&rent_from_buf, &clean_rent)); + + // clone is to zero the alignment padding + assert!(are_bytes_equal(&rent_from_buf.clone(), &clean_rent)); } // Test epoch rewards sysvar @@ -3734,13 +3734,12 @@ mod tests { ); result.unwrap(); - // clone is to zero the alignment padding - let rewards_from_buf = bincode::deserialize::(&got_rewards_buf) - .unwrap() - .clone(); + let rewards_from_buf = bincode::deserialize::(&got_rewards_buf).unwrap(); assert_eq!(rewards_from_buf, src_rewards); - assert!(are_bytes_equal(&rewards_from_buf, &clean_rewards)); + + // clone is to zero the alignment padding + assert!(are_bytes_equal(&rewards_from_buf.clone(), &clean_rewards)); } // Test last restart slot sysvar