1use crate::arena;
2use crate::engine_access::{EngineAccess, NullEngine};
3use crate::error::HookError;
4use crate::hooks::HookContext;
5use crate::host_fns;
6use crate::program::LoadedProgram;
7use crate::result::{ExecuteResult, HookResult, Verdict};
8use crate::runtime::{StoreData, WasmRuntime};
9use wasmtime::{Linker, Store};
10
11const HOST_ABI_VERSION: i32 = rns_hooks_abi::ABI_VERSION;
13
14pub struct HookManager {
19 runtime: WasmRuntime,
20 linker: Linker<StoreData>,
21}
22
23impl HookManager {
24 pub fn new() -> Result<Self, HookError> {
25 let runtime = WasmRuntime::new().map_err(|e| HookError::CompileError(e.to_string()))?;
26 let mut linker = Linker::new(runtime.engine());
27 host_fns::register_host_functions(&mut linker)
28 .map_err(|e| HookError::CompileError(e.to_string()))?;
29 Ok(HookManager { runtime, linker })
30 }
31
32 pub fn compile(
37 &self,
38 name: String,
39 bytes: &[u8],
40 priority: i32,
41 ) -> Result<LoadedProgram, HookError> {
42 let module = self
43 .runtime
44 .compile(bytes)
45 .map_err(|e| HookError::CompileError(e.to_string()))?;
46 self.validate_abi_version(&name, &module)?;
47 Ok(LoadedProgram::new(name, module, priority))
48 }
49
50 fn validate_abi_version(
53 &self,
54 name: &str,
55 module: &wasmtime::Module,
56 ) -> Result<(), HookError> {
57 let has_export = module
59 .exports()
60 .any(|e| e.name() == "__rns_abi_version");
61 if !has_export {
62 return Err(HookError::AbiVersionMismatch {
63 hook_name: name.to_string(),
64 expected: HOST_ABI_VERSION,
65 found: None,
66 });
67 }
68
69 static NULL_ENGINE: NullEngine = NullEngine;
71 let mut store = Store::new(self.runtime.engine(), StoreData {
72 engine_access: &NULL_ENGINE as *const dyn EngineAccess,
73 now: 0.0,
74 injected_actions: Vec::new(),
75 log_messages: Vec::new(),
76 });
77 store
78 .set_fuel(self.runtime.fuel())
79 .map_err(|e| HookError::CompileError(e.to_string()))?;
80
81 let instance = self
82 .linker
83 .instantiate(&mut store, module)
84 .map_err(|e| HookError::InstantiationError(e.to_string()))?;
85
86 let func = instance
87 .get_typed_func::<(), i32>(&mut store, "__rns_abi_version")
88 .map_err(|e| HookError::CompileError(format!(
89 "__rns_abi_version has wrong signature: {}", e
90 )))?;
91
92 let version = func
93 .call(&mut store, ())
94 .map_err(|e| HookError::Trap(format!(
95 "__rns_abi_version trapped: {}", e
96 )))?;
97
98 if version != HOST_ABI_VERSION {
99 return Err(HookError::AbiVersionMismatch {
100 hook_name: name.to_string(),
101 expected: HOST_ABI_VERSION,
102 found: Some(version),
103 });
104 }
105
106 Ok(())
107 }
108
109 pub fn load_file(
111 &self,
112 name: String,
113 path: &std::path::Path,
114 priority: i32,
115 ) -> Result<LoadedProgram, HookError> {
116 let bytes = std::fs::read(path)?;
117 self.compile(name, &bytes, priority)
118 }
119
120 pub fn execute_program(
133 &self,
134 program: &mut LoadedProgram,
135 ctx: &HookContext,
136 engine_access: &dyn EngineAccess,
137 now: f64,
138 data_override: Option<&[u8]>,
139 ) -> Option<ExecuteResult> {
140 if !program.enabled {
141 return None;
142 }
143
144 let engine_access_ptr: *const dyn EngineAccess = unsafe {
147 std::mem::transmute(engine_access as *const dyn EngineAccess)
148 };
149
150 let (mut store, instance) = if let Some(cached) = program.cached.take() {
153 let (mut s, i) = cached;
154 s.data_mut().reset_per_call(engine_access_ptr, now);
156 if let Err(e) = s.set_fuel(self.runtime.fuel()) {
157 log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
158 program.cached = Some((s, i));
159 return None;
160 }
161 (s, i)
162 } else {
163 let store_data = StoreData {
164 engine_access: engine_access_ptr,
165 now,
166 injected_actions: Vec::new(),
167 log_messages: Vec::new(),
168 };
169
170 let mut store = Store::new(self.runtime.engine(), store_data);
171 if let Err(e) = store.set_fuel(self.runtime.fuel()) {
172 log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
173 return None;
174 }
175
176 let instance = match self.linker.instantiate(&mut store, &program.module) {
177 Ok(inst) => inst,
178 Err(e) => {
179 log::warn!("failed to instantiate hook '{}': {}", program.name, e);
180 program.record_trap();
181 return None;
182 }
183 };
184
185 (store, instance)
186 };
187
188 let memory = match instance.get_memory(&mut store, "memory") {
190 Some(mem) => mem,
191 None => {
192 log::warn!("hook '{}' has no exported memory", program.name);
193 program.record_trap();
194 program.cached = Some((store, instance));
195 return None;
196 }
197 };
198
199 if let Err(e) = arena::write_context(&memory, &mut store, ctx) {
200 log::warn!("failed to write context for hook '{}': {}", program.name, e);
201 program.record_trap();
202 program.cached = Some((store, instance));
203 return None;
204 }
205
206 if let Some(override_data) = data_override {
208 if let Err(e) = arena::write_data_override(&memory, &mut store, override_data) {
209 log::warn!(
210 "failed to write data override for hook '{}': {}",
211 program.name,
212 e
213 );
214 }
216 }
217
218 let func = match instance.get_typed_func::<i32, i32>(&mut store, &program.export_name) {
220 Ok(f) => f,
221 Err(e) => {
222 log::warn!(
223 "hook '{}' missing export '{}': {}",
224 program.name,
225 program.export_name,
226 e
227 );
228 program.record_trap();
229 program.cached = Some((store, instance));
230 return None;
231 }
232 };
233
234 let result_offset = match func.call(&mut store, arena::ARENA_BASE as i32) {
235 Ok(offset) => offset,
236 Err(e) => {
237 let auto_disabled = program.record_trap();
239 if auto_disabled {
240 log::error!(
241 "hook '{}' auto-disabled after {} consecutive traps",
242 program.name,
243 program.consecutive_traps
244 );
245 } else {
246 log::warn!("hook '{}' trapped: {}", program.name, e);
247 }
248 program.cached = Some((store, instance));
249 return None;
250 }
251 };
252
253 let ret = match arena::read_result(&memory, &store, result_offset as usize) {
255 Ok(result) => {
256 program.record_success();
257
258 let modified_data = if Verdict::from_u32(result.verdict) == Some(Verdict::Modify) {
260 arena::read_modified_data(&memory, &store, &result)
261 } else {
262 None
263 };
264
265 let injected_actions = std::mem::take(&mut store.data_mut().injected_actions);
267
268 Some(ExecuteResult {
269 hook_result: Some(result),
270 injected_actions,
271 modified_data,
272 })
273 }
274 Err(e) => {
275 log::warn!("hook '{}' returned invalid result: {}", program.name, e);
276 program.record_trap();
277 None
278 }
279 };
280
281 program.cached = Some((store, instance));
283 ret
284 }
285
286 pub fn run_chain(
293 &self,
294 programs: &mut [LoadedProgram],
295 ctx: &HookContext,
296 engine_access: &dyn EngineAccess,
297 now: f64,
298 ) -> Option<ExecuteResult> {
299 let mut accumulated_actions = Vec::new();
300 let mut last_result: Option<HookResult> = None;
301 let mut last_modified_data: Option<Vec<u8>> = None;
302 let is_packet_ctx = matches!(ctx, HookContext::Packet(_));
303
304 for program in programs.iter_mut() {
305 if !program.enabled {
306 continue;
307 }
308 let override_ref = if is_packet_ctx {
309 last_modified_data.as_deref()
310 } else {
311 None
312 };
313 if let Some(exec_result) =
314 self.execute_program(program, ctx, engine_access, now, override_ref)
315 {
316 accumulated_actions.extend(exec_result.injected_actions);
317
318 if let Some(ref result) = exec_result.hook_result {
319 let verdict = Verdict::from_u32(result.verdict);
320 match verdict {
321 Some(Verdict::Drop) | Some(Verdict::Halt) => {
322 return Some(ExecuteResult {
323 hook_result: exec_result.hook_result,
324 injected_actions: accumulated_actions,
325 modified_data: exec_result.modified_data.or(last_modified_data),
326 });
327 }
328 Some(Verdict::Modify) => {
329 last_result = exec_result.hook_result;
330 if is_packet_ctx {
331 if let Some(data) = exec_result.modified_data {
332 last_modified_data = Some(data);
333 }
334 }
335 }
336 _ => {} }
338 }
339 }
340 }
341
342 if last_result.is_some() || !accumulated_actions.is_empty() {
343 Some(ExecuteResult {
344 hook_result: last_result,
345 injected_actions: accumulated_actions,
346 modified_data: last_modified_data,
347 })
348 } else {
349 None
350 }
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crate::engine_access::NullEngine;
358
359 fn make_manager() -> HookManager {
360 HookManager::new().expect("failed to create HookManager")
361 }
362
363 const WAT_CONTINUE: &str = r#"
365 (module
366 (memory (export "memory") 1)
367 (func (export "__rns_abi_version") (result i32) (i32.const 1))
368 (func (export "on_hook") (param i32) (result i32)
369 ;; Write HookResult at offset 0x2000
370 ;; verdict = 0 (Continue)
371 (i32.store (i32.const 0x2000) (i32.const 0))
372 ;; modified_data_offset = 0
373 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
374 ;; modified_data_len = 0
375 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
376 ;; inject_actions_offset = 0
377 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
378 ;; inject_actions_count = 0
379 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
380 ;; log_offset = 0
381 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
382 ;; log_len = 0
383 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
384 (i32.const 0x2000)
385 )
386 )
387 "#;
388
389 const WAT_DROP: &str = r#"
391 (module
392 (memory (export "memory") 1)
393 (func (export "__rns_abi_version") (result i32) (i32.const 1))
394 (func (export "on_hook") (param i32) (result i32)
395 (i32.store (i32.const 0x2000) (i32.const 1))
396 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
397 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
398 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
399 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
400 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
401 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
402 (i32.const 0x2000)
403 )
404 )
405 "#;
406
407 const WAT_TRAP: &str = r#"
409 (module
410 (memory (export "memory") 1)
411 (func (export "__rns_abi_version") (result i32) (i32.const 1))
412 (func (export "on_hook") (param i32) (result i32)
413 unreachable
414 )
415 )
416 "#;
417
418 const WAT_INFINITE: &str = r#"
420 (module
421 (memory (export "memory") 1)
422 (func (export "__rns_abi_version") (result i32) (i32.const 1))
423 (func (export "on_hook") (param i32) (result i32)
424 (loop $inf (br $inf))
425 (i32.const 0)
426 )
427 )
428 "#;
429
430 const WAT_HOST_HAS_PATH: &str = r#"
432 (module
433 (import "env" "host_has_path" (func $has_path (param i32) (result i32)))
434 (memory (export "memory") 1)
435 (func (export "__rns_abi_version") (result i32) (i32.const 1))
436 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
437 ;; Check if path exists for a 16-byte dest at offset 0x3000
438 ;; (we'll write the dest hash there in the test)
439 (if (call $has_path (i32.const 0x3000))
440 (then
441 ;; Drop
442 (i32.store (i32.const 0x2000) (i32.const 1))
443 )
444 (else
445 ;; Continue
446 (i32.store (i32.const 0x2000) (i32.const 0))
447 )
448 )
449 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
450 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
451 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
452 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
453 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
454 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
455 (i32.const 0x2000)
456 )
457 )
458 "#;
459
460 #[test]
461 fn pass_through() {
462 let mgr = make_manager();
463 let mut prog = mgr
464 .compile("test".into(), WAT_CONTINUE.as_bytes(), 0)
465 .unwrap();
466 let ctx = HookContext::Tick;
467 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
468 let exec = result.unwrap();
470 let r = exec.hook_result.unwrap();
471 assert_eq!(r.verdict, Verdict::Continue as u32);
472 }
473
474 #[test]
475 fn drop_hook() {
476 let mgr = make_manager();
477 let mut prog = mgr.compile("dropper".into(), WAT_DROP.as_bytes(), 0).unwrap();
478 let ctx = HookContext::Tick;
479 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
480 let exec = result.unwrap();
481 let r = exec.hook_result.unwrap();
482 assert!(r.is_drop());
483 }
484
485 #[test]
486 fn trap_failopen() {
487 let mgr = make_manager();
488 let mut prog = mgr.compile("trap".into(), WAT_TRAP.as_bytes(), 0).unwrap();
489 let ctx = HookContext::Tick;
490 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
491 assert!(result.is_none());
492 assert_eq!(prog.consecutive_traps, 1);
493 assert!(prog.enabled);
494 }
495
496 #[test]
497 fn auto_disable() {
498 let mgr = make_manager();
499 let mut prog = mgr.compile("bad".into(), WAT_TRAP.as_bytes(), 0).unwrap();
500 let ctx = HookContext::Tick;
501 for _ in 0..10 {
502 let _ = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
503 }
504 assert!(!prog.enabled);
505 assert_eq!(prog.consecutive_traps, 10);
506 }
507
508 #[test]
509 fn fuel_exhaustion() {
510 let mgr = make_manager();
511 let mut prog = mgr
512 .compile("loop".into(), WAT_INFINITE.as_bytes(), 0)
513 .unwrap();
514 let ctx = HookContext::Tick;
515 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
516 assert!(result.is_none());
518 assert_eq!(prog.consecutive_traps, 1);
519 }
520
521 #[test]
522 fn chain_ordering() {
523 let mgr = make_manager();
524 let high = mgr
525 .compile("high".into(), WAT_DROP.as_bytes(), 100)
526 .unwrap();
527 let low = mgr
528 .compile("low".into(), WAT_CONTINUE.as_bytes(), 0)
529 .unwrap();
530 let mut programs = vec![high, low];
532 programs.sort_by(|a, b| b.priority.cmp(&a.priority));
534
535 let ctx = HookContext::Tick;
536 let result = mgr.run_chain(&mut programs, &ctx, &NullEngine, 0.0);
537 let exec = result.unwrap();
539 let r = exec.hook_result.unwrap();
540 assert!(r.is_drop());
541 }
542
543 #[test]
544 fn attach_detach() {
545 use crate::hooks::HookSlot;
546
547 let mgr = make_manager();
548 let mut slot = HookSlot {
549 programs: Vec::new(),
550 runner: crate::hooks::hook_noop,
551 };
552
553 let p1 = mgr
554 .compile("alpha".into(), WAT_CONTINUE.as_bytes(), 10)
555 .unwrap();
556 let p2 = mgr
557 .compile("beta".into(), WAT_DROP.as_bytes(), 20)
558 .unwrap();
559
560 slot.attach(p1);
561 assert_eq!(slot.programs.len(), 1);
562 assert!(slot.runner as *const () as usize != crate::hooks::hook_noop as *const () as usize);
563
564 slot.attach(p2);
565 assert_eq!(slot.programs.len(), 2);
566 assert_eq!(slot.programs[0].name, "beta");
568 assert_eq!(slot.programs[1].name, "alpha");
569
570 let removed = slot.detach("beta");
571 assert!(removed.is_some());
572 assert_eq!(slot.programs.len(), 1);
573 assert_eq!(slot.programs[0].name, "alpha");
574
575 let removed2 = slot.detach("alpha");
576 assert!(removed2.is_some());
577 assert!(slot.programs.is_empty());
578 assert_eq!(slot.runner as *const () as usize, crate::hooks::hook_noop as *const () as usize);
579 }
580
581 #[test]
582 fn host_has_path() {
583 use crate::engine_access::EngineAccess;
584
585 struct MockEngine;
586 impl EngineAccess for MockEngine {
587 fn has_path(&self, _dest: &[u8; 16]) -> bool {
588 true
589 }
590 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
591 None
592 }
593 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
594 None
595 }
596 fn is_blackholed(&self, _: &[u8; 16]) -> bool {
597 false
598 }
599 fn interface_name(&self, _: u64) -> Option<String> {
600 None
601 }
602 fn interface_mode(&self, _: u64) -> Option<u8> {
603 None
604 }
605 fn identity_hash(&self) -> Option<[u8; 16]> {
606 None
607 }
608 fn announce_rate(&self, _: u64) -> Option<i32> {
609 None
610 }
611 fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
612 None
613 }
614 }
615
616 let mgr = make_manager();
617 let mut prog = mgr
618 .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
619 .unwrap();
620 let ctx = HookContext::Tick;
621 let result = mgr.execute_program(&mut prog, &ctx, &MockEngine, 0.0, None);
622 let exec = result.unwrap();
624 let r = exec.hook_result.unwrap();
625 assert!(r.is_drop());
626 }
627
628 #[test]
629 fn host_has_path_null_engine() {
630 let mgr = make_manager();
632 let mut prog = mgr
633 .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
634 .unwrap();
635 let ctx = HookContext::Tick;
636 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
637 let exec = result.unwrap();
638 let r = exec.hook_result.unwrap();
639 assert_eq!(r.verdict, Verdict::Continue as u32);
640 }
641
642 struct MockEngineCustom {
646 announce_rate_val: Option<i32>,
647 link_state_val: Option<u8>,
648 }
649
650 impl EngineAccess for MockEngineCustom {
651 fn has_path(&self, _: &[u8; 16]) -> bool { false }
652 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> { None }
653 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> { None }
654 fn is_blackholed(&self, _: &[u8; 16]) -> bool { false }
655 fn interface_name(&self, _: u64) -> Option<String> { None }
656 fn interface_mode(&self, _: u64) -> Option<u8> { None }
657 fn identity_hash(&self) -> Option<[u8; 16]> { None }
658 fn announce_rate(&self, _: u64) -> Option<i32> { self.announce_rate_val }
659 fn link_state(&self, _: &[u8; 16]) -> Option<u8> { self.link_state_val }
660 }
661
662 const WAT_ANNOUNCE_RATE: &str = r#"
664 (module
665 (import "env" "host_get_announce_rate" (func $get_rate (param i64) (result i32)))
666 (memory (export "memory") 1)
667 (func (export "__rns_abi_version") (result i32) (i32.const 1))
668 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
669 (if (i32.ge_s (call $get_rate (i64.const 42)) (i32.const 0))
670 (then
671 (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
672 )
673 (else
674 (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
675 )
676 )
677 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
678 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
679 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
680 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
681 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
682 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
683 (i32.const 0x2000)
684 )
685 )
686 "#;
687
688 #[test]
689 fn host_get_announce_rate_found() {
690 let engine = MockEngineCustom { announce_rate_val: Some(1500), link_state_val: None };
692 let mgr = make_manager();
693 let mut prog = mgr.compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0).unwrap();
694 let ctx = HookContext::Tick;
695 let exec = mgr.execute_program(&mut prog, &ctx, &engine, 0.0, None).unwrap();
696 assert!(exec.hook_result.unwrap().is_drop());
697 }
698
699 #[test]
700 fn host_get_announce_rate_not_found() {
701 let engine = MockEngineCustom { announce_rate_val: None, link_state_val: None };
703 let mgr = make_manager();
704 let mut prog = mgr.compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0).unwrap();
705 let ctx = HookContext::Tick;
706 let exec = mgr.execute_program(&mut prog, &ctx, &engine, 0.0, None).unwrap();
707 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
708 }
709
710 const WAT_LINK_STATE: &str = r#"
713 (module
714 (import "env" "host_get_link_state" (func $link_state (param i32) (result i32)))
715 (memory (export "memory") 1)
716 (func (export "__rns_abi_version") (result i32) (i32.const 1))
717 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
718 (if (i32.eq (call $link_state (i32.const 0x3000)) (i32.const 2))
719 (then
720 (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
721 )
722 (else
723 (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
724 )
725 )
726 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
727 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
728 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
729 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
730 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
731 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
732 (i32.const 0x2000)
733 )
734 )
735 "#;
736
737 #[test]
738 fn host_get_link_state_active() {
739 let engine = MockEngineCustom { announce_rate_val: None, link_state_val: Some(2) };
741 let mgr = make_manager();
742 let mut prog = mgr.compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0).unwrap();
743 let ctx = HookContext::Tick;
744 let exec = mgr.execute_program(&mut prog, &ctx, &engine, 0.0, None).unwrap();
745 assert!(exec.hook_result.unwrap().is_drop());
746 }
747
748 #[test]
749 fn host_get_link_state_not_found() {
750 let engine = MockEngineCustom { announce_rate_val: None, link_state_val: None };
752 let mgr = make_manager();
753 let mut prog = mgr.compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0).unwrap();
754 let ctx = HookContext::Tick;
755 let exec = mgr.execute_program(&mut prog, &ctx, &engine, 0.0, None).unwrap();
756 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
757 }
758
759 const WAT_INJECT_ACTION: &str = r#"
764 (module
765 (import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))
766 (memory (export "memory") 1)
767 (func (export "__rns_abi_version") (result i32) (i32.const 1))
768 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
769 ;; Write the data payload at 0x3100
770 (i32.store8 (i32.const 0x3100) (i32.const 0xDE))
771 (i32.store8 (i32.const 0x3101) (i32.const 0xAD))
772 (i32.store8 (i32.const 0x3102) (i32.const 0xBE))
773 (i32.store8 (i32.const 0x3103) (i32.const 0xEF))
774
775 ;; Write ActionWire at 0x3000:
776 ;; byte 0: tag = 0 (SendOnInterface)
777 (i32.store8 (i32.const 0x3000) (i32.const 0))
778 ;; bytes 1-8: interface = 1 (u64 LE)
779 (i64.store (i32.const 0x3001) (i64.const 1))
780 ;; bytes 9-12: data_offset = 0x3100 (u32 LE)
781 (i32.store (i32.const 0x3009) (i32.const 0x3100))
782 ;; bytes 13-16: data_len = 4 (u32 LE)
783 (i32.store (i32.const 0x300D) (i32.const 4))
784
785 ;; Call inject: ptr=0x3000, len=17 (1 + 8 + 4 + 4)
786 (drop (call $inject (i32.const 0x3000) (i32.const 17)))
787
788 ;; Return Continue
789 (i32.store (i32.const 0x2000) (i32.const 0))
790 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
791 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
792 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
793 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
794 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
795 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
796 (i32.const 0x2000)
797 )
798 )
799 "#;
800
801 #[test]
802 fn host_inject_action_send() {
803 let mgr = make_manager();
804 let mut prog = mgr.compile("inject".into(), WAT_INJECT_ACTION.as_bytes(), 0).unwrap();
805 let ctx = HookContext::Tick;
806 let exec = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None).unwrap();
807 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
808 assert_eq!(exec.injected_actions.len(), 1);
809 match &exec.injected_actions[0] {
810 crate::wire::ActionWire::SendOnInterface { interface, raw } => {
811 assert_eq!(*interface, 1);
812 assert_eq!(raw, &[0xDE, 0xAD, 0xBE, 0xEF]);
813 }
814 other => panic!("expected SendOnInterface, got {:?}", other),
815 }
816 }
817
818 const WAT_MODIFY: &str = r#"
820 (module
821 (memory (export "memory") 1)
822 (func (export "__rns_abi_version") (result i32) (i32.const 1))
823 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
824 ;; Write modified data at 0x2100
825 (i32.store8 (i32.const 0x2100) (i32.const 0xAA))
826 (i32.store8 (i32.const 0x2101) (i32.const 0xBB))
827 (i32.store8 (i32.const 0x2102) (i32.const 0xCC))
828 (i32.store8 (i32.const 0x2103) (i32.const 0xDD))
829
830 ;; verdict = 2 (Modify)
831 (i32.store (i32.const 0x2000) (i32.const 2))
832 ;; modified_data_offset = 0x2100
833 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x2100))
834 ;; modified_data_len = 4
835 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
836 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
837 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
838 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
839 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
840 (i32.const 0x2000)
841 )
842 )
843 "#;
844
845 #[test]
846 fn modify_extracts_data() {
847 let mgr = make_manager();
848 let mut prog = mgr.compile("mod".into(), WAT_MODIFY.as_bytes(), 0).unwrap();
849 let ctx = HookContext::Tick;
850 let exec = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None).unwrap();
851 let r = exec.hook_result.unwrap();
852 assert_eq!(r.verdict, Verdict::Modify as u32);
853 let data = exec.modified_data.unwrap();
854 assert_eq!(data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
855 }
856
857 #[test]
858 fn chain_accumulates_injected_actions() {
859 let mgr = make_manager();
862 let injector = mgr.compile("injector".into(), WAT_INJECT_ACTION.as_bytes(), 100).unwrap();
863 let dropper = mgr.compile("dropper".into(), WAT_DROP.as_bytes(), 0).unwrap();
864 let mut programs = vec![injector, dropper];
865 programs.sort_by(|a, b| b.priority.cmp(&a.priority));
866
867 let ctx = HookContext::Tick;
868 let exec = mgr.run_chain(&mut programs, &ctx, &NullEngine, 0.0).unwrap();
869 assert!(exec.hook_result.unwrap().is_drop());
871 assert_eq!(exec.injected_actions.len(), 1);
873 }
874
875 const WAT_COUNTER: &str = r#"
881 (module
882 (memory (export "memory") 1)
883 (func (export "__rns_abi_version") (result i32) (i32.const 1))
884 (global $counter (mut i32) (i32.const 0))
885 (func (export "on_hook") (param i32) (result i32)
886 ;; Increment counter
887 (global.set $counter (i32.add (global.get $counter) (i32.const 1)))
888 ;; Write counter value at 0x3000 (scratch area)
889 (i32.store (i32.const 0x3000) (global.get $counter))
890 ;; Return Continue with the counter stashed in modified_data region
891 ;; verdict = 2 (Modify) so we can extract the counter via modified_data
892 (i32.store (i32.const 0x2000) (i32.const 2))
893 ;; modified_data_offset = 0x3000
894 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x3000))
895 ;; modified_data_len = 4
896 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
897 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
898 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
899 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
900 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
901 (i32.const 0x2000)
902 )
903 )
904 "#;
905
906 fn extract_counter(exec: &ExecuteResult) -> u32 {
907 let data = exec.modified_data.as_ref().expect("no modified data");
908 assert_eq!(data.len(), 4);
909 u32::from_le_bytes([data[0], data[1], data[2], data[3]])
910 }
911
912 #[test]
913 fn instance_persistence_counter() {
914 let mgr = make_manager();
915 let mut prog = mgr.compile("counter".into(), WAT_COUNTER.as_bytes(), 0).unwrap();
916 let ctx = HookContext::Tick;
917
918 let exec1 = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None).unwrap();
920 assert_eq!(extract_counter(&exec1), 1);
921
922 let exec2 = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None).unwrap();
923 assert_eq!(extract_counter(&exec2), 2);
924
925 let exec3 = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None).unwrap();
926 assert_eq!(extract_counter(&exec3), 3);
927 }
928
929 #[test]
930 fn instance_persistence_resets_on_drop_cache() {
931 let mgr = make_manager();
932 let mut prog = mgr.compile("counter".into(), WAT_COUNTER.as_bytes(), 0).unwrap();
933 let ctx = HookContext::Tick;
934
935 mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None).unwrap();
937 let exec2 = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None).unwrap();
938 assert_eq!(extract_counter(&exec2), 2);
939
940 prog.drop_cache();
942
943 let exec3 = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None).unwrap();
945 assert_eq!(extract_counter(&exec3), 1);
946 }
947
948 const WAT_NO_ABI_VERSION: &str = r#"
952 (module
953 (memory (export "memory") 1)
954 (func (export "on_hook") (param i32) (result i32)
955 (i32.store (i32.const 0x2000) (i32.const 0))
956 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
957 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
958 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
959 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
960 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
961 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
962 (i32.const 0x2000)
963 )
964 )
965 "#;
966
967 const WAT_WRONG_ABI_VERSION: &str = r#"
969 (module
970 (memory (export "memory") 1)
971 (func (export "__rns_abi_version") (result i32) (i32.const 9999))
972 (func (export "on_hook") (param i32) (result i32)
973 (i32.store (i32.const 0x2000) (i32.const 0))
974 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
975 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
976 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
977 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
978 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
979 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
980 (i32.const 0x2000)
981 )
982 )
983 "#;
984
985 #[test]
986 fn rejects_missing_abi_version() {
987 let mgr = make_manager();
988 let result = mgr.compile("no_abi".into(), WAT_NO_ABI_VERSION.as_bytes(), 0);
989 match result {
990 Err(HookError::AbiVersionMismatch { hook_name, expected, found }) => {
991 assert_eq!(hook_name, "no_abi");
992 assert_eq!(expected, HOST_ABI_VERSION);
993 assert_eq!(found, None);
994 }
995 other => panic!("expected AbiVersionMismatch with found=None, got {:?}", other.err()),
996 }
997 }
998
999 #[test]
1000 fn rejects_wrong_abi_version() {
1001 let mgr = make_manager();
1002 let result = mgr.compile("bad_abi".into(), WAT_WRONG_ABI_VERSION.as_bytes(), 0);
1003 match result {
1004 Err(HookError::AbiVersionMismatch { hook_name, expected, found }) => {
1005 assert_eq!(hook_name, "bad_abi");
1006 assert_eq!(expected, HOST_ABI_VERSION);
1007 assert_eq!(found, Some(9999));
1008 }
1009 other => panic!("expected AbiVersionMismatch with found=Some(9999), got {:?}", other.err()),
1010 }
1011 }
1012
1013 #[test]
1014 fn accepts_correct_abi_version() {
1015 let mgr = make_manager();
1016 let result = mgr.compile("good_abi".into(), WAT_CONTINUE.as_bytes(), 0);
1017 assert!(result.is_ok(), "compile should succeed with correct ABI version");
1018 }
1019}