Repository: stellar/rs-soroban-env
Pull request #947 (2023-07-13 08:28:56)

Relative objects in wasm
This is a _much_ simpler approach to the "object read-access-control" problem covered in PRs #940 and #930 (and previously #132). The idea here is:

1. We split object references as before in two types differentiated by their low bit, but now they're "absolute" and "relative". Relative references indirect through a context-local table that just holds absolute references.
2. We only _really_ care about this issue when dealing with wasm contracts. Local-testing / native contracts have no isolation / complete system visibility anyways, there's no point really worrying about "who can see what".
3. Therefore the wasm VM argument-marshalling path is a natural point to do the relative-to-absolute translation. We translate relative references to absolute on the way in to the host, and absolute to relative on the way out. Wasm never gets to see any absolute references. If they forge one, we refuse it on the way in.

(There is a slight downside in the current implementation which is that if you return the same absolute reference multiple times we'll create new relative references on each return. If this is annoying we can use a hashtable or something instead of a vector. But the vector has both performance and conceptual simplicity on its side, and in any case the user _shouldn't_ care about specific object references, and I expect most contracts do not run long enough or return the same object references repeatedly enough for this to matter either way)

graydon: @dmkozh I believe this is ready to go now, and is as minimal as I can make it. I added a test and localized the changes to the host (and actually _just_ the VM frame type). At this point more of the diff is just legibility change (I changed the VM frame type from a tuple to a struct) than anything else.

