use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use super::guard::WasmGuard;
use crate::abi::{GuardRequest, GuardVerdict, WasmGuardAbi};
use crate::error::WasmGuardError;
use crate::host::{create_shared_engine, register_host_functions, WasmHostState};
use crate::metrics::{
tenant_id_label, GuardPoolMetrics, GuardPoolMetricsSnapshot, MAX_GUARD_METRIC_CARDINALITY,
};
use sha2::Digest;
use wasmtime::{Engine, InstancePre, Linker, Memory, Module, Store};
use crate::host::MAX_MEMORY_BYTES;
pub const DEFAULT_MAX_MODULE_SIZE: usize = 10 * 1024 * 1024;
const DEFAULT_TENANT_WARM_INSTANCE_CAPACITY: usize = 4;
const DEFAULT_TENANT_WARM_RING_LIMIT: usize = MAX_GUARD_METRIC_CARDINALITY;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WasmFormat {
CoreModule,
Component,
}
pub fn detect_wasm_format(bytes: &[u8]) -> Result<WasmFormat, WasmGuardError> {
if wasmparser::Parser::is_component(bytes) {
Ok(WasmFormat::Component)
} else if wasmparser::Parser::is_core_wasm(bytes) {
Ok(WasmFormat::CoreModule)
} else {
Err(WasmGuardError::UnrecognizedFormat)
}
}
pub fn create_backend(
engine: Arc<Engine>,
wasm_bytes: &[u8],
fuel_limit: u64,
config: HashMap<String, String>,
) -> Result<Box<dyn crate::abi::WasmGuardAbi>, WasmGuardError> {
let format = detect_wasm_format(wasm_bytes)?;
match format {
WasmFormat::CoreModule => {
let mut backend = WasmtimeBackend::with_engine_and_config(engine, config);
backend.load_module(wasm_bytes, fuel_limit)?;
Ok(Box::new(backend))
}
WasmFormat::Component => {
let mut backend =
crate::component::ComponentBackend::with_engine_and_config(engine, config);
backend.load_module(wasm_bytes, fuel_limit)?;
Ok(Box::new(backend))
}
}
}
pub fn load_signed_guard(
engine: Arc<Engine>,
wasm_path: &str,
fuel_limit: u64,
manifest: &crate::manifest::GuardManifest,
) -> Result<Box<dyn crate::abi::WasmGuardAbi>, WasmGuardError> {
let wasm_bytes = std::fs::read(wasm_path).map_err(|e| WasmGuardError::ModuleLoad {
path: wasm_path.to_string(),
reason: e.to_string(),
})?;
let verify_span =
crate::observability::guard_verify_span(crate::observability::VERIFY_MODE_ED25519, None);
let verification = (|| {
let _verify_guard = verify_span.enter();
crate::manifest::verify_wit_world(manifest.wit_world.as_deref())?;
crate::manifest::verify_guard_signature(wasm_path, &wasm_bytes, manifest)?;
crate::manifest::verify_wasm_hash(&wasm_bytes, &manifest.wasm_sha256)
})();
match verification {
Ok(()) => {
verify_span.record("result", crate::observability::VERIFY_RESULT_OK);
}
Err(err) => {
verify_span.record("result", crate::observability::VERIFY_RESULT_FAIL);
return Err(err);
}
}
create_backend(engine, &wasm_bytes, fuel_limit, manifest.config.clone())
}
#[derive(Clone)]
struct CachedInstancePre {
module_hash: String,
instance_pre: InstancePre<WasmHostState>,
}
struct WarmInstancePre {
module_hash: String,
instance_pre: InstancePre<WasmHostState>,
}
struct TenantWarmRing {
capacity: usize,
entries: VecDeque<WarmInstancePre>,
}
impl TenantWarmRing {
fn new(capacity: usize) -> Self {
Self {
capacity,
entries: VecDeque::with_capacity(capacity),
}
}
fn checkout(&mut self, module_hash: &str) -> (Option<InstancePre<WasmHostState>>, usize) {
let mut evicted = 0;
while let Some(entry) = self.entries.pop_front() {
if entry.module_hash == module_hash {
return (Some(entry.instance_pre), evicted);
}
evicted += 1;
}
(None, evicted)
}
fn push(&mut self, entry: WarmInstancePre) -> bool {
if self.capacity == 0 {
return true;
}
let evicted = if self.entries.len() >= self.capacity {
self.entries.pop_front();
true
} else {
false
};
self.entries.push_back(entry);
evicted
}
fn len(&self) -> usize {
self.entries.len()
}
}
struct InstancePrePool {
cache: Option<CachedInstancePre>,
tenant_rings: HashMap<String, TenantWarmRing>,
tenant_lru: VecDeque<String>,
tenant_capacity: usize,
tenant_limit: usize,
metrics: GuardPoolMetrics,
}
impl InstancePrePool {
fn new(tenant_capacity: usize) -> Self {
Self::with_limits(tenant_capacity, DEFAULT_TENANT_WARM_RING_LIMIT)
}
fn with_limits(tenant_capacity: usize, tenant_limit: usize) -> Self {
Self {
cache: None,
tenant_rings: HashMap::new(),
tenant_lru: VecDeque::new(),
tenant_capacity,
tenant_limit,
metrics: GuardPoolMetrics::new(),
}
}
fn checkout(
&mut self,
tenant_id: &str,
module_hash: &str,
) -> Result<InstancePre<WasmHostState>, WasmGuardError> {
let tenant_id = tenant_id_label(tenant_id);
self.metrics.record_checkout(&tenant_id);
if let Some(ring) = self.tenant_rings.get_mut(&tenant_id) {
let (instance_pre, evicted) = ring.checkout(module_hash);
let warm_size = ring.len();
self.record_evicts(&tenant_id, evicted);
self.metrics.set_warm_size(&tenant_id, warm_size);
self.touch_tenant(&tenant_id);
if let Some(instance_pre) = instance_pre {
return Ok(instance_pre);
}
}
match self.cache.as_ref() {
Some(cache) if cache.module_hash == module_hash => Ok(cache.instance_pre.clone()),
_ => Err(WasmGuardError::BackendUnavailable),
}
}
fn install(&mut self, module_hash: String, instance_pre: InstancePre<WasmHostState>) {
self.cache = Some(CachedInstancePre {
module_hash,
instance_pre,
});
self.tenant_rings.clear();
self.tenant_lru.clear();
self.metrics.reset_warm_sizes();
}
fn return_instance_pre(
&mut self,
tenant_id: &str,
module_hash: &str,
instance_pre: InstancePre<WasmHostState>,
) {
let tenant_id = tenant_id_label(tenant_id);
if self.tenant_capacity == 0 {
self.metrics.record_evict(&tenant_id);
self.metrics.set_warm_size(&tenant_id, 0);
return;
}
if !self.ensure_tenant_ring(&tenant_id) {
self.metrics.record_evict(&tenant_id);
self.metrics.set_warm_size(&tenant_id, 0);
return;
}
let evicted = match self.tenant_rings.get_mut(&tenant_id) {
Some(ring) => ring.push(WarmInstancePre {
module_hash: module_hash.to_string(),
instance_pre,
}),
None => {
self.metrics.record_evict(&tenant_id);
self.metrics.set_warm_size(&tenant_id, 0);
return;
}
};
let warm_size = match self.tenant_rings.get(&tenant_id) {
Some(ring) => ring.len(),
None => 0,
};
if evicted {
self.metrics.record_evict(&tenant_id);
}
self.metrics.set_warm_size(&tenant_id, warm_size);
self.touch_tenant(&tenant_id);
}
fn ensure_tenant_ring(&mut self, tenant_id: &str) -> bool {
if self.tenant_rings.contains_key(tenant_id) {
return true;
}
if self.tenant_limit == 0 {
return false;
}
while self.tenant_rings.len() >= self.tenant_limit {
if !self.evict_lru_tenant() {
break;
}
}
if self.tenant_rings.len() >= self.tenant_limit {
return false;
}
self.tenant_rings.insert(
tenant_id.to_string(),
TenantWarmRing::new(self.tenant_capacity),
);
self.touch_tenant(tenant_id);
true
}
fn evict_lru_tenant(&mut self) -> bool {
while let Some(evicted_tenant_id) = self.tenant_lru.pop_front() {
if let Some(ring) = self.tenant_rings.remove(&evicted_tenant_id) {
self.record_evicts(&evicted_tenant_id, ring.len());
self.metrics.set_warm_size(&evicted_tenant_id, 0);
return true;
}
}
let evicted_tenant_id = match self.tenant_rings.keys().min() {
Some(tenant_id) => tenant_id.clone(),
None => return false,
};
match self.tenant_rings.remove(&evicted_tenant_id) {
Some(ring) => {
self.record_evicts(&evicted_tenant_id, ring.len());
self.metrics.set_warm_size(&evicted_tenant_id, 0);
true
}
None => false,
}
}
fn touch_tenant(&mut self, tenant_id: &str) {
if let Some(position) = self.tenant_lru.iter().position(|entry| entry == tenant_id) {
self.tenant_lru.remove(position);
}
self.tenant_lru.push_back(tenant_id.to_string());
}
fn record_evicts(&mut self, tenant_id: &str, evicted: usize) {
for _ in 0..evicted {
self.metrics.record_evict(tenant_id);
}
}
#[cfg(test)]
fn tenant_warm_size(&self, tenant_id: &str) -> usize {
self.tenant_rings
.get(&tenant_id_label(tenant_id))
.map(TenantWarmRing::len)
.unwrap_or(0)
}
#[cfg(test)]
fn force_tenant_ring_entry(
&mut self,
tenant_id: &str,
module_hash: String,
instance_pre: InstancePre<WasmHostState>,
) {
let tenant_id = tenant_id_label(tenant_id);
if !self.ensure_tenant_ring(&tenant_id) {
self.metrics.record_evict(&tenant_id);
self.metrics.set_warm_size(&tenant_id, 0);
return;
}
let warm_size = match self.tenant_rings.get_mut(&tenant_id) {
Some(ring) => {
let evicted = ring.push(WarmInstancePre {
module_hash: module_hash.to_string(),
instance_pre,
});
if evicted {
self.metrics.record_evict(&tenant_id);
}
ring.len()
}
None => 0,
};
self.metrics.set_warm_size(&tenant_id, warm_size);
self.touch_tenant(&tenant_id);
}
fn clear(&mut self) {
self.cache = None;
self.tenant_rings.clear();
self.tenant_lru.clear();
self.metrics.reset_warm_sizes();
}
fn cached_module_hash(&self) -> Option<&str> {
self.cache.as_ref().map(|cache| cache.module_hash.as_str())
}
fn metrics_snapshot(&self, tenant_id: &str) -> Option<GuardPoolMetricsSnapshot> {
self.metrics.snapshot(tenant_id)
}
fn registered_tenant_count(&self) -> usize {
self.tenant_rings.len()
}
}
fn build_instance_pre(
engine: &Engine,
module: &Module,
) -> Result<InstancePre<WasmHostState>, WasmGuardError> {
let mut linker: Linker<WasmHostState> = Linker::new(engine);
register_host_functions(&mut linker)?;
linker
.instantiate_pre(module)
.map_err(|e| WasmGuardError::Compilation(e.to_string()))
}
pub struct WasmtimeBackend {
engine: Arc<Engine>,
module: Option<Module>,
fuel_limit: u64,
config: HashMap<String, String>,
max_memory_bytes: usize,
max_module_size: usize,
last_fuel_consumed: Option<u64>,
module_hash: Option<String>,
instance_pre_pool: InstancePrePool,
}
impl WasmtimeBackend {
pub fn new() -> Result<Self, WasmGuardError> {
let engine = create_shared_engine()?;
Ok(Self {
engine,
module: None,
fuel_limit: 0,
config: HashMap::new(),
max_memory_bytes: MAX_MEMORY_BYTES,
max_module_size: DEFAULT_MAX_MODULE_SIZE,
last_fuel_consumed: None,
module_hash: None,
instance_pre_pool: InstancePrePool::new(DEFAULT_TENANT_WARM_INSTANCE_CAPACITY),
})
}
pub fn with_engine(engine: Arc<Engine>) -> Self {
Self {
engine,
module: None,
fuel_limit: 0,
config: HashMap::new(),
max_memory_bytes: MAX_MEMORY_BYTES,
max_module_size: DEFAULT_MAX_MODULE_SIZE,
last_fuel_consumed: None,
module_hash: None,
instance_pre_pool: InstancePrePool::new(DEFAULT_TENANT_WARM_INSTANCE_CAPACITY),
}
}
pub fn with_engine_and_config(engine: Arc<Engine>, config: HashMap<String, String>) -> Self {
Self {
engine,
module: None,
fuel_limit: 0,
config,
max_memory_bytes: MAX_MEMORY_BYTES,
max_module_size: DEFAULT_MAX_MODULE_SIZE,
last_fuel_consumed: None,
module_hash: None,
instance_pre_pool: InstancePrePool::new(DEFAULT_TENANT_WARM_INSTANCE_CAPACITY),
}
}
#[must_use]
pub fn with_limits(mut self, max_memory_bytes: usize, max_module_size: usize) -> Self {
self.max_memory_bytes = max_memory_bytes;
self.max_module_size = max_module_size;
self
}
#[must_use]
pub fn with_warm_instance_capacity(mut self, capacity: usize) -> Self {
self.instance_pre_pool = InstancePrePool::new(capacity);
self
}
#[must_use]
pub fn instance_pre_cache_module_hash(&self) -> Option<&str> {
self.instance_pre_pool.cached_module_hash()
}
#[must_use]
pub fn pool_metrics_snapshot(&self, tenant_id: &str) -> Option<GuardPoolMetricsSnapshot> {
self.instance_pre_pool.metrics_snapshot(tenant_id)
}
#[must_use]
pub fn pool_registered_tenant_count(&self) -> usize {
self.instance_pre_pool.registered_tenant_count()
}
}
impl Default for WasmtimeBackend {
fn default() -> Self {
match Self::new() {
Ok(b) => b,
Err(_) => Self {
engine: Arc::new(Engine::default()),
module: None,
fuel_limit: 0,
config: HashMap::new(),
max_memory_bytes: MAX_MEMORY_BYTES,
max_module_size: DEFAULT_MAX_MODULE_SIZE,
last_fuel_consumed: None,
module_hash: None,
instance_pre_pool: InstancePrePool::new(DEFAULT_TENANT_WARM_INSTANCE_CAPACITY),
},
}
}
}
impl WasmGuardAbi for WasmtimeBackend {
fn load_module(&mut self, wasm_bytes: &[u8], fuel_limit: u64) -> Result<(), WasmGuardError> {
if wasm_bytes.len() > self.max_module_size {
return Err(WasmGuardError::ModuleTooLarge {
size: wasm_bytes.len(),
limit: self.max_module_size,
});
}
let module = Module::new(&self.engine, wasm_bytes)
.map_err(|e| WasmGuardError::Compilation(e.to_string()))?;
for import in module.imports() {
if import.module() != "chio" {
return Err(WasmGuardError::ImportViolation {
module: import.module().to_string(),
name: import.name().to_string(),
});
}
}
let module_hash = hex::encode(sha2::Sha256::digest(wasm_bytes));
let instance_pre = build_instance_pre(&self.engine, &module)?;
self.module = Some(module);
self.fuel_limit = fuel_limit;
self.module_hash = Some(module_hash.clone());
self.instance_pre_pool.install(module_hash, instance_pre);
Ok(())
}
fn evaluate(&mut self, request: &GuardRequest) -> Result<GuardVerdict, WasmGuardError> {
let module = self
.module
.as_ref()
.ok_or(WasmGuardError::BackendUnavailable)?;
let module_hash = self
.module_hash
.as_deref()
.ok_or(WasmGuardError::BackendUnavailable)?;
let tenant_id = request.agent_id.as_str();
let host_state =
WasmHostState::with_memory_limit(self.config.clone(), self.max_memory_bytes);
let mut store = Store::new(&self.engine, host_state);
store.limiter(|state| &mut state.limits);
store
.set_fuel(self.fuel_limit)
.map_err(|e| WasmGuardError::Trap(e.to_string()))?;
let instance_pre = match self.instance_pre_pool.checkout(tenant_id, module_hash) {
Ok(instance_pre) => instance_pre,
Err(_) => {
let instance_pre = build_instance_pre(&self.engine, module)?;
self.instance_pre_pool
.install(module_hash.to_string(), instance_pre.clone());
instance_pre
}
};
let instance = pollster::block_on(instance_pre.instantiate_async(&mut store))
.map_err(|e| WasmGuardError::Trap(e.to_string()))?;
self.instance_pre_pool
.return_instance_pre(tenant_id, module_hash, instance_pre);
let request_json = serde_json::to_vec(request)
.map_err(|e| WasmGuardError::Serialization(e.to_string()))?;
let memory = instance
.get_memory(&mut store, "memory")
.ok_or_else(|| WasmGuardError::MissingExport("memory".to_string()))?;
let chio_alloc_fn = instance
.get_typed_func::<i32, i32>(&mut store, "chio_alloc")
.ok();
let request_len: i32 = request_json.len() as i32;
let request_ptr: i32 = if let Some(ref alloc_fn) = chio_alloc_fn {
match pollster::block_on(alloc_fn.call_async(&mut store, request_len)) {
Ok(ptr) => {
let mem_size = memory.data_size(&store);
if ptr >= 0 && (ptr as usize).saturating_add(request_len as usize) <= mem_size {
ptr
} else {
return Err(WasmGuardError::Memory(format!(
"chio_alloc returned out-of-bounds pointer {ptr} for request length {request_len} and memory size {mem_size}"
)));
}
}
Err(e) => {
return Err(WasmGuardError::Trap(format!("chio_alloc call failed: {e}")));
}
}
} else {
0
};
memory
.write(&mut store, request_ptr as usize, &request_json)
.map_err(|e| WasmGuardError::Memory(e.to_string()))?;
let evaluate_fn = instance
.get_typed_func::<(i32, i32), i32>(&mut store, "evaluate")
.map_err(|e| WasmGuardError::MissingExport(format!("evaluate: {e}")))?;
let result =
pollster::block_on(evaluate_fn.call_async(&mut store, (request_ptr, request_len)))
.map_err(|e| {
let msg = e.to_string();
if msg.contains("fuel") {
let consumed = self
.fuel_limit
.saturating_sub(store.get_fuel().unwrap_or(0));
self.last_fuel_consumed = Some(consumed);
WasmGuardError::FuelExhausted {
consumed,
limit: self.fuel_limit,
}
} else {
WasmGuardError::Trap(msg)
}
})?;
let remaining = store.get_fuel().unwrap_or(0);
let consumed = self.fuel_limit.saturating_sub(remaining);
self.last_fuel_consumed = Some(consumed);
let verdict = match result {
crate::abi::VERDICT_ALLOW => Ok(GuardVerdict::Allow),
crate::abi::VERDICT_DENY => {
let deny_reason_fn = instance
.get_typed_func::<(i32, i32), i32>(&mut store, "chio_deny_reason")
.ok();
let reason = if let Some(ref reason_fn) = deny_reason_fn {
read_structured_deny_reason(reason_fn, &memory, &mut store)
} else {
read_deny_reason(&memory, &store)
};
Ok(GuardVerdict::Deny { reason })
}
_ => {
Err(WasmGuardError::Trap(format!(
"unexpected return value from evaluate: {result}"
)))
}
};
for (level, msg) in &store.data().logs {
match level {
0 => tracing::trace!(target: "wasm_guard", "{msg}"),
1 => tracing::debug!(target: "wasm_guard", "{msg}"),
2 => tracing::info!(target: "wasm_guard", "{msg}"),
3 => tracing::warn!(target: "wasm_guard", "{msg}"),
4 => tracing::error!(target: "wasm_guard", "{msg}"),
_ => {}
}
}
verdict
}
fn backend_name(&self) -> &str {
"wasmtime"
}
fn last_fuel_consumed(&self) -> Option<u64> {
self.last_fuel_consumed
}
fn clear_instance_pre_cache(&mut self) {
self.instance_pre_pool.clear();
}
}
fn read_structured_deny_reason(
reason_fn: &wasmtime::TypedFunc<(i32, i32), i32>,
memory: &Memory,
store: &mut Store<WasmHostState>,
) -> Option<String> {
const DENY_BUF_OFFSET: i32 = 65536;
const DENY_BUF_LEN: i32 = 4096;
let bytes_written = match pollster::block_on(
reason_fn.call_async(&mut *store, (DENY_BUF_OFFSET, DENY_BUF_LEN)),
) {
Ok(n) if n > 0 && n <= DENY_BUF_LEN => n,
Ok(_) => return None, Err(_) => return None, };
let mut buf = vec![0u8; bytes_written as usize];
if memory
.read(store, DENY_BUF_OFFSET as usize, &mut buf)
.is_err()
{
return None;
}
match serde_json::from_slice::<crate::abi::GuestDenyResponse>(&buf) {
Ok(resp) => Some(resp.reason),
Err(_) => {
std::str::from_utf8(&buf)
.ok()
.map(|s| s.trim_end_matches('\0').to_string())
.filter(|s| !s.is_empty())
}
}
}
fn read_deny_reason(memory: &Memory, store: &Store<WasmHostState>) -> Option<String> {
const DENY_REASON_OFFSET: usize = 65536;
const MAX_REASON_LEN: usize = 4096;
let data = memory.data(store);
if data.len() <= DENY_REASON_OFFSET {
return None;
}
let region = &data[DENY_REASON_OFFSET..];
let end = region
.iter()
.take(MAX_REASON_LEN)
.position(|&b| b == 0)
.unwrap_or(region.len().min(MAX_REASON_LEN));
if end == 0 {
return None;
}
std::str::from_utf8(®ion[..end]).ok().map(String::from)
}
use crate::manifest::GuardManifest;
use crate::placeholders::{resolve_placeholders_in_json, PlaceholderEnv, PlaceholderError};
pub const KNOWN_HOST_FUNCTIONS: &[&str] =
&["chio.log", "chio.get_config", "chio.get_time_unix_secs"];
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PolicyCustomGuard {
pub name: String,
#[serde(default = "default_guard_version")]
pub version: String,
#[serde(flatten)]
pub module: PolicyModuleSource,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub config: serde_json::Value,
#[serde(default = "default_policy_fuel_limit")]
pub fuel_limit: u64,
#[serde(default = "default_policy_priority")]
pub priority: u32,
#[serde(default)]
pub advisory: bool,
#[serde(default)]
pub signer_public_key: Option<String>,
#[serde(default)]
pub allow_unsigned: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum PolicyModuleSource {
Path {
module_path: String,
},
Inline {
module_bytes: Vec<u8>,
},
}
impl PolicyModuleSource {
pub fn path(&self) -> Option<&str> {
match self {
Self::Path { module_path } => Some(module_path.as_str()),
Self::Inline { .. } => None,
}
}
}
fn default_guard_version() -> String {
"0.0.0".to_string()
}
fn default_policy_fuel_limit() -> u64 {
crate::config::DEFAULT_FUEL_LIMIT
}
fn default_policy_priority() -> u32 {
1000
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PolicyCustomGuards {
#[serde(default)]
pub modules: Vec<PolicyCustomGuard>,
}
#[derive(Debug, thiserror::Error)]
pub enum LoadError {
#[error(
"guard {guard:?} requested capability {capability:?} which is not permitted by policy"
)]
CapabilityDenied {
guard: String,
capability: String,
},
#[error("guard {guard:?} module imports {import:?} which is not declared in capabilities")]
UndeclaredHostImport {
guard: String,
import: String,
},
#[error("placeholder resolution failed for guard {guard:?}: {source}")]
Placeholder {
guard: String,
#[source]
source: PlaceholderError,
},
#[error("guard {guard:?} config must be a JSON object, got {kind}")]
ConfigShape {
guard: String,
kind: &'static str,
},
#[error("guard {guard:?} config value at {key:?} must resolve to a string")]
ConfigNotString {
guard: String,
key: String,
},
#[error(transparent)]
Runtime(#[from] WasmGuardError),
}
#[derive(Debug)]
pub struct WasmGuardHandle {
guard: WasmGuard,
granted_capabilities: Vec<String>,
priority: u32,
}
impl WasmGuardHandle {
pub fn into_guard(self) -> WasmGuard {
self.guard
}
pub fn guard(&self) -> &WasmGuard {
&self.guard
}
pub fn granted_capabilities(&self) -> &[String] {
&self.granted_capabilities
}
pub fn priority(&self) -> u32 {
self.priority
}
}
pub fn load_guards_from_policy(
policy: &PolicyCustomGuards,
env: &dyn PlaceholderEnv,
policy_allowed_host_fns: &[String],
engine: Arc<Engine>,
) -> Result<Vec<WasmGuardHandle>, LoadError> {
let mut handles: Vec<WasmGuardHandle> = Vec::with_capacity(policy.modules.len());
let mut sorted: Vec<PolicyCustomGuard> = policy.modules.clone();
sorted.sort_by_key(|g| (g.priority, g.advisory as u8));
for guard_spec in &sorted {
for requested in &guard_spec.capabilities {
if !policy_allowed_host_fns.iter().any(|a| a == requested) {
return Err(LoadError::CapabilityDenied {
guard: guard_spec.name.clone(),
capability: requested.clone(),
});
}
}
let granted: Vec<String> = guard_spec.capabilities.clone();
let resolved_config =
resolve_placeholders_in_json(&guard_spec.config, env).map_err(|source| {
LoadError::Placeholder {
guard: guard_spec.name.clone(),
source,
}
})?;
let config_map = json_object_to_string_map(&resolved_config, &guard_spec.name)?;
let wasm_bytes = match &guard_spec.module {
PolicyModuleSource::Path { module_path } => {
let bytes = std::fs::read(module_path).map_err(|e| {
LoadError::Runtime(WasmGuardError::ModuleLoad {
path: module_path.clone(),
reason: e.to_string(),
})
})?;
let transient_manifest = GuardManifest {
name: guard_spec.name.clone(),
version: guard_spec.version.clone(),
abi_version: "1".to_string(),
wit_world: Some(crate::manifest::REQUIRED_WIT_WORLD.to_string()),
wasm_path: module_path.clone(),
wasm_sha256: hex::encode(sha2::Sha256::digest(&bytes)),
config: std::collections::HashMap::new(),
signer_public_key: guard_spec.signer_public_key.clone(),
allow_unsigned: guard_spec.allow_unsigned,
};
crate::manifest::verify_guard_signature(module_path, &bytes, &transient_manifest)
.map_err(LoadError::Runtime)?;
bytes
}
PolicyModuleSource::Inline { module_bytes } => {
if guard_spec.signer_public_key.is_some() {
return Err(LoadError::Runtime(WasmGuardError::SignatureVerification(
format!(
"guard {:?} has signer_public_key but inline module_bytes have no sidecar",
guard_spec.name
),
)));
}
if !guard_spec.allow_unsigned {
return Err(LoadError::Runtime(WasmGuardError::SignatureVerification(
format!(
"guard {:?} inline module_bytes require allow_unsigned=true",
guard_spec.name
),
)));
}
module_bytes.clone()
}
};
verify_module_imports_within_capabilities(&engine, &wasm_bytes, guard_spec, &granted)?;
let mut backend =
WasmtimeBackend::with_engine_and_config(engine.clone(), config_map.clone());
backend
.load_module(&wasm_bytes, guard_spec.fuel_limit)
.map_err(LoadError::Runtime)?;
let manifest_sha = hex::encode(sha2::Sha256::digest(&wasm_bytes));
let guard = WasmGuard::new_with_metadata(
guard_spec.name.clone(),
guard_spec.version.clone(),
Box::new(backend),
guard_spec.advisory,
Some(manifest_sha),
);
handles.push(WasmGuardHandle {
guard,
granted_capabilities: granted,
priority: guard_spec.priority,
});
}
Ok(handles)
}
fn json_object_to_string_map(
value: &serde_json::Value,
guard_name: &str,
) -> Result<std::collections::HashMap<String, String>, LoadError> {
use serde_json::Value;
let mut out = std::collections::HashMap::new();
match value {
Value::Object(map) => {
for (k, v) in map {
match v {
Value::String(s) => {
out.insert(k.clone(), s.clone());
}
Value::Null => {
}
Value::Bool(b) => {
out.insert(k.clone(), b.to_string());
}
Value::Number(n) => {
out.insert(k.clone(), n.to_string());
}
_ => {
return Err(LoadError::ConfigNotString {
guard: guard_name.to_string(),
key: k.clone(),
});
}
}
}
Ok(out)
}
Value::Null => Ok(out),
Value::Array(_) => Err(LoadError::ConfigShape {
guard: guard_name.to_string(),
kind: "array",
}),
Value::Bool(_) => Err(LoadError::ConfigShape {
guard: guard_name.to_string(),
kind: "bool",
}),
Value::Number(_) => Err(LoadError::ConfigShape {
guard: guard_name.to_string(),
kind: "number",
}),
Value::String(_) => Err(LoadError::ConfigShape {
guard: guard_name.to_string(),
kind: "string",
}),
}
}
fn verify_module_imports_within_capabilities(
engine: &Engine,
wasm_bytes: &[u8],
guard_spec: &PolicyCustomGuard,
granted: &[String],
) -> Result<(), LoadError> {
if detect_wasm_format(wasm_bytes).unwrap_or(WasmFormat::CoreModule) != WasmFormat::CoreModule {
return Ok(());
}
let module = Module::new(engine, wasm_bytes)
.map_err(|e| LoadError::Runtime(WasmGuardError::Compilation(e.to_string())))?;
for import in module.imports() {
if import.module() == "chio" {
let qualified = format!("chio.{}", import.name());
if !granted.iter().any(|g| g == &qualified) {
return Err(LoadError::UndeclaredHostImport {
guard: guard_spec.name.clone(),
import: qualified,
});
}
}
}
Ok(())
}
#[cfg(test)]
#[path = "wasmtime_backend_tests.rs"]
mod tests;