#[cfg(any(feature = "wasm", feature = "native"))]
use crate::arena;
use crate::engine_access::EngineAccess;
#[cfg(feature = "wasm")]
use crate::engine_access::NullEngine;
use crate::error::HookError;
use crate::hooks::HookContext;
#[cfg(feature = "wasm")]
use crate::host_fns;
use crate::program::{LoadedProgram, ProgramBackend};
use crate::result::{ExecuteResult, HookResult, Verdict};
#[cfg(feature = "wasm")]
use crate::runtime::{StoreData, WasmRuntime};
#[cfg(feature = "wasm")]
use wasmtime::{Linker, Store};
#[cfg(feature = "wasm")]
const HOST_ABI_VERSION: i32 = rns_hooks_abi::ABI_VERSION;
pub struct HookManager {
#[cfg(feature = "wasm")]
runtime: WasmRuntime,
#[cfg(feature = "wasm")]
linker: Linker<StoreData>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HookBackend {
Wasm,
Native,
Builtin,
}
impl HookBackend {
pub fn as_str(self) -> &'static str {
match self {
HookBackend::Wasm => "wasm",
HookBackend::Native => "native",
HookBackend::Builtin => "builtin",
}
}
}
impl std::str::FromStr for HookBackend {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.to_ascii_lowercase().as_str() {
"wasm" => Ok(HookBackend::Wasm),
"native" | "dylib" | "dynamic" => Ok(HookBackend::Native),
"builtin" | "built-in" | "static" => Ok(HookBackend::Builtin),
other => Err(format!("unknown hook type '{}'", other)),
}
}
}
impl HookManager {
pub fn new() -> Result<Self, HookError> {
#[cfg(feature = "wasm")]
{
let runtime = WasmRuntime::new().map_err(|e| HookError::CompileError(e.to_string()))?;
let mut linker = Linker::new(runtime.engine());
host_fns::register_host_functions(&mut linker)
.map_err(|e| HookError::CompileError(e.to_string()))?;
Ok(HookManager { runtime, linker })
}
#[cfg(not(feature = "wasm"))]
{
Ok(HookManager {})
}
}
pub fn compile(
&self,
name: String,
bytes: &[u8],
priority: i32,
) -> Result<LoadedProgram, HookError> {
#[cfg(not(feature = "wasm"))]
{
let _ = (name, bytes, priority);
return Err(HookError::CompileError(
"WASM hook backend not enabled".to_string(),
));
}
#[cfg(feature = "wasm")]
{
let module = self
.runtime
.compile(bytes)
.map_err(|e| HookError::CompileError(format!("{:#}", e)))?;
self.validate_abi_version(&name, &module)?;
Ok(LoadedProgram::new(name, module, priority))
}
}
#[cfg(feature = "wasm")]
fn validate_abi_version(&self, name: &str, module: &wasmtime::Module) -> Result<(), HookError> {
let has_export = module.exports().any(|e| e.name() == "__rns_abi_version");
if !has_export {
return Err(HookError::AbiVersionMismatch {
hook_name: name.to_string(),
expected: HOST_ABI_VERSION,
found: None,
});
}
static NULL_ENGINE: NullEngine = NullEngine;
let mut store = self
.runtime
.new_store(StoreData::new(
name.to_string(),
&NULL_ENGINE as *const dyn EngineAccess,
0.0,
false,
self.runtime.max_memory(),
))
.map_err(|e| HookError::CompileError(e.to_string()))?;
let instance = guard_guest_call(|| self.linker.instantiate(&mut store, module))
.map_err(|e| HookError::InstantiationError(e.to_string()))?;
let func = instance
.get_typed_func::<(), i32>(&mut store, "__rns_abi_version")
.map_err(|e| {
HookError::CompileError(format!("__rns_abi_version has wrong signature: {}", e))
})?;
let version = guard_guest_call(|| func.call(&mut store, ()))
.map_err(|e| HookError::Trap(format!("__rns_abi_version trapped: {}", e)))?;
if version != HOST_ABI_VERSION {
return Err(HookError::AbiVersionMismatch {
hook_name: name.to_string(),
expected: HOST_ABI_VERSION,
found: Some(version),
});
}
Ok(())
}
pub fn load_file(
&self,
name: String,
path: &std::path::Path,
priority: i32,
) -> Result<LoadedProgram, HookError> {
let bytes = std::fs::read(path)?;
self.compile(name, &bytes, priority)
}
pub fn load_file_backend(
&self,
name: String,
path: &std::path::Path,
priority: i32,
backend: HookBackend,
) -> Result<LoadedProgram, HookError> {
match backend {
HookBackend::Wasm => self.load_file(name, path, priority),
HookBackend::Builtin => Err(HookError::CompileError(
"built-in hooks are loaded by ID, not file path".to_string(),
)),
HookBackend::Native => {
#[cfg(feature = "native")]
{
let native = crate::native::NativeProgram::load(path)?;
Ok(LoadedProgram::new_native(name, native, priority))
}
#[cfg(not(feature = "native"))]
{
let _ = (name, path, priority);
Err(HookError::CompileError(
"native hook backend not enabled".to_string(),
))
}
}
}
}
pub fn load_builtin(
&self,
name: String,
id: impl Into<String>,
priority: i32,
) -> Result<LoadedProgram, HookError> {
let builtin = crate::builtin::BuiltinProgram::load(id)?;
Ok(LoadedProgram::new_builtin(name, builtin, priority))
}
pub fn execute_program(
&self,
program: &mut LoadedProgram,
ctx: &HookContext,
engine_access: &dyn EngineAccess,
now: f64,
data_override: Option<&[u8]>,
) -> Option<ExecuteResult> {
self.execute_program_with_provider_events(
program,
ctx,
engine_access,
now,
false,
data_override,
)
}
pub fn execute_program_with_provider_events(
&self,
program: &mut LoadedProgram,
ctx: &HookContext,
engine_access: &dyn EngineAccess,
now: f64,
provider_events_enabled: bool,
data_override: Option<&[u8]>,
) -> Option<ExecuteResult> {
if !program.enabled {
return None;
}
if matches!(program.backend, ProgramBackend::Builtin(_)) {
let result = match &program.backend {
ProgramBackend::Builtin(builtin) => builtin.execute(
ctx,
data_override,
engine_access,
now,
provider_events_enabled,
),
#[cfg(feature = "wasm")]
ProgramBackend::Wasm(_) => unreachable!(),
#[cfg(feature = "native")]
ProgramBackend::Native(_) => unreachable!(),
};
return match result {
Ok(mut exec) => {
program.record_success();
for event in &mut exec.provider_events {
event.hook_name = program.name.clone();
}
Some(exec)
}
Err(e) => {
let auto_disabled = program.record_trap();
if auto_disabled {
log::error!(
"built-in hook '{}' auto-disabled after {} consecutive errors",
program.name,
program.consecutive_traps
);
} else {
log::warn!("built-in hook '{}' failed: {}", program.name, e);
}
None
}
};
}
#[cfg(feature = "native")]
if matches!(program.backend, ProgramBackend::Native(_)) {
let ctx_bytes = match arena::context_to_bytes(ctx, data_override) {
Ok(bytes) => bytes,
Err(e) => {
log::warn!(
"failed to encode native hook context '{}': {}",
program.name,
e
);
program.record_trap();
return None;
}
};
let result = match &program.backend {
ProgramBackend::Native(native) => {
native.execute(&ctx_bytes, engine_access, provider_events_enabled)
}
ProgramBackend::Builtin(_) => unreachable!(),
#[cfg(feature = "wasm")]
ProgramBackend::Wasm(_) => unreachable!(),
};
return match result {
Ok(mut exec) => {
program.record_success();
for event in &mut exec.provider_events {
event.hook_name = program.name.clone();
}
Some(exec)
}
Err(e) => {
let auto_disabled = program.record_trap();
if auto_disabled {
log::error!(
"native hook '{}' auto-disabled after {} consecutive errors",
program.name,
program.consecutive_traps
);
} else {
log::warn!("native hook '{}' failed: {}", program.name, e);
}
None
}
};
}
#[cfg(not(feature = "wasm"))]
{
let _ = (
ctx,
engine_access,
now,
provider_events_enabled,
data_override,
);
return None;
}
#[cfg(feature = "wasm")]
{
self.execute_wasm_program(
program,
ctx,
engine_access,
now,
provider_events_enabled,
data_override,
)
}
}
#[cfg(feature = "wasm")]
fn execute_wasm_program(
&self,
program: &mut LoadedProgram,
ctx: &HookContext,
engine_access: &dyn EngineAccess,
now: f64,
provider_events_enabled: bool,
data_override: Option<&[u8]>,
) -> Option<ExecuteResult> {
let engine_access_ptr: *const dyn EngineAccess =
unsafe { std::mem::transmute(engine_access as *const dyn EngineAccess) };
let cached = match &mut program.backend {
ProgramBackend::Builtin(_) => unreachable!(),
ProgramBackend::Wasm(wasm) => wasm.cached.take(),
#[cfg(feature = "native")]
ProgramBackend::Native(_) => unreachable!(),
};
let (mut store, instance) = if let Some(cached) = cached {
let (mut s, i) = cached;
s.data_mut()
.reset_per_call(engine_access_ptr, now, provider_events_enabled);
if let Err(e) = s.set_fuel(self.runtime.fuel()) {
log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
self.cache_wasm_instance(program, s, i);
return None;
}
(s, i)
} else {
let store_data = StoreData::new(
program.name.clone(),
engine_access_ptr,
now,
provider_events_enabled,
self.runtime.max_memory(),
);
let mut store = match self.runtime.new_store(store_data) {
Ok(store) => store,
Err(e) => {
log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
return None;
}
};
let module = match &program.backend {
ProgramBackend::Builtin(_) => unreachable!(),
ProgramBackend::Wasm(wasm) => wasm.module.clone(),
#[cfg(feature = "native")]
ProgramBackend::Native(_) => unreachable!(),
};
let instance = match guard_guest_call(|| self.linker.instantiate(&mut store, &module)) {
Ok(inst) => inst,
Err(e) => {
log::warn!("failed to instantiate hook '{}': {}", program.name, e);
program.record_trap();
return None;
}
};
(store, instance)
};
let memory = match instance.get_memory(&mut store, "memory") {
Some(mem) => mem,
None => {
log::warn!("hook '{}' has no exported memory", program.name);
program.record_trap();
self.cache_wasm_instance(program, store, instance);
return None;
}
};
if let Err(e) = arena::write_context(&memory, &mut store, ctx) {
log::warn!("failed to write context for hook '{}': {}", program.name, e);
program.record_trap();
self.cache_wasm_instance(program, store, instance);
return None;
}
if let Some(override_data) = data_override {
if let Err(e) = arena::write_data_override(&memory, &mut store, override_data) {
log::warn!(
"failed to write data override for hook '{}': {}",
program.name,
e
);
}
}
let export_name = match &program.backend {
ProgramBackend::Builtin(_) => unreachable!(),
ProgramBackend::Wasm(wasm) => wasm.export_name.clone(),
#[cfg(feature = "native")]
ProgramBackend::Native(_) => unreachable!(),
};
let func = match instance.get_typed_func::<i32, i32>(&mut store, &export_name) {
Ok(f) => f,
Err(e) => {
log::warn!(
"hook '{}' missing export '{}': {}",
program.name,
export_name,
e
);
program.record_trap();
self.cache_wasm_instance(program, store, instance);
return None;
}
};
let call = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
func.call(&mut store, arena::ARENA_BASE as i32)
}));
let result_offset = match call {
Ok(Ok(offset)) => offset,
Err(panic) => {
let auto_disabled = program.record_trap();
log::warn!(
"hook '{}' aborted: host panic: {}",
program.name,
panic_message(&*panic)
);
if auto_disabled {
log::error!(
"hook '{}' auto-disabled after {} consecutive traps",
program.name,
program.consecutive_traps
);
}
return None;
}
Ok(Err(e)) => {
let auto_disabled = program.record_trap();
if auto_disabled {
log::error!(
"hook '{}' auto-disabled after {} consecutive traps",
program.name,
program.consecutive_traps
);
} else {
log::warn!("hook '{}' trapped: {}", program.name, e);
}
self.cache_wasm_instance(program, store, instance);
return None;
}
};
let ret = match arena::read_result(&memory, &store, result_offset as u32 as usize) {
Ok(result) => {
program.record_success();
let modified_data = if Verdict::from_u32(result.verdict) == Some(Verdict::Modify) {
arena::read_modified_data(&memory, &store, &result)
} else {
None
};
let injected_actions = std::mem::take(&mut store.data_mut().injected_actions);
let provider_events = std::mem::take(&mut store.data_mut().provider_events)
.into_iter()
.map(|event| crate::result::EmittedProviderEvent {
hook_name: program.name.clone(),
payload_type: event.payload_type,
payload: event.payload,
})
.collect();
Some(ExecuteResult {
hook_result: Some(result),
injected_actions,
provider_events,
modified_data,
})
}
Err(e) => {
log::warn!("hook '{}' returned invalid result: {}", program.name, e);
program.record_trap();
None
}
};
self.cache_wasm_instance(program, store, instance);
ret
}
#[cfg(feature = "wasm")]
fn cache_wasm_instance(
&self,
program: &mut LoadedProgram,
store: Store<StoreData>,
instance: wasmtime::Instance,
) {
match &mut program.backend {
ProgramBackend::Builtin(_) => unreachable!(),
ProgramBackend::Wasm(wasm) => wasm.cached = Some((store, instance)),
#[cfg(feature = "native")]
ProgramBackend::Native(_) => unreachable!(),
}
}
pub fn run_chain(
&self,
programs: &mut [LoadedProgram],
ctx: &HookContext,
engine_access: &dyn EngineAccess,
now: f64,
) -> Option<ExecuteResult> {
self.run_chain_with_provider_events(programs, ctx, engine_access, now, false)
}
pub fn run_chain_with_provider_events(
&self,
programs: &mut [LoadedProgram],
ctx: &HookContext,
engine_access: &dyn EngineAccess,
now: f64,
provider_events_enabled: bool,
) -> Option<ExecuteResult> {
let mut accumulated_actions = Vec::new();
let mut accumulated_provider_events = Vec::new();
let mut last_result: Option<HookResult> = None;
let mut last_modified_data: Option<Vec<u8>> = None;
let is_packet_ctx = matches!(ctx, HookContext::Packet { .. });
for program in programs.iter_mut() {
if !program.enabled {
continue;
}
let override_ref = if is_packet_ctx {
last_modified_data.as_deref()
} else {
None
};
if let Some(exec_result) = self.execute_program_with_provider_events(
program,
ctx,
engine_access,
now,
provider_events_enabled,
override_ref,
) {
accumulated_actions.extend(exec_result.injected_actions);
accumulated_provider_events.extend(exec_result.provider_events);
if let Some(ref result) = exec_result.hook_result {
let verdict = Verdict::from_u32(result.verdict);
match verdict {
Some(Verdict::Drop) | Some(Verdict::Halt) => {
return Some(ExecuteResult {
hook_result: exec_result.hook_result,
injected_actions: accumulated_actions,
provider_events: accumulated_provider_events,
modified_data: exec_result.modified_data.or(last_modified_data),
});
}
Some(Verdict::Modify) => {
last_result = exec_result.hook_result;
if is_packet_ctx {
if let Some(data) = exec_result.modified_data {
last_modified_data = Some(data);
}
}
}
_ => {} }
}
}
}
if last_result.is_some()
|| !accumulated_actions.is_empty()
|| !accumulated_provider_events.is_empty()
{
Some(ExecuteResult {
hook_result: last_result,
injected_actions: accumulated_actions,
provider_events: accumulated_provider_events,
modified_data: last_modified_data,
})
} else {
None
}
}
}
#[cfg(feature = "wasm")]
fn guard_guest_call<T>(
call: impl FnOnce() -> Result<T, wasmtime::Error>,
) -> Result<T, wasmtime::Error> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(call)) {
Ok(result) => result,
Err(panic) => Err(wasmtime::format_err!(
"host panic: {}",
panic_message(&*panic)
)),
}
}
#[cfg(feature = "wasm")]
fn panic_message(panic: &(dyn std::any::Any + Send)) -> &str {
if let Some(msg) = panic.downcast_ref::<&str>() {
msg
} else if let Some(msg) = panic.downcast_ref::<String>() {
msg
} else {
"unknown panic"
}
}
#[cfg(all(test, feature = "wasm"))]
mod tests {
use super::*;
use crate::engine_access::NullEngine;
fn make_manager() -> HookManager {
HookManager::new().expect("failed to create HookManager")
}
const WAT_CONTINUE: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
;; Write HookResult at offset 0x2000
;; verdict = 0 (Continue)
(i32.store (i32.const 0x2000) (i32.const 0))
;; modified_data_offset = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
;; modified_data_len = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
;; inject_actions_offset = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
;; inject_actions_count = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
;; log_offset = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
;; log_len = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
const WAT_DROP: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(i32.store (i32.const 0x2000) (i32.const 1))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
const WAT_TRAP: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
unreachable
)
)
"#;
const WAT_INFINITE: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(loop $inf (br $inf))
(i32.const 0)
)
)
"#;
const WAT_FUEL_RECOVERY: &str = r#"
(module
(memory (export "memory") 1)
(global $calls (mut i32) (i32.const 0))
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(global.set $calls (i32.add (global.get $calls) (i32.const 1)))
(if (i32.eq (global.get $calls) (i32.const 1))
(then (loop $inf (br $inf))))
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.const 0x2004) (i32.const 0))
(i32.store (i32.const 0x2008) (i32.const 0))
(i32.store (i32.const 0x200c) (i32.const 0))
(i32.store (i32.const 0x2010) (i32.const 0))
(i32.store (i32.const 0x2014) (i32.const 0))
(i32.store (i32.const 0x2018) (i32.const 0))
(i32.const 0x2000))
)
"#;
const WAT_BULK_MEMORY: &str = r#"
(module
(memory (export "memory") 2)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(memory.fill (i32.const 0x10000) (i32.const 0x5a) (i32.const 0x4000))
(memory.copy (i32.const 0x14000) (i32.const 0x10000) (i32.const 0x4000))
(i32.store (i32.const 0x2000) (i32.const 2))
(i32.store (i32.const 0x2004) (i32.const 0x14000))
(i32.store (i32.const 0x2008) (i32.const 0x4000))
(i32.store (i32.const 0x200c) (i32.const 0))
(i32.store (i32.const 0x2010) (i32.const 0))
(i32.store (i32.const 0x2014) (i32.const 0))
(i32.store (i32.const 0x2018) (i32.const 0))
(i32.const 0x2000))
)
"#;
const WAT_HOST_HAS_PATH: &str = r#"
(module
(import "env" "host_has_path" (func $has_path (param i32) (result i32)))
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param $ctx_ptr i32) (result i32)
;; Check if path exists for a 16-byte dest at offset 0x3000
;; (we'll write the dest hash there in the test)
(if (call $has_path (i32.const 0x3000))
(then
;; Drop
(i32.store (i32.const 0x2000) (i32.const 1))
)
(else
;; Continue
(i32.store (i32.const 0x2000) (i32.const 0))
)
)
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn pass_through() {
let mgr = make_manager();
let mut prog = mgr
.compile("test".into(), WAT_CONTINUE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
let exec = result.unwrap();
let r = exec.hook_result.unwrap();
assert_eq!(r.verdict, Verdict::Continue as u32);
}
#[test]
fn drop_hook() {
let mgr = make_manager();
let mut prog = mgr
.compile("dropper".into(), WAT_DROP.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
let exec = result.unwrap();
let r = exec.hook_result.unwrap();
assert!(r.is_drop());
}
#[test]
fn trap_failopen() {
let mgr = make_manager();
let mut prog = mgr.compile("trap".into(), WAT_TRAP.as_bytes(), 0).unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
assert!(result.is_none());
assert_eq!(prog.consecutive_traps, 1);
assert!(prog.enabled);
}
#[test]
fn auto_disable() {
let mgr = make_manager();
let mut prog = mgr.compile("bad".into(), WAT_TRAP.as_bytes(), 0).unwrap();
let ctx = HookContext::Tick;
for _ in 0..10 {
let _ = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
}
assert!(!prog.enabled);
assert_eq!(prog.consecutive_traps, 10);
}
#[test]
fn fuel_exhaustion() {
let mgr = make_manager();
let mut prog = mgr
.compile("loop".into(), WAT_INFINITE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
assert!(result.is_none());
assert_eq!(prog.consecutive_traps, 1);
}
#[test]
fn fuel_exhaustion_recovers_cached_store_on_next_call() {
let mgr = make_manager();
let mut prog = mgr
.compile("fuel-recovery".into(), WAT_FUEL_RECOVERY.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
assert!(mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.is_none());
assert_eq!(prog.consecutive_traps, 1);
let recovered = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.expect("fuel must be reset before reusing the cached store");
assert_eq!(
recovered.hook_result.unwrap().verdict,
Verdict::Continue as u32
);
assert_eq!(prog.consecutive_traps, 0);
}
#[test]
fn bulk_memory_fill_and_copy_preserve_all_bytes() {
let mgr = make_manager();
let mut prog = mgr
.compile("bulk-memory".into(), WAT_BULK_MEMORY.as_bytes(), 0)
.unwrap();
let result = mgr
.execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
.unwrap();
let copied = result.modified_data.unwrap();
assert_eq!(copied.len(), 0x4000);
assert!(copied.iter().all(|byte| *byte == 0x5a));
}
#[test]
fn chain_ordering() {
let mgr = make_manager();
let high = mgr
.compile("high".into(), WAT_DROP.as_bytes(), 100)
.unwrap();
let low = mgr
.compile("low".into(), WAT_CONTINUE.as_bytes(), 0)
.unwrap();
let mut programs = vec![high, low];
programs.sort_by_key(|program| std::cmp::Reverse(program.priority));
let ctx = HookContext::Tick;
let result = mgr.run_chain(&mut programs, &ctx, &NullEngine, 0.0);
let exec = result.unwrap();
let r = exec.hook_result.unwrap();
assert!(r.is_drop());
}
#[test]
fn attach_detach() {
use crate::hooks::HookSlot;
let mgr = make_manager();
let mut slot = HookSlot {
programs: Vec::new(),
runner: crate::hooks::hook_noop,
};
let p1 = mgr
.compile("alpha".into(), WAT_CONTINUE.as_bytes(), 10)
.unwrap();
let p2 = mgr.compile("beta".into(), WAT_DROP.as_bytes(), 20).unwrap();
slot.attach(p1);
assert_eq!(slot.programs.len(), 1);
assert!(!std::ptr::eq(
slot.runner as *const (),
crate::hooks::hook_noop as *const (),
));
slot.attach(p2);
assert_eq!(slot.programs.len(), 2);
assert_eq!(slot.programs[0].name, "beta");
assert_eq!(slot.programs[1].name, "alpha");
let removed = slot.detach("beta");
assert!(removed.is_some());
assert_eq!(slot.programs.len(), 1);
assert_eq!(slot.programs[0].name, "alpha");
let removed2 = slot.detach("alpha");
assert!(removed2.is_some());
assert!(slot.programs.is_empty());
assert_eq!(
slot.runner as *const () as usize,
crate::hooks::hook_noop as *const () as usize
);
}
#[test]
fn host_has_path() {
use crate::engine_access::EngineAccess;
struct MockEngine;
impl EngineAccess for MockEngine {
fn has_path(&self, _dest: &[u8; 16]) -> bool {
true
}
fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
None
}
fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
None
}
fn is_blackholed(&self, _: &[u8; 16]) -> bool {
false
}
fn interface_name(&self, _: u64) -> Option<String> {
None
}
fn interface_mode(&self, _: u64) -> Option<u8> {
None
}
fn identity_hash(&self) -> Option<[u8; 16]> {
None
}
fn announce_rate(&self, _: u64) -> Option<i32> {
None
}
fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
None
}
}
let mgr = make_manager();
let mut prog = mgr
.compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &MockEngine, 0.0, None);
let exec = result.unwrap();
let r = exec.hook_result.unwrap();
assert!(r.is_drop());
}
#[test]
fn host_has_path_null_engine() {
let mgr = make_manager();
let mut prog = mgr
.compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
let exec = result.unwrap();
let r = exec.hook_result.unwrap();
assert_eq!(r.verdict, Verdict::Continue as u32);
}
struct MockEngineCustom {
announce_rate_val: Option<i32>,
link_state_val: Option<u8>,
}
impl EngineAccess for MockEngineCustom {
fn has_path(&self, _: &[u8; 16]) -> bool {
false
}
fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
None
}
fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
None
}
fn is_blackholed(&self, _: &[u8; 16]) -> bool {
false
}
fn interface_name(&self, _: u64) -> Option<String> {
None
}
fn interface_mode(&self, _: u64) -> Option<u8> {
None
}
fn identity_hash(&self) -> Option<[u8; 16]> {
None
}
fn announce_rate(&self, _: u64) -> Option<i32> {
self.announce_rate_val
}
fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
self.link_state_val
}
}
const WAT_ANNOUNCE_RATE: &str = r#"
(module
(import "env" "host_get_announce_rate" (func $get_rate (param i64) (result i32)))
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param $ctx_ptr i32) (result i32)
(if (i32.ge_s (call $get_rate (i64.const 42)) (i32.const 0))
(then
(i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
)
(else
(i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
)
)
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn host_get_announce_rate_found() {
let engine = MockEngineCustom {
announce_rate_val: Some(1500),
link_state_val: None,
};
let mgr = make_manager();
let mut prog = mgr
.compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &engine, 0.0, None)
.unwrap();
assert!(exec.hook_result.unwrap().is_drop());
}
#[test]
fn host_get_announce_rate_not_found() {
let engine = MockEngineCustom {
announce_rate_val: None,
link_state_val: None,
};
let mgr = make_manager();
let mut prog = mgr
.compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &engine, 0.0, None)
.unwrap();
assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
}
const WAT_LINK_STATE: &str = r#"
(module
(import "env" "host_get_link_state" (func $link_state (param i32) (result i32)))
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param $ctx_ptr i32) (result i32)
(if (i32.eq (call $link_state (i32.const 0x3000)) (i32.const 2))
(then
(i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
)
(else
(i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
)
)
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn host_get_link_state_active() {
let engine = MockEngineCustom {
announce_rate_val: None,
link_state_val: Some(2),
};
let mgr = make_manager();
let mut prog = mgr
.compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &engine, 0.0, None)
.unwrap();
assert!(exec.hook_result.unwrap().is_drop());
}
#[test]
fn host_get_link_state_not_found() {
let engine = MockEngineCustom {
announce_rate_val: None,
link_state_val: None,
};
let mgr = make_manager();
let mut prog = mgr
.compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &engine, 0.0, None)
.unwrap();
assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
}
const WAT_INJECT_ACTION: &str = r#"
(module
(import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param $ctx_ptr i32) (result i32)
;; Write the data payload at 0x3100
(i32.store8 (i32.const 0x3100) (i32.const 0xDE))
(i32.store8 (i32.const 0x3101) (i32.const 0xAD))
(i32.store8 (i32.const 0x3102) (i32.const 0xBE))
(i32.store8 (i32.const 0x3103) (i32.const 0xEF))
;; Write ActionWire at 0x3000:
;; byte 0: tag = 0 (SendOnInterface)
(i32.store8 (i32.const 0x3000) (i32.const 0))
;; bytes 1-8: interface = 1 (u64 LE)
(i64.store (i32.const 0x3001) (i64.const 1))
;; bytes 9-12: data_offset = 0x3100 (u32 LE)
(i32.store (i32.const 0x3009) (i32.const 0x3100))
;; bytes 13-16: data_len = 4 (u32 LE)
(i32.store (i32.const 0x300D) (i32.const 4))
;; Call inject: ptr=0x3000, len=17 (1 + 8 + 4 + 4)
(drop (call $inject (i32.const 0x3000) (i32.const 17)))
;; Return Continue
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn host_inject_action_send() {
let mgr = make_manager();
let mut prog = mgr
.compile("inject".into(), WAT_INJECT_ACTION.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
assert_eq!(exec.injected_actions.len(), 1);
match &exec.injected_actions[0] {
crate::wire::ActionWire::SendOnInterface { interface, raw } => {
assert_eq!(*interface, 1);
assert_eq!(raw, &[0xDE, 0xAD, 0xBE, 0xEF]);
}
other => panic!("expected SendOnInterface, got {:?}", other),
}
}
const WAT_MODIFY: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param $ctx_ptr i32) (result i32)
;; Write modified data at 0x2100
(i32.store8 (i32.const 0x2100) (i32.const 0xAA))
(i32.store8 (i32.const 0x2101) (i32.const 0xBB))
(i32.store8 (i32.const 0x2102) (i32.const 0xCC))
(i32.store8 (i32.const 0x2103) (i32.const 0xDD))
;; verdict = 2 (Modify)
(i32.store (i32.const 0x2000) (i32.const 2))
;; modified_data_offset = 0x2100
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x2100))
;; modified_data_len = 4
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn modify_extracts_data() {
let mgr = make_manager();
let mut prog = mgr.compile("mod".into(), WAT_MODIFY.as_bytes(), 0).unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
let r = exec.hook_result.unwrap();
assert_eq!(r.verdict, Verdict::Modify as u32);
let data = exec.modified_data.unwrap();
assert_eq!(data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
}
#[test]
fn chain_accumulates_injected_actions() {
let mgr = make_manager();
let injector = mgr
.compile("injector".into(), WAT_INJECT_ACTION.as_bytes(), 100)
.unwrap();
let dropper = mgr
.compile("dropper".into(), WAT_DROP.as_bytes(), 0)
.unwrap();
let mut programs = vec![injector, dropper];
programs.sort_by_key(|program| std::cmp::Reverse(program.priority));
let ctx = HookContext::Tick;
let exec = mgr
.run_chain(&mut programs, &ctx, &NullEngine, 0.0)
.unwrap();
assert!(exec.hook_result.unwrap().is_drop());
assert_eq!(exec.injected_actions.len(), 1);
}
const WAT_COUNTER: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(global $counter (mut i32) (i32.const 0))
(func (export "on_hook") (param i32) (result i32)
;; Increment counter
(global.set $counter (i32.add (global.get $counter) (i32.const 1)))
;; Write counter value at 0x3000 (scratch area)
(i32.store (i32.const 0x3000) (global.get $counter))
;; Return Continue with the counter stashed in modified_data region
;; verdict = 2 (Modify) so we can extract the counter via modified_data
(i32.store (i32.const 0x2000) (i32.const 2))
;; modified_data_offset = 0x3000
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x3000))
;; modified_data_len = 4
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
fn extract_counter(exec: &ExecuteResult) -> u32 {
let data = exec.modified_data.as_ref().expect("no modified data");
assert_eq!(data.len(), 4);
u32::from_le_bytes([data[0], data[1], data[2], data[3]])
}
#[test]
fn instance_persistence_counter() {
let mgr = make_manager();
let mut prog = mgr
.compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec1 = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(extract_counter(&exec1), 1);
let exec2 = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(extract_counter(&exec2), 2);
let exec3 = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(extract_counter(&exec3), 3);
}
#[test]
fn instance_persistence_resets_on_drop_cache() {
let mgr = make_manager();
let mut prog = mgr
.compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
let exec2 = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(extract_counter(&exec2), 2);
prog.drop_cache();
let exec3 = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(extract_counter(&exec3), 1);
}
const WAT_NO_ABI_VERSION: &str = r#"
(module
(memory (export "memory") 1)
(func (export "on_hook") (param i32) (result i32)
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
const WAT_WRONG_ABI_VERSION: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 9999))
(func (export "on_hook") (param i32) (result i32)
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn rejects_missing_abi_version() {
let mgr = make_manager();
let result = mgr.compile("no_abi".into(), WAT_NO_ABI_VERSION.as_bytes(), 0);
match result {
Err(HookError::AbiVersionMismatch {
hook_name,
expected,
found,
}) => {
assert_eq!(hook_name, "no_abi");
assert_eq!(expected, HOST_ABI_VERSION);
assert_eq!(found, None);
}
other => panic!(
"expected AbiVersionMismatch with found=None, got {:?}",
other.err()
),
}
}
#[test]
fn rejects_wrong_abi_version() {
let mgr = make_manager();
let result = mgr.compile("bad_abi".into(), WAT_WRONG_ABI_VERSION.as_bytes(), 0);
match result {
Err(HookError::AbiVersionMismatch {
hook_name,
expected,
found,
}) => {
assert_eq!(hook_name, "bad_abi");
assert_eq!(expected, HOST_ABI_VERSION);
assert_eq!(found, Some(9999));
}
other => panic!(
"expected AbiVersionMismatch with found=Some(9999), got {:?}",
other.err()
),
}
}
#[test]
fn accepts_correct_abi_version() {
let mgr = make_manager();
let result = mgr.compile("good_abi".into(), WAT_CONTINUE.as_bytes(), 0);
assert!(
result.is_ok(),
"compile should succeed with correct ABI version"
);
}
#[test]
fn host_emit_event_collects_provider_event() {
let mgr = make_manager();
let wat = r#"
(module
(import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0x3000) "packet")
(data (i32.const 0x3010) "\01\02\03")
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
let mut prog = LoadedProgram::new("emit".into(), module, 0);
let ctx = HookContext::Tick;
let exec = mgr
.execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, true, None)
.unwrap();
assert_eq!(exec.provider_events.len(), 1);
assert_eq!(exec.provider_events[0].hook_name, "emit");
assert_eq!(exec.provider_events[0].payload_type, "packet");
assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
}
#[test]
fn host_emit_event_is_ignored_when_disabled() {
let mgr = make_manager();
let wat = r#"
(module
(import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0x3000) "packet")
(data (i32.const 0x3010) "\01\02\03")
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
let mut prog = LoadedProgram::new("emit".into(), module, 0);
let ctx = HookContext::Tick;
let exec = mgr
.execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, false, None)
.unwrap();
assert!(exec.provider_events.is_empty());
}
#[test]
fn run_chain_returns_continue_only_provider_events() {
let manager = make_manager();
let wasm = wat::parse_str(
r#"(module
(import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 4096) "\00\00\00\00")
(data (i32.const 8192) "packet")
(data (i32.const 8208) "\01\02\03")
(func (export "__rns_abi_version") (result i32) i32.const 1)
(func (export "on_hook") (param i32) (result i32)
i32.const 8192
i32.const 6
i32.const 8208
i32.const 3
call $emit
drop
i32.const 4096
)
)"#,
)
.unwrap();
let mut programs = vec![manager.compile("emit".into(), &wasm, 0).unwrap()];
let pkt_ctx = crate::PacketContext {
flags: 0,
hops: 0,
destination_hash: [0; 16],
context: 0,
packet_hash: [0; 32],
interface_id: 1,
data_offset: 0,
data_len: 0,
};
let ctx = crate::HookContext::Packet {
ctx: &pkt_ctx,
raw: &[],
};
let exec = manager
.run_chain_with_provider_events(&mut programs, &ctx, &NullEngine, 0.0, true)
.expect("expected provider event result");
assert!(exec.hook_result.is_none());
assert!(exec.injected_actions.is_empty());
assert_eq!(exec.provider_events.len(), 1);
assert_eq!(exec.provider_events[0].payload_type, "packet");
assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
}
fn hook_module(imports: &str, pages: u32, body: &str, out_len: u32) -> String {
let verdict = if out_len == 0 { 0 } else { 2 };
format!(
r#"(module
{imports}
(memory (export "memory") {pages})
(global $calls (mut i32) (i32.const 0))
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(local $i i32)
(global.set $calls (i32.add (global.get $calls) (i32.const 1)))
{body}
(i32.store (i32.const 0x2000) (i32.const {verdict}))
(i32.store (i32.const 0x2004) (i32.const 0x3000))
(i32.store (i32.const 0x2008) (i32.const {out_len}))
(i32.store (i32.const 0x200c) (i32.const 0))
(i32.store (i32.const 0x2010) (i32.const 0))
(i32.store (i32.const 0x2014) (i32.const 0))
(i32.store (i32.const 0x2018) (i32.const 0))
(i32.const 0x2000)))"#
)
}
fn read_i32s(data: &[u8]) -> Vec<i32> {
data.chunks_exact(4)
.map(|c| i32::from_le_bytes(c.try_into().unwrap()))
.collect()
}
fn cached_store(prog: &LoadedProgram) -> &Store<StoreData> {
match &prog.backend {
ProgramBackend::Wasm(wasm) => &wasm.cached.as_ref().expect("cached store").0,
_ => panic!("expected a wasm program"),
}
}
struct AnsweringEngine;
impl EngineAccess for AnsweringEngine {
fn has_path(&self, _: &[u8; 16]) -> bool {
true
}
fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
Some(2)
}
fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
Some([7; 16])
}
fn is_blackholed(&self, _: &[u8; 16]) -> bool {
true
}
fn interface_name(&self, _: u64) -> Option<String> {
Some("eth0".into())
}
fn interface_mode(&self, _: u64) -> Option<u8> {
Some(1)
}
fn identity_hash(&self) -> Option<[u8; 16]> {
Some([9; 16])
}
fn announce_rate(&self, _: u64) -> Option<i32> {
Some(1)
}
fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
Some(2)
}
}
const ALL_MEMORY_HOST_FNS: &str = r#"
(import "env" "host_log" (func $log (param i32 i32)))
(import "env" "host_has_path" (func $has_path (param i32) (result i32)))
(import "env" "host_get_hops" (func $hops (param i32) (result i32)))
(import "env" "host_get_next_hop" (func $next_hop (param i32 i32) (result i32)))
(import "env" "host_is_blackholed" (func $blackholed (param i32) (result i32)))
(import "env" "host_get_interface_name" (func $ifname (param i64 i32 i32) (result i32)))
(import "env" "host_get_transport_identity" (func $identity (param i32) (result i32)))
(import "env" "host_get_link_state" (func $link (param i32) (result i32)))
(import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))
(import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
"#;
#[test]
fn host_fns_reject_out_of_range_guest_pointers() {
let mgr = make_manager();
let body = r#"
(call $log (i32.const -16) (i32.const 0x7fffffff))
(call $log (i32.const 0x100) (i32.const -1))
(i32.store (i32.const 0x3000) (call $has_path (i32.const -16)))
(i32.store (i32.const 0x3004) (call $hops (i32.const -16)))
(i32.store (i32.const 0x3008) (call $next_hop (i32.const 0x100) (i32.const -16)))
(i32.store (i32.const 0x300c) (call $next_hop (i32.const -16) (i32.const 0x100)))
(i32.store (i32.const 0x3010) (call $blackholed (i32.const -16)))
(i32.store (i32.const 0x3014)
(call $ifname (i64.const 1) (i32.const -16) (i32.const 0x7fffffff)))
(i32.store (i32.const 0x3018) (call $identity (i32.const -16)))
(i32.store (i32.const 0x301c) (call $link (i32.const -16)))
(i32.store (i32.const 0x3020) (call $inject (i32.const -16) (i32.const 0x7fffffff)))
(i32.store (i32.const 0x3024)
(call $emit (i32.const -16) (i32.const 16) (i32.const 0) (i32.const 0)))
(i32.store (i32.const 0x3028)
(call $emit (i32.const 0) (i32.const 0) (i32.const -16) (i32.const 16)))
(i32.store (i32.const 0x302c) (call $next_hop (i32.const 0x100) (i32.const 0x200)))
"#;
let wat = hook_module(ALL_MEMORY_HOST_FNS, 1, body, 48);
let mut prog = mgr.compile("bounds".into(), wat.as_bytes(), 0).unwrap();
let expected = vec![0, -1, 0, 0, 0, -1, 0, -1, -1, -1, -1, 1];
for _ in 0..2 {
let exec = mgr
.execute_program_with_provider_events(
&mut prog,
&HookContext::Tick,
&AnsweringEngine,
0.0,
true,
None,
)
.expect("hook keeps running after out-of-range pointers");
assert_eq!(read_i32s(&exec.modified_data.unwrap()), expected);
assert!(exec.injected_actions.is_empty());
assert!(exec.provider_events.is_empty());
}
assert_eq!(prog.consecutive_traps, 0);
}
#[test]
fn result_offset_near_end_of_address_space_is_rejected() {
let mgr = make_manager();
let wat = r#"(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32) (i32.const -4)))"#;
let mut prog = mgr.compile("offset".into(), wat.as_bytes(), 0).unwrap();
assert!(mgr
.execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
.is_none());
assert_eq!(prog.consecutive_traps, 1);
let mut valid = mgr
.compile("valid".into(), WAT_CONTINUE.as_bytes(), 0)
.unwrap();
assert!(mgr
.execute_program(&mut valid, &HookContext::Tick, &NullEngine, 0.0, None)
.is_some());
}
#[test]
fn host_panic_is_contained_and_program_recovers() {
struct PanickingEngine;
impl EngineAccess for PanickingEngine {
fn has_path(&self, _: &[u8; 16]) -> bool {
panic!("engine lookup failed")
}
fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
None
}
fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
None
}
fn is_blackholed(&self, _: &[u8; 16]) -> bool {
false
}
fn interface_name(&self, _: u64) -> Option<String> {
None
}
fn interface_mode(&self, _: u64) -> Option<u8> {
None
}
fn identity_hash(&self) -> Option<[u8; 16]> {
None
}
fn announce_rate(&self, _: u64) -> Option<i32> {
None
}
fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
None
}
}
let mgr = make_manager();
let mut prog = mgr
.compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
.unwrap();
assert!(mgr
.execute_program(&mut prog, &HookContext::Tick, &PanickingEngine, 0.0, None)
.is_none());
assert_eq!(prog.consecutive_traps, 1);
let exec = mgr
.execute_program(&mut prog, &HookContext::Tick, &AnsweringEngine, 0.0, None)
.expect("program runs again after a contained host panic");
assert!(exec.hook_result.unwrap().is_drop());
assert_eq!(prog.consecutive_traps, 0);
}
#[test]
fn memory_grow_past_limit_traps_and_host_continues() {
let mgr = make_manager();
let body = r#"
(if (i32.eq (global.get $calls) (i32.const 1))
(then (drop (memory.grow (i32.const 8192)))))
(i32.store (i32.const 0x3000) (memory.size))
"#;
let wat = hook_module("", 1, body, 4);
let mut prog = mgr.compile("grow".into(), wat.as_bytes(), 0).unwrap();
assert!(mgr
.execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
.is_none());
assert_eq!(prog.consecutive_traps, 1);
let exec = mgr
.execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
.expect("program runs after a refused grow");
assert_eq!(read_i32s(&exec.modified_data.unwrap()), vec![1]);
}
#[test]
fn memory_grow_within_limit_succeeds() {
let mgr = make_manager();
let body = r#"
(i32.store (i32.const 0x3000) (memory.grow (i32.const 100)))
(i32.store (i32.const 0x3004) (memory.size))
"#;
let wat = hook_module("", 1, body, 8);
let mut prog = mgr.compile("grow".into(), wat.as_bytes(), 0).unwrap();
let exec = mgr
.execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(read_i32s(&exec.modified_data.unwrap()), vec![1, 101]);
}
#[test]
fn rejects_memory64_modules() {
let mgr = make_manager();
let wat = r#"(module
(memory (export "memory") i64 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1)))"#;
let err = mgr
.compile("m64".into(), wat.as_bytes(), 0)
.err()
.expect("memory64 module is rejected");
assert!(err.to_string().contains("memory64"), "{}", err);
}
#[test]
fn rejects_declared_minimum_memory_above_limit() {
let mgr = make_manager();
let exported = r#"(module
(memory (export "memory") 1024)
(func (export "__rns_abi_version") (result i32) (i32.const 1)))"#;
let err = mgr
.compile("big".into(), exported.as_bytes(), 0)
.err()
.expect("module above the memory limit is rejected");
assert!(err.to_string().contains("minimum memory"), "{}", err);
let internal = r#"(module
(memory 1024)
(func (export "__rns_abi_version") (result i32) (i32.const 1)))"#;
assert!(mgr.compile("big".into(), internal.as_bytes(), 0).is_err());
}
#[test]
fn module_at_memory_limit_runs() {
let mgr = make_manager();
let pages = (crate::runtime::DEFAULT_MAX_MEMORY / 65536) as u32;
let body = r#"(i32.store (i32.const 0x3000) (memory.size))"#;
let wat = hook_module("", pages, body, 4);
let mut prog = mgr.compile("full".into(), wat.as_bytes(), 0).unwrap();
let exec = mgr
.execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(read_i32s(&exec.modified_data.unwrap()), vec![pages as i32]);
}
#[test]
fn host_log_forwards_at_most_the_per_call_cap() {
let mgr = make_manager();
let imports = r#"(import "env" "host_log" (func $log (param i32 i32)))"#;
let body = r#"
(local.set $i (i32.const 0))
(block $done (loop $next
(br_if $done (i32.ge_u (local.get $i) (i32.const 100)))
(call $log (i32.const 0x4000) (i32.const 4096))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $next)))
"#;
let wat = hook_module(imports, 1, body, 0);
let mut prog = mgr.compile("log".into(), wat.as_bytes(), 0).unwrap();
for _ in 0..2 {
mgr.execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
.unwrap();
let data = cached_store(&prog).data();
assert_eq!(
data.log_messages_forwarded,
crate::runtime::MAX_LOG_MESSAGES_PER_CALL
);
assert_eq!(
data.log_messages_dropped,
100 - crate::runtime::MAX_LOG_MESSAGES_PER_CALL
);
}
}
fn inject_repeatedly(count: u32, payload_len: u32) -> String {
let imports =
r#"(import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))"#;
let body = format!(
r#"
(i32.store8 (i32.const 0x3100) (i32.const 0))
(i64.store (i32.const 0x3101) (i64.const 1))
(i32.store (i32.const 0x3109) (i32.const 0x10000))
(i32.store (i32.const 0x310d) (i32.const {payload_len}))
(i32.store (i32.const 0x3000) (i32.const 0))
(local.set $i (i32.const 0))
(block $done (loop $next
(br_if $done (i32.ge_u (local.get $i) (i32.const {count})))
(i32.store (i32.const 0x3004) (call $inject (i32.const 0x3100) (i32.const 17)))
(if (i32.eqz (i32.load (i32.const 0x3004)))
(then (i32.store (i32.const 0x3000)
(i32.add (i32.load (i32.const 0x3000)) (i32.const 1)))))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $next)))
"#
);
hook_module(imports, 3, &body, 8)
}
#[test]
fn host_inject_action_refuses_actions_beyond_count_cap() {
let mgr = make_manager();
let cap = crate::runtime::MAX_INJECTED_ACTIONS_PER_CALL;
let wat = inject_repeatedly(cap as u32 + 6, 4);
let mut prog = mgr.compile("inject".into(), wat.as_bytes(), 0).unwrap();
let exec = mgr
.execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(
read_i32s(&exec.modified_data.unwrap()),
vec![cap as i32, host_fns::LIMIT_REACHED]
);
assert_eq!(exec.injected_actions.len(), cap);
}
#[test]
fn host_inject_action_refuses_actions_beyond_byte_cap() {
let mgr = make_manager();
let wat = inject_repeatedly(5, 65536);
let mut prog = mgr.compile("inject".into(), wat.as_bytes(), 0).unwrap();
let exec = mgr
.execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(
read_i32s(&exec.modified_data.unwrap()),
vec![3, host_fns::LIMIT_REACHED]
);
assert_eq!(exec.injected_actions.len(), 3);
for action in &exec.injected_actions {
match action {
crate::wire::ActionWire::SendOnInterface { raw, .. } => {
assert_eq!(raw.len(), 65536)
}
other => panic!("expected SendOnInterface, got {:?}", other),
}
}
}
#[test]
fn host_emit_event_refuses_events_beyond_count_cap() {
let mgr = make_manager();
let cap = crate::runtime::MAX_PROVIDER_EVENTS_PER_CALL;
let imports =
r#"(import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))"#;
let body = format!(
r#"
(i32.store (i32.const 0x3000) (i32.const 0))
(local.set $i (i32.const 0))
(block $done (loop $next
(br_if $done (i32.ge_u (local.get $i) (i32.const {count})))
(i32.store (i32.const 0x3004)
(call $emit (i32.const 0x3100) (i32.const 4) (i32.const 0x3200) (i32.const 8)))
(if (i32.eqz (i32.load (i32.const 0x3004)))
(then (i32.store (i32.const 0x3000)
(i32.add (i32.load (i32.const 0x3000)) (i32.const 1)))))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $next)))
"#,
count = cap + 10
);
let wat = hook_module(imports, 1, &body, 8);
let mut prog = mgr.compile("emit".into(), wat.as_bytes(), 0).unwrap();
let exec = mgr
.execute_program_with_provider_events(
&mut prog,
&HookContext::Tick,
&NullEngine,
0.0,
true,
None,
)
.unwrap();
assert_eq!(
read_i32s(&exec.modified_data.unwrap()),
vec![cap as i32, host_fns::LIMIT_REACHED]
);
assert_eq!(exec.provider_events.len(), cap);
}
#[test]
fn host_copies_consume_fuel_per_byte() {
let mgr = make_manager();
let imports =
r#"(import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))"#;
let fuel_used = |payload_len: u32| {
let body = format!(
r#"(drop (call $emit (i32.const 0x100) (i32.const 4)
(i32.const 0x4000) (i32.const {payload_len})))"#
);
let wat = hook_module(imports, 2, &body, 0);
let mut prog = mgr.compile("fuel".into(), wat.as_bytes(), 0).unwrap();
let exec = mgr
.execute_program_with_provider_events(
&mut prog,
&HookContext::Tick,
&NullEngine,
0.0,
true,
None,
)
.unwrap();
assert_eq!(exec.provider_events[0].payload.len(), payload_len as usize);
mgr.runtime.fuel() - cached_store(&prog).get_fuel().unwrap()
};
let small = fuel_used(0);
let large = fuel_used(100_000);
assert!(
large - small >= 100_000,
"fuel used: {} without payload, {} with 100000 bytes",
small,
large
);
}
}