diff --git a/Cargo.lock b/Cargo.lock index ae9ab4a95..2c22a1d56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,21 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -154,6 +169,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af0f70f64b53a02c58134d6e8b17a7d0cd7fd53ae20ec9a9aeb84a912594e2a1" +dependencies = [ + "bit-set", + "bit-vec", +] + [[package]] name = "bumpalo" version = "3.13.0" @@ -1336,6 +1361,7 @@ name = "soroban-env-host" version = "0.0.17" dependencies = [ "backtrace", + "bs", "bytes-lit", "curve25519-dalek", "ed25519-dalek", diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml index 29b4b704a..14f6834ec 100644 --- a/soroban-env-host/Cargo.toml +++ b/soroban-env-host/Cargo.toml @@ -36,6 +36,7 @@ k256 = {version = "0.13.1", features=["ecdsa", "arithmetic"]} # is needed to build the host for wasm (a rare but supported config). getrandom = { version = "0.2", features=["js"] } sha3 = "0.10.8" +bs = "0.1.0" [dev-dependencies] env_logger = "0.9.0" diff --git a/soroban-env-host/src/auth.rs b/soroban-env-host/src/auth.rs index 7de7b84d4..3a4953f50 100644 --- a/soroban-env-host/src/auth.rs +++ b/soroban-env-host/src/auth.rs @@ -702,7 +702,7 @@ impl AuthorizationManager { // Use the respective push (like // `push_create_contract_host_fn_frame`) functions instead to push // the frame with the required info. - Frame::HostFunction(_) => return Ok(()), + Frame::InitialInvokeHostFunctionOp(..) => return Ok(()), Frame::Token(id, fn_name, ..) => (id.metered_clone(host.budget_ref())?, *fn_name), #[cfg(any(test, feature = "testutils"))] Frame::TestContract(tc) => (tc.id.clone(), tc.func), diff --git a/soroban-env-host/src/cost_runner/cost_types/visit_object.rs b/soroban-env-host/src/cost_runner/cost_types/visit_object.rs index cdf91e882..e7a809b58 100644 --- a/soroban-env-host/src/cost_runner/cost_types/visit_object.rs +++ b/soroban-env-host/src/cost_runner/cost_types/visit_object.rs @@ -1,6 +1,6 @@ use std::hint::black_box; -use crate::{cost_runner::CostRunner, host_object::HostObject, xdr::ContractCostType, Object}; +use crate::{cost_runner::CostRunner, host_object::HostObjectBody, xdr::ContractCostType, Object}; pub struct VisitObjectRun; @@ -17,7 +17,7 @@ impl CostRunner for VisitObjectRun { host.unchecked_visit_val_obj(sample[iter as usize % sample.len()], |obj| match obj .unwrap() { - HostObject::I64(i) => Ok(*i), + HostObjectBody::I64(i) => Ok(*i), _ => panic!("unexpected type, check HCM"), }) .unwrap(), diff --git a/soroban-env-host/src/events/diagnostic.rs b/soroban-env-host/src/events/diagnostic.rs index 032a839a4..126cef459 100644 --- a/soroban-env-host/src/events/diagnostic.rs +++ b/soroban-env-host/src/events/diagnostic.rs @@ -65,7 +65,7 @@ impl Host { pub(crate) fn get_current_contract_id_unmetered(&self) -> Result, HostError> { self.with_current_frame_opt(|frame| match frame { Some(Frame::ContractVM(vm, ..)) => Ok(Some(vm.contract_id.clone())), - Some(Frame::HostFunction(_)) => Ok(None), + Some(Frame::InitialInvokeHostFunctionOp(..)) => Ok(None), Some(Frame::Token(id, ..)) => Ok(Some(id.clone())), #[cfg(any(test, feature = "testutils"))] Some(Frame::TestContract(tc)) => Ok(Some(tc.id.clone())), diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs index b36842c64..91e7c31b7 100644 --- a/soroban-env-host/src/host.rs +++ b/soroban-env-host/src/host.rs @@ -10,7 +10,7 @@ use crate::{ err, events::{diagnostic::DiagnosticLevel, Events, InternalEventsBuffer}, expiration_ledger_bumps::{ExpirationLedgerBumps, LedgerBump}, - host_object::{HostMap, HostObject, HostObjectType, HostVec}, + host_object::{HostMap, HostObject, HostObjectType, HostVec, Ticket}, impl_bignum_host_fns_rhs_u32, impl_wrapping_obj_from_num, impl_wrapping_obj_to_num, num::*, storage::{InstanceStorageMap, Storage}, @@ -94,6 +94,7 @@ pub(crate) struct HostImpl { source_account: RefCell>, ledger: RefCell>, pub(crate) objects: RefCell>, + pub(crate) ticket: RefCell, storage: RefCell, pub(crate) context: RefCell>, // Note: budget is refcounted and is _not_ deep-cloned when you call HostImpl::deep_clone, @@ -164,6 +165,7 @@ impl_checked_borrow_helpers!( try_borrow_objects, try_borrow_objects_mut ); +impl_checked_borrow_helpers!(ticket, Ticket, try_borrow_ticket, try_borrow_ticket_mut); impl_checked_borrow_helpers!(storage, Storage, try_borrow_storage, try_borrow_storage_mut); impl_checked_borrow_helpers!( context, @@ -234,6 +236,7 @@ impl Host { source_account: RefCell::new(None), ledger: RefCell::new(None), objects: Default::default(), + ticket: Default::default(), storage: RefCell::new(storage), context: Default::default(), budget, @@ -1499,7 +1502,7 @@ impl VmCallerEnv for Host { m: MapObject, k: Val, ) -> Result { - self.visit_obj(m, move |hm: &HostMap| { + self.visit_obj_propagating_tickets(m, move |hm: &HostMap| { hm.get(&k, self)?.copied().ok_or_else(|| { self.err( ScErrorType::Object, @@ -1548,7 +1551,7 @@ impl VmCallerEnv for Host { m: MapObject, k: Val, ) -> Result { - self.visit_obj(m, |hm: &HostMap| { + self.visit_obj_propagating_tickets(m, |hm: &HostMap| { if let Some((pk, _)) = hm.get_prev(&k, self)? { Ok(*pk) } else { @@ -1567,7 +1570,7 @@ impl VmCallerEnv for Host { m: MapObject, k: Val, ) -> Result { - self.visit_obj(m, |hm: &HostMap| { + self.visit_obj_propagating_tickets(m, |hm: &HostMap| { if let Some((pk, _)) = hm.get_next(&k, self)? { Ok(*pk) } else { @@ -1581,7 +1584,7 @@ impl VmCallerEnv for Host { } fn map_min_key(&self, _vmcaller: &mut VmCaller, m: MapObject) -> Result { - self.visit_obj(m, |hm: &HostMap| match hm.get_min(self)? { + self.visit_obj_propagating_tickets(m, |hm: &HostMap| match hm.get_min(self)? { Some((pk, pv)) => Ok(*pk), None => Ok( Error::from_type_and_code(ScErrorType::Object, ScErrorCode::IndexBounds).to_val(), @@ -1590,7 +1593,7 @@ impl VmCallerEnv for Host { } fn map_max_key(&self, _vmcaller: &mut VmCaller, m: MapObject) -> Result { - self.visit_obj(m, |hm: &HostMap| match hm.get_max(self)? { + self.visit_obj_propagating_tickets(m, |hm: &HostMap| match hm.get_max(self)? { Some((pk, pv)) => Ok(*pk), None => Ok( Error::from_type_and_code(ScErrorType::Object, ScErrorCode::IndexBounds).to_val(), @@ -1768,7 +1771,7 @@ impl VmCallerEnv for Host { i: U32Val, ) -> Result { let i: u32 = i.into(); - self.visit_obj(v, move |hv: &HostVec| { + self.visit_obj_propagating_tickets(v, move |hv: &HostVec| { hv.get(i as usize, self.as_budget()).map(|r| *r) }) } @@ -1833,13 +1836,13 @@ impl VmCallerEnv for Host { } fn vec_front(&self, _vmcaller: &mut VmCaller, v: VecObject) -> Result { - self.visit_obj(v, |hv: &HostVec| { + self.visit_obj_propagating_tickets(v, |hv: &HostVec| { hv.front(self.as_budget()).map(|hval| *hval) }) } fn vec_back(&self, _vmcaller: &mut VmCaller, v: VecObject) -> Result { - self.visit_obj(v, |hv: &HostVec| { + self.visit_obj_propagating_tickets(v, |hv: &HostVec| { hv.back(self.as_budget()).map(|hval| *hval) }) } @@ -2837,7 +2840,7 @@ impl VmCallerEnv for Host { Frame::ContractVM(vm, function, ..) => { get_host_val_tuple(&vm.contract_id, &function)? } - Frame::HostFunction(_) => continue, + Frame::InitialInvokeHostFunctionOp(..) => continue, Frame::Token(id, function, ..) => get_host_val_tuple(id, function)?, #[cfg(any(test, feature = "testutils"))] Frame::TestContract(tc) => get_host_val_tuple(&tc.id, &tc.func)?, @@ -2894,7 +2897,7 @@ impl VmCallerEnv for Host { let args = self.with_current_frame(|f| { let args = match f { Frame::ContractVM(_, _, args, _) => args, - Frame::HostFunction(_) => { + Frame::InitialInvokeHostFunctionOp(..) => { return Err(self.err( ScErrorType::Context, ScErrorCode::InvalidAction, diff --git a/soroban-env-host/src/host/comparison.rs b/soroban-env-host/src/host/comparison.rs index 126ec756f..a8fbe58fb 100644 --- a/soroban-env-host/src/host/comparison.rs +++ b/soroban-env-host/src/host/comparison.rs @@ -16,7 +16,7 @@ use soroban_env_common::{ use crate::{ budget::{AsBudget, Budget}, - host_object::HostObject, + host_object::HostObjectBody, Host, HostError, }; @@ -28,30 +28,30 @@ use super::declared_size::DeclaredSizeForMetering; // // Note that these must have the same order as the impl // of Ord for ScVal, re https://github.com/stellar/rs-soroban-env/issues/743 -fn host_obj_discriminant(ho: &HostObject) -> usize { +fn host_obj_discriminant(ho: &HostObjectBody) -> usize { match ho { - HostObject::U64(_) => 0, - HostObject::I64(_) => 1, - HostObject::TimePoint(_) => 2, - HostObject::Duration(_) => 3, - HostObject::U128(_) => 4, - HostObject::I128(_) => 5, - HostObject::U256(_) => 6, - HostObject::I256(_) => 7, - HostObject::Bytes(_) => 8, - HostObject::String(_) => 9, - HostObject::Symbol(_) => 10, - HostObject::Vec(_) => 11, - HostObject::Map(_) => 12, - HostObject::Address(_) => 13, + HostObjectBody::U64(_) => 0, + HostObjectBody::I64(_) => 1, + HostObjectBody::TimePoint(_) => 2, + HostObjectBody::Duration(_) => 3, + HostObjectBody::U128(_) => 4, + HostObjectBody::I128(_) => 5, + HostObjectBody::U256(_) => 6, + HostObjectBody::I256(_) => 7, + HostObjectBody::Bytes(_) => 8, + HostObjectBody::String(_) => 9, + HostObjectBody::Symbol(_) => 10, + HostObjectBody::Vec(_) => 11, + HostObjectBody::Map(_) => 12, + HostObjectBody::Address(_) => 13, } } -impl Compare for Host { +impl Compare for Host { type Error = HostError; - fn compare(&self, a: &HostObject, b: &HostObject) -> Result { - use HostObject::*; + fn compare(&self, a: &HostObjectBody, b: &HostObjectBody) -> Result { + use HostObjectBody::*; match (a, b) { (U64(a), U64(b)) => self.as_budget().compare(a, b), (I64(a), I64(b)) => self.as_budget().compare(a, b), diff --git a/soroban-env-host/src/host/conversion.rs b/soroban-env-host/src/host/conversion.rs index 58acda121..1d54b378a 100644 --- a/soroban-env-host/src/host/conversion.rs +++ b/soroban-env-host/src/host/conversion.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use super::metered_clone::{self, charge_container_bulk_init_with_elts, MeteredClone}; use crate::budget::AsBudget; use crate::err; -use crate::host_object::{HostMap, HostObject, HostVec}; +use crate::host_object::{HostMap, HostObjectBody, HostVec}; use crate::xdr::{Hash, LedgerKey, LedgerKeyContractData, ScVal, ScVec, Uint256}; use crate::{xdr::ContractCostType, Host, HostError, Val}; use soroban_env_common::num::{ @@ -413,7 +413,7 @@ impl Host { )); } Some(ho) => match ho { - HostObject::Vec(vv) => { + HostObjectBody::Vec(vv) => { metered_clone::charge_heap_alloc::( vv.len() as u64, self.as_budget(), @@ -425,24 +425,24 @@ impl Host { )?; ScVal::Vec(Some(ScVec(self.map_err(sv.try_into())?))) } - HostObject::Map(mm) => ScVal::Map(Some(self.host_map_to_scmap(mm)?)), - HostObject::U64(u) => ScVal::U64(*u), - HostObject::I64(i) => ScVal::I64(*i), - HostObject::TimePoint(tp) => { + HostObjectBody::Map(mm) => ScVal::Map(Some(self.host_map_to_scmap(mm)?)), + HostObjectBody::U64(u) => ScVal::U64(*u), + HostObjectBody::I64(i) => ScVal::I64(*i), + HostObjectBody::TimePoint(tp) => { ScVal::Timepoint(tp.metered_clone(self.as_budget())?) } - HostObject::Duration(d) => { + HostObjectBody::Duration(d) => { ScVal::Duration(d.metered_clone(self.as_budget())?) } - HostObject::U128(u) => ScVal::U128(UInt128Parts { + HostObjectBody::U128(u) => ScVal::U128(UInt128Parts { hi: int128_helpers::u128_hi(*u), lo: int128_helpers::u128_lo(*u), }), - HostObject::I128(i) => ScVal::I128(Int128Parts { + HostObjectBody::I128(i) => ScVal::I128(Int128Parts { hi: int128_helpers::i128_hi(*i), lo: int128_helpers::i128_lo(*i), }), - HostObject::U256(u) => { + HostObjectBody::U256(u) => { let (hi_hi, hi_lo, lo_hi, lo_lo) = u256_into_pieces(*u); ScVal::U256(UInt256Parts { hi_hi, @@ -451,7 +451,7 @@ impl Host { lo_lo, }) } - HostObject::I256(i) => { + HostObjectBody::I256(i) => { let (hi_hi, hi_lo, lo_hi, lo_lo) = i256_into_pieces(*i); ScVal::I256(Int256Parts { hi_hi, @@ -460,10 +460,16 @@ impl Host { lo_lo, }) } - HostObject::Bytes(b) => ScVal::Bytes(b.metered_clone(self.as_budget())?), - HostObject::String(s) => ScVal::String(s.metered_clone(self.as_budget())?), - HostObject::Symbol(s) => ScVal::Symbol(s.metered_clone(self.as_budget())?), - HostObject::Address(addr) => { + HostObjectBody::Bytes(b) => { + ScVal::Bytes(b.metered_clone(self.as_budget())?) + } + HostObjectBody::String(s) => { + ScVal::String(s.metered_clone(self.as_budget())?) + } + HostObjectBody::Symbol(s) => { + ScVal::Symbol(s.metered_clone(self.as_budget())?) + } + HostObjectBody::Address(addr) => { ScVal::Address(addr.metered_clone(self.as_budget())?) } }, diff --git a/soroban-env-host/src/host/declared_size.rs b/soroban-env-host/src/host/declared_size.rs index 2d9a5ecc2..51ef262dd 100644 --- a/soroban-env-host/src/host/declared_size.rs +++ b/soroban-env-host/src/host/declared_size.rs @@ -95,7 +95,7 @@ impl_declared_size_type!(SymbolStr, SCSYMBOL_LIMIT); impl_declared_size_type!(SymbolSmallIter, 8); impl_declared_size_type!(U256, 32); impl_declared_size_type!(I256, 32); -impl_declared_size_type!(HostObject, 48); +impl_declared_size_type!(HostObject, 72); impl_declared_size_type!(LedgerBump, 20); // xdr types impl_declared_size_type!(TimePoint, 8); diff --git a/soroban-env-host/src/host/frame.rs b/soroban-env-host/src/host/frame.rs index bb23b6e31..c0ce53b2f 100644 --- a/soroban-env-host/src/host/frame.rs +++ b/soroban-env-host/src/host/frame.rs @@ -7,6 +7,7 @@ use crate::{ auth::AuthorizationManagerSnapshot, budget::AsBudget, err, + host_object::Ticket, storage::{InstanceStorageMap, StorageMap}, xdr::{ContractCostType, ContractExecutable, Hash, HostFunction, HostFunctionType, ScVal}, Error, Host, HostError, Symbol, SymbolStr, TryFromVal, TryIntoVal, Val, @@ -44,8 +45,8 @@ pub(crate) enum ContractReentryMode { /// contracts. const RESERVED_CONTRACT_FN_PREFIX: &str = "__"; -/// Saves host state (storage and objects) for rolling back a (sub-)transaction -/// on error. A helper type used by [`FrameGuard`]. +/// Saves host state (storage, events, auth) for rolling back a (sub-)transaction +/// on error. // Notes on metering: `RollbackPoint` are metered under Frame operations #[derive(Clone)] pub(super) struct RollbackPoint { @@ -89,6 +90,7 @@ pub(crate) struct Context { pub(crate) frame: Frame, prng: Option, pub(crate) storage: Option, + ticket: Ticket, } /// Holds contextual information about a single invocation, either @@ -96,16 +98,15 @@ pub(crate) struct Context { /// invocation. /// /// Frames are arranged into a stack in [`HostImpl::context`], and are pushed -/// with [`Host::push_frame`], which returns a [`FrameGuard`] that will -/// pop the frame on scope-exit. +/// with [`Host::push_frame`], popped with [`Host::pop_frame`]. /// -/// Frames are also the units of (sub-)transactions: each frame captures -/// the host state when it is pushed, and the [`FrameGuard`] will either -/// commit or roll back that state when it pops the stack. +/// Frames are also the units of (sub-)transactions: each frame captures the +/// host state when it is pushed and either commits or rolls back that state +/// when popped from the stack. #[derive(Clone)] pub(crate) enum Frame { ContractVM(Rc, Symbol, Vec, ScContractInstance), - HostFunction(HostFunctionType), + InitialInvokeHostFunctionOp(HostFunctionType, Vec), Token(Hash, Symbol, Vec, ScContractInstance), #[cfg(any(test, feature = "testutils"))] TestContract(TestContractFrame), @@ -126,10 +127,14 @@ impl Host { auth_manager.push_frame(self, &frame)?; auth_snapshot = Some(auth_manager.snapshot()); } + let caller_ticket = self.get_current_ticket()?; + let callee_ticket = self.try_borrow_ticket_mut()?.bump()?; + self.propagate_tickets_to_args(caller_ticket, callee_ticket, &frame)?; let ctx = Context { frame, prng: None, storage: None, + ticket: callee_ticket, }; self.try_borrow_context_mut()?.push(ctx); Ok(RollbackPoint { @@ -143,12 +148,13 @@ impl Host { /// the current context and optionally rolls back the [`Host`]'s objects /// and storage map to the state in the provided [`RollbackPoint`]. pub(super) fn pop_frame(&self, orp: Option) -> Result<(), HostError> { - // Instance storage is tied to the frame and only exists in-memory. So - // instead of snapshotting it and rolling it back, we jsut flush the - // changes only when rollback is not needed. if orp.is_none() { + // Instance storage is tied to the frame and only exists in-memory. + // So instead of snapshotting it and rolling it back, we jsut flush + // the changes only when rollback is not needed. self.flush_instance_storage()?; } + self.try_borrow_context_mut()? .pop() .expect("unmatched host frame push/pop"); @@ -242,6 +248,79 @@ impl Host { } } + pub(super) fn with_current_context(&self, f: F) -> Result + where + F: FnOnce(&Context) -> Result, + { + let Ok(context_guard) = self.0.context.try_borrow() else { + return Err(self.err(ScErrorType::Context, ScErrorCode::InternalError, "context is already borrowed", &[])); + }; + if let Some(context) = context_guard.last() { + f(context) + } else { + drop(context_guard); + Err(self.err( + ScErrorType::Context, + ScErrorCode::MissingValue, + "no contract running", + &[], + )) + } + } + + pub(crate) fn get_current_ticket(&self) -> Result { + self.with_current_context_opt(|ctxopt| { + Ok(if let Some(ctx) = ctxopt { + ctx.ticket + } else { + // We define the "current ticket" when there's no context as the + // zero/default ticket, which denotes the implicit context + // outside the host; all actual reified contexts have nonzero + // ticket. + Ticket::default() + }) + }) + } + + /// For each argument passed from caller to callee in the new frame, if the argument is readable by the + /// caller then mark it as also readable by the callee, otherwise fail with an error. + fn propagate_tickets_to_args( + &self, + caller_ticket: Ticket, + callee_ticket: Ticket, + frame: &Frame, + ) -> Result<(), HostError> { + let args: &Vec = match frame { + Frame::ContractVM(_, _, args, _) => args, + Frame::InitialInvokeHostFunctionOp(_, args) => args, + Frame::Token(_, _, args, _) => args, + #[cfg(any(test, feature = "testutils"))] + Frame::TestContract(tc) => &tc.args, + }; + // eprintln!("propagating tickets {:?} -> {:?} on {} arguments", caller_ticket, callee_ticket, args.len()); + for v in args.iter() { + self.propagate_ticket(*v, caller_ticket, callee_ticket)? + } + Ok(()) + } + + /// Same as [`Self::with_current_context`] but passes `None` when there is no current + /// frame, rather than logging an error. + pub(crate) fn with_current_context_opt(&self, f: F) -> Result + where + F: FnOnce(Option<&Context>) -> Result, + { + let Ok(context_guard) = self.0.context.try_borrow() else { + return Err(self.err(ScErrorType::Context, ScErrorCode::InternalError, "context is already borrowed", &[])); + }; + if let Some(context) = context_guard.last() { + f(Some(context)) + } else { + drop(context_guard); + f(None) + } + } + /// Same as [`Self::with_current_frame`] but passes `None` when there is no current /// frame, rather than logging an error. pub(crate) fn with_current_frame_opt(&self, f: F) -> Result @@ -310,6 +389,7 @@ impl Host { F: FnOnce() -> Result, { self.charge_budget(ContractCostType::GuardFrame, None)?; + let caller_ticket = self.get_current_ticket()?; let start_depth = self.try_borrow_context()?.len(); let rp = self.push_frame(frame)?; let res = f(); @@ -322,12 +402,15 @@ impl Host { } else { res }; - if res.is_err() { + if let Ok(ret) = res { + // Propagate visibility of return value + // and pop frame on success. + let callee_ticket = self.get_current_ticket()?; + self.propagate_ticket(ret, callee_ticket, caller_ticket)?; + self.pop_frame(None)?; + } else { // Pop and rollback on error. self.pop_frame(Some(rp))?; - } else { - // Just pop on success. - self.pop_frame(None)?; } // Every push and pop should be matched; if not there is a bug. let end_depth = self.try_borrow_context()?.len(); @@ -341,7 +424,7 @@ impl Host { pub(crate) fn get_current_contract_id_opt_internal(&self) -> Result, HostError> { self.with_current_frame(|frame| match frame { Frame::ContractVM(vm, ..) => Ok(Some(vm.contract_id.metered_clone(&self.0.budget)?)), - Frame::HostFunction(_) => Ok(None), + Frame::InitialInvokeHostFunctionOp(..) => Ok(None), Frame::Token(id, ..) => Ok(Some(id.metered_clone(&self.0.budget)?)), #[cfg(any(test, feature = "testutils"))] Frame::TestContract(tc) => Ok(Some(tc.id.clone())), @@ -370,7 +453,7 @@ impl Host { let hash = match frames.as_slice() { [.., c2, _] => match &c2.frame { Frame::ContractVM(vm, ..) => Ok(vm.contract_id.metered_clone(&self.0.budget)?), - Frame::HostFunction(_) => Err(self.err( + Frame::InitialInvokeHostFunctionOp(..) => Err(self.err( ScErrorType::Context, ScErrorCode::UnexpectedType, "invoker is not a contract", @@ -399,7 +482,7 @@ impl Host { // There are always two frames when WASM is executed in the VM. [.., c2, _] => match &c2.frame { Frame::ContractVM(..) => Ok(InvokerType::Contract), - Frame::HostFunction(_) => Ok(InvokerType::Account), + Frame::InitialInvokeHostFunctionOp(..) => Ok(InvokerType::Account), Frame::Token(..) => Ok(InvokerType::Contract), #[cfg(any(test, feature = "testutils"))] Frame::TestContract(_) => Ok(InvokerType::Contract), @@ -497,7 +580,7 @@ impl Host { Frame::Token(id, ..) => id, #[cfg(any(test, feature = "testutils"))] Frame::TestContract(tc) => &tc.id, - Frame::HostFunction(_) => continue, + Frame::InitialInvokeHostFunctionOp(..) => continue, }; if id == exist_id { if matches!(reentry_mode, ContractReentryMode::SelfAllowed) @@ -631,37 +714,40 @@ impl Host { let hf_type = hf.discriminant(); match hf { HostFunction::InvokeContract(args) => { + let nargs = args.len(); if let [ScVal::Address(ScAddress::Contract(contract_id)), ScVal::Symbol(scsym), rest @ ..] = args.as_slice() { - self.with_frame(Frame::HostFunction(hf_type), || { - // Metering: conversions to host objects are covered. Cost of collecting - // Vals into Vec is ignored. Since 1. Vals are cheap to clone 2. the - // max number of args is fairly limited. - - let symbol: Symbol = scsym.as_slice().try_into_val(self)?; - let args = self.scvals_to_rawvals(rest)?; - // since the `HostFunction` frame must be the bottom of the call stack, - // reentry is irrelevant, we always pass in `ContractReentryMode::Prohibited`. - self.call_n_internal( - contract_id, - symbol, - &args[..], - ContractReentryMode::Prohibited, - false, - ) - }) + // Metering: conversions to host objects are covered. Cost of collecting + // Vals into Vec is ignored. Since 1. Vals are cheap to clone 2. the + // max number of args is fairly limited. + let callargs = self.scvals_to_rawvals(rest)?; + self.with_frame( + Frame::InitialInvokeHostFunctionOp(hf_type, callargs.clone()), + || { + let symbol: Symbol = scsym.as_slice().try_into_val(self)?; + // since the `HostFunction` frame must be the bottom of the call stack, + // reentry is irrelevant, we always pass in `ContractReentryMode::Prohibited`. + self.call_n_internal( + contract_id, + symbol, + &callargs[..], + ContractReentryMode::Prohibited, + false, + ) + }, + ) } else { Err(err!( self, (ScErrorType::Context, ScErrorCode::UnexpectedSize), "unexpected number of arguments to 'call' host function", - args.len() + nargs )) } } HostFunction::CreateContract(args) => { - self.with_frame(Frame::HostFunction(hf_type), || { + self.with_frame(Frame::InitialInvokeHostFunctionOp(hf_type, vec![]), || { let deployer: Option = match &args.contract_id_preimage { ContractIdPreimage::Address(preimage_from_addr) => Some( self.add_host_object( @@ -677,7 +763,7 @@ impl Host { }) } HostFunction::UploadContractWasm(wasm) => self - .with_frame(Frame::HostFunction(hf_type), || { + .with_frame(Frame::InitialInvokeHostFunctionOp(hf_type, vec![]), || { self.upload_contract_wasm(wasm.to_vec()).map(::from) }), } @@ -697,7 +783,7 @@ impl Host { } let storage_map = match &ctx.frame { Frame::ContractVM(_, _, _, instance) => &instance.storage, - Frame::HostFunction(_) => { + Frame::InitialInvokeHostFunctionOp(..) => { return Err(self.err( ScErrorType::Context, ScErrorCode::InvalidAction, @@ -738,7 +824,7 @@ impl Host { Frame::ContractVM(_, _, _, instance) => { instance.executable.metered_clone(self.budget_ref())? } - Frame::HostFunction(_) => { + Frame::InitialInvokeHostFunctionOp(..) => { return Err(self.err( ScErrorType::Context, ScErrorCode::InternalError, diff --git a/soroban-env-host/src/host/validity.rs b/soroban-env-host/src/host/validity.rs index 15959f777..c8a9554c8 100644 --- a/soroban-env-host/src/host/validity.rs +++ b/soroban-env-host/src/host/validity.rs @@ -4,7 +4,7 @@ use soroban_env_common::xdr::{ScErrorCode, ScErrorType}; use soroban_env_common::U32Val; use crate::events::{CONTRACT_EVENT_TOPICS_LIMIT, TOPIC_BYTES_LENGTH_LIMIT}; -use crate::{host_object::HostObject, Host, HostError, Object, Val, VecObject}; +use crate::{host_object::HostObjectBody, Host, HostError, Object, Val, VecObject}; impl Host { // Notes on metering: free @@ -92,7 +92,7 @@ impl Host { &[], )), Some(ho) => match ho { - HostObject::Bytes(b) => { + HostObjectBody::Bytes(b) => { if b.len() > TOPIC_BYTES_LENGTH_LIMIT { // TODO: use more event-specific error codes than `UnexpectedType`. // Something like "topic bytes exceeds length limit" @@ -130,7 +130,7 @@ impl Host { &[], )), Some(ho) => match ho { - HostObject::Vec(vv) => { + HostObjectBody::Vec(vv) => { if vv.len() > CONTRACT_EVENT_TOPICS_LIMIT { // TODO: proper error code "event topics exceeds count limit" return Err(self.err( diff --git a/soroban-env-host/src/host_object.rs b/soroban-env-host/src/host_object.rs index 004a707fa..a8bf43f35 100644 --- a/soroban-env-host/src/host_object.rs +++ b/soroban-env-host/src/host_object.rs @@ -1,6 +1,8 @@ +use bs::Bs; use soroban_env_common::{ - xdr::ContractCostType, Compare, DurationSmall, I128Small, I256Small, I64Small, SymbolSmall, - SymbolStr, Tag, TimepointSmall, U128Small, U256Small, U64Small, + xdr::{ContractCostType, ScErrorCode, ScErrorType}, + Compare, DurationSmall, Error, I128Small, I256Small, I64Small, SymbolSmall, SymbolStr, Tag, + TimepointSmall, U128Small, U256Small, U64Small, }; use crate::{ @@ -18,11 +20,79 @@ use super::{ U64Object, Val, VecObject, }; +/// A ticket is a number that represents permission to read a set of host +/// objects: all those created with the ticket, or readable transitively through +/// other objects readable with the ticket. +/// +/// Every object holds a compact (typically 1-word) [`Tickets`] bitset +/// containing all the tickets that are allowed to read it. These are updated +/// lazily on access: whenever an object reference X is read out of an object Y, +/// the ticket T(Y) of Y is unioned into the ticket of X, propagating the fact +/// that "since Y is reachable from T(Y), X is also reachable from T(Y)". +/// +/// The [`Host`] maintains a "current" ticket, which is incremented every time a +/// new frame is pushed, and is used for the initial ticket-set of new objects, +/// as well as the ticket checked for read access to objects. +/// +/// By default this means that an object created in a frame is inaccessible from +/// other frames. The exception is when a user explicitly grants access to an +/// object by _passing_ a reference to it from one frame to another. This sets +/// the ticket of the object to include the callee frame's ticket, which in turn +/// makes all objects transitively reachable through that object readable by the +/// callee. +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct Ticket(usize); +impl Ticket { + // Increments the ticket and returns its new value. + pub(crate) fn bump(&mut self) -> Result { + match self.0.checked_add(1) { + Some(t) => { + self.0 = t; + Ok(Ticket(t)) + } + None => { + Err(Error::from_type_and_code(ScErrorType::Value, ScErrorCode::ArithDomain).into()) + } + } + } +} + +#[derive(Clone, Default, PartialEq, Eq)] +pub(crate) struct Tickets(Bs); +impl Tickets { + fn from_ticket(ticket: Ticket) -> Tickets { + let mut tickets = Self::default(); + tickets.allow_ticket(ticket); + tickets + } + fn allow_ticket(&mut self, ticket: Ticket) { + self.0.insert(ticket.0); + } + fn allow_tickets(&mut self, tickets: &Tickets) { + self.0.union_with(&tickets.0) + } + fn is_allowed(&self, ticket: Ticket) -> bool { + self.0.contains(ticket.0) + } +} +impl core::fmt::Debug for Tickets { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let elts: Vec = (&self.0).into(); + f.debug_tuple("Tickets").field(&elts).finish() + } +} + pub(crate) type HostMap = MeteredOrdMap; pub(crate) type HostVec = MeteredVector; #[derive(Clone)] -pub enum HostObject { +pub(crate) struct HostObject { + pub(crate) tickets: Tickets, + pub(crate) body: HostObjectBody, +} + +#[derive(Clone)] +pub enum HostObjectBody { Vec(HostVec), Map(HostMap), U64(u64), @@ -39,7 +109,7 @@ pub enum HostObject { Address(xdr::ScAddress), } -impl HostObject { +impl HostObjectBody { // Temporarily performs a shallow comparison against a Val of the // associated small value type, returning None if the Val is of // the wrong type. @@ -49,58 +119,58 @@ impl HostObject { rv: Val, ) -> Result, HostError> { let res = match self { - HostObject::U64(u) => { + HostObjectBody::U64(u) => { let Ok(small) = U64Small::try_from(rv) else { return Ok(None) }; let small: u64 = small.into(); Some(budget.compare(u, &small)?) } - HostObject::I64(i) => { + HostObjectBody::I64(i) => { let Ok(small) = I64Small::try_from(rv) else { return Ok(None) }; let small: i64 = small.into(); Some(budget.compare(i, &small)?) } - HostObject::TimePoint(tp) => { + HostObjectBody::TimePoint(tp) => { let Ok(small) = TimepointSmall::try_from(rv) else { return Ok(None) }; let small: u64 = small.into(); Some(budget.compare(&tp.0, &small)?) } - HostObject::Duration(d) => { + HostObjectBody::Duration(d) => { let Ok(small) = DurationSmall::try_from(rv) else { return Ok(None) }; let small: u64 = small.into(); Some(budget.compare(&d.0, &small)?) } - HostObject::U128(u) => { + HostObjectBody::U128(u) => { let Ok(small) = U128Small::try_from(rv) else { return Ok(None) }; let small: u128 = small.into(); Some(budget.compare(u, &small)?) } - HostObject::I128(i) => { + HostObjectBody::I128(i) => { let Ok(small) = I128Small::try_from(rv) else { return Ok(None) }; let small: i128 = small.into(); Some(budget.compare(i, &small)?) } - HostObject::U256(u) => { + HostObjectBody::U256(u) => { let Ok(small) = U256Small::try_from(rv) else { return Ok(None) }; let small: U256 = small.into(); Some(budget.compare(u, &small)?) } - HostObject::I256(i) => { + HostObjectBody::I256(i) => { let Ok(small) = I256Small::try_from(rv) else { return Ok(None) }; let small: I256 = small.into(); Some(budget.compare(i, &small)?) } - HostObject::Symbol(s) => { + HostObjectBody::Symbol(s) => { let Ok(small) = SymbolSmall::try_from(rv) else { return Ok(None) }; let small: SymbolStr = small.into(); let rhs: &[u8] = small.as_ref(); Some(budget.compare(&s.as_vec().as_slice(), &rhs)?) } - HostObject::Vec(_) - | HostObject::Map(_) - | HostObject::Bytes(_) - | HostObject::String(_) - | HostObject::Address(_) => None, + HostObjectBody::Vec(_) + | HostObjectBody::Map(_) + | HostObjectBody::Bytes(_) + | HostObjectBody::String(_) + | HostObjectBody::Address(_) => None, }; Ok(res) } @@ -109,8 +179,8 @@ impl HostObject { pub trait HostObjectType: MeteredClone { type Wrapper: Into; fn new_from_handle(handle: u32) -> Self::Wrapper; - fn inject(self) -> HostObject; - fn try_extract(obj: &HostObject) -> Option<&Self>; + fn inject(self) -> HostObjectBody; + fn try_extract(obj: &HostObjectBody) -> Option<&Self>; } // Some host objects are "a slab of memory" which we want @@ -128,13 +198,13 @@ macro_rules! declare_host_object_type { fn new_from_handle(handle: u32) -> Self::Wrapper { unsafe { $TAG::from_handle(handle) } } - fn inject(self) -> HostObject { - HostObject::$CASE(self) + fn inject(self) -> HostObjectBody { + HostObjectBody::$CASE(self) } - fn try_extract(obj: &HostObject) -> Option<&Self> { + fn try_extract(obj: &HostObjectBody) -> Option<&Self> { match obj { - HostObject::$CASE(v) => Some(v), + HostObjectBody::$CASE(v) => Some(v), _ => None, } } @@ -184,7 +254,11 @@ impl Host { // charge for the new host object, which is just the amortized cost of a single // `HostObject` allocation metered_clone::charge_heap_alloc::(1, self.as_budget())?; - self.try_borrow_objects_mut()?.push(HOT::inject(hot)); + let tickets = Tickets::from_ticket(self.get_current_ticket()?); + // eprintln!("adding host object with tickets {:?}", tickets); + let body = HOT::inject(hot); + self.try_borrow_objects_mut()? + .push(HostObject { tickets, body }); let handle = prev_len as u32; Ok(HOT::new_from_handle(handle)) } @@ -197,13 +271,54 @@ impl Host { f: F, ) -> Result where - F: FnOnce(Option<&HostObject>) -> Result, + F: FnOnce(Option<&HostObjectBody>) -> Result, { self.charge_budget(ContractCostType::VisitObject, None)?; let r = self.try_borrow_objects()?; let obj: Object = obj.into(); let handle: u32 = obj.get_handle(); - f(r.get(handle as usize)) + let tkt = self.get_current_ticket()?; + match r.get(handle as usize) { + Some(hobj) if hobj.tickets.is_allowed(tkt) => f(Some(&hobj.body)), + // We treat "missing object" and "bad ticket" identically to + // eliminate risk of even leaking existence-of-an-object. + _ => f(None), + } + } + + /// If `val` is an [`Object`] that is readable by `caller_ticket`, adds + /// `callee_ticket` to the object's set of [`Ticket`]s. This should + /// be called any time `val` is passed from a caller contract to a callee. + /// + /// # Errors + /// + /// If `val` is an object that is not readable by `caller_ticket` or is + /// a handle to a nonexistent object, an error is returned. + pub(crate) fn propagate_ticket( + &self, + val: Val, + curr_ticket: Ticket, + new_ticket: Ticket, + ) -> Result<(), HostError> { + //eprintln!("checking if {:?} is object", val); + let Ok(obj) = Object::try_from(val) else { return Ok(()) }; + self.charge_budget(ContractCostType::VisitObject, None)?; + let mut r = self.try_borrow_objects_mut()?; + let handle: u32 = obj.get_handle(); + //eprintln!("propagating tickets {:?} -> {:?} on object {}", curr_ticket, new_ticket, handle); + match r.get_mut(handle as usize) { + Some(hobj) if hobj.tickets.is_allowed(curr_ticket) => { + hobj.tickets.allow_ticket(new_ticket); + // eprintln!("updated ticket on obj {:?} to {:?}", obj, hobj.tickets); + Ok(()) + } + _ => Err(self.err( + xdr::ScErrorType::Object, + xdr::ScErrorCode::MissingValue, + "unknown object reference in propagate_ticket", + &[], + )), + } } pub(crate) fn check_val_integrity(&self, val: Val) -> Result<(), HostError> { @@ -223,20 +338,20 @@ impl Host { &[], )), Some(hobj) => match (hobj, obj.to_val().get_tag()) { - (HostObject::Vec(_), Tag::VecObject) - | (HostObject::Map(_), Tag::MapObject) - | (HostObject::U64(_), Tag::U64Object) - | (HostObject::I64(_), Tag::I64Object) - | (HostObject::TimePoint(_), Tag::TimepointObject) - | (HostObject::Duration(_), Tag::DurationObject) - | (HostObject::U128(_), Tag::U128Object) - | (HostObject::I128(_), Tag::I128Object) - | (HostObject::U256(_), Tag::U256Object) - | (HostObject::I256(_), Tag::I256Object) - | (HostObject::Bytes(_), Tag::BytesObject) - | (HostObject::String(_), Tag::StringObject) - | (HostObject::Symbol(_), Tag::SymbolObject) - | (HostObject::Address(_), Tag::AddressObject) => Ok(()), + (HostObjectBody::Vec(_), Tag::VecObject) + | (HostObjectBody::Map(_), Tag::MapObject) + | (HostObjectBody::U64(_), Tag::U64Object) + | (HostObjectBody::I64(_), Tag::I64Object) + | (HostObjectBody::TimePoint(_), Tag::TimepointObject) + | (HostObjectBody::Duration(_), Tag::DurationObject) + | (HostObjectBody::U128(_), Tag::U128Object) + | (HostObjectBody::I128(_), Tag::I128Object) + | (HostObjectBody::U256(_), Tag::U256Object) + | (HostObjectBody::I256(_), Tag::I256Object) + | (HostObjectBody::Bytes(_), Tag::BytesObject) + | (HostObjectBody::String(_), Tag::StringObject) + | (HostObjectBody::Symbol(_), Tag::SymbolObject) + | (HostObjectBody::Address(_), Tag::AddressObject) => Ok(()), _ => Err(self.err( xdr::ScErrorType::Object, xdr::ScErrorCode::UnexpectedType, @@ -263,14 +378,14 @@ impl Host { None => Err(self.err( xdr::ScErrorType::Object, xdr::ScErrorCode::MissingValue, - "unknown object reference", + "unknown object reference in visit_obj", &[], )), Some(hobj) => match HOT::try_extract(hobj) { None => Err(self.err( xdr::ScErrorType::Object, xdr::ScErrorCode::UnexpectedType, - "object reference type does not match tag", + "object reference type does not match tag in visit_obj", &[], )), Some(hot) => f(hot), @@ -278,4 +393,62 @@ impl Host { }) } } + + /// Similar to [`visit_obj`] but assumes that `f` is a function that + /// extracts a [`Val`] from `outer_obj` and, if the extracted `Val` is an + /// [`Object`], propagates the ticket-set from `outer_obj` to that object. + /// This lazily propagates the reachability relationship from containers to + /// any objects contained within them. It should be used in preference to + /// `visit_obj` for any host function extracting a value from a container. + pub(crate) fn visit_obj_propagating_tickets( + &self, + outer_obj: impl Into, + f: F, + ) -> Result + where + F: FnOnce(&HOT) -> Result, + { + self.charge_budget(ContractCostType::VisitObject, None)?; + let mut r = self.try_borrow_objects_mut()?; + let outer_obj: Object = outer_obj.into(); + let outer_handle: u32 = outer_obj.get_handle(); + let tkt = self.get_current_ticket()?; + let (outer_tickets, val) = match r.get(outer_handle as usize) { + Some(hobj) if hobj.tickets.is_allowed(tkt) => match HOT::try_extract(&hobj.body) { + None => { + return Err(self.err( + xdr::ScErrorType::Object, + xdr::ScErrorCode::UnexpectedType, + "object reference type does not match tag in visit_obj_propagating_tag", + &[], + )) + } + Some(hot) => (hobj.tickets.clone(), f(hot)?), + }, + // We treat "missing object" and "bad ticket" identically to + // eliminate risk of even leaking existence-of-an-object. + _ => { + return Err(self.err( + xdr::ScErrorType::Object, + xdr::ScErrorCode::MissingValue, + "unknown object reference in visit_obj_propagating_tag", + &[], + )) + } + }; + + // If `f` returned an object reference, we assume -- the point of + // calling this function is to imply -- that `f` extracted that + // return-value reference from the outer object reference and so we + // propagate the ticket from the outer to inner. + if let Ok(inner_obj) = Object::try_from(val) { + self.charge_budget(ContractCostType::VisitObject, None)?; + let inner_handle = inner_obj.get_handle(); + match r.get_mut(inner_handle as usize) { + Some(hobj) => hobj.tickets.allow_tickets(&outer_tickets), + None => (), + } + } + Ok(val) + } } diff --git a/soroban-env-host/src/test/token.rs b/soroban-env-host/src/test/token.rs index 1885d3842..c8942cee3 100644 --- a/soroban-env-host/src/test/token.rs +++ b/soroban-env-host/src/test/token.rs @@ -293,7 +293,7 @@ impl TokenTest { { self.host.set_source_account(account_id)?; self.host.with_frame( - Frame::HostFunction(HostFunctionType::InvokeContract), + Frame::InitialInvokeHostFunctionOp(HostFunctionType::InvokeContract, vec![]), || { let res = f(); match res {