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_HOST_HAS_PATH: &str = r#"
730 (module
731 (import "env" "host_has_path" (func $has_path (param i32) (result i32)))
732 (memory (export "memory") 1)
733 (func (export "__rns_abi_version") (result i32) (i32.const 1))
734 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
735 ;; Check if path exists for a 16-byte dest at offset 0x3000
736 ;; (we'll write the dest hash there in the test)
737 (if (call $has_path (i32.const 0x3000))
738 (then
739 ;; Drop
740 (i32.store (i32.const 0x2000) (i32.const 1))
741 )
742 (else
743 ;; Continue
744 (i32.store (i32.const 0x2000) (i32.const 0))
745 )
746 )
747 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
748 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
749 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
750 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
751 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
752 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
753 (i32.const 0x2000)
754 )
755 )
756 "#;
757
758 #[test]
759 fn pass_through() {
760 let mgr = make_manager();
761 let mut prog = mgr
762 .compile("test".into(), WAT_CONTINUE.as_bytes(), 0)
763 .unwrap();
764 let ctx = HookContext::Tick;
765 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
766 let exec = result.unwrap();
768 let r = exec.hook_result.unwrap();
769 assert_eq!(r.verdict, Verdict::Continue as u32);
770 }
771
772 #[test]
773 fn drop_hook() {
774 let mgr = make_manager();
775 let mut prog = mgr
776 .compile("dropper".into(), WAT_DROP.as_bytes(), 0)
777 .unwrap();
778 let ctx = HookContext::Tick;
779 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
780 let exec = result.unwrap();
781 let r = exec.hook_result.unwrap();
782 assert!(r.is_drop());
783 }
784
785 #[test]
786 fn trap_failopen() {
787 let mgr = make_manager();
788 let mut prog = mgr.compile("trap".into(), WAT_TRAP.as_bytes(), 0).unwrap();
789 let ctx = HookContext::Tick;
790 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
791 assert!(result.is_none());
792 assert_eq!(prog.consecutive_traps, 1);
793 assert!(prog.enabled);
794 }
795
796 #[test]
797 fn auto_disable() {
798 let mgr = make_manager();
799 let mut prog = mgr.compile("bad".into(), WAT_TRAP.as_bytes(), 0).unwrap();
800 let ctx = HookContext::Tick;
801 for _ in 0..10 {
802 let _ = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
803 }
804 assert!(!prog.enabled);
805 assert_eq!(prog.consecutive_traps, 10);
806 }
807
808 #[test]
809 fn fuel_exhaustion() {
810 let mgr = make_manager();
811 let mut prog = mgr
812 .compile("loop".into(), WAT_INFINITE.as_bytes(), 0)
813 .unwrap();
814 let ctx = HookContext::Tick;
815 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
816 assert!(result.is_none());
818 assert_eq!(prog.consecutive_traps, 1);
819 }
820
821 #[test]
822 fn chain_ordering() {
823 let mgr = make_manager();
824 let high = mgr
825 .compile("high".into(), WAT_DROP.as_bytes(), 100)
826 .unwrap();
827 let low = mgr
828 .compile("low".into(), WAT_CONTINUE.as_bytes(), 0)
829 .unwrap();
830 let mut programs = vec![high, low];
832 programs.sort_by(|a, b| b.priority.cmp(&a.priority));
834
835 let ctx = HookContext::Tick;
836 let result = mgr.run_chain(&mut programs, &ctx, &NullEngine, 0.0);
837 let exec = result.unwrap();
839 let r = exec.hook_result.unwrap();
840 assert!(r.is_drop());
841 }
842
843 #[test]
844 fn attach_detach() {
845 use crate::hooks::HookSlot;
846
847 let mgr = make_manager();
848 let mut slot = HookSlot {
849 programs: Vec::new(),
850 runner: crate::hooks::hook_noop,
851 };
852
853 let p1 = mgr
854 .compile("alpha".into(), WAT_CONTINUE.as_bytes(), 10)
855 .unwrap();
856 let p2 = mgr.compile("beta".into(), WAT_DROP.as_bytes(), 20).unwrap();
857
858 slot.attach(p1);
859 assert_eq!(slot.programs.len(), 1);
860 assert!(!std::ptr::eq(
861 slot.runner as *const (),
862 crate::hooks::hook_noop as *const (),
863 ));
864
865 slot.attach(p2);
866 assert_eq!(slot.programs.len(), 2);
867 assert_eq!(slot.programs[0].name, "beta");
869 assert_eq!(slot.programs[1].name, "alpha");
870
871 let removed = slot.detach("beta");
872 assert!(removed.is_some());
873 assert_eq!(slot.programs.len(), 1);
874 assert_eq!(slot.programs[0].name, "alpha");
875
876 let removed2 = slot.detach("alpha");
877 assert!(removed2.is_some());
878 assert!(slot.programs.is_empty());
879 assert_eq!(
880 slot.runner as *const () as usize,
881 crate::hooks::hook_noop as *const () as usize
882 );
883 }
884
885 #[test]
886 fn host_has_path() {
887 use crate::engine_access::EngineAccess;
888
889 struct MockEngine;
890 impl EngineAccess for MockEngine {
891 fn has_path(&self, _dest: &[u8; 16]) -> bool {
892 true
893 }
894 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
895 None
896 }
897 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
898 None
899 }
900 fn is_blackholed(&self, _: &[u8; 16]) -> bool {
901 false
902 }
903 fn interface_name(&self, _: u64) -> Option<String> {
904 None
905 }
906 fn interface_mode(&self, _: u64) -> Option<u8> {
907 None
908 }
909 fn identity_hash(&self) -> Option<[u8; 16]> {
910 None
911 }
912 fn announce_rate(&self, _: u64) -> Option<i32> {
913 None
914 }
915 fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
916 None
917 }
918 }
919
920 let mgr = make_manager();
921 let mut prog = mgr
922 .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
923 .unwrap();
924 let ctx = HookContext::Tick;
925 let result = mgr.execute_program(&mut prog, &ctx, &MockEngine, 0.0, None);
926 let exec = result.unwrap();
928 let r = exec.hook_result.unwrap();
929 assert!(r.is_drop());
930 }
931
932 #[test]
933 fn host_has_path_null_engine() {
934 let mgr = make_manager();
936 let mut prog = mgr
937 .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
938 .unwrap();
939 let ctx = HookContext::Tick;
940 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
941 let exec = result.unwrap();
942 let r = exec.hook_result.unwrap();
943 assert_eq!(r.verdict, Verdict::Continue as u32);
944 }
945
946 struct MockEngineCustom {
950 announce_rate_val: Option<i32>,
951 link_state_val: Option<u8>,
952 }
953
954 impl EngineAccess for MockEngineCustom {
955 fn has_path(&self, _: &[u8; 16]) -> bool {
956 false
957 }
958 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
959 None
960 }
961 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
962 None
963 }
964 fn is_blackholed(&self, _: &[u8; 16]) -> bool {
965 false
966 }
967 fn interface_name(&self, _: u64) -> Option<String> {
968 None
969 }
970 fn interface_mode(&self, _: u64) -> Option<u8> {
971 None
972 }
973 fn identity_hash(&self) -> Option<[u8; 16]> {
974 None
975 }
976 fn announce_rate(&self, _: u64) -> Option<i32> {
977 self.announce_rate_val
978 }
979 fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
980 self.link_state_val
981 }
982 }
983
984 const WAT_ANNOUNCE_RATE: &str = r#"
986 (module
987 (import "env" "host_get_announce_rate" (func $get_rate (param i64) (result i32)))
988 (memory (export "memory") 1)
989 (func (export "__rns_abi_version") (result i32) (i32.const 1))
990 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
991 (if (i32.ge_s (call $get_rate (i64.const 42)) (i32.const 0))
992 (then
993 (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
994 )
995 (else
996 (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
997 )
998 )
999 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1000 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1001 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1002 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1003 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1004 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1005 (i32.const 0x2000)
1006 )
1007 )
1008 "#;
1009
1010 #[test]
1011 fn host_get_announce_rate_found() {
1012 let engine = MockEngineCustom {
1014 announce_rate_val: Some(1500),
1015 link_state_val: None,
1016 };
1017 let mgr = make_manager();
1018 let mut prog = mgr
1019 .compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
1020 .unwrap();
1021 let ctx = HookContext::Tick;
1022 let exec = mgr
1023 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1024 .unwrap();
1025 assert!(exec.hook_result.unwrap().is_drop());
1026 }
1027
1028 #[test]
1029 fn host_get_announce_rate_not_found() {
1030 let engine = MockEngineCustom {
1032 announce_rate_val: None,
1033 link_state_val: None,
1034 };
1035 let mgr = make_manager();
1036 let mut prog = mgr
1037 .compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
1038 .unwrap();
1039 let ctx = HookContext::Tick;
1040 let exec = mgr
1041 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1042 .unwrap();
1043 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
1044 }
1045
1046 const WAT_LINK_STATE: &str = r#"
1049 (module
1050 (import "env" "host_get_link_state" (func $link_state (param i32) (result i32)))
1051 (memory (export "memory") 1)
1052 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1053 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
1054 (if (i32.eq (call $link_state (i32.const 0x3000)) (i32.const 2))
1055 (then
1056 (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
1057 )
1058 (else
1059 (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
1060 )
1061 )
1062 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1063 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1064 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1065 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1066 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1067 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1068 (i32.const 0x2000)
1069 )
1070 )
1071 "#;
1072
1073 #[test]
1074 fn host_get_link_state_active() {
1075 let engine = MockEngineCustom {
1077 announce_rate_val: None,
1078 link_state_val: Some(2),
1079 };
1080 let mgr = make_manager();
1081 let mut prog = mgr
1082 .compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
1083 .unwrap();
1084 let ctx = HookContext::Tick;
1085 let exec = mgr
1086 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1087 .unwrap();
1088 assert!(exec.hook_result.unwrap().is_drop());
1089 }
1090
1091 #[test]
1092 fn host_get_link_state_not_found() {
1093 let engine = MockEngineCustom {
1095 announce_rate_val: None,
1096 link_state_val: None,
1097 };
1098 let mgr = make_manager();
1099 let mut prog = mgr
1100 .compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
1101 .unwrap();
1102 let ctx = HookContext::Tick;
1103 let exec = mgr
1104 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1105 .unwrap();
1106 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
1107 }
1108
1109 const WAT_INJECT_ACTION: &str = r#"
1114 (module
1115 (import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))
1116 (memory (export "memory") 1)
1117 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1118 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
1119 ;; Write the data payload at 0x3100
1120 (i32.store8 (i32.const 0x3100) (i32.const 0xDE))
1121 (i32.store8 (i32.const 0x3101) (i32.const 0xAD))
1122 (i32.store8 (i32.const 0x3102) (i32.const 0xBE))
1123 (i32.store8 (i32.const 0x3103) (i32.const 0xEF))
1124
1125 ;; Write ActionWire at 0x3000:
1126 ;; byte 0: tag = 0 (SendOnInterface)
1127 (i32.store8 (i32.const 0x3000) (i32.const 0))
1128 ;; bytes 1-8: interface = 1 (u64 LE)
1129 (i64.store (i32.const 0x3001) (i64.const 1))
1130 ;; bytes 9-12: data_offset = 0x3100 (u32 LE)
1131 (i32.store (i32.const 0x3009) (i32.const 0x3100))
1132 ;; bytes 13-16: data_len = 4 (u32 LE)
1133 (i32.store (i32.const 0x300D) (i32.const 4))
1134
1135 ;; Call inject: ptr=0x3000, len=17 (1 + 8 + 4 + 4)
1136 (drop (call $inject (i32.const 0x3000) (i32.const 17)))
1137
1138 ;; Return Continue
1139 (i32.store (i32.const 0x2000) (i32.const 0))
1140 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1141 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1142 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1143 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1144 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1145 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1146 (i32.const 0x2000)
1147 )
1148 )
1149 "#;
1150
1151 #[test]
1152 fn host_inject_action_send() {
1153 let mgr = make_manager();
1154 let mut prog = mgr
1155 .compile("inject".into(), WAT_INJECT_ACTION.as_bytes(), 0)
1156 .unwrap();
1157 let ctx = HookContext::Tick;
1158 let exec = mgr
1159 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1160 .unwrap();
1161 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
1162 assert_eq!(exec.injected_actions.len(), 1);
1163 match &exec.injected_actions[0] {
1164 crate::wire::ActionWire::SendOnInterface { interface, raw } => {
1165 assert_eq!(*interface, 1);
1166 assert_eq!(raw, &[0xDE, 0xAD, 0xBE, 0xEF]);
1167 }
1168 other => panic!("expected SendOnInterface, got {:?}", other),
1169 }
1170 }
1171
1172 const WAT_MODIFY: &str = r#"
1174 (module
1175 (memory (export "memory") 1)
1176 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1177 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
1178 ;; Write modified data at 0x2100
1179 (i32.store8 (i32.const 0x2100) (i32.const 0xAA))
1180 (i32.store8 (i32.const 0x2101) (i32.const 0xBB))
1181 (i32.store8 (i32.const 0x2102) (i32.const 0xCC))
1182 (i32.store8 (i32.const 0x2103) (i32.const 0xDD))
1183
1184 ;; verdict = 2 (Modify)
1185 (i32.store (i32.const 0x2000) (i32.const 2))
1186 ;; modified_data_offset = 0x2100
1187 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x2100))
1188 ;; modified_data_len = 4
1189 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
1190 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1191 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1192 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1193 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1194 (i32.const 0x2000)
1195 )
1196 )
1197 "#;
1198
1199 #[test]
1200 fn modify_extracts_data() {
1201 let mgr = make_manager();
1202 let mut prog = mgr.compile("mod".into(), WAT_MODIFY.as_bytes(), 0).unwrap();
1203 let ctx = HookContext::Tick;
1204 let exec = mgr
1205 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1206 .unwrap();
1207 let r = exec.hook_result.unwrap();
1208 assert_eq!(r.verdict, Verdict::Modify as u32);
1209 let data = exec.modified_data.unwrap();
1210 assert_eq!(data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
1211 }
1212
1213 #[test]
1214 fn chain_accumulates_injected_actions() {
1215 let mgr = make_manager();
1218 let injector = mgr
1219 .compile("injector".into(), WAT_INJECT_ACTION.as_bytes(), 100)
1220 .unwrap();
1221 let dropper = mgr
1222 .compile("dropper".into(), WAT_DROP.as_bytes(), 0)
1223 .unwrap();
1224 let mut programs = vec![injector, dropper];
1225 programs.sort_by(|a, b| b.priority.cmp(&a.priority));
1226
1227 let ctx = HookContext::Tick;
1228 let exec = mgr
1229 .run_chain(&mut programs, &ctx, &NullEngine, 0.0)
1230 .unwrap();
1231 assert!(exec.hook_result.unwrap().is_drop());
1233 assert_eq!(exec.injected_actions.len(), 1);
1235 }
1236
1237 const WAT_COUNTER: &str = r#"
1243 (module
1244 (memory (export "memory") 1)
1245 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1246 (global $counter (mut i32) (i32.const 0))
1247 (func (export "on_hook") (param i32) (result i32)
1248 ;; Increment counter
1249 (global.set $counter (i32.add (global.get $counter) (i32.const 1)))
1250 ;; Write counter value at 0x3000 (scratch area)
1251 (i32.store (i32.const 0x3000) (global.get $counter))
1252 ;; Return Continue with the counter stashed in modified_data region
1253 ;; verdict = 2 (Modify) so we can extract the counter via modified_data
1254 (i32.store (i32.const 0x2000) (i32.const 2))
1255 ;; modified_data_offset = 0x3000
1256 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x3000))
1257 ;; modified_data_len = 4
1258 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
1259 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1260 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1261 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1262 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1263 (i32.const 0x2000)
1264 )
1265 )
1266 "#;
1267
1268 fn extract_counter(exec: &ExecuteResult) -> u32 {
1269 let data = exec.modified_data.as_ref().expect("no modified data");
1270 assert_eq!(data.len(), 4);
1271 u32::from_le_bytes([data[0], data[1], data[2], data[3]])
1272 }
1273
1274 #[test]
1275 fn instance_persistence_counter() {
1276 let mgr = make_manager();
1277 let mut prog = mgr
1278 .compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
1279 .unwrap();
1280 let ctx = HookContext::Tick;
1281
1282 let exec1 = mgr
1284 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1285 .unwrap();
1286 assert_eq!(extract_counter(&exec1), 1);
1287
1288 let exec2 = mgr
1289 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1290 .unwrap();
1291 assert_eq!(extract_counter(&exec2), 2);
1292
1293 let exec3 = mgr
1294 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1295 .unwrap();
1296 assert_eq!(extract_counter(&exec3), 3);
1297 }
1298
1299 #[test]
1300 fn instance_persistence_resets_on_drop_cache() {
1301 let mgr = make_manager();
1302 let mut prog = mgr
1303 .compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
1304 .unwrap();
1305 let ctx = HookContext::Tick;
1306
1307 mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1309 .unwrap();
1310 let exec2 = mgr
1311 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1312 .unwrap();
1313 assert_eq!(extract_counter(&exec2), 2);
1314
1315 prog.drop_cache();
1317
1318 let exec3 = mgr
1320 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1321 .unwrap();
1322 assert_eq!(extract_counter(&exec3), 1);
1323 }
1324
1325 const WAT_NO_ABI_VERSION: &str = r#"
1329 (module
1330 (memory (export "memory") 1)
1331 (func (export "on_hook") (param i32) (result i32)
1332 (i32.store (i32.const 0x2000) (i32.const 0))
1333 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1334 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1335 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1336 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1337 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1338 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1339 (i32.const 0x2000)
1340 )
1341 )
1342 "#;
1343
1344 const WAT_WRONG_ABI_VERSION: &str = r#"
1346 (module
1347 (memory (export "memory") 1)
1348 (func (export "__rns_abi_version") (result i32) (i32.const 9999))
1349 (func (export "on_hook") (param i32) (result i32)
1350 (i32.store (i32.const 0x2000) (i32.const 0))
1351 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1352 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1353 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1354 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1355 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1356 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1357 (i32.const 0x2000)
1358 )
1359 )
1360 "#;
1361
1362 #[test]
1363 fn rejects_missing_abi_version() {
1364 let mgr = make_manager();
1365 let result = mgr.compile("no_abi".into(), WAT_NO_ABI_VERSION.as_bytes(), 0);
1366 match result {
1367 Err(HookError::AbiVersionMismatch {
1368 hook_name,
1369 expected,
1370 found,
1371 }) => {
1372 assert_eq!(hook_name, "no_abi");
1373 assert_eq!(expected, HOST_ABI_VERSION);
1374 assert_eq!(found, None);
1375 }
1376 other => panic!(
1377 "expected AbiVersionMismatch with found=None, got {:?}",
1378 other.err()
1379 ),
1380 }
1381 }
1382
1383 #[test]
1384 fn rejects_wrong_abi_version() {
1385 let mgr = make_manager();
1386 let result = mgr.compile("bad_abi".into(), WAT_WRONG_ABI_VERSION.as_bytes(), 0);
1387 match result {
1388 Err(HookError::AbiVersionMismatch {
1389 hook_name,
1390 expected,
1391 found,
1392 }) => {
1393 assert_eq!(hook_name, "bad_abi");
1394 assert_eq!(expected, HOST_ABI_VERSION);
1395 assert_eq!(found, Some(9999));
1396 }
1397 other => panic!(
1398 "expected AbiVersionMismatch with found=Some(9999), got {:?}",
1399 other.err()
1400 ),
1401 }
1402 }
1403
1404 #[test]
1405 fn accepts_correct_abi_version() {
1406 let mgr = make_manager();
1407 let result = mgr.compile("good_abi".into(), WAT_CONTINUE.as_bytes(), 0);
1408 assert!(
1409 result.is_ok(),
1410 "compile should succeed with correct ABI version"
1411 );
1412 }
1413
1414 #[test]
1415 fn host_emit_event_collects_provider_event() {
1416 let mgr = make_manager();
1417 let wat = r#"
1418 (module
1419 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1420 (memory (export "memory") 1)
1421 (data (i32.const 0x3000) "packet")
1422 (data (i32.const 0x3010) "\01\02\03")
1423 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1424 (func (export "on_hook") (param i32) (result i32)
1425 (drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
1426 (i32.store (i32.const 0x2000) (i32.const 0))
1427 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1428 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1429 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1430 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1431 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1432 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1433 (i32.const 0x2000)
1434 )
1435 )
1436 "#;
1437 let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
1438 let mut prog = LoadedProgram::new("emit".into(), module, 0);
1439 let ctx = HookContext::Tick;
1440
1441 let exec = mgr
1442 .execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, true, None)
1443 .unwrap();
1444 assert_eq!(exec.provider_events.len(), 1);
1445 assert_eq!(exec.provider_events[0].hook_name, "emit");
1446 assert_eq!(exec.provider_events[0].payload_type, "packet");
1447 assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
1448 }
1449
1450 #[test]
1451 fn host_emit_event_is_ignored_when_disabled() {
1452 let mgr = make_manager();
1453 let wat = r#"
1454 (module
1455 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1456 (memory (export "memory") 1)
1457 (data (i32.const 0x3000) "packet")
1458 (data (i32.const 0x3010) "\01\02\03")
1459 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1460 (func (export "on_hook") (param i32) (result i32)
1461 (drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
1462 (i32.store (i32.const 0x2000) (i32.const 0))
1463 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1464 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1465 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1466 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1467 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1468 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1469 (i32.const 0x2000)
1470 )
1471 )
1472 "#;
1473 let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
1474 let mut prog = LoadedProgram::new("emit".into(), module, 0);
1475 let ctx = HookContext::Tick;
1476
1477 let exec = mgr
1478 .execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, false, None)
1479 .unwrap();
1480 assert!(exec.provider_events.is_empty());
1481 }
1482
1483 #[test]
1484 fn run_chain_returns_continue_only_provider_events() {
1485 let manager = make_manager();
1486 let wasm = wat::parse_str(
1487 r#"(module
1488 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1489 (memory (export "memory") 1)
1490 (data (i32.const 4096) "\00\00\00\00")
1491 (data (i32.const 8192) "packet")
1492 (data (i32.const 8208) "\01\02\03")
1493 (func (export "__rns_abi_version") (result i32) i32.const 1)
1494 (func (export "on_hook") (param i32) (result i32)
1495 i32.const 8192
1496 i32.const 6
1497 i32.const 8208
1498 i32.const 3
1499 call $emit
1500 drop
1501 i32.const 4096
1502 )
1503 )"#,
1504 )
1505 .unwrap();
1506
1507 let mut programs = vec![manager.compile("emit".into(), &wasm, 0).unwrap()];
1508 let pkt_ctx = crate::PacketContext {
1509 flags: 0,
1510 hops: 0,
1511 destination_hash: [0; 16],
1512 context: 0,
1513 packet_hash: [0; 32],
1514 interface_id: 1,
1515 data_offset: 0,
1516 data_len: 0,
1517 };
1518 let ctx = crate::HookContext::Packet {
1519 ctx: &pkt_ctx,
1520 raw: &[],
1521 };
1522
1523 let exec = manager
1524 .run_chain_with_provider_events(&mut programs, &ctx, &NullEngine, 0.0, true)
1525 .expect("expected provider event result");
1526 assert!(exec.hook_result.is_none());
1527 assert!(exec.injected_actions.is_empty());
1528 assert_eq!(exec.provider_events.len(), 1);
1529 assert_eq!(exec.provider_events[0].payload_type, "packet");
1530 assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
1531 }
1532}