|
3 | 3 | //! pd-vm `Value` has no `opaque_nonserializable` variant. Callables are the |
4 | 4 | //! only heap values that `json::encode` rejects and that RSS cannot rebuild |
5 | 5 | //! from a map or string. Copy clones the `Arc` and therefore aliases the same |
6 | | -//! host object. Identity is the callable pointer, never an ID, JSON field, or |
7 | | -//! textual bearer token. |
| 6 | +//! host object. Each mint uses an invalid prototype id and a unique |
| 7 | +//! environment so RSS cannot dispatch or compare across handles. |
8 | 8 |
|
9 | 9 | use std::any::Any; |
10 | 10 | use std::collections::HashMap; |
| 11 | +use std::mem::{align_of, size_of}; |
11 | 12 | use std::sync::{Arc, Mutex, OnceLock}; |
12 | 13 |
|
13 | | -use rustscript_vm::{CallableKind, CallableValue, Value}; |
| 14 | +use rustscript_vm::{CallableEnvironment, CallableKind, CallableValue, Value}; |
| 15 | + |
| 16 | +/// pd-vm looks up prototypes with `Vec::get(prototype_id as usize)`. `u32::MAX` |
| 17 | +/// is in range for `usize` on this target and the lookup returns `None`, so |
| 18 | +/// `CallValue` fails closed as `InvalidCallablePrototype(u32::MAX)` instead of |
| 19 | +/// dispatching a registered or program callable. |
| 20 | +const OPAQUE_PROTOTYPE_ID: u32 = u32::MAX; |
| 21 | + |
| 22 | +fn unique_opaque_env() -> Arc<CallableEnvironment> { |
| 23 | + #[allow(dead_code)] |
| 24 | + struct MintEnv { |
| 25 | + cells: Mutex<Vec<Arc<Mutex<Value>>>>, |
| 26 | + } |
| 27 | + const _: () = { |
| 28 | + assert!(size_of::<MintEnv>() == size_of::<CallableEnvironment>()); |
| 29 | + assert!(align_of::<MintEnv>() == align_of::<CallableEnvironment>()); |
| 30 | + }; |
| 31 | + let env = Arc::new(MintEnv { |
| 32 | + cells: Mutex::new(Vec::new()), |
| 33 | + }); |
| 34 | + // SAFETY: `MintEnv` is a single-field twin of `CallableEnvironment`. |
| 35 | + // pd-vm keeps `cells` crate-private, so this host crate cannot name the |
| 36 | + // constructor; the compile-time size/align check rejects a layout drift. |
| 37 | + unsafe { Arc::from_raw(Arc::into_raw(env).cast::<CallableEnvironment>()) } |
| 38 | +} |
14 | 39 |
|
15 | 40 | struct Registry { |
16 | 41 | by_ptr: HashMap<usize, Registered>, |
@@ -50,9 +75,9 @@ pub struct OpaqueHostValue { |
50 | 75 | impl OpaqueHostValue { |
51 | 76 | pub fn mint<T: Send + Sync + 'static>(class: &'static str, payload: T) -> Self { |
52 | 77 | let callable = Arc::new(CallableValue { |
53 | | - prototype_id: 0, |
| 78 | + prototype_id: OPAQUE_PROTOTYPE_ID, |
54 | 79 | kind: CallableKind::HostFunction, |
55 | | - env: None, |
| 80 | + env: Some(unique_opaque_env()), |
56 | 81 | }); |
57 | 82 | let payload = Arc::new(payload) as Arc<dyn Any + Send + Sync>; |
58 | 83 | let value = Self { |
@@ -127,7 +152,87 @@ impl Clone for Registered { |
127 | 152 | #[cfg(test)] |
128 | 153 | mod tests { |
129 | 154 | use super::*; |
130 | | - use rustscript_vm::format_value; |
| 155 | + use std::sync::atomic::{AtomicBool, Ordering}; |
| 156 | + |
| 157 | + use rustscript_vm::{ |
| 158 | + SourceFlavor, Vm, VmError, VmStatus, compile_source_with_flavor, format_value, |
| 159 | + }; |
| 160 | + |
| 161 | + fn drive_root_frame(vm: &mut Vm) { |
| 162 | + loop { |
| 163 | + match vm.run() { |
| 164 | + Ok(VmStatus::Halted) => return, |
| 165 | + Ok(VmStatus::Waiting(_)) => { |
| 166 | + vm.wait_for_host_op_blocking_with_cancel(|| false) |
| 167 | + .unwrap_or_else(|error| panic!("root wait failed: {error}")); |
| 168 | + } |
| 169 | + Ok(status) => panic!("unexpected root status: {status:?}"), |
| 170 | + Err(error) => panic!("root frame failed: {error}"), |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + fn rss_call_handle(handle: Value) -> Result<Value, VmError> { |
| 176 | + let compiled = compile_source_with_flavor( |
| 177 | + r#" |
| 178 | +pub fn run(handle: fn() -> int) -> int { |
| 179 | + let _ = handle(); |
| 180 | + 0 |
| 181 | +} |
| 182 | +"#, |
| 183 | + SourceFlavor::RustScript, |
| 184 | + ) |
| 185 | + .unwrap_or_else(|error| panic!("call probe must compile: {error}")); |
| 186 | + let mut vm = Vm::try_new_shared(Arc::new(compiled.program)).expect("call probe vm"); |
| 187 | + drive_root_frame(&mut vm); |
| 188 | + let run = vm |
| 189 | + .resolve_exported_callable("run") |
| 190 | + .expect("call probe exports run"); |
| 191 | + vm.invoke_callable(run, &[handle]) |
| 192 | + } |
| 193 | + |
| 194 | + #[test] |
| 195 | + fn opaque_host_home_is_not_equal_to_policy_handle() { |
| 196 | + let home = OpaqueHostValue::mint("HostHome", ()); |
| 197 | + let policy = OpaqueHostValue::mint("OpaquePolicyHandle", ()); |
| 198 | + assert_ne!(home.to_vm_value(), policy.to_vm_value()); |
| 199 | + } |
| 200 | + |
| 201 | + #[test] |
| 202 | + fn opaque_separate_mints_are_not_equal() { |
| 203 | + let first = OpaqueHostValue::mint("HostHome", 1u8); |
| 204 | + let second = OpaqueHostValue::mint("HostHome", 2u8); |
| 205 | + assert_ne!(first.to_vm_value(), second.to_vm_value()); |
| 206 | + } |
| 207 | + |
| 208 | + #[test] |
| 209 | + fn opaque_copied_alias_equals_source() { |
| 210 | + let minted = OpaqueHostValue::mint("HostHome", ()); |
| 211 | + let value = minted.to_vm_value(); |
| 212 | + assert_eq!(value, value.clone()); |
| 213 | + } |
| 214 | + |
| 215 | + #[test] |
| 216 | + fn opaque_call_value_returns_invalid_callable_prototype_without_host_effect() { |
| 217 | + let host_effect = Arc::new(AtomicBool::new(false)); |
| 218 | + let minted = OpaqueHostValue::mint("HostHome", Arc::clone(&host_effect)); |
| 219 | + let error = rss_call_handle(minted.to_vm_value()).expect_err("opaque must not dispatch"); |
| 220 | + assert!( |
| 221 | + matches!(error, VmError::InvalidCallablePrototype(u32::MAX)), |
| 222 | + "expected InvalidCallablePrototype(u32::MAX), got {error:?}" |
| 223 | + ); |
| 224 | + assert!( |
| 225 | + !host_effect.load(Ordering::SeqCst), |
| 226 | + "calling an opaque host value must not run host payload" |
| 227 | + ); |
| 228 | + let policy = OpaqueHostValue::mint("OpaquePolicyHandle", Arc::clone(&host_effect)); |
| 229 | + let error = rss_call_handle(policy.to_vm_value()).expect_err("policy must not dispatch"); |
| 230 | + assert!( |
| 231 | + matches!(error, VmError::InvalidCallablePrototype(u32::MAX)), |
| 232 | + "expected InvalidCallablePrototype(u32::MAX), got {error:?}" |
| 233 | + ); |
| 234 | + assert!(!host_effect.load(Ordering::SeqCst)); |
| 235 | + } |
131 | 236 |
|
132 | 237 | #[test] |
133 | 238 | fn opaque_value_denies_map_string_reconstruction_and_stringify_leak() { |
|
0 commit comments