1#[cfg(any(feature = "wasm", feature = "native"))]
2use crate::arena;
3use crate::engine_access::EngineAccess;
4#[cfg(feature = "wasm")]
5use crate::engine_access::NullEngine;
6use crate::error::HookError;
7use crate::hooks::HookContext;
8#[cfg(feature = "wasm")]
9use crate::host_fns;
10use crate::program::{LoadedProgram, ProgramBackend};
11use crate::result::{ExecuteResult, HookResult, Verdict};
12#[cfg(feature = "wasm")]
13use crate::runtime::{StoreData, WasmRuntime};
14#[cfg(feature = "wasm")]
15use wasmtime::{Linker, Store};
16
17#[cfg(feature = "wasm")]
19const HOST_ABI_VERSION: i32 = rns_hooks_abi::ABI_VERSION;
20
21pub struct HookManager {
26 #[cfg(feature = "wasm")]
27 runtime: WasmRuntime,
28 #[cfg(feature = "wasm")]
29 linker: Linker<StoreData>,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum HookBackend {
34 Wasm,
35 Native,
36 Builtin,
37}
38
39impl HookBackend {
40 pub fn as_str(self) -> &'static str {
41 match self {
42 HookBackend::Wasm => "wasm",
43 HookBackend::Native => "native",
44 HookBackend::Builtin => "builtin",
45 }
46 }
47}
48
49impl std::str::FromStr for HookBackend {
50 type Err = String;
51
52 fn from_str(value: &str) -> Result<Self, Self::Err> {
53 match value.to_ascii_lowercase().as_str() {
54 "wasm" => Ok(HookBackend::Wasm),
55 "native" | "dylib" | "dynamic" => Ok(HookBackend::Native),
56 "builtin" | "built-in" | "static" => Ok(HookBackend::Builtin),
57 other => Err(format!("unknown hook type '{}'", other)),
58 }
59 }
60}
61
62impl HookManager {
63 pub fn new() -> Result<Self, HookError> {
64 #[cfg(feature = "wasm")]
65 {
66 let runtime = WasmRuntime::new().map_err(|e| HookError::CompileError(e.to_string()))?;
67 let mut linker = Linker::new(runtime.engine());
68 host_fns::register_host_functions(&mut linker)
69 .map_err(|e| HookError::CompileError(e.to_string()))?;
70 Ok(HookManager { runtime, linker })
71 }
72 #[cfg(not(feature = "wasm"))]
73 {
74 Ok(HookManager {})
75 }
76 }
77
78 pub fn compile(
83 &self,
84 name: String,
85 bytes: &[u8],
86 priority: i32,
87 ) -> Result<LoadedProgram, HookError> {
88 #[cfg(not(feature = "wasm"))]
89 {
90 let _ = (name, bytes, priority);
91 return Err(HookError::CompileError(
92 "WASM hook backend not enabled".to_string(),
93 ));
94 }
95 #[cfg(feature = "wasm")]
96 {
97 let module = self
98 .runtime
99 .compile(bytes)
100 .map_err(|e| HookError::CompileError(e.to_string()))?;
101 self.validate_abi_version(&name, &module)?;
102 Ok(LoadedProgram::new(name, module, priority))
103 }
104 }
105
106 #[cfg(feature = "wasm")]
109 fn validate_abi_version(&self, name: &str, module: &wasmtime::Module) -> Result<(), HookError> {
110 let has_export = module.exports().any(|e| e.name() == "__rns_abi_version");
112 if !has_export {
113 return Err(HookError::AbiVersionMismatch {
114 hook_name: name.to_string(),
115 expected: HOST_ABI_VERSION,
116 found: None,
117 });
118 }
119
120 static NULL_ENGINE: NullEngine = NullEngine;
122 let mut store = Store::new(
123 self.runtime.engine(),
124 StoreData {
125 engine_access: &NULL_ENGINE as *const dyn EngineAccess,
126 now: 0.0,
127 injected_actions: Vec::new(),
128 log_messages: Vec::new(),
129 provider_events: Vec::new(),
130 provider_events_enabled: false,
131 },
132 );
133 store
134 .set_fuel(self.runtime.fuel())
135 .map_err(|e| HookError::CompileError(e.to_string()))?;
136
137 let instance = self
138 .linker
139 .instantiate(&mut store, module)
140 .map_err(|e| HookError::InstantiationError(e.to_string()))?;
141
142 let func = instance
143 .get_typed_func::<(), i32>(&mut store, "__rns_abi_version")
144 .map_err(|e| {
145 HookError::CompileError(format!("__rns_abi_version has wrong signature: {}", e))
146 })?;
147
148 let version = func
149 .call(&mut store, ())
150 .map_err(|e| HookError::Trap(format!("__rns_abi_version trapped: {}", e)))?;
151
152 if version != HOST_ABI_VERSION {
153 return Err(HookError::AbiVersionMismatch {
154 hook_name: name.to_string(),
155 expected: HOST_ABI_VERSION,
156 found: Some(version),
157 });
158 }
159
160 Ok(())
161 }
162
163 pub fn load_file(
165 &self,
166 name: String,
167 path: &std::path::Path,
168 priority: i32,
169 ) -> Result<LoadedProgram, HookError> {
170 let bytes = std::fs::read(path)?;
171 self.compile(name, &bytes, priority)
172 }
173
174 pub fn load_file_backend(
175 &self,
176 name: String,
177 path: &std::path::Path,
178 priority: i32,
179 backend: HookBackend,
180 ) -> Result<LoadedProgram, HookError> {
181 match backend {
182 HookBackend::Wasm => self.load_file(name, path, priority),
183 HookBackend::Builtin => Err(HookError::CompileError(
184 "built-in hooks are loaded by ID, not file path".to_string(),
185 )),
186 HookBackend::Native => {
187 #[cfg(feature = "native")]
188 {
189 let native = crate::native::NativeProgram::load(path)?;
190 Ok(LoadedProgram::new_native(name, native, priority))
191 }
192 #[cfg(not(feature = "native"))]
193 {
194 let _ = (name, path, priority);
195 Err(HookError::CompileError(
196 "native hook backend not enabled".to_string(),
197 ))
198 }
199 }
200 }
201 }
202
203 pub fn load_builtin(
204 &self,
205 name: String,
206 id: impl Into<String>,
207 priority: i32,
208 ) -> Result<LoadedProgram, HookError> {
209 let builtin = crate::builtin::BuiltinProgram::load(id)?;
210 Ok(LoadedProgram::new_builtin(name, builtin, priority))
211 }
212
213 pub fn execute_program(
226 &self,
227 program: &mut LoadedProgram,
228 ctx: &HookContext,
229 engine_access: &dyn EngineAccess,
230 now: f64,
231 data_override: Option<&[u8]>,
232 ) -> Option<ExecuteResult> {
233 self.execute_program_with_provider_events(
234 program,
235 ctx,
236 engine_access,
237 now,
238 false,
239 data_override,
240 )
241 }
242
243 pub fn execute_program_with_provider_events(
244 &self,
245 program: &mut LoadedProgram,
246 ctx: &HookContext,
247 engine_access: &dyn EngineAccess,
248 now: f64,
249 provider_events_enabled: bool,
250 data_override: Option<&[u8]>,
251 ) -> Option<ExecuteResult> {
252 if !program.enabled {
253 return None;
254 }
255
256 if matches!(program.backend, ProgramBackend::Builtin(_)) {
257 let result = match &program.backend {
258 ProgramBackend::Builtin(builtin) => builtin.execute(
259 ctx,
260 data_override,
261 engine_access,
262 now,
263 provider_events_enabled,
264 ),
265 #[cfg(feature = "wasm")]
266 ProgramBackend::Wasm(_) => unreachable!(),
267 #[cfg(feature = "native")]
268 ProgramBackend::Native(_) => unreachable!(),
269 };
270 return match result {
271 Ok(mut exec) => {
272 program.record_success();
273 for event in &mut exec.provider_events {
274 event.hook_name = program.name.clone();
275 }
276 Some(exec)
277 }
278 Err(e) => {
279 let auto_disabled = program.record_trap();
280 if auto_disabled {
281 log::error!(
282 "built-in hook '{}' auto-disabled after {} consecutive errors",
283 program.name,
284 program.consecutive_traps
285 );
286 } else {
287 log::warn!("built-in hook '{}' failed: {}", program.name, e);
288 }
289 None
290 }
291 };
292 }
293
294 #[cfg(feature = "native")]
295 if matches!(program.backend, ProgramBackend::Native(_)) {
296 let ctx_bytes = match arena::context_to_bytes(ctx, data_override) {
297 Ok(bytes) => bytes,
298 Err(e) => {
299 log::warn!(
300 "failed to encode native hook context '{}': {}",
301 program.name,
302 e
303 );
304 program.record_trap();
305 return None;
306 }
307 };
308 let result = match &program.backend {
309 ProgramBackend::Native(native) => {
310 native.execute(&ctx_bytes, engine_access, provider_events_enabled)
311 }
312 ProgramBackend::Builtin(_) => unreachable!(),
313 #[cfg(feature = "wasm")]
314 ProgramBackend::Wasm(_) => unreachable!(),
315 };
316 return match result {
317 Ok(mut exec) => {
318 program.record_success();
319 for event in &mut exec.provider_events {
320 event.hook_name = program.name.clone();
321 }
322 Some(exec)
323 }
324 Err(e) => {
325 let auto_disabled = program.record_trap();
326 if auto_disabled {
327 log::error!(
328 "native hook '{}' auto-disabled after {} consecutive errors",
329 program.name,
330 program.consecutive_traps
331 );
332 } else {
333 log::warn!("native hook '{}' failed: {}", program.name, e);
334 }
335 None
336 }
337 };
338 }
339
340 #[cfg(not(feature = "wasm"))]
341 {
342 let _ = (
343 ctx,
344 engine_access,
345 now,
346 provider_events_enabled,
347 data_override,
348 );
349 return None;
350 }
351
352 #[cfg(feature = "wasm")]
353 {
354 self.execute_wasm_program(
355 program,
356 ctx,
357 engine_access,
358 now,
359 provider_events_enabled,
360 data_override,
361 )
362 }
363 }
364
365 #[cfg(feature = "wasm")]
366 fn execute_wasm_program(
367 &self,
368 program: &mut LoadedProgram,
369 ctx: &HookContext,
370 engine_access: &dyn EngineAccess,
371 now: f64,
372 provider_events_enabled: bool,
373 data_override: Option<&[u8]>,
374 ) -> Option<ExecuteResult> {
375 let engine_access_ptr: *const dyn EngineAccess =
378 unsafe { std::mem::transmute(engine_access as *const dyn EngineAccess) };
379
380 let cached = match &mut program.backend {
383 ProgramBackend::Builtin(_) => unreachable!(),
384 ProgramBackend::Wasm(wasm) => wasm.cached.take(),
385 #[cfg(feature = "native")]
386 ProgramBackend::Native(_) => unreachable!(),
387 };
388 let (mut store, instance) = if let Some(cached) = cached {
389 let (mut s, i) = cached;
390 s.data_mut()
392 .reset_per_call(engine_access_ptr, now, provider_events_enabled);
393 if let Err(e) = s.set_fuel(self.runtime.fuel()) {
394 log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
395 self.cache_wasm_instance(program, s, i);
396 return None;
397 }
398 (s, i)
399 } else {
400 let store_data = StoreData {
401 engine_access: engine_access_ptr,
402 now,
403 injected_actions: Vec::new(),
404 log_messages: Vec::new(),
405 provider_events: Vec::new(),
406 provider_events_enabled,
407 };
408
409 let mut store = Store::new(self.runtime.engine(), store_data);
410 if let Err(e) = store.set_fuel(self.runtime.fuel()) {
411 log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
412 return None;
413 }
414
415 let module = match &program.backend {
416 ProgramBackend::Builtin(_) => unreachable!(),
417 ProgramBackend::Wasm(wasm) => wasm.module.clone(),
418 #[cfg(feature = "native")]
419 ProgramBackend::Native(_) => unreachable!(),
420 };
421 let instance = match self.linker.instantiate(&mut store, &module) {
422 Ok(inst) => inst,
423 Err(e) => {
424 log::warn!("failed to instantiate hook '{}': {}", program.name, e);
425 program.record_trap();
426 return None;
427 }
428 };
429
430 (store, instance)
431 };
432
433 let memory = match instance.get_memory(&mut store, "memory") {
435 Some(mem) => mem,
436 None => {
437 log::warn!("hook '{}' has no exported memory", program.name);
438 program.record_trap();
439 self.cache_wasm_instance(program, store, instance);
440 return None;
441 }
442 };
443
444 if let Err(e) = arena::write_context(&memory, &mut store, ctx) {
445 log::warn!("failed to write context for hook '{}': {}", program.name, e);
446 program.record_trap();
447 self.cache_wasm_instance(program, store, instance);
448 return None;
449 }
450
451 if let Some(override_data) = data_override {
453 if let Err(e) = arena::write_data_override(&memory, &mut store, override_data) {
454 log::warn!(
455 "failed to write data override for hook '{}': {}",
456 program.name,
457 e
458 );
459 }
461 }
462
463 let export_name = match &program.backend {
465 ProgramBackend::Builtin(_) => unreachable!(),
466 ProgramBackend::Wasm(wasm) => wasm.export_name.clone(),
467 #[cfg(feature = "native")]
468 ProgramBackend::Native(_) => unreachable!(),
469 };
470 let func = match instance.get_typed_func::<i32, i32>(&mut store, &export_name) {
471 Ok(f) => f,
472 Err(e) => {
473 log::warn!(
474 "hook '{}' missing export '{}': {}",
475 program.name,
476 export_name,
477 e
478 );
479 program.record_trap();
480 self.cache_wasm_instance(program, store, instance);
481 return None;
482 }
483 };
484
485 let result_offset = match func.call(&mut store, arena::ARENA_BASE as i32) {
486 Ok(offset) => offset,
487 Err(e) => {
488 let auto_disabled = program.record_trap();
490 if auto_disabled {
491 log::error!(
492 "hook '{}' auto-disabled after {} consecutive traps",
493 program.name,
494 program.consecutive_traps
495 );
496 } else {
497 log::warn!("hook '{}' trapped: {}", program.name, e);
498 }
499 self.cache_wasm_instance(program, store, instance);
500 return None;
501 }
502 };
503
504 let ret = match arena::read_result(&memory, &store, result_offset as usize) {
506 Ok(result) => {
507 program.record_success();
508
509 let modified_data = if Verdict::from_u32(result.verdict) == Some(Verdict::Modify) {
511 arena::read_modified_data(&memory, &store, &result)
512 } else {
513 None
514 };
515
516 let injected_actions = std::mem::take(&mut store.data_mut().injected_actions);
518 let provider_events = std::mem::take(&mut store.data_mut().provider_events)
519 .into_iter()
520 .map(|event| crate::result::EmittedProviderEvent {
521 hook_name: program.name.clone(),
522 payload_type: event.payload_type,
523 payload: event.payload,
524 })
525 .collect();
526
527 Some(ExecuteResult {
528 hook_result: Some(result),
529 injected_actions,
530 provider_events,
531 modified_data,
532 })
533 }
534 Err(e) => {
535 log::warn!("hook '{}' returned invalid result: {}", program.name, e);
536 program.record_trap();
537 None
538 }
539 };
540
541 self.cache_wasm_instance(program, store, instance);
543 ret
544 }
545
546 #[cfg(feature = "wasm")]
547 fn cache_wasm_instance(
548 &self,
549 program: &mut LoadedProgram,
550 store: Store<StoreData>,
551 instance: wasmtime::Instance,
552 ) {
553 match &mut program.backend {
554 ProgramBackend::Builtin(_) => unreachable!(),
555 ProgramBackend::Wasm(wasm) => wasm.cached = Some((store, instance)),
556 #[cfg(feature = "native")]
557 ProgramBackend::Native(_) => unreachable!(),
558 }
559 }
560
561 pub fn run_chain(
568 &self,
569 programs: &mut [LoadedProgram],
570 ctx: &HookContext,
571 engine_access: &dyn EngineAccess,
572 now: f64,
573 ) -> Option<ExecuteResult> {
574 self.run_chain_with_provider_events(programs, ctx, engine_access, now, false)
575 }
576
577 pub fn run_chain_with_provider_events(
578 &self,
579 programs: &mut [LoadedProgram],
580 ctx: &HookContext,
581 engine_access: &dyn EngineAccess,
582 now: f64,
583 provider_events_enabled: bool,
584 ) -> Option<ExecuteResult> {
585 let mut accumulated_actions = Vec::new();
586 let mut accumulated_provider_events = Vec::new();
587 let mut last_result: Option<HookResult> = None;
588 let mut last_modified_data: Option<Vec<u8>> = None;
589 let is_packet_ctx = matches!(ctx, HookContext::Packet { .. });
590
591 for program in programs.iter_mut() {
592 if !program.enabled {
593 continue;
594 }
595 let override_ref = if is_packet_ctx {
596 last_modified_data.as_deref()
597 } else {
598 None
599 };
600 if let Some(exec_result) = self.execute_program_with_provider_events(
601 program,
602 ctx,
603 engine_access,
604 now,
605 provider_events_enabled,
606 override_ref,
607 ) {
608 accumulated_actions.extend(exec_result.injected_actions);
609 accumulated_provider_events.extend(exec_result.provider_events);
610
611 if let Some(ref result) = exec_result.hook_result {
612 let verdict = Verdict::from_u32(result.verdict);
613 match verdict {
614 Some(Verdict::Drop) | Some(Verdict::Halt) => {
615 return Some(ExecuteResult {
616 hook_result: exec_result.hook_result,
617 injected_actions: accumulated_actions,
618 provider_events: accumulated_provider_events,
619 modified_data: exec_result.modified_data.or(last_modified_data),
620 });
621 }
622 Some(Verdict::Modify) => {
623 last_result = exec_result.hook_result;
624 if is_packet_ctx {
625 if let Some(data) = exec_result.modified_data {
626 last_modified_data = Some(data);
627 }
628 }
629 }
630 _ => {} }
632 }
633 }
634 }
635
636 if last_result.is_some()
637 || !accumulated_actions.is_empty()
638 || !accumulated_provider_events.is_empty()
639 {
640 Some(ExecuteResult {
641 hook_result: last_result,
642 injected_actions: accumulated_actions,
643 provider_events: accumulated_provider_events,
644 modified_data: last_modified_data,
645 })
646 } else {
647 None
648 }
649 }
650}
651
652#[cfg(all(test, feature = "wasm"))]
653mod tests {
654 use super::*;
655 use crate::engine_access::NullEngine;
656
657 fn make_manager() -> HookManager {
658 HookManager::new().expect("failed to create HookManager")
659 }
660
661 const WAT_CONTINUE: &str = r#"
663 (module
664 (memory (export "memory") 1)
665 (func (export "__rns_abi_version") (result i32) (i32.const 1))
666 (func (export "on_hook") (param i32) (result i32)
667 ;; Write HookResult at offset 0x2000
668 ;; verdict = 0 (Continue)
669 (i32.store (i32.const 0x2000) (i32.const 0))
670 ;; modified_data_offset = 0
671 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
672 ;; modified_data_len = 0
673 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
674 ;; inject_actions_offset = 0
675 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
676 ;; inject_actions_count = 0
677 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
678 ;; log_offset = 0
679 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
680 ;; log_len = 0
681 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
682 (i32.const 0x2000)
683 )
684 )
685 "#;
686
687 const WAT_DROP: &str = r#"
689 (module
690 (memory (export "memory") 1)
691 (func (export "__rns_abi_version") (result i32) (i32.const 1))
692 (func (export "on_hook") (param i32) (result i32)
693 (i32.store (i32.const 0x2000) (i32.const 1))
694 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
695 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
696 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
697 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
698 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
699 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
700 (i32.const 0x2000)
701 )
702 )
703 "#;
704
705 const WAT_TRAP: &str = r#"
707 (module
708 (memory (export "memory") 1)
709 (func (export "__rns_abi_version") (result i32) (i32.const 1))
710 (func (export "on_hook") (param i32) (result i32)
711 unreachable
712 )
713 )
714 "#;
715
716 const WAT_INFINITE: &str = r#"
718 (module
719 (memory (export "memory") 1)
720 (func (export "__rns_abi_version") (result i32) (i32.const 1))
721 (func (export "on_hook") (param i32) (result i32)
722 (loop $inf (br $inf))
723 (i32.const 0)
724 )
725 )
726 "#;
727
728 const WAT_FUEL_RECOVERY: &str = r#"
731 (module
732 (memory (export "memory") 1)
733 (global $calls (mut i32) (i32.const 0))
734 (func (export "__rns_abi_version") (result i32) (i32.const 1))
735 (func (export "on_hook") (param i32) (result i32)
736 (global.set $calls (i32.add (global.get $calls) (i32.const 1)))
737 (if (i32.eq (global.get $calls) (i32.const 1))
738 (then (loop $inf (br $inf))))
739 (i32.store (i32.const 0x2000) (i32.const 0))
740 (i32.store (i32.const 0x2004) (i32.const 0))
741 (i32.store (i32.const 0x2008) (i32.const 0))
742 (i32.store (i32.const 0x200c) (i32.const 0))
743 (i32.store (i32.const 0x2010) (i32.const 0))
744 (i32.store (i32.const 0x2014) (i32.const 0))
745 (i32.store (i32.const 0x2018) (i32.const 0))
746 (i32.const 0x2000))
747 )
748 "#;
749
750 const WAT_BULK_MEMORY: &str = r#"
753 (module
754 (memory (export "memory") 2)
755 (func (export "__rns_abi_version") (result i32) (i32.const 1))
756 (func (export "on_hook") (param i32) (result i32)
757 (memory.fill (i32.const 0x10000) (i32.const 0x5a) (i32.const 0x4000))
758 (memory.copy (i32.const 0x14000) (i32.const 0x10000) (i32.const 0x4000))
759 (i32.store (i32.const 0x2000) (i32.const 2))
760 (i32.store (i32.const 0x2004) (i32.const 0x14000))
761 (i32.store (i32.const 0x2008) (i32.const 0x4000))
762 (i32.store (i32.const 0x200c) (i32.const 0))
763 (i32.store (i32.const 0x2010) (i32.const 0))
764 (i32.store (i32.const 0x2014) (i32.const 0))
765 (i32.store (i32.const 0x2018) (i32.const 0))
766 (i32.const 0x2000))
767 )
768 "#;
769
770 const WAT_HOST_HAS_PATH: &str = r#"
772 (module
773 (import "env" "host_has_path" (func $has_path (param i32) (result i32)))
774 (memory (export "memory") 1)
775 (func (export "__rns_abi_version") (result i32) (i32.const 1))
776 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
777 ;; Check if path exists for a 16-byte dest at offset 0x3000
778 ;; (we'll write the dest hash there in the test)
779 (if (call $has_path (i32.const 0x3000))
780 (then
781 ;; Drop
782 (i32.store (i32.const 0x2000) (i32.const 1))
783 )
784 (else
785 ;; Continue
786 (i32.store (i32.const 0x2000) (i32.const 0))
787 )
788 )
789 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
790 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
791 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
792 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
793 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
794 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
795 (i32.const 0x2000)
796 )
797 )
798 "#;
799
800 #[test]
801 fn pass_through() {
802 let mgr = make_manager();
803 let mut prog = mgr
804 .compile("test".into(), WAT_CONTINUE.as_bytes(), 0)
805 .unwrap();
806 let ctx = HookContext::Tick;
807 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
808 let exec = result.unwrap();
810 let r = exec.hook_result.unwrap();
811 assert_eq!(r.verdict, Verdict::Continue as u32);
812 }
813
814 #[test]
815 fn drop_hook() {
816 let mgr = make_manager();
817 let mut prog = mgr
818 .compile("dropper".into(), WAT_DROP.as_bytes(), 0)
819 .unwrap();
820 let ctx = HookContext::Tick;
821 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
822 let exec = result.unwrap();
823 let r = exec.hook_result.unwrap();
824 assert!(r.is_drop());
825 }
826
827 #[test]
828 fn trap_failopen() {
829 let mgr = make_manager();
830 let mut prog = mgr.compile("trap".into(), WAT_TRAP.as_bytes(), 0).unwrap();
831 let ctx = HookContext::Tick;
832 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
833 assert!(result.is_none());
834 assert_eq!(prog.consecutive_traps, 1);
835 assert!(prog.enabled);
836 }
837
838 #[test]
839 fn auto_disable() {
840 let mgr = make_manager();
841 let mut prog = mgr.compile("bad".into(), WAT_TRAP.as_bytes(), 0).unwrap();
842 let ctx = HookContext::Tick;
843 for _ in 0..10 {
844 let _ = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
845 }
846 assert!(!prog.enabled);
847 assert_eq!(prog.consecutive_traps, 10);
848 }
849
850 #[test]
851 fn fuel_exhaustion() {
852 let mgr = make_manager();
853 let mut prog = mgr
854 .compile("loop".into(), WAT_INFINITE.as_bytes(), 0)
855 .unwrap();
856 let ctx = HookContext::Tick;
857 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
858 assert!(result.is_none());
860 assert_eq!(prog.consecutive_traps, 1);
861 }
862
863 #[test]
864 fn fuel_exhaustion_recovers_cached_store_on_next_call() {
865 let mgr = make_manager();
866 let mut prog = mgr
867 .compile("fuel-recovery".into(), WAT_FUEL_RECOVERY.as_bytes(), 0)
868 .unwrap();
869 let ctx = HookContext::Tick;
870
871 assert!(mgr
872 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
873 .is_none());
874 assert_eq!(prog.consecutive_traps, 1);
875
876 let recovered = mgr
877 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
878 .expect("fuel must be reset before reusing the cached store");
879 assert_eq!(
880 recovered.hook_result.unwrap().verdict,
881 Verdict::Continue as u32
882 );
883 assert_eq!(prog.consecutive_traps, 0);
884 }
885
886 #[test]
887 fn bulk_memory_fill_and_copy_preserve_all_bytes() {
888 let mgr = make_manager();
889 let mut prog = mgr
890 .compile("bulk-memory".into(), WAT_BULK_MEMORY.as_bytes(), 0)
891 .unwrap();
892
893 let result = mgr
894 .execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
895 .unwrap();
896 let copied = result.modified_data.unwrap();
897 assert_eq!(copied.len(), 0x4000);
898 assert!(copied.iter().all(|byte| *byte == 0x5a));
899 }
900
901 #[test]
902 fn chain_ordering() {
903 let mgr = make_manager();
904 let high = mgr
905 .compile("high".into(), WAT_DROP.as_bytes(), 100)
906 .unwrap();
907 let low = mgr
908 .compile("low".into(), WAT_CONTINUE.as_bytes(), 0)
909 .unwrap();
910 let mut programs = vec![high, low];
912 programs.sort_by_key(|program| std::cmp::Reverse(program.priority));
914
915 let ctx = HookContext::Tick;
916 let result = mgr.run_chain(&mut programs, &ctx, &NullEngine, 0.0);
917 let exec = result.unwrap();
919 let r = exec.hook_result.unwrap();
920 assert!(r.is_drop());
921 }
922
923 #[test]
924 fn attach_detach() {
925 use crate::hooks::HookSlot;
926
927 let mgr = make_manager();
928 let mut slot = HookSlot {
929 programs: Vec::new(),
930 runner: crate::hooks::hook_noop,
931 };
932
933 let p1 = mgr
934 .compile("alpha".into(), WAT_CONTINUE.as_bytes(), 10)
935 .unwrap();
936 let p2 = mgr.compile("beta".into(), WAT_DROP.as_bytes(), 20).unwrap();
937
938 slot.attach(p1);
939 assert_eq!(slot.programs.len(), 1);
940 assert!(!std::ptr::eq(
941 slot.runner as *const (),
942 crate::hooks::hook_noop as *const (),
943 ));
944
945 slot.attach(p2);
946 assert_eq!(slot.programs.len(), 2);
947 assert_eq!(slot.programs[0].name, "beta");
949 assert_eq!(slot.programs[1].name, "alpha");
950
951 let removed = slot.detach("beta");
952 assert!(removed.is_some());
953 assert_eq!(slot.programs.len(), 1);
954 assert_eq!(slot.programs[0].name, "alpha");
955
956 let removed2 = slot.detach("alpha");
957 assert!(removed2.is_some());
958 assert!(slot.programs.is_empty());
959 assert_eq!(
960 slot.runner as *const () as usize,
961 crate::hooks::hook_noop as *const () as usize
962 );
963 }
964
965 #[test]
966 fn host_has_path() {
967 use crate::engine_access::EngineAccess;
968
969 struct MockEngine;
970 impl EngineAccess for MockEngine {
971 fn has_path(&self, _dest: &[u8; 16]) -> bool {
972 true
973 }
974 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
975 None
976 }
977 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
978 None
979 }
980 fn is_blackholed(&self, _: &[u8; 16]) -> bool {
981 false
982 }
983 fn interface_name(&self, _: u64) -> Option<String> {
984 None
985 }
986 fn interface_mode(&self, _: u64) -> Option<u8> {
987 None
988 }
989 fn identity_hash(&self) -> Option<[u8; 16]> {
990 None
991 }
992 fn announce_rate(&self, _: u64) -> Option<i32> {
993 None
994 }
995 fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
996 None
997 }
998 }
999
1000 let mgr = make_manager();
1001 let mut prog = mgr
1002 .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
1003 .unwrap();
1004 let ctx = HookContext::Tick;
1005 let result = mgr.execute_program(&mut prog, &ctx, &MockEngine, 0.0, None);
1006 let exec = result.unwrap();
1008 let r = exec.hook_result.unwrap();
1009 assert!(r.is_drop());
1010 }
1011
1012 #[test]
1013 fn host_has_path_null_engine() {
1014 let mgr = make_manager();
1016 let mut prog = mgr
1017 .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
1018 .unwrap();
1019 let ctx = HookContext::Tick;
1020 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
1021 let exec = result.unwrap();
1022 let r = exec.hook_result.unwrap();
1023 assert_eq!(r.verdict, Verdict::Continue as u32);
1024 }
1025
1026 struct MockEngineCustom {
1030 announce_rate_val: Option<i32>,
1031 link_state_val: Option<u8>,
1032 }
1033
1034 impl EngineAccess for MockEngineCustom {
1035 fn has_path(&self, _: &[u8; 16]) -> bool {
1036 false
1037 }
1038 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
1039 None
1040 }
1041 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
1042 None
1043 }
1044 fn is_blackholed(&self, _: &[u8; 16]) -> bool {
1045 false
1046 }
1047 fn interface_name(&self, _: u64) -> Option<String> {
1048 None
1049 }
1050 fn interface_mode(&self, _: u64) -> Option<u8> {
1051 None
1052 }
1053 fn identity_hash(&self) -> Option<[u8; 16]> {
1054 None
1055 }
1056 fn announce_rate(&self, _: u64) -> Option<i32> {
1057 self.announce_rate_val
1058 }
1059 fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
1060 self.link_state_val
1061 }
1062 }
1063
1064 const WAT_ANNOUNCE_RATE: &str = r#"
1066 (module
1067 (import "env" "host_get_announce_rate" (func $get_rate (param i64) (result i32)))
1068 (memory (export "memory") 1)
1069 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1070 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
1071 (if (i32.ge_s (call $get_rate (i64.const 42)) (i32.const 0))
1072 (then
1073 (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
1074 )
1075 (else
1076 (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
1077 )
1078 )
1079 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1080 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1081 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1082 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1083 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1084 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1085 (i32.const 0x2000)
1086 )
1087 )
1088 "#;
1089
1090 #[test]
1091 fn host_get_announce_rate_found() {
1092 let engine = MockEngineCustom {
1094 announce_rate_val: Some(1500),
1095 link_state_val: None,
1096 };
1097 let mgr = make_manager();
1098 let mut prog = mgr
1099 .compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
1100 .unwrap();
1101 let ctx = HookContext::Tick;
1102 let exec = mgr
1103 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1104 .unwrap();
1105 assert!(exec.hook_result.unwrap().is_drop());
1106 }
1107
1108 #[test]
1109 fn host_get_announce_rate_not_found() {
1110 let engine = MockEngineCustom {
1112 announce_rate_val: None,
1113 link_state_val: None,
1114 };
1115 let mgr = make_manager();
1116 let mut prog = mgr
1117 .compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
1118 .unwrap();
1119 let ctx = HookContext::Tick;
1120 let exec = mgr
1121 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1122 .unwrap();
1123 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
1124 }
1125
1126 const WAT_LINK_STATE: &str = r#"
1129 (module
1130 (import "env" "host_get_link_state" (func $link_state (param i32) (result i32)))
1131 (memory (export "memory") 1)
1132 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1133 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
1134 (if (i32.eq (call $link_state (i32.const 0x3000)) (i32.const 2))
1135 (then
1136 (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
1137 )
1138 (else
1139 (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
1140 )
1141 )
1142 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1143 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1144 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1145 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1146 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1147 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1148 (i32.const 0x2000)
1149 )
1150 )
1151 "#;
1152
1153 #[test]
1154 fn host_get_link_state_active() {
1155 let engine = MockEngineCustom {
1157 announce_rate_val: None,
1158 link_state_val: Some(2),
1159 };
1160 let mgr = make_manager();
1161 let mut prog = mgr
1162 .compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
1163 .unwrap();
1164 let ctx = HookContext::Tick;
1165 let exec = mgr
1166 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1167 .unwrap();
1168 assert!(exec.hook_result.unwrap().is_drop());
1169 }
1170
1171 #[test]
1172 fn host_get_link_state_not_found() {
1173 let engine = MockEngineCustom {
1175 announce_rate_val: None,
1176 link_state_val: None,
1177 };
1178 let mgr = make_manager();
1179 let mut prog = mgr
1180 .compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
1181 .unwrap();
1182 let ctx = HookContext::Tick;
1183 let exec = mgr
1184 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1185 .unwrap();
1186 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
1187 }
1188
1189 const WAT_INJECT_ACTION: &str = r#"
1194 (module
1195 (import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))
1196 (memory (export "memory") 1)
1197 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1198 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
1199 ;; Write the data payload at 0x3100
1200 (i32.store8 (i32.const 0x3100) (i32.const 0xDE))
1201 (i32.store8 (i32.const 0x3101) (i32.const 0xAD))
1202 (i32.store8 (i32.const 0x3102) (i32.const 0xBE))
1203 (i32.store8 (i32.const 0x3103) (i32.const 0xEF))
1204
1205 ;; Write ActionWire at 0x3000:
1206 ;; byte 0: tag = 0 (SendOnInterface)
1207 (i32.store8 (i32.const 0x3000) (i32.const 0))
1208 ;; bytes 1-8: interface = 1 (u64 LE)
1209 (i64.store (i32.const 0x3001) (i64.const 1))
1210 ;; bytes 9-12: data_offset = 0x3100 (u32 LE)
1211 (i32.store (i32.const 0x3009) (i32.const 0x3100))
1212 ;; bytes 13-16: data_len = 4 (u32 LE)
1213 (i32.store (i32.const 0x300D) (i32.const 4))
1214
1215 ;; Call inject: ptr=0x3000, len=17 (1 + 8 + 4 + 4)
1216 (drop (call $inject (i32.const 0x3000) (i32.const 17)))
1217
1218 ;; Return Continue
1219 (i32.store (i32.const 0x2000) (i32.const 0))
1220 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1221 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1222 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1223 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1224 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1225 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1226 (i32.const 0x2000)
1227 )
1228 )
1229 "#;
1230
1231 #[test]
1232 fn host_inject_action_send() {
1233 let mgr = make_manager();
1234 let mut prog = mgr
1235 .compile("inject".into(), WAT_INJECT_ACTION.as_bytes(), 0)
1236 .unwrap();
1237 let ctx = HookContext::Tick;
1238 let exec = mgr
1239 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1240 .unwrap();
1241 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
1242 assert_eq!(exec.injected_actions.len(), 1);
1243 match &exec.injected_actions[0] {
1244 crate::wire::ActionWire::SendOnInterface { interface, raw } => {
1245 assert_eq!(*interface, 1);
1246 assert_eq!(raw, &[0xDE, 0xAD, 0xBE, 0xEF]);
1247 }
1248 other => panic!("expected SendOnInterface, got {:?}", other),
1249 }
1250 }
1251
1252 const WAT_MODIFY: &str = r#"
1254 (module
1255 (memory (export "memory") 1)
1256 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1257 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
1258 ;; Write modified data at 0x2100
1259 (i32.store8 (i32.const 0x2100) (i32.const 0xAA))
1260 (i32.store8 (i32.const 0x2101) (i32.const 0xBB))
1261 (i32.store8 (i32.const 0x2102) (i32.const 0xCC))
1262 (i32.store8 (i32.const 0x2103) (i32.const 0xDD))
1263
1264 ;; verdict = 2 (Modify)
1265 (i32.store (i32.const 0x2000) (i32.const 2))
1266 ;; modified_data_offset = 0x2100
1267 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x2100))
1268 ;; modified_data_len = 4
1269 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
1270 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1271 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1272 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1273 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1274 (i32.const 0x2000)
1275 )
1276 )
1277 "#;
1278
1279 #[test]
1280 fn modify_extracts_data() {
1281 let mgr = make_manager();
1282 let mut prog = mgr.compile("mod".into(), WAT_MODIFY.as_bytes(), 0).unwrap();
1283 let ctx = HookContext::Tick;
1284 let exec = mgr
1285 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1286 .unwrap();
1287 let r = exec.hook_result.unwrap();
1288 assert_eq!(r.verdict, Verdict::Modify as u32);
1289 let data = exec.modified_data.unwrap();
1290 assert_eq!(data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
1291 }
1292
1293 #[test]
1294 fn chain_accumulates_injected_actions() {
1295 let mgr = make_manager();
1298 let injector = mgr
1299 .compile("injector".into(), WAT_INJECT_ACTION.as_bytes(), 100)
1300 .unwrap();
1301 let dropper = mgr
1302 .compile("dropper".into(), WAT_DROP.as_bytes(), 0)
1303 .unwrap();
1304 let mut programs = vec![injector, dropper];
1305 programs.sort_by_key(|program| std::cmp::Reverse(program.priority));
1306
1307 let ctx = HookContext::Tick;
1308 let exec = mgr
1309 .run_chain(&mut programs, &ctx, &NullEngine, 0.0)
1310 .unwrap();
1311 assert!(exec.hook_result.unwrap().is_drop());
1313 assert_eq!(exec.injected_actions.len(), 1);
1315 }
1316
1317 const WAT_COUNTER: &str = r#"
1323 (module
1324 (memory (export "memory") 1)
1325 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1326 (global $counter (mut i32) (i32.const 0))
1327 (func (export "on_hook") (param i32) (result i32)
1328 ;; Increment counter
1329 (global.set $counter (i32.add (global.get $counter) (i32.const 1)))
1330 ;; Write counter value at 0x3000 (scratch area)
1331 (i32.store (i32.const 0x3000) (global.get $counter))
1332 ;; Return Continue with the counter stashed in modified_data region
1333 ;; verdict = 2 (Modify) so we can extract the counter via modified_data
1334 (i32.store (i32.const 0x2000) (i32.const 2))
1335 ;; modified_data_offset = 0x3000
1336 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x3000))
1337 ;; modified_data_len = 4
1338 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
1339 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1340 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1341 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1342 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1343 (i32.const 0x2000)
1344 )
1345 )
1346 "#;
1347
1348 fn extract_counter(exec: &ExecuteResult) -> u32 {
1349 let data = exec.modified_data.as_ref().expect("no modified data");
1350 assert_eq!(data.len(), 4);
1351 u32::from_le_bytes([data[0], data[1], data[2], data[3]])
1352 }
1353
1354 #[test]
1355 fn instance_persistence_counter() {
1356 let mgr = make_manager();
1357 let mut prog = mgr
1358 .compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
1359 .unwrap();
1360 let ctx = HookContext::Tick;
1361
1362 let exec1 = mgr
1364 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1365 .unwrap();
1366 assert_eq!(extract_counter(&exec1), 1);
1367
1368 let exec2 = mgr
1369 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1370 .unwrap();
1371 assert_eq!(extract_counter(&exec2), 2);
1372
1373 let exec3 = mgr
1374 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1375 .unwrap();
1376 assert_eq!(extract_counter(&exec3), 3);
1377 }
1378
1379 #[test]
1380 fn instance_persistence_resets_on_drop_cache() {
1381 let mgr = make_manager();
1382 let mut prog = mgr
1383 .compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
1384 .unwrap();
1385 let ctx = HookContext::Tick;
1386
1387 mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1389 .unwrap();
1390 let exec2 = mgr
1391 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1392 .unwrap();
1393 assert_eq!(extract_counter(&exec2), 2);
1394
1395 prog.drop_cache();
1397
1398 let exec3 = mgr
1400 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1401 .unwrap();
1402 assert_eq!(extract_counter(&exec3), 1);
1403 }
1404
1405 const WAT_NO_ABI_VERSION: &str = r#"
1409 (module
1410 (memory (export "memory") 1)
1411 (func (export "on_hook") (param i32) (result i32)
1412 (i32.store (i32.const 0x2000) (i32.const 0))
1413 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1414 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1415 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1416 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1417 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1418 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1419 (i32.const 0x2000)
1420 )
1421 )
1422 "#;
1423
1424 const WAT_WRONG_ABI_VERSION: &str = r#"
1426 (module
1427 (memory (export "memory") 1)
1428 (func (export "__rns_abi_version") (result i32) (i32.const 9999))
1429 (func (export "on_hook") (param i32) (result i32)
1430 (i32.store (i32.const 0x2000) (i32.const 0))
1431 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1432 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1433 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1434 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1435 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1436 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1437 (i32.const 0x2000)
1438 )
1439 )
1440 "#;
1441
1442 #[test]
1443 fn rejects_missing_abi_version() {
1444 let mgr = make_manager();
1445 let result = mgr.compile("no_abi".into(), WAT_NO_ABI_VERSION.as_bytes(), 0);
1446 match result {
1447 Err(HookError::AbiVersionMismatch {
1448 hook_name,
1449 expected,
1450 found,
1451 }) => {
1452 assert_eq!(hook_name, "no_abi");
1453 assert_eq!(expected, HOST_ABI_VERSION);
1454 assert_eq!(found, None);
1455 }
1456 other => panic!(
1457 "expected AbiVersionMismatch with found=None, got {:?}",
1458 other.err()
1459 ),
1460 }
1461 }
1462
1463 #[test]
1464 fn rejects_wrong_abi_version() {
1465 let mgr = make_manager();
1466 let result = mgr.compile("bad_abi".into(), WAT_WRONG_ABI_VERSION.as_bytes(), 0);
1467 match result {
1468 Err(HookError::AbiVersionMismatch {
1469 hook_name,
1470 expected,
1471 found,
1472 }) => {
1473 assert_eq!(hook_name, "bad_abi");
1474 assert_eq!(expected, HOST_ABI_VERSION);
1475 assert_eq!(found, Some(9999));
1476 }
1477 other => panic!(
1478 "expected AbiVersionMismatch with found=Some(9999), got {:?}",
1479 other.err()
1480 ),
1481 }
1482 }
1483
1484 #[test]
1485 fn accepts_correct_abi_version() {
1486 let mgr = make_manager();
1487 let result = mgr.compile("good_abi".into(), WAT_CONTINUE.as_bytes(), 0);
1488 assert!(
1489 result.is_ok(),
1490 "compile should succeed with correct ABI version"
1491 );
1492 }
1493
1494 #[test]
1495 fn host_emit_event_collects_provider_event() {
1496 let mgr = make_manager();
1497 let wat = r#"
1498 (module
1499 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1500 (memory (export "memory") 1)
1501 (data (i32.const 0x3000) "packet")
1502 (data (i32.const 0x3010) "\01\02\03")
1503 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1504 (func (export "on_hook") (param i32) (result i32)
1505 (drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
1506 (i32.store (i32.const 0x2000) (i32.const 0))
1507 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1508 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1509 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1510 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1511 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1512 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1513 (i32.const 0x2000)
1514 )
1515 )
1516 "#;
1517 let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
1518 let mut prog = LoadedProgram::new("emit".into(), module, 0);
1519 let ctx = HookContext::Tick;
1520
1521 let exec = mgr
1522 .execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, true, None)
1523 .unwrap();
1524 assert_eq!(exec.provider_events.len(), 1);
1525 assert_eq!(exec.provider_events[0].hook_name, "emit");
1526 assert_eq!(exec.provider_events[0].payload_type, "packet");
1527 assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
1528 }
1529
1530 #[test]
1531 fn host_emit_event_is_ignored_when_disabled() {
1532 let mgr = make_manager();
1533 let wat = r#"
1534 (module
1535 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1536 (memory (export "memory") 1)
1537 (data (i32.const 0x3000) "packet")
1538 (data (i32.const 0x3010) "\01\02\03")
1539 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1540 (func (export "on_hook") (param i32) (result i32)
1541 (drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
1542 (i32.store (i32.const 0x2000) (i32.const 0))
1543 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1544 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1545 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1546 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1547 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1548 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1549 (i32.const 0x2000)
1550 )
1551 )
1552 "#;
1553 let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
1554 let mut prog = LoadedProgram::new("emit".into(), module, 0);
1555 let ctx = HookContext::Tick;
1556
1557 let exec = mgr
1558 .execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, false, None)
1559 .unwrap();
1560 assert!(exec.provider_events.is_empty());
1561 }
1562
1563 #[test]
1564 fn run_chain_returns_continue_only_provider_events() {
1565 let manager = make_manager();
1566 let wasm = wat::parse_str(
1567 r#"(module
1568 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1569 (memory (export "memory") 1)
1570 (data (i32.const 4096) "\00\00\00\00")
1571 (data (i32.const 8192) "packet")
1572 (data (i32.const 8208) "\01\02\03")
1573 (func (export "__rns_abi_version") (result i32) i32.const 1)
1574 (func (export "on_hook") (param i32) (result i32)
1575 i32.const 8192
1576 i32.const 6
1577 i32.const 8208
1578 i32.const 3
1579 call $emit
1580 drop
1581 i32.const 4096
1582 )
1583 )"#,
1584 )
1585 .unwrap();
1586
1587 let mut programs = vec![manager.compile("emit".into(), &wasm, 0).unwrap()];
1588 let pkt_ctx = crate::PacketContext {
1589 flags: 0,
1590 hops: 0,
1591 destination_hash: [0; 16],
1592 context: 0,
1593 packet_hash: [0; 32],
1594 interface_id: 1,
1595 data_offset: 0,
1596 data_len: 0,
1597 };
1598 let ctx = crate::HookContext::Packet {
1599 ctx: &pkt_ctx,
1600 raw: &[],
1601 };
1602
1603 let exec = manager
1604 .run_chain_with_provider_events(&mut programs, &ctx, &NullEngine, 0.0, true)
1605 .expect("expected provider event result");
1606 assert!(exec.hook_result.is_none());
1607 assert!(exec.injected_actions.is_empty());
1608 assert_eq!(exec.provider_events.len(), 1);
1609 assert_eq!(exec.provider_events[0].payload_type, "packet");
1610 assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
1611 }
1612}