use relon_eval_api::{CapabilityBit, CapabilityGate, RelonFunction, RuntimeError};
use relon_parser::TokenRange;
use std::sync::Arc;
use crate::state::HostFnRegistry;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SandboxConfig {
pub bounds_check: bool,
pub deadline_check: bool,
pub capability_check: bool,
pub div_check: bool,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
bounds_check: true,
deadline_check: true,
capability_check: true,
div_check: true,
}
}
}
impl SandboxConfig {
pub fn unchecked() -> Self {
Self {
bounds_check: false,
deadline_check: false,
capability_check: false,
div_check: false,
}
}
}
#[repr(u64)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SandboxTrapKind {
DivisionByZero = 1,
BoundsViolation = 2,
CapabilityDenied = 3,
ResourceExhausted = 4,
HostFnMissing = 5,
NumericOverflow = 6,
HostFnError = 7,
}
impl SandboxTrapKind {
pub fn from_code(code: u64) -> SandboxTrapKind {
match code {
1 => SandboxTrapKind::DivisionByZero,
2 => SandboxTrapKind::BoundsViolation,
3 => SandboxTrapKind::CapabilityDenied,
4 => SandboxTrapKind::ResourceExhausted,
5 => SandboxTrapKind::HostFnMissing,
6 => SandboxTrapKind::NumericOverflow,
_ => SandboxTrapKind::HostFnError,
}
}
pub fn to_runtime_error(self, range: TokenRange) -> RuntimeError {
match self {
SandboxTrapKind::DivisionByZero => RuntimeError::DivisionByZero(range),
SandboxTrapKind::BoundsViolation => RuntimeError::IndexOutOfBounds { range },
SandboxTrapKind::CapabilityDenied => RuntimeError::CapabilityDenied {
cap_bit: None,
reason: "llvm-native: host-fn call denied by capability gate".to_string(),
range,
},
SandboxTrapKind::ResourceExhausted => {
RuntimeError::StepLimitExceeded { limit: None, range }
}
SandboxTrapKind::NumericOverflow => RuntimeError::NumericOverflow(range),
SandboxTrapKind::HostFnMissing | SandboxTrapKind::HostFnError => {
RuntimeError::Unsupported {
reason: "llvm-native: native-fn dispatch failed (host fn missing / errored / \
returned a non-scalar value)"
.to_string(),
}
}
}
}
}
pub const MAX_CAP_BIT: u32 = 64;
#[derive(Default, Clone)]
pub struct CapabilityVtable {
caps_mask: i64,
host_fns: HostFnRegistry,
}
impl std::fmt::Debug for CapabilityVtable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CapabilityVtable")
.field("caps_mask", &format_args!("{:#018b}", self.caps_mask))
.field("host_fn_count", &self.host_fns.len())
.finish()
}
}
impl CapabilityVtable {
pub fn with_capacity(_n: usize) -> Self {
Self {
caps_mask: 0,
host_fns: HostFnRegistry::new(),
}
}
pub fn grant(&mut self, cap_bit: u32) {
if cap_bit < MAX_CAP_BIT {
self.caps_mask |= 1i64 << cap_bit;
}
}
pub fn register_via_gate<G: CapabilityGate>(
&mut self,
gate: &G,
cap_bit: CapabilityBit,
) -> bool {
match gate.check(cap_bit) {
Ok(()) => {
self.grant(cap_bit.bit_index());
true
}
Err(_) => false,
}
}
pub fn is_granted(&self, cap_bit: u32) -> bool {
cap_bit < MAX_CAP_BIT && (self.caps_mask & (1i64 << cap_bit)) != 0
}
pub fn caps_mask(&self) -> i64 {
self.caps_mask
}
pub fn register_host_fn(&mut self, import_idx: u32, func: Arc<dyn RelonFunction>) {
self.host_fns.register(import_idx, func);
}
pub fn resolve_host_fn(&self, import_idx: u32) -> Option<&Arc<dyn RelonFunction>> {
self.host_fns.resolve(import_idx)
}
pub fn host_fns(&self) -> &HostFnRegistry {
&self.host_fns
}
pub fn host_fn_count(&self) -> usize {
self.host_fns.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use relon_eval_api::{Capabilities, NativeArgs, Value};
#[test]
fn config_default_enables_all_guards() {
let cfg = SandboxConfig::default();
assert!(cfg.bounds_check);
assert!(cfg.deadline_check);
assert!(cfg.capability_check);
assert!(cfg.div_check);
}
#[test]
fn config_unchecked_disables_all_guards() {
let cfg = SandboxConfig::unchecked();
assert!(!cfg.bounds_check);
assert!(!cfg.deadline_check);
assert!(!cfg.capability_check);
assert!(!cfg.div_check);
}
#[test]
fn trap_kind_round_trips_through_u64_code() {
for kind in [
SandboxTrapKind::DivisionByZero,
SandboxTrapKind::BoundsViolation,
SandboxTrapKind::CapabilityDenied,
SandboxTrapKind::ResourceExhausted,
SandboxTrapKind::HostFnMissing,
SandboxTrapKind::NumericOverflow,
SandboxTrapKind::HostFnError,
] {
let code = kind as u64;
assert_eq!(SandboxTrapKind::from_code(code), kind);
}
assert_eq!(SandboxTrapKind::from_code(0), SandboxTrapKind::HostFnError);
assert_eq!(SandboxTrapKind::from_code(99), SandboxTrapKind::HostFnError);
}
#[test]
fn trap_kind_numbering_mirrors_cranelift_and_native_trap() {
assert_eq!(SandboxTrapKind::CapabilityDenied as u64, 3);
assert_eq!(SandboxTrapKind::HostFnMissing as u64, 5);
assert_eq!(SandboxTrapKind::HostFnError as u64, 7);
assert_eq!(
SandboxTrapKind::CapabilityDenied as u64,
crate::state::NativeTrap::CapabilityDenied as u64
);
assert_eq!(
SandboxTrapKind::HostFnMissing as u64,
crate::state::NativeTrap::HostFnMissing as u64
);
assert_eq!(
SandboxTrapKind::HostFnError as u64,
crate::state::NativeTrap::HostFnError as u64
);
}
#[test]
fn trap_kind_maps_to_runtime_error_variant() {
let range = TokenRange::default();
assert!(matches!(
SandboxTrapKind::DivisionByZero.to_runtime_error(range),
RuntimeError::DivisionByZero(_)
));
assert!(matches!(
SandboxTrapKind::BoundsViolation.to_runtime_error(range),
RuntimeError::IndexOutOfBounds { .. }
));
assert!(matches!(
SandboxTrapKind::CapabilityDenied.to_runtime_error(range),
RuntimeError::CapabilityDenied { .. }
));
assert!(matches!(
SandboxTrapKind::ResourceExhausted.to_runtime_error(range),
RuntimeError::StepLimitExceeded { .. }
));
assert!(matches!(
SandboxTrapKind::NumericOverflow.to_runtime_error(range),
RuntimeError::NumericOverflow(_)
));
assert!(matches!(
SandboxTrapKind::HostFnMissing.to_runtime_error(range),
RuntimeError::Unsupported { .. }
));
}
#[test]
fn grant_and_is_granted_round_trip() {
let mut vt = CapabilityVtable::with_capacity(64);
assert!(!vt.is_granted(2));
vt.grant(2);
assert!(vt.is_granted(2));
assert!(!vt.is_granted(3));
assert_eq!(vt.caps_mask(), 1i64 << 2);
}
#[test]
fn grant_ignores_out_of_range_bits() {
let mut vt = CapabilityVtable::with_capacity(64);
vt.grant(64);
vt.grant(200);
assert_eq!(vt.caps_mask(), 0);
assert!(!vt.is_granted(64));
}
#[test]
fn register_via_gate_denies_when_capability_not_granted() {
let caps = Capabilities::default();
let mut vt = CapabilityVtable::with_capacity(64);
let populated = vt.register_via_gate(&caps, CapabilityBit::ReadsFs);
assert!(!populated, "denied gate must leave the mask bit clear");
assert!(!vt.is_granted(CapabilityBit::ReadsFs.bit_index()));
assert_eq!(vt.caps_mask(), 0);
}
#[test]
fn register_via_gate_populates_when_capability_granted() {
let caps = Capabilities::all_granted();
let mut vt = CapabilityVtable::with_capacity(64);
let populated = vt.register_via_gate(&caps, CapabilityBit::Network);
assert!(populated, "granted gate must set the mask bit");
assert!(vt.is_granted(CapabilityBit::Network.bit_index()));
assert_eq!(vt.caps_mask(), 1i64 << CapabilityBit::Network.bit_index());
}
struct AddOne;
impl RelonFunction for AddOne {
fn call(&self, args: NativeArgs, _r: TokenRange) -> Result<Value, RuntimeError> {
match args.positional.first() {
Some(Value::Int(x)) => Ok(Value::Int(x + 1)),
_ => Err(RuntimeError::Unsupported {
reason: "AddOne expects Int".into(),
}),
}
}
}
#[test]
fn host_fn_registry_round_trip() {
let mut vt = CapabilityVtable::with_capacity(64);
assert!(vt.resolve_host_fn(0).is_none());
vt.register_host_fn(0, Arc::new(AddOne));
assert_eq!(vt.host_fn_count(), 1);
let f = vt.resolve_host_fn(0).expect("registered");
let r = f
.call(
NativeArgs::from_positional(vec![Value::Int(41)], {
use relon_eval_api::NativeFnCaps;
struct NoCb;
impl NativeFnCaps for NoCb {
fn call_relon(
&self,
_f: &Value,
_a: Vec<Value>,
_r: TokenRange,
) -> Result<Value, RuntimeError> {
Err(RuntimeError::Unsupported {
reason: "no cb".into(),
})
}
}
Arc::new(NoCb)
}),
TokenRange::default(),
)
.expect("dispatch");
assert_eq!(r, Value::Int(42));
}
}