Skip to content

Commit d38bea3

Browse files
committed
fix(config): isolate opaque host identities
Mint each opaque host callable with a unique environment and an invalid prototype id so RSS cannot compare or CallValue across handles.
1 parent 213c192 commit d38bea3

1 file changed

Lines changed: 111 additions & 6 deletions

File tree

src/host_opaque.rs

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,39 @@
33
//! pd-vm `Value` has no `opaque_nonserializable` variant. Callables are the
44
//! only heap values that `json::encode` rejects and that RSS cannot rebuild
55
//! 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.
88
99
use std::any::Any;
1010
use std::collections::HashMap;
11+
use std::mem::{align_of, size_of};
1112
use std::sync::{Arc, Mutex, OnceLock};
1213

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+
}
1439

1540
struct Registry {
1641
by_ptr: HashMap<usize, Registered>,
@@ -50,9 +75,9 @@ pub struct OpaqueHostValue {
5075
impl OpaqueHostValue {
5176
pub fn mint<T: Send + Sync + 'static>(class: &'static str, payload: T) -> Self {
5277
let callable = Arc::new(CallableValue {
53-
prototype_id: 0,
78+
prototype_id: OPAQUE_PROTOTYPE_ID,
5479
kind: CallableKind::HostFunction,
55-
env: None,
80+
env: Some(unique_opaque_env()),
5681
});
5782
let payload = Arc::new(payload) as Arc<dyn Any + Send + Sync>;
5883
let value = Self {
@@ -127,7 +152,87 @@ impl Clone for Registered {
127152
#[cfg(test)]
128153
mod tests {
129154
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+
}
131236

132237
#[test]
133238
fn opaque_value_denies_map_string_reconstruction_and_stringify_leak() {

0 commit comments

Comments
 (0)