Patch:
diff --git a/soroban-env-host/src/auth.rs b/soroban-env-host/src/auth.rs
index dbf1f96f..26aba98e 100644
--- a/soroban-env-host/src/auth.rs
+++ b/soroban-env-host/src/auth.rs
@@ -943,7 +943,7 @@ impl AuthorizationManager {
     // This should be called for every `Host` `push_frame`.
     pub(crate) fn push_frame(&self, host: &Host, frame: &Frame) -> Result<(), HostError> {
         let (contract_id, function_name) = match frame {
-            Frame::ContractVM(vm, fn_name, ..) => {
+            Frame::ContractVM { vm, fn_name, .. } => {
                 (vm.contract_id.metered_clone(host.budget_ref())?, *fn_name)
             }
             // Skip the top-level host function stack frames as they don't
diff --git a/soroban-env-host/src/events/diagnostic.rs b/soroban-env-host/src/events/diagnostic.rs
index 032a839a..5866d3a7 100644
--- a/soroban-env-host/src/events/diagnostic.rs
+++ b/soroban-env-host/src/events/diagnostic.rs
@@ -64,7 +64,7 @@ impl Host {
     // Will not return error if frame is missing
     pub(crate) fn get_current_contract_id_unmetered(&self) -> Result<Option<Hash>, HostError> {
         self.with_current_frame_opt(|frame| match frame {
-            Some(Frame::ContractVM(vm, ..)) => Ok(Some(vm.contract_id.clone())),
+            Some(Frame::ContractVM { vm, .. }) => Ok(Some(vm.contract_id.clone())),
             Some(Frame::HostFunction(_)) => Ok(None),
             Some(Frame::Token(id, ..)) => Ok(Some(id.clone())),
             #[cfg(any(test, feature = "testutils"))]
diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs
index b3eaf0e4..1d7e5df8 100644
--- a/soroban-env-host/src/host.rs
+++ b/soroban-env-host/src/host.rs
@@ -1140,7 +1140,7 @@ impl VmCallerEnv for Host {
                     &vm,
                     pos,
                     vals.as_mut_slice(),
-                    |buf| Val::from_payload(u64::from_le_bytes(*buf)),
+                    |buf| self.relative_to_absolute(Val::from_payload(u64::from_le_bytes(*buf))),
                 )?;
 
                 self.log_diagnostics(&msg, &vals)
@@ -1679,7 +1679,7 @@ impl VmCallerEnv for Host {
             &vm,
             vals_pos,
             vals.as_mut_slice(),
-            |buf| Val::from_payload(u64::from_le_bytes(*buf)),
+            |buf| self.relative_to_absolute(Val::from_payload(u64::from_le_bytes(*buf))),
         )?;
         for v in vals.iter() {
             self.check_val_integrity(*v)?;
@@ -1740,7 +1740,11 @@ impl VmCallerEnv for Host {
                 &vm,
                 vals_pos.into(),
                 mapobj.map.as_slice(),
-                |pair| u64::to_le_bytes(pair.1.get_payload()),
+                |pair| {
+                    Ok(u64::to_le_bytes(
+                        self.absolute_to_relative(pair.1)?.get_payload(),
+                    ))
+                },
             )?;
             Ok(())
         })?;
@@ -1969,7 +1973,7 @@ impl VmCallerEnv for Host {
             &vm,
             pos,
             vals.as_mut_slice(),
-            |buf| Val::from_payload(u64::from_le_bytes(*buf)),
+            |buf| self.relative_to_absolute(Val::from_payload(u64::from_le_bytes(*buf))),
         )?;
         for v in vals.iter() {
             self.check_val_integrity(*v)?;
@@ -1991,7 +1995,11 @@ impl VmCallerEnv for Host {
                 &vm,
                 vals_pos.into(),
                 vecobj.as_slice(),
-                |x| u64::to_le_bytes(x.get_payload()),
+                |x| {
+                    Ok(u64::to_le_bytes(
+                        self.absolute_to_relative(*x)?.get_payload(),
+                    ))
+                },
             )
         })?;
         Ok(Val::VOID)
@@ -2848,8 +2856,8 @@ impl VmCallerEnv for Host {
         let mut outer = Vec::with_capacity(contexts.len());
         for context in contexts.iter() {
             let vals = match &context.frame {
-                Frame::ContractVM(vm, function, ..) => {
-                    get_host_val_tuple(&vm.contract_id, &function)?
+                Frame::ContractVM { vm, fn_name, .. } => {
+                    get_host_val_tuple(&vm.contract_id, fn_name)?
                 }
                 Frame::HostFunction(_) => continue,
                 Frame::Token(id, function, ..) => get_host_val_tuple(id, function)?,
@@ -2907,7 +2915,7 @@ impl VmCallerEnv for Host {
     ) -> Result<Void, Self::Error> {
         let args = self.with_current_frame(|f| {
             let args = match f {
-                Frame::ContractVM(_, _, args, _) => args,
+                Frame::ContractVM { args, .. } => args,
                 Frame::HostFunction(_) => {
                     return Err(self.err(
                         ScErrorType::Context,
diff --git a/soroban-env-host/src/host/frame.rs b/soroban-env-host/src/host/frame.rs
index 81ad16c8..7a865bd2 100644
--- a/soroban-env-host/src/host/frame.rs
+++ b/soroban-env-host/src/host/frame.rs
@@ -9,7 +9,7 @@ use crate::{
     err,
     storage::{InstanceStorageMap, StorageMap},
     xdr::{ContractCostType, ContractExecutable, Hash, HostFunction, HostFunctionType, ScVal},
-    Error, Host, HostError, Symbol, SymbolStr, TryFromVal, TryIntoVal, Val,
+    Error, Host, HostError, Object, Symbol, SymbolStr, TryFromVal, TryIntoVal, Val,
 };
 
 #[cfg(any(test, feature = "testutils"))]
@@ -104,7 +104,13 @@ pub(crate) struct Context {
 /// commit or roll back that state when it pops the stack.
 #[derive(Clone)]
 pub(crate) enum Frame {
-    ContractVM(Rc<Vm>, Symbol, Vec<Val>, ScContractInstance),
+    ContractVM {
+        vm: Rc<Vm>,
+        fn_name: Symbol,
+        args: Vec<Val>,
+        instance: ScContractInstance,
+        relative_objects: Vec<Object>,
+    },
     HostFunction(HostFunctionType),
     Token(Hash, Symbol, Vec<Val>, ScContractInstance),
     #[cfg(any(test, feature = "testutils"))]
@@ -246,6 +252,30 @@ impl Host {
         }
     }
 
+    pub(crate) fn with_current_frame_relative_object_table<F, U>(
+        &self,
+        f: F,
+    ) -> Result<U, HostError>
+    where
+        F: FnOnce(&mut Vec<Object>) -> Result<U, HostError>,
+    {
+        self.with_current_context_mut(|ctx| {
+            if let Frame::ContractVM {
+                relative_objects, ..
+            } = &mut ctx.frame
+            {
+                f(relative_objects)
+            } else {
+                Err(self.err(
+                    ScErrorType::Context,
+                    ScErrorCode::InternalError,
+                    "accessing relative object table in non-VM frame",
+                    &[],
+                ))
+            }
+        })
+    }
+
     pub(crate) fn with_current_prng<F, U>(&self, f: F) -> Result<U, HostError>
     where
         F: FnOnce(&mut Prng) -> Result<U, HostError>,
@@ -327,7 +357,7 @@ impl Host {
     /// frame at its top.
     pub(crate) fn get_current_contract_id_opt_internal(&self) -> Result<Option<Hash>, HostError> {
         self.with_current_frame(|frame| match frame {
-            Frame::ContractVM(vm, ..) => Ok(Some(vm.contract_id.metered_clone(&self.0.budget)?)),
+            Frame::ContractVM { vm, .. } => Ok(Some(vm.contract_id.metered_clone(&self.0.budget)?)),
             Frame::HostFunction(_) => Ok(None),
             Frame::Token(id, ..) => Ok(Some(id.metered_clone(&self.0.budget)?)),
             #[cfg(any(test, feature = "testutils"))]
@@ -356,7 +386,7 @@ impl Host {
         // the previous frame must exist and must be a contract
         let hash = match frames.as_slice() {
             [.., c2, _] => match &c2.frame {
-                Frame::ContractVM(vm, ..) => Ok(vm.contract_id.metered_clone(&self.0.budget)?),
+                Frame::ContractVM { vm, .. } => Ok(vm.contract_id.metered_clone(&self.0.budget)?),
                 Frame::HostFunction(_) => Err(self.err(
                     ScErrorType::Context,
                     ScErrorCode::UnexpectedType,
@@ -385,7 +415,7 @@ impl Host {
         let st = match frames.as_slice() {
             // There are always two frames when WASM is executed in the VM.
             [.., c2, _] => match &c2.frame {
-                Frame::ContractVM(..) => Ok(InvokerType::Contract),
+                Frame::ContractVM { .. } => Ok(InvokerType::Contract),
                 Frame::HostFunction(_) => Ok(InvokerType::Account),
                 Frame::Token(..) => Ok(InvokerType::Contract),
                 #[cfg(any(test, feature = "testutils"))]
@@ -428,8 +458,8 @@ impl Host {
     fn call_contract_fn(&self, id: &Hash, func: &Symbol, args: &[Val]) -> Result<Val, HostError> {
         // Create key for storage
         let storage_key = self.contract_instance_ledger_key(id)?;
-        let contract_instance = self.retrieve_contract_instance_from_storage(&storage_key)?;
-        match &contract_instance.executable {
+        let instance = self.retrieve_contract_instance_from_storage(&storage_key)?;
+        match &instance.executable {
             ContractExecutable::Wasm(wasm_hash) => {
                 let code_entry = self.retrieve_wasm_from_storage(&wasm_hash)?;
                 let vm = Vm::new(
@@ -437,13 +467,20 @@ impl Host {
                     id.metered_clone(&self.0.budget)?,
                     code_entry.as_slice(),
                 )?;
+                let relative_objects = Vec::new();
                 self.with_frame(
-                    Frame::ContractVM(vm.clone(), *func, args.to_vec(), contract_instance),
+                    Frame::ContractVM {
+                        vm: vm.clone(),
+                        fn_name: *func,
+                        args: args.to_vec(),
+                        instance,
+                        relative_objects,
+                    },
                     || vm.invoke_function_raw(self, func, args),
                 )
             }
             ContractExecutable::Token => self.with_frame(
-                Frame::Token(id.clone(), *func, args.to_vec(), contract_instance),
+                Frame::Token(id.clone(), *func, args.to_vec(), instance),
                 || {
                     use crate::native_contract::{NativeContract, Token};
                     Token.call(func, self, args)
@@ -480,7 +517,7 @@ impl Host {
             let mut is_last_non_host_frame = true;
             for ctx in self.try_borrow_context()?.iter().rev() {
                 let exist_id = match &ctx.frame {
-                    Frame::ContractVM(vm, ..) => &vm.contract_id,
+                    Frame::ContractVM { vm, .. } => &vm.contract_id,
                     Frame::Token(id, ..) => id,
                     #[cfg(any(test, feature = "testutils"))]
                     Frame::TestContract(tc) => &tc.id,
@@ -683,7 +720,7 @@ impl Host {
             return Ok(());
         }
         let storage_map = match &ctx.frame {
-            Frame::ContractVM(_, _, _, instance) => &instance.storage,
+            Frame::ContractVM { instance, .. } => &instance.storage,
             Frame::HostFunction(_) => {
                 return Err(self.err(
                     ScErrorType::Context,
@@ -722,7 +759,7 @@ impl Host {
                     return Ok(None);
                 }
                 let executable = match &ctx.frame {
-                    Frame::ContractVM(_, _, _, instance) => {
+                    Frame::ContractVM { instance, .. } => {
                         instance.executable.metered_clone(self.budget_ref())?
                     }
                     Frame::HostFunction(_) => {
diff --git a/soroban-env-host/src/host/mem_helper.rs b/soroban-env-host/src/host/mem_helper.rs
index 97d19b02..fd7945de 100644
--- a/soroban-env-host/src/host/mem_helper.rs
+++ b/soroban-env-host/src/host/mem_helper.rs
@@ -18,7 +18,7 @@ impl Host {
         let pos: u32 = pos.into();
         let len: u32 = len.into();
         self.with_current_frame(|frame| match frame {
-            Frame::ContractVM(vm, ..) => {
+            Frame::ContractVM { vm, .. } => {
                 let vm = vm.clone();
                 Ok(VmSlice { vm, pos, len })
             }
@@ -67,7 +67,7 @@ impl Host {
         vm: &Rc<Vm>,
         mem_pos: u32,
         buf: &[VAL],
-        to_le_bytes: impl Fn(&VAL) -> [u8; VAL_SZ],
+        to_le_bytes: impl Fn(&VAL) -> Result<[u8; VAL_SZ], HostError>,
     ) -> Result<(), HostError> {
         let val_sz = self.usize_to_u32(VAL_SZ)?;
         let len = self.usize_to_u32(buf.len())?;
@@ -96,7 +96,7 @@ impl Host {
                     &[],
                 ));
             }
-            let tmp: [u8; VAL_SZ] = to_le_bytes(src);
+            let tmp: [u8; VAL_SZ] = to_le_bytes(src)?;
             dst.copy_from_slice(&tmp);
         }
         Ok(())
@@ -108,7 +108,7 @@ impl Host {
         vm: &Rc<Vm>,
         mem_pos: u32,
         buf: &mut [VAL],
-        from_le_bytes: impl Fn(&[u8; VAL_SZ]) -> VAL,
+        from_le_bytes: impl Fn(&[u8; VAL_SZ]) -> Result<VAL, HostError>,
     ) -> Result<(), HostError> {
         let val_sz = self.usize_to_u32(VAL_SZ)?;
         let len = self.usize_to_u32(buf.len())?;
@@ -131,7 +131,7 @@ impl Host {
         for (dst, src) in buf.iter_mut().zip(mem_slice.chunks(VAL_SZ)) {
             if let Ok(src) = TryInto::<&[u8; VAL_SZ]>::try_into(src) {
                 tmp.copy_from_slice(src);
-                *dst = from_le_bytes(&tmp);
+                *dst = from_le_bytes(&tmp)?;
             } else {
                 // This should be impossible unless there's an error above, but just in case.
                 return Err(self.err(
diff --git a/soroban-env-host/src/host_object.rs b/soroban-env-host/src/host_object.rs
index 004a707f..dd3b0c03 100644
--- a/soroban-env-host/src/host_object.rs
+++ b/soroban-env-host/src/host_object.rs
@@ -1,6 +1,9 @@
+#![allow(dead_code)]
+
 use soroban_env_common::{
-    xdr::ContractCostType, Compare, DurationSmall, I128Small, I256Small, I64Small, SymbolSmall,
-    SymbolStr, Tag, TimepointSmall, U128Small, U256Small, U64Small,
+    xdr::{ContractCostType, ScErrorCode, ScErrorType},
+    Compare, DurationSmall, I128Small, I256Small, I64Small, SymbolSmall, SymbolStr, Tag,
+    TimepointSmall, U128Small, U256Small, U64Small,
 };
 
 use crate::{
@@ -169,7 +172,140 @@ declare_mem_host_object_type!(xdr::ScString, StringObject, String);
 declare_mem_host_object_type!(xdr::ScSymbol, SymbolObject, Symbol);
 declare_host_object_type!(xdr::ScAddress, AddressObject, Address);
 
+// Objects come in two flavors: relative and absolute. They are differentiated
+// by the low bit of the object handle: relative objects have 0, absolutes have
+// 1. The remaining bits (left shifted by 1) are the index in a corresponding
+// relative or absolute object table.
+//
+// Relative objects are the ones we pass to and from wasm/VM code, and are
+// looked up in a per-VM-frame "relative objects" indirection table, to find an
+// absolute object. Absolute objects are the underlying context-insensitive
+// handles that point into the host object table (and so absolutes can also be
+// used outside contexts, eg. in fields held in host objects themselves or while
+// setting-up the host). Relative-to-absolute translation is done very close to
+// the VM, when marshalling call args and return values (and host-function calls
+// and returns). Host code should never see relative object handles, and if you
+// ever try to look one up in the host object table, it will fail.
+//
+// The point of relative object handles is to isolate the objects seen by one VM
+// from those seen by any other (and secondarily to avoid "system objects" like
+// those allocated by the auth and event subsystems from perturbing object
+// numbers seen by user code). User code should not perceive any objects other
+// than ones they are specifically passed (or reachable through them). So their
+// view of the world is limited to objects that made it into their relative
+// object table.
+//
+// Also note: the relative/absolute object reference translation is _not_ done
+// when running native contracts, either builtin or in local-testing mode, so
+// you will not get identical object numbers in those cases. Since there is no
+// real isolation between native contracts -- they can even dereference unsafe
+// pointers if they want -- there's no point bothering with the translation (and
+// there's no really obvious place to perform it systematically, like in the
+// wasm marshalling path).
+
+pub fn is_relative_object_handle(handle: u32) -> bool {
+    handle & 1 == 0
+}
+
+pub fn handle_to_index(handle: u32) -> usize {
+    (handle as usize) >> 1
+}
+
+pub fn index_to_handle(host: &Host, index: usize, relative: bool) -> Result<u32, HostError> {
+    if let Ok(smaller) = u32::try_from(index) {
+        if let Some(shifted) = smaller.checked_shl(1) {
+            if relative {
+                return Ok(shifted);
+            } else {
+                return Ok(shifted | 1);
+            }
+        }
+    }
+    Err(host.err_arith_overflow())
+}
+
 impl Host {
+    pub(crate) fn relative_to_absolute(&self, val: Val) -> Result<Val, HostError> {
+        if let Ok(obj) = Object::try_from(val) {
+            let handle = obj.get_handle();
+            return if is_relative_object_handle(handle) {
+                let index = handle_to_index(handle);
+                let abs_opt = self.with_current_frame_relative_object_table(|table| {
+                    Ok(table.get(index).map(|x| *x))
+                })?;
+                match abs_opt {
+                    Some(abs) if abs.to_val().get_tag() == val.get_tag() => Ok(abs.into()),
+                    // User forged a type tag. This is _relatively_ harmless
+                    // since we converted from relative to absolute and
+                    // literally changed object references altogether while
+                    // doing so -- i.e. we now have a correctly-typed absolute
+                    // object reference we _could_ proceed to use as requested
+                    // -- but a user passing an ill-typed relative object
+                    // reference is probably either a bug or part of some
+                    // strange type of attack, and in any case we _would_ signal
+                    // this as an object-integrity type mismatch if we hadn't
+                    // done the translation (eg. in native testing mode), so for
+                    // symmetry sake we will return the same error here.
+                    Some(_) => Err(self.err(
+                        ScErrorType::Object,
+                        ScErrorCode::UnexpectedType,
+                        "relative and absolute object types differ",
+                        &[],
+                    )),
+                    // User is referring to something outside the bounds of
+                    // their relative table, erroneously.
+                    None => Err(self.err(
+                        ScErrorType::Context,
+                        ScErrorCode::InvalidInput,
+                        "unknown relative object reference",
+                        &[Val::from_u32(handle).to_val()],
+                    )),
+                }
+            } else {
+                // This also gets "invalid input" because it came from the user
+                // VM: they tried to forge an absolute.
+                Err(self.err(
+                    ScErrorType::Context,
+                    ScErrorCode::InvalidInput,
+                    "relative_to_absolute given an absolute reference",
+                    &[Val::from_u32(handle).to_val()],
+                ))
+            };
+        }
+        Ok(val)
+    }
+
+    pub(crate) fn absolute_to_relative(&self, val: Val) -> Result<Val, HostError> {
+        if let Ok(obj) = Object::try_from(val) {
+            let handle = obj.get_handle();
+            return if is_relative_object_handle(handle) {
+                // This gets "internal error" because we should never have found
+                // ourselves in posession of a relative reference to return to
+                // the user VM in the first place. Logic bug.
+                Err(self.err(
+                    ScErrorType::Context,
+                    ScErrorCode::InternalError,
+                    "absolute_to_relative given a relative reference",
+                    // NB: we convert to a U32Val here otherwise the _events_ system
+                    // will fault when trying to look up this argument as a relative
+                    // object reference.
+                    &[Val::from_u32(handle).to_val()],
+                ))
+            } else {
+                // Push a new entry into the relative-objects vector.
+                metered_clone::charge_heap_alloc::<Object>(1, self.as_budget())?;
+                let index = self.with_current_frame_relative_object_table(|table| {
+                    let index = table.len();
+                    table.push(obj);
+                    Ok(index)
+                })?;
+                let handle = index_to_handle(self, index, true)?;
+                Ok(Object::from_handle_and_tag(handle, val.get_tag()).into())
+            };
+        }
+        Ok(val)
+    }
+
     /// Moves a value of some type implementing [`HostObjectType`] into the host's
     /// object array, returning a [`HostObj`] containing the new object's array
     /// index, tagged with the [`xdr::ScObjectType`].
@@ -177,15 +313,12 @@ impl Host {
         &self,
         hot: HOT,
     ) -> Result<HOT::Wrapper, HostError> {
-        let prev_len = self.try_borrow_objects()?.len();
-        if prev_len > u32::MAX as usize {
-            return Err(self.err_arith_overflow());
-        }
+        let index = self.try_borrow_objects()?.len();
+        let handle = index_to_handle(self, index, false)?;
         // charge for the new host object, which is just the amortized cost of a single
         // `HostObject` allocation
         metered_clone::charge_heap_alloc::<HostObject>(1, self.as_budget())?;
         self.try_borrow_objects_mut()?.push(HOT::inject(hot));
-        let handle = prev_len as u32;
         Ok(HOT::new_from_handle(handle))
     }
 
@@ -203,7 +336,16 @@ impl Host {
         let r = self.try_borrow_objects()?;
         let obj: Object = obj.into();
         let handle: u32 = obj.get_handle();
-        f(r.get(handle as usize))
+        if is_relative_object_handle(handle) {
+            Err(self.err(
+                ScErrorType::Context,
+                ScErrorCode::InternalError,
+                "looking up relative object",
+                &[Val::from_u32(handle).to_val()],
+            ))
+        } else {
+            f(r.get(handle_to_index(handle)))
+        }
     }
 
     pub(crate) fn check_val_integrity(&self, val: Val) -> Result<(), HostError> {
diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs
index 07d0e283..0915cef8 100644
--- a/soroban-env-host/src/vm.rs
+++ b/soroban-env-host/src/vm.rs
@@ -272,9 +272,8 @@ impl Vm {
                 }
             }
         }
-        Ok(
-            <_ as WasmiMarshal>::try_marshal_from_value(wasm_ret[0].clone())
-                .ok_or(ConversionError)?,
+        host.relative_to_absolute(
+            Val::try_marshal_from_value(wasm_ret[0].clone()).ok_or(ConversionError)?,
         )
     }
 
@@ -287,8 +286,8 @@ impl Vm {
         host.charge_budget(ContractCostType::InvokeVmFunction, None)?;
         let wasm_args: Vec<Value> = args
             .iter()
-            .map(|i| Value::I64(i.get_payload() as i64))
-            .collect();
+            .map(|i| host.absolute_to_relative(*i).map(|v| v.marshal_from_self()))
+            .collect::<Result<Vec<Value>, HostError>>()?;
         let func_ss: SymbolStr = func_sym.try_into_val(host)?;
         let ext = match self
             .instance
diff --git a/soroban-env-host/src/vm/dispatch.rs b/soroban-env-host/src/vm/dispatch.rs
index 505e97d6..4cd959e1 100644
--- a/soroban-env-host/src/vm/dispatch.rs
+++ b/soroban-env-host/src/vm/dispatch.rs
@@ -1,9 +1,9 @@
 use super::FuelRefillable;
 use crate::{xdr::ContractCostType, Host, HostError, VmCaller, VmCallerEnv};
 use crate::{
-    AddressObject, BytesObject, DurationObject, Error, I128Object, I256Object, I256Val, I64Object,
-    MapObject, StorageType, StringObject, Symbol, SymbolObject, TimepointObject, U128Object,
-    U256Object, U256Val, U32Val, U64Object, Val, VecObject,
+    AddressObject, Bool, BytesObject, DurationObject, Error, I128Object, I256Object, I256Val,
+    I32Val, I64Object, MapObject, StorageType, StringObject, Symbol, SymbolObject, TimepointObject,
+    U128Object, U256Object, U256Val, U32Val, U64Object, U64Val, Val, VecObject, Void,
 };
 use soroban_env_common::{call_macro_with_all_host_functions, WasmiMarshal};
 use wasmi::{
@@ -11,6 +11,76 @@ use wasmi::{
     Value,
 };
 
+pub(crate) trait RelativeObjectConversion: WasmiMarshal {
+    fn absolute_to_relative(self, _host: &Host) -> Result<Self, HostError> {
+        Ok(self)
+    }
+    fn relative_to_absolute(self, _host: &Host) -> Result<Self, HostError> {
+        Ok(self)
+    }
+    fn try_marshal_from_relative_value(v: wasmi::Value, host: &Host) -> Result<Self, Trap> {
+        let val = Self::try_marshal_from_value(v).ok_or(BadSignature)?;
+        Ok(val.relative_to_absolute(host)?)
+    }
+    fn marshal_relative_from_self(self, host: &Host) -> Result<wasmi::Value, Trap> {
+        let rel = self.absolute_to_relative(host)?;
+        Ok(Self::marshal_from_self(rel))
+    }
+}
+
+macro_rules! impl_relative_object_conversion {
+    ($T:ty) => {
+        impl RelativeObjectConversion for $T {
+            fn absolute_to_relative(self, host: &Host) -> Result<Self, HostError> {
+                Ok(Self::try_from(host.absolute_to_relative(self.into())?)?)
+            }
+
+            fn relative_to_absolute(self, host: &Host) -> Result<Self, HostError> {
+                Ok(Self::try_from(host.relative_to_absolute(self.into())?)?)
+            }
+        }
+    };
+}
+
+// Define a relative-to-absolute impl for any type that is (a) mentioned
+// in a host function type signature in env and (b) might possibly carry an
+// object reference. If you miss one, this file won't compile, so it's safe.
+impl_relative_object_conversion!(Val);
+impl_relative_object_conversion!(Symbol);
+
+impl_relative_object_conversion!(AddressObject);
+impl_relative_object_conversion!(BytesObject);
+impl_relative_object_conversion!(DurationObject);
+
+impl_relative_object_conversion!(TimepointObject);
+impl_relative_object_conversion!(SymbolObject);
+impl_relative_object_conversion!(StringObject);
+
+impl_relative_object_conversion!(VecObject);
+impl_relative_object_conversion!(MapObject);
+
+impl_relative_object_conversion!(I64Object);
+impl_relative_object_conversion!(I128Object);
+impl_relative_object_conversion!(I256Object);
+
+impl_relative_object_conversion!(U64Object);
+impl_relative_object_conversion!(U128Object);
+impl_relative_object_conversion!(U256Object);
+
+impl_relative_object_conversion!(U64Val);
+impl_relative_object_conversion!(U256Val);
+impl_relative_object_conversion!(I256Val);
+
+// Trivial / non-relativizing impls are ok for types that can't carry objects.
+impl RelativeObjectConversion for i64 {}
+impl RelativeObjectConversion for u64 {}
+impl RelativeObjectConversion for Void {}
+impl RelativeObjectConversion for Bool {}
+impl RelativeObjectConversion for Error {}
+impl RelativeObjectConversion for StorageType {}
+impl RelativeObjectConversion for U32Val {}
+impl RelativeObjectConversion for I32Val {}
+
 ///////////////////////////////////////////////////////////////////////////////
 /// X-macro use: dispatch functions
 ///////////////////////////////////////////////////////////////////////////////
@@ -90,11 +160,11 @@ macro_rules! generate_dispatch_functions {
                     // happens to be a natural switching point for that: we have
                     // conversions to and from both Val and i64 / u64 for
                     // wasmi::Value.
-                    let res: Result<_, HostError> = host.$fn_id(&mut vmcaller, $(<$type>::try_marshal_from_value(Value::I64($arg)).ok_or(BadSignature)?),*);
+                    let res: Result<_, HostError> = host.$fn_id(&mut vmcaller, $(<$type>::try_marshal_from_relative_value(Value::I64($arg), &host)?),*);
 
                     let res = match res {
                         Ok(ok) => {
-                            let val: Value = ok.marshal_from_self();
+                            let val: Value = ok.marshal_relative_from_self(&host)?;
                             if let Value::I64(v) = val {
                                 Ok((v,))
                             } else {


Test patch:
diff --git a/soroban-env-host/src/test/basic.rs b/soroban-env-host/src/test/basic.rs
index 64352090..b3e13dc8 100644
--- a/soroban-env-host/src/test/basic.rs
+++ b/soroban-env-host/src/test/basic.rs
@@ -16,7 +16,7 @@ fn u64_roundtrip() -> Result<(), HostError> {
     let v2: Val = u2.try_into_val(&host)?;
     assert_eq!(v2.get_tag(), Tag::U64Object);
     let obj: Object = v2.try_into()?;
-    assert_eq!(obj.get_handle(), 0);
+    assert_eq!(obj.get_handle(), 1);
     let k = u64::try_from_val(&host, &v2)?;
     assert_eq!(u2, k);
     Ok(())
@@ -35,7 +35,7 @@ fn i64_roundtrip() -> Result<(), HostError> {
     let v2: Val = i2.try_into_val(&host)?;
     assert_eq!(v2.get_tag(), Tag::I64Object);
     let obj: Object = v2.try_into()?;
-    assert_eq!(obj.get_handle(), 0);
+    assert_eq!(obj.get_handle(), 1);
     let k = i64::try_from_val(&host, &v2)?;
     assert_eq!(i2, k);
     Ok(())
diff --git a/soroban-env-host/src/test/hostile.rs b/soroban-env-host/src/test/hostile.rs
index 10b75902..53082be7 100644
--- a/soroban-env-host/src/test/hostile.rs
+++ b/soroban-env-host/src/test/hostile.rs
@@ -1,6 +1,6 @@
 use soroban_env_common::{
     xdr::{ScErrorCode, ScErrorType},
-    Env, Symbol,
+    Env, EnvBase, Symbol, Val, VecObject,
 };
 use soroban_test_wasms::HOSTILE;
 
@@ -116,3 +116,77 @@ fn hostile_objs_traps() -> Result<(), HostError> {
     ));
     Ok(())
 }
+
+#[test]
+fn hostile_forged_objects_trap() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    let contract_id_obj = host.register_test_contract_wasm(HOSTILE);
+
+    host.set_diagnostic_level(crate::DiagnosticLevel::Debug)?;
+    host.with_budget(|b| b.reset_default())?;
+    host.with_budget(|b| b.reset_unlimited_cpu())?;
+
+    fn forged_val_to_forge_call_args(host: &Host, val: Val) -> Result<VecObject, HostError> {
+        let payload = val.get_payload();
+        let lo = payload as u32;
+        let hi = (payload >> 32) as u32;
+        host.vec_new_from_slice(&[lo.into(), hi.into()])
+    }
+
+    // Here we're passing a vector of two numbers that, when reassembled into a
+    // payload and cast to an object, denote an absolute object reference. These
+    // should fail because relative-to-absolute conversion will reject it.
+    let absolute_vec = host.vec_new_from_slice(&[1u32.into(), 2u32.into()])?;
+    let res = host.call(
+        contract_id_obj,
+        Symbol::try_from_small_str("forgeref")?,
+        forged_val_to_forge_call_args(&host, absolute_vec.to_val())?,
+    );
+    assert!(HostError::result_matches_err(
+        res.clone(),
+        (ScErrorType::Context, ScErrorCode::InvalidInput)
+    ));
+
+    // Here we just pick a big handle number -- but with a zero bit set, so it's
+    // a relative handle -- to poke around "random object space" to see if we
+    // can get an object. This will fail because it doesn't denote anything in
+    // the relative object table (it's past the end).
+    let big_vec_ref = unsafe { VecObject::from_handle(0xffff0) };
+    let res = host.call(
+        contract_id_obj,
+        Symbol::try_from_small_str("forgeref")?,
+        forged_val_to_forge_call_args(&host, big_vec_ref.to_val())?,
+    );
+    assert!(HostError::result_matches_err(
+        res.clone(),
+        (ScErrorType::Context, ScErrorCode::InvalidInput)
+    ));
+
+    // Here we call a function that tries to forge the type of an object
+    // reference and call a method on it. This fails in the relative-to-absolute
+    // conversion path, where we check identity of types.
+    let res = host.call(
+        contract_id_obj,
+        Symbol::try_from_small_str("forgety1")?,
+        host.vec_new_from_slice(&[absolute_vec.to_val()])?,
+    );
+    assert!(HostError::result_matches_err(
+        res.clone(),
+        (ScErrorType::Object, ScErrorCode::UnexpectedType)
+    ));
+
+    // Here we call a function that tries to forge the type of an object
+    // reference and just pass it as an _argument_ to another function. This
+    // fails in the same place as the previous test.
+    let res = host.call(
+        contract_id_obj,
+        Symbol::try_from_small_str("forgety2")?,
+        host.vec_new_from_slice(&[absolute_vec.to_val()])?,
+    );
+    assert!(HostError::result_matches_err(
+        res.clone(),
+        (ScErrorType::Object, ScErrorCode::UnexpectedType)
+    ));
+
+    Ok(())
+}
diff --git a/soroban-env-host/src/test/vec.rs b/soroban-env-host/src/test/vec.rs
index 480a858b..b9d87cd2 100644
--- a/soroban-env-host/src/test/vec.rs
+++ b/soroban-env-host/src/test/vec.rs
@@ -16,8 +16,8 @@ fn vec_as_seen_by_host() -> Result<(), HostError> {
     assert_eq!(val1.get_tag(), Tag::VecObject);
     let obj0: Object = val0.try_into()?;
     let obj1: Object = val1.try_into()?;
-    assert_eq!(obj0.get_handle(), 0);
-    assert_eq!(obj1.get_handle(), 1);
+    assert_eq!(obj0.get_handle(), 1);
+    assert_eq!(obj1.get_handle(), 3);
     assert_eq!(obj0.as_val().get_tag(), Tag::VecObject);
     assert_eq!(obj1.as_val().get_tag(), Tag::VecObject);
     // Check that we got 2 distinct Vec objects
diff --git a/soroban-test-wasms/wasm-workspace/Cargo.lock b/soroban-test-wasms/wasm-workspace/Cargo.lock
index e9e081e2..69f928e3 100644
--- a/soroban-test-wasms/wasm-workspace/Cargo.lock
+++ b/soroban-test-wasms/wasm-workspace/Cargo.lock
@@ -447,6 +447,7 @@ dependencies = [
 name = "example_hostile"
 version = "0.0.0"
 dependencies = [
+ "soroban-env-common",
  "soroban-sdk",
 ]
 
@@ -1051,7 +1052,7 @@ dependencies = [
  "soroban-env-macros",
  "soroban-wasmi",
  "static_assertions",
- "stellar-xdr",
+ "stellar-xdr 0.0.17 (git+https://github.com/stellar/rs-stellar-xdr?rev=54c183e91573d8a5882ff1dda21d444ef42cc426)",
 ]
 
 [[package]]
@@ -1096,14 +1097,14 @@ dependencies = [
  "quote",
  "serde",
  "serde_json",
- "stellar-xdr",
+ "stellar-xdr 0.0.17 (git+https://github.com/stellar/rs-stellar-xdr?rev=54c183e91573d8a5882ff1dda21d444ef42cc426)",
  "syn 2.0.18",
  "thiserror",
 ]
 
 [[package]]
 name = "soroban-ledger-snapshot"
-version = "0.9.1"
+version = "0.9.2"
 dependencies = [
  "serde",
  "serde_json",
@@ -1124,7 +1125,7 @@ dependencies = [
 
 [[package]]
 name = "soroban-sdk"
-version = "0.9.1"
+version = "0.9.2"
 dependencies = [
  "arbitrary",
  "bytes-lit",
@@ -1140,7 +1141,7 @@ dependencies = [
 
 [[package]]
 name = "soroban-sdk-macros"
-version = "0.9.1"
+version = "0.9.2"
 dependencies = [
  "crate-git-revision",
  "darling",
@@ -1152,30 +1153,30 @@ dependencies = [
  "soroban-env-common",
  "soroban-spec",
  "soroban-spec-rust",
- "stellar-xdr",
+ "stellar-xdr 0.0.17 (git+https://github.com/stellar/rs-stellar-xdr?rev=0f16673441898162c9996da6117be2280ef8fd84)",
  "syn 2.0.18",
 ]
 
 [[package]]
 name = "soroban-spec"
-version = "0.9.1"
+version = "0.9.2"
 dependencies = [
  "base64 0.13.1",
- "stellar-xdr",
+ "stellar-xdr 0.0.17 (git+https://github.com/stellar/rs-stellar-xdr?rev=0f16673441898162c9996da6117be2280ef8fd84)",
  "thiserror",
  "wasmparser",
 ]
 
 [[package]]
 name = "soroban-spec-rust"
-version = "0.9.1"
+version = "0.9.2"
 dependencies = [
  "prettyplease",
  "proc-macro2",
  "quote",
  "sha2 0.9.9",
  "soroban-spec",
- "stellar-xdr",
+ "stellar-xdr 0.0.17 (git+https://github.com/stellar/rs-stellar-xdr?rev=0f16673441898162c9996da6117be2280ef8fd84)",
  "syn 2.0.18",
  "thiserror",
 ]
@@ -1228,6 +1229,17 @@ dependencies = [
 name = "stellar-xdr"
 version = "0.0.17"
 source = "git+https://github.com/stellar/rs-stellar-xdr?rev=0f16673441898162c9996da6117be2280ef8fd84#0f16673441898162c9996da6117be2280ef8fd84"
+dependencies = [
+ "crate-git-revision",
+ "hex",
+ "serde",
+ "serde_with",
+]
+
+[[package]]
+name = "stellar-xdr"
+version = "0.0.17"
+source = "git+https://github.com/stellar/rs-stellar-xdr?rev=54c183e91573d8a5882ff1dda21d444ef42cc426#54c183e91573d8a5882ff1dda21d444ef42cc426"
 dependencies = [
  "arbitrary",
  "base64 0.13.1",
diff --git a/soroban-test-wasms/wasm-workspace/Cargo.toml b/soroban-test-wasms/wasm-workspace/Cargo.toml
index be58d2b5..a43e70f5 100644
--- a/soroban-test-wasms/wasm-workspace/Cargo.toml
+++ b/soroban-test-wasms/wasm-workspace/Cargo.toml
@@ -43,6 +43,13 @@ panic = "abort"
 codegen-units = 1
 lto = true
 
+[workspace.dependencies.soroban-sdk]
+version = "0.9.2"
+git = "https://github.com/stellar/rs-soroban-sdk"
+
+[workspace.dependencies.soroban-env-common]
+version = "0.0.17"
+git = "https://github.com/stellar/rs-soroban-env"
 
 # Always build from the local instance of env as we need to rebuild test WASMs
 # only due to the env changes.
@@ -51,10 +58,6 @@ soroban-env-common = { path = "../../soroban-env-common" }
 soroban-env-guest = { path = "../../soroban-env-guest" }
 soroban-env-host = { path = "../../soroban-env-host" }
 
-[workspace.dependencies.soroban-sdk]
-version = "0.9.1"
-git = "https://github.com/stellar/rs-soroban-sdk"
-
 # Always build using the local SDK. Usually the env change is accompanied with
 # the corresponding SDK change.
 [patch."https://github.com/stellar/rs-soroban-sdk"]
diff --git a/soroban-test-wasms/wasm-workspace/hostile/Cargo.toml b/soroban-test-wasms/wasm-workspace/hostile/Cargo.toml
index 4ce36a01..412f25a3 100644
--- a/soroban-test-wasms/wasm-workspace/hostile/Cargo.toml
+++ b/soroban-test-wasms/wasm-workspace/hostile/Cargo.toml
@@ -12,4 +12,5 @@ crate-type = ["cdylib", "rlib"]
 doctest = false
 
 [dependencies]
-soroban-sdk = { workspace = true }
\ No newline at end of file
+soroban-sdk = { workspace = true }
+soroban-env-common = { workspace = true }
\ No newline at end of file
diff --git a/soroban-test-wasms/wasm-workspace/hostile/src/lib.rs b/soroban-test-wasms/wasm-workspace/hostile/src/lib.rs
index 53eafb86..703fe8ec 100644
--- a/soroban-test-wasms/wasm-workspace/hostile/src/lib.rs
+++ b/soroban-test-wasms/wasm-workspace/hostile/src/lib.rs
@@ -1,5 +1,6 @@
 #![no_std]
-use soroban_sdk::{contract, contractimpl, Bytes, Env};
+use soroban_env_common::BytesObject;
+use soroban_sdk::{contract, contractimpl, Bytes, Env, FromVal, Val, Vec};
 
 #[contract]
 pub struct Contract;
@@ -57,4 +58,29 @@ impl Contract {
             Bytes::from_slice(&env, &local_buf);
         }
     }
+
+    pub fn forgeref(env: Env, lo: u32, hi: u32) -> u32 {
+        let payload: u64 = lo as u64 | ((hi as u64) << 32);
+        let v: Vec<u32> = Vec::from_val(&env, &Val::from_payload(payload));
+        v.get(0).unwrap()
+    }
+
+    // Forge a type and call a method on it.
+    pub fn forgety1(env: Env, v: Vec<u32>) -> u32 {
+        let b = Bytes::from_val(
+            &env,
+            &unsafe { BytesObject::from_handle(v.to_object().get_handle()) }.to_val(),
+        );
+        b.get(0).unwrap() as u32
+    }
+
+    // Forge a type and pass it as an argument.
+    #[allow(unused_mut)]
+    pub fn forgety2(env: Env, mut v: Vec<Bytes>) {
+        let b = Bytes::from_val(
+            &env,
+            &unsafe { BytesObject::from_handle(v.to_object().get_handle()) }.to_val(),
+        );
+        v.push_back(b)
+    }
 }