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 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(format!("{:#}", e)))?;
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 = self
123 .runtime
124 .new_store(StoreData::new(
125 name.to_string(),
126 &NULL_ENGINE as *const dyn EngineAccess,
127 0.0,
128 false,
129 self.runtime.max_memory(),
130 ))
131 .map_err(|e| HookError::CompileError(e.to_string()))?;
132
133 let instance = guard_guest_call(|| self.linker.instantiate(&mut store, module))
134 .map_err(|e| HookError::InstantiationError(e.to_string()))?;
135
136 let func = instance
137 .get_typed_func::<(), i32>(&mut store, "__rns_abi_version")
138 .map_err(|e| {
139 HookError::CompileError(format!("__rns_abi_version has wrong signature: {}", e))
140 })?;
141
142 let version = guard_guest_call(|| func.call(&mut store, ()))
143 .map_err(|e| HookError::Trap(format!("__rns_abi_version trapped: {}", e)))?;
144
145 if version != HOST_ABI_VERSION {
146 return Err(HookError::AbiVersionMismatch {
147 hook_name: name.to_string(),
148 expected: HOST_ABI_VERSION,
149 found: Some(version),
150 });
151 }
152
153 Ok(())
154 }
155
156 pub fn load_file(
158 &self,
159 name: String,
160 path: &std::path::Path,
161 priority: i32,
162 ) -> Result<LoadedProgram, HookError> {
163 let bytes = std::fs::read(path)?;
164 self.compile(name, &bytes, priority)
165 }
166
167 pub fn load_file_backend(
168 &self,
169 name: String,
170 path: &std::path::Path,
171 priority: i32,
172 backend: HookBackend,
173 ) -> Result<LoadedProgram, HookError> {
174 match backend {
175 HookBackend::Wasm => self.load_file(name, path, priority),
176 HookBackend::Builtin => Err(HookError::CompileError(
177 "built-in hooks are loaded by ID, not file path".to_string(),
178 )),
179 HookBackend::Native => {
180 #[cfg(feature = "native")]
181 {
182 let native = crate::native::NativeProgram::load(path)?;
183 Ok(LoadedProgram::new_native(name, native, priority))
184 }
185 #[cfg(not(feature = "native"))]
186 {
187 let _ = (name, path, priority);
188 Err(HookError::CompileError(
189 "native hook backend not enabled".to_string(),
190 ))
191 }
192 }
193 }
194 }
195
196 pub fn load_builtin(
197 &self,
198 name: String,
199 id: impl Into<String>,
200 priority: i32,
201 ) -> Result<LoadedProgram, HookError> {
202 let builtin = crate::builtin::BuiltinProgram::load(id)?;
203 Ok(LoadedProgram::new_builtin(name, builtin, priority))
204 }
205
206 pub fn execute_program(
219 &self,
220 program: &mut LoadedProgram,
221 ctx: &HookContext,
222 engine_access: &dyn EngineAccess,
223 now: f64,
224 data_override: Option<&[u8]>,
225 ) -> Option<ExecuteResult> {
226 self.execute_program_with_provider_events(
227 program,
228 ctx,
229 engine_access,
230 now,
231 false,
232 data_override,
233 )
234 }
235
236 pub fn execute_program_with_provider_events(
237 &self,
238 program: &mut LoadedProgram,
239 ctx: &HookContext,
240 engine_access: &dyn EngineAccess,
241 now: f64,
242 provider_events_enabled: bool,
243 data_override: Option<&[u8]>,
244 ) -> Option<ExecuteResult> {
245 if !program.enabled {
246 return None;
247 }
248
249 if matches!(program.backend, ProgramBackend::Builtin(_)) {
250 let result = match &program.backend {
251 ProgramBackend::Builtin(builtin) => builtin.execute(
252 ctx,
253 data_override,
254 engine_access,
255 now,
256 provider_events_enabled,
257 ),
258 #[cfg(feature = "wasm")]
259 ProgramBackend::Wasm(_) => unreachable!(),
260 #[cfg(feature = "native")]
261 ProgramBackend::Native(_) => unreachable!(),
262 };
263 return match result {
264 Ok(mut exec) => {
265 program.record_success();
266 for event in &mut exec.provider_events {
267 event.hook_name = program.name.clone();
268 }
269 Some(exec)
270 }
271 Err(e) => {
272 let auto_disabled = program.record_trap();
273 if auto_disabled {
274 log::error!(
275 "built-in hook '{}' auto-disabled after {} consecutive errors",
276 program.name,
277 program.consecutive_traps
278 );
279 } else {
280 log::warn!("built-in hook '{}' failed: {}", program.name, e);
281 }
282 None
283 }
284 };
285 }
286
287 #[cfg(feature = "native")]
288 if matches!(program.backend, ProgramBackend::Native(_)) {
289 let ctx_bytes = match arena::context_to_bytes(ctx, data_override) {
290 Ok(bytes) => bytes,
291 Err(e) => {
292 log::warn!(
293 "failed to encode native hook context '{}': {}",
294 program.name,
295 e
296 );
297 program.record_trap();
298 return None;
299 }
300 };
301 let result = match &program.backend {
302 ProgramBackend::Native(native) => {
303 native.execute(&ctx_bytes, engine_access, provider_events_enabled)
304 }
305 ProgramBackend::Builtin(_) => unreachable!(),
306 #[cfg(feature = "wasm")]
307 ProgramBackend::Wasm(_) => unreachable!(),
308 };
309 return match result {
310 Ok(mut exec) => {
311 program.record_success();
312 for event in &mut exec.provider_events {
313 event.hook_name = program.name.clone();
314 }
315 Some(exec)
316 }
317 Err(e) => {
318 let auto_disabled = program.record_trap();
319 if auto_disabled {
320 log::error!(
321 "native hook '{}' auto-disabled after {} consecutive errors",
322 program.name,
323 program.consecutive_traps
324 );
325 } else {
326 log::warn!("native hook '{}' failed: {}", program.name, e);
327 }
328 None
329 }
330 };
331 }
332
333 #[cfg(not(feature = "wasm"))]
334 {
335 let _ = (
336 ctx,
337 engine_access,
338 now,
339 provider_events_enabled,
340 data_override,
341 );
342 None
343 }
344
345 #[cfg(feature = "wasm")]
346 {
347 self.execute_wasm_program(
348 program,
349 ctx,
350 engine_access,
351 now,
352 provider_events_enabled,
353 data_override,
354 )
355 }
356 }
357
358 #[cfg(feature = "wasm")]
359 fn execute_wasm_program(
360 &self,
361 program: &mut LoadedProgram,
362 ctx: &HookContext,
363 engine_access: &dyn EngineAccess,
364 now: f64,
365 provider_events_enabled: bool,
366 data_override: Option<&[u8]>,
367 ) -> Option<ExecuteResult> {
368 let engine_access_ptr: *const dyn EngineAccess =
371 unsafe { std::mem::transmute(engine_access as *const dyn EngineAccess) };
372
373 let cached = match &mut program.backend {
376 ProgramBackend::Builtin(_) => unreachable!(),
377 ProgramBackend::Wasm(wasm) => wasm.cached.take(),
378 #[cfg(feature = "native")]
379 ProgramBackend::Native(_) => unreachable!(),
380 };
381 let (mut store, instance) = if let Some(cached) = cached {
382 let (mut s, i) = cached;
383 s.data_mut()
385 .reset_per_call(engine_access_ptr, now, provider_events_enabled);
386 if let Err(e) = s.set_fuel(self.runtime.fuel()) {
387 log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
388 self.cache_wasm_instance(program, s, i);
389 return None;
390 }
391 (s, i)
392 } else {
393 let store_data = StoreData::new(
394 program.name.clone(),
395 engine_access_ptr,
396 now,
397 provider_events_enabled,
398 self.runtime.max_memory(),
399 );
400
401 let mut store = match self.runtime.new_store(store_data) {
402 Ok(store) => store,
403 Err(e) => {
404 log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
405 return None;
406 }
407 };
408
409 let module = match &program.backend {
410 ProgramBackend::Builtin(_) => unreachable!(),
411 ProgramBackend::Wasm(wasm) => wasm.module.clone(),
412 #[cfg(feature = "native")]
413 ProgramBackend::Native(_) => unreachable!(),
414 };
415 let instance = match guard_guest_call(|| self.linker.instantiate(&mut store, &module)) {
416 Ok(inst) => inst,
417 Err(e) => {
418 log::warn!("failed to instantiate hook '{}': {}", program.name, e);
419 program.record_trap();
420 return None;
421 }
422 };
423
424 (store, instance)
425 };
426
427 let memory = match instance.get_memory(&mut store, "memory") {
429 Some(mem) => mem,
430 None => {
431 log::warn!("hook '{}' has no exported memory", program.name);
432 program.record_trap();
433 self.cache_wasm_instance(program, store, instance);
434 return None;
435 }
436 };
437
438 if let Err(e) = arena::write_context(&memory, &mut store, ctx) {
439 log::warn!("failed to write context for hook '{}': {}", program.name, e);
440 program.record_trap();
441 self.cache_wasm_instance(program, store, instance);
442 return None;
443 }
444
445 if let Some(override_data) = data_override {
447 if let Err(e) = arena::write_data_override(&memory, &mut store, override_data) {
448 log::warn!(
449 "failed to write data override for hook '{}': {}",
450 program.name,
451 e
452 );
453 }
455 }
456
457 let export_name = match &program.backend {
459 ProgramBackend::Builtin(_) => unreachable!(),
460 ProgramBackend::Wasm(wasm) => wasm.export_name.clone(),
461 #[cfg(feature = "native")]
462 ProgramBackend::Native(_) => unreachable!(),
463 };
464 let func = match instance.get_typed_func::<i32, i32>(&mut store, &export_name) {
465 Ok(f) => f,
466 Err(e) => {
467 log::warn!(
468 "hook '{}' missing export '{}': {}",
469 program.name,
470 export_name,
471 e
472 );
473 program.record_trap();
474 self.cache_wasm_instance(program, store, instance);
475 return None;
476 }
477 };
478
479 let call = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
480 func.call(&mut store, arena::ARENA_BASE as i32)
481 }));
482 let result_offset = match call {
483 Ok(Ok(offset)) => offset,
484 Err(panic) => {
485 let auto_disabled = program.record_trap();
489 log::warn!(
490 "hook '{}' aborted: host panic: {}",
491 program.name,
492 panic_message(&*panic)
493 );
494 if auto_disabled {
495 log::error!(
496 "hook '{}' auto-disabled after {} consecutive traps",
497 program.name,
498 program.consecutive_traps
499 );
500 }
501 return None;
502 }
503 Ok(Err(e)) => {
504 let auto_disabled = program.record_trap();
506 if auto_disabled {
507 log::error!(
508 "hook '{}' auto-disabled after {} consecutive traps",
509 program.name,
510 program.consecutive_traps
511 );
512 } else {
513 log::warn!("hook '{}' trapped: {}", program.name, e);
514 }
515 self.cache_wasm_instance(program, store, instance);
516 return None;
517 }
518 };
519
520 let ret = match arena::read_result(&memory, &store, result_offset as u32 as usize) {
523 Ok(result) => {
524 program.record_success();
525
526 let modified_data = if Verdict::from_u32(result.verdict) == Some(Verdict::Modify) {
528 arena::read_modified_data(&memory, &store, &result)
529 } else {
530 None
531 };
532
533 let injected_actions = std::mem::take(&mut store.data_mut().injected_actions);
535 let provider_events = std::mem::take(&mut store.data_mut().provider_events)
536 .into_iter()
537 .map(|event| crate::result::EmittedProviderEvent {
538 hook_name: program.name.clone(),
539 payload_type: event.payload_type,
540 payload: event.payload,
541 })
542 .collect();
543
544 Some(ExecuteResult {
545 hook_result: Some(result),
546 injected_actions,
547 provider_events,
548 modified_data,
549 })
550 }
551 Err(e) => {
552 log::warn!("hook '{}' returned invalid result: {}", program.name, e);
553 program.record_trap();
554 None
555 }
556 };
557
558 self.cache_wasm_instance(program, store, instance);
560 ret
561 }
562
563 #[cfg(feature = "wasm")]
564 fn cache_wasm_instance(
565 &self,
566 program: &mut LoadedProgram,
567 store: Store<StoreData>,
568 instance: wasmtime::Instance,
569 ) {
570 match &mut program.backend {
571 ProgramBackend::Builtin(_) => unreachable!(),
572 ProgramBackend::Wasm(wasm) => wasm.cached = Some((store, instance)),
573 #[cfg(feature = "native")]
574 ProgramBackend::Native(_) => unreachable!(),
575 }
576 }
577
578 pub fn run_chain(
585 &self,
586 programs: &mut [LoadedProgram],
587 ctx: &HookContext,
588 engine_access: &dyn EngineAccess,
589 now: f64,
590 ) -> Option<ExecuteResult> {
591 self.run_chain_with_provider_events(programs, ctx, engine_access, now, false)
592 }
593
594 pub fn run_chain_with_provider_events(
595 &self,
596 programs: &mut [LoadedProgram],
597 ctx: &HookContext,
598 engine_access: &dyn EngineAccess,
599 now: f64,
600 provider_events_enabled: bool,
601 ) -> Option<ExecuteResult> {
602 let mut accumulated_actions = Vec::new();
603 let mut accumulated_provider_events = Vec::new();
604 let mut last_result: Option<HookResult> = None;
605 let mut last_modified_data: Option<Vec<u8>> = None;
606 let is_packet_ctx = matches!(ctx, HookContext::Packet { .. });
607
608 for program in programs.iter_mut() {
609 if !program.enabled {
610 continue;
611 }
612 let override_ref = if is_packet_ctx {
613 last_modified_data.as_deref()
614 } else {
615 None
616 };
617 if let Some(exec_result) = self.execute_program_with_provider_events(
618 program,
619 ctx,
620 engine_access,
621 now,
622 provider_events_enabled,
623 override_ref,
624 ) {
625 accumulated_actions.extend(exec_result.injected_actions);
626 accumulated_provider_events.extend(exec_result.provider_events);
627
628 if let Some(ref result) = exec_result.hook_result {
629 let verdict = Verdict::from_u32(result.verdict);
630 match verdict {
631 Some(Verdict::Drop) | Some(Verdict::Halt) => {
632 return Some(ExecuteResult {
633 hook_result: exec_result.hook_result,
634 injected_actions: accumulated_actions,
635 provider_events: accumulated_provider_events,
636 modified_data: exec_result.modified_data.or(last_modified_data),
637 });
638 }
639 Some(Verdict::Modify) => {
640 last_result = exec_result.hook_result;
641 if is_packet_ctx {
642 if let Some(data) = exec_result.modified_data {
643 last_modified_data = Some(data);
644 }
645 }
646 }
647 _ => {} }
649 }
650 }
651 }
652
653 if last_result.is_some()
654 || !accumulated_actions.is_empty()
655 || !accumulated_provider_events.is_empty()
656 {
657 Some(ExecuteResult {
658 hook_result: last_result,
659 injected_actions: accumulated_actions,
660 provider_events: accumulated_provider_events,
661 modified_data: last_modified_data,
662 })
663 } else {
664 None
665 }
666 }
667}
668
669#[cfg(feature = "wasm")]
672fn guard_guest_call<T>(
673 call: impl FnOnce() -> Result<T, wasmtime::Error>,
674) -> Result<T, wasmtime::Error> {
675 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(call)) {
676 Ok(result) => result,
677 Err(panic) => Err(wasmtime::format_err!(
678 "host panic: {}",
679 panic_message(&*panic)
680 )),
681 }
682}
683
684#[cfg(feature = "wasm")]
685fn panic_message(panic: &(dyn std::any::Any + Send)) -> &str {
686 if let Some(msg) = panic.downcast_ref::<&str>() {
687 msg
688 } else if let Some(msg) = panic.downcast_ref::<String>() {
689 msg
690 } else {
691 "unknown panic"
692 }
693}
694
695#[cfg(all(test, feature = "wasm"))]
696mod tests {
697 use super::*;
698 use crate::engine_access::NullEngine;
699
700 fn make_manager() -> HookManager {
701 HookManager::new().expect("failed to create HookManager")
702 }
703
704 const WAT_CONTINUE: &str = r#"
706 (module
707 (memory (export "memory") 1)
708 (func (export "__rns_abi_version") (result i32) (i32.const 1))
709 (func (export "on_hook") (param i32) (result i32)
710 ;; Write HookResult at offset 0x2000
711 ;; verdict = 0 (Continue)
712 (i32.store (i32.const 0x2000) (i32.const 0))
713 ;; modified_data_offset = 0
714 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
715 ;; modified_data_len = 0
716 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
717 ;; inject_actions_offset = 0
718 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
719 ;; inject_actions_count = 0
720 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
721 ;; log_offset = 0
722 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
723 ;; log_len = 0
724 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
725 (i32.const 0x2000)
726 )
727 )
728 "#;
729
730 const WAT_DROP: &str = r#"
732 (module
733 (memory (export "memory") 1)
734 (func (export "__rns_abi_version") (result i32) (i32.const 1))
735 (func (export "on_hook") (param i32) (result i32)
736 (i32.store (i32.const 0x2000) (i32.const 1))
737 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
738 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
739 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
740 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
741 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
742 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
743 (i32.const 0x2000)
744 )
745 )
746 "#;
747
748 const WAT_TRAP: &str = r#"
750 (module
751 (memory (export "memory") 1)
752 (func (export "__rns_abi_version") (result i32) (i32.const 1))
753 (func (export "on_hook") (param i32) (result i32)
754 unreachable
755 )
756 )
757 "#;
758
759 const WAT_INFINITE: &str = r#"
761 (module
762 (memory (export "memory") 1)
763 (func (export "__rns_abi_version") (result i32) (i32.const 1))
764 (func (export "on_hook") (param i32) (result i32)
765 (loop $inf (br $inf))
766 (i32.const 0)
767 )
768 )
769 "#;
770
771 const WAT_FUEL_RECOVERY: &str = r#"
774 (module
775 (memory (export "memory") 1)
776 (global $calls (mut i32) (i32.const 0))
777 (func (export "__rns_abi_version") (result i32) (i32.const 1))
778 (func (export "on_hook") (param i32) (result i32)
779 (global.set $calls (i32.add (global.get $calls) (i32.const 1)))
780 (if (i32.eq (global.get $calls) (i32.const 1))
781 (then (loop $inf (br $inf))))
782 (i32.store (i32.const 0x2000) (i32.const 0))
783 (i32.store (i32.const 0x2004) (i32.const 0))
784 (i32.store (i32.const 0x2008) (i32.const 0))
785 (i32.store (i32.const 0x200c) (i32.const 0))
786 (i32.store (i32.const 0x2010) (i32.const 0))
787 (i32.store (i32.const 0x2014) (i32.const 0))
788 (i32.store (i32.const 0x2018) (i32.const 0))
789 (i32.const 0x2000))
790 )
791 "#;
792
793 const WAT_BULK_MEMORY: &str = r#"
796 (module
797 (memory (export "memory") 2)
798 (func (export "__rns_abi_version") (result i32) (i32.const 1))
799 (func (export "on_hook") (param i32) (result i32)
800 (memory.fill (i32.const 0x10000) (i32.const 0x5a) (i32.const 0x4000))
801 (memory.copy (i32.const 0x14000) (i32.const 0x10000) (i32.const 0x4000))
802 (i32.store (i32.const 0x2000) (i32.const 2))
803 (i32.store (i32.const 0x2004) (i32.const 0x14000))
804 (i32.store (i32.const 0x2008) (i32.const 0x4000))
805 (i32.store (i32.const 0x200c) (i32.const 0))
806 (i32.store (i32.const 0x2010) (i32.const 0))
807 (i32.store (i32.const 0x2014) (i32.const 0))
808 (i32.store (i32.const 0x2018) (i32.const 0))
809 (i32.const 0x2000))
810 )
811 "#;
812
813 const WAT_HOST_HAS_PATH: &str = r#"
815 (module
816 (import "env" "host_has_path" (func $has_path (param i32) (result i32)))
817 (memory (export "memory") 1)
818 (func (export "__rns_abi_version") (result i32) (i32.const 1))
819 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
820 ;; Check if path exists for a 16-byte dest at offset 0x3000
821 ;; (we'll write the dest hash there in the test)
822 (if (call $has_path (i32.const 0x3000))
823 (then
824 ;; Drop
825 (i32.store (i32.const 0x2000) (i32.const 1))
826 )
827 (else
828 ;; Continue
829 (i32.store (i32.const 0x2000) (i32.const 0))
830 )
831 )
832 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
833 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
834 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
835 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
836 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
837 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
838 (i32.const 0x2000)
839 )
840 )
841 "#;
842
843 #[test]
844 fn pass_through() {
845 let mgr = make_manager();
846 let mut prog = mgr
847 .compile("test".into(), WAT_CONTINUE.as_bytes(), 0)
848 .unwrap();
849 let ctx = HookContext::Tick;
850 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
851 let exec = result.unwrap();
853 let r = exec.hook_result.unwrap();
854 assert_eq!(r.verdict, Verdict::Continue as u32);
855 }
856
857 #[test]
858 fn drop_hook() {
859 let mgr = make_manager();
860 let mut prog = mgr
861 .compile("dropper".into(), WAT_DROP.as_bytes(), 0)
862 .unwrap();
863 let ctx = HookContext::Tick;
864 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
865 let exec = result.unwrap();
866 let r = exec.hook_result.unwrap();
867 assert!(r.is_drop());
868 }
869
870 #[test]
871 fn trap_failopen() {
872 let mgr = make_manager();
873 let mut prog = mgr.compile("trap".into(), WAT_TRAP.as_bytes(), 0).unwrap();
874 let ctx = HookContext::Tick;
875 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
876 assert!(result.is_none());
877 assert_eq!(prog.consecutive_traps, 1);
878 assert!(prog.enabled);
879 }
880
881 #[test]
882 fn auto_disable() {
883 let mgr = make_manager();
884 let mut prog = mgr.compile("bad".into(), WAT_TRAP.as_bytes(), 0).unwrap();
885 let ctx = HookContext::Tick;
886 for _ in 0..10 {
887 let _ = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
888 }
889 assert!(!prog.enabled);
890 assert_eq!(prog.consecutive_traps, 10);
891 }
892
893 #[test]
894 fn fuel_exhaustion() {
895 let mgr = make_manager();
896 let mut prog = mgr
897 .compile("loop".into(), WAT_INFINITE.as_bytes(), 0)
898 .unwrap();
899 let ctx = HookContext::Tick;
900 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
901 assert!(result.is_none());
903 assert_eq!(prog.consecutive_traps, 1);
904 }
905
906 #[test]
907 fn fuel_exhaustion_recovers_cached_store_on_next_call() {
908 let mgr = make_manager();
909 let mut prog = mgr
910 .compile("fuel-recovery".into(), WAT_FUEL_RECOVERY.as_bytes(), 0)
911 .unwrap();
912 let ctx = HookContext::Tick;
913
914 assert!(mgr
915 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
916 .is_none());
917 assert_eq!(prog.consecutive_traps, 1);
918
919 let recovered = mgr
920 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
921 .expect("fuel must be reset before reusing the cached store");
922 assert_eq!(
923 recovered.hook_result.unwrap().verdict,
924 Verdict::Continue as u32
925 );
926 assert_eq!(prog.consecutive_traps, 0);
927 }
928
929 #[test]
930 fn bulk_memory_fill_and_copy_preserve_all_bytes() {
931 let mgr = make_manager();
932 let mut prog = mgr
933 .compile("bulk-memory".into(), WAT_BULK_MEMORY.as_bytes(), 0)
934 .unwrap();
935
936 let result = mgr
937 .execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
938 .unwrap();
939 let copied = result.modified_data.unwrap();
940 assert_eq!(copied.len(), 0x4000);
941 assert!(copied.iter().all(|byte| *byte == 0x5a));
942 }
943
944 #[test]
945 fn chain_ordering() {
946 let mgr = make_manager();
947 let high = mgr
948 .compile("high".into(), WAT_DROP.as_bytes(), 100)
949 .unwrap();
950 let low = mgr
951 .compile("low".into(), WAT_CONTINUE.as_bytes(), 0)
952 .unwrap();
953 let mut programs = vec![high, low];
955 programs.sort_by_key(|program| std::cmp::Reverse(program.priority));
957
958 let ctx = HookContext::Tick;
959 let result = mgr.run_chain(&mut programs, &ctx, &NullEngine, 0.0);
960 let exec = result.unwrap();
962 let r = exec.hook_result.unwrap();
963 assert!(r.is_drop());
964 }
965
966 #[test]
967 fn attach_detach() {
968 use crate::hooks::HookSlot;
969
970 let mgr = make_manager();
971 let mut slot = HookSlot {
972 programs: Vec::new(),
973 runner: crate::hooks::hook_noop,
974 };
975
976 let p1 = mgr
977 .compile("alpha".into(), WAT_CONTINUE.as_bytes(), 10)
978 .unwrap();
979 let p2 = mgr.compile("beta".into(), WAT_DROP.as_bytes(), 20).unwrap();
980
981 slot.attach(p1);
982 assert_eq!(slot.programs.len(), 1);
983 assert!(!std::ptr::eq(
984 slot.runner as *const (),
985 crate::hooks::hook_noop as *const (),
986 ));
987
988 slot.attach(p2);
989 assert_eq!(slot.programs.len(), 2);
990 assert_eq!(slot.programs[0].name, "beta");
992 assert_eq!(slot.programs[1].name, "alpha");
993
994 let removed = slot.detach("beta");
995 assert!(removed.is_some());
996 assert_eq!(slot.programs.len(), 1);
997 assert_eq!(slot.programs[0].name, "alpha");
998
999 let removed2 = slot.detach("alpha");
1000 assert!(removed2.is_some());
1001 assert!(slot.programs.is_empty());
1002 assert_eq!(
1003 slot.runner as *const () as usize,
1004 crate::hooks::hook_noop as *const () as usize
1005 );
1006 }
1007
1008 #[test]
1009 fn host_has_path() {
1010 use crate::engine_access::EngineAccess;
1011
1012 struct MockEngine;
1013 impl EngineAccess for MockEngine {
1014 fn has_path(&self, _dest: &[u8; 16]) -> bool {
1015 true
1016 }
1017 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
1018 None
1019 }
1020 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
1021 None
1022 }
1023 fn is_blackholed(&self, _: &[u8; 16]) -> bool {
1024 false
1025 }
1026 fn interface_name(&self, _: u64) -> Option<String> {
1027 None
1028 }
1029 fn interface_mode(&self, _: u64) -> Option<u8> {
1030 None
1031 }
1032 fn identity_hash(&self) -> Option<[u8; 16]> {
1033 None
1034 }
1035 fn announce_rate(&self, _: u64) -> Option<i32> {
1036 None
1037 }
1038 fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
1039 None
1040 }
1041 }
1042
1043 let mgr = make_manager();
1044 let mut prog = mgr
1045 .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
1046 .unwrap();
1047 let ctx = HookContext::Tick;
1048 let result = mgr.execute_program(&mut prog, &ctx, &MockEngine, 0.0, None);
1049 let exec = result.unwrap();
1051 let r = exec.hook_result.unwrap();
1052 assert!(r.is_drop());
1053 }
1054
1055 #[test]
1056 fn host_has_path_null_engine() {
1057 let mgr = make_manager();
1059 let mut prog = mgr
1060 .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
1061 .unwrap();
1062 let ctx = HookContext::Tick;
1063 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
1064 let exec = result.unwrap();
1065 let r = exec.hook_result.unwrap();
1066 assert_eq!(r.verdict, Verdict::Continue as u32);
1067 }
1068
1069 struct MockEngineCustom {
1073 announce_rate_val: Option<i32>,
1074 link_state_val: Option<u8>,
1075 }
1076
1077 impl EngineAccess for MockEngineCustom {
1078 fn has_path(&self, _: &[u8; 16]) -> bool {
1079 false
1080 }
1081 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
1082 None
1083 }
1084 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
1085 None
1086 }
1087 fn is_blackholed(&self, _: &[u8; 16]) -> bool {
1088 false
1089 }
1090 fn interface_name(&self, _: u64) -> Option<String> {
1091 None
1092 }
1093 fn interface_mode(&self, _: u64) -> Option<u8> {
1094 None
1095 }
1096 fn identity_hash(&self) -> Option<[u8; 16]> {
1097 None
1098 }
1099 fn announce_rate(&self, _: u64) -> Option<i32> {
1100 self.announce_rate_val
1101 }
1102 fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
1103 self.link_state_val
1104 }
1105 }
1106
1107 const WAT_ANNOUNCE_RATE: &str = r#"
1109 (module
1110 (import "env" "host_get_announce_rate" (func $get_rate (param i64) (result i32)))
1111 (memory (export "memory") 1)
1112 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1113 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
1114 (if (i32.ge_s (call $get_rate (i64.const 42)) (i32.const 0))
1115 (then
1116 (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
1117 )
1118 (else
1119 (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
1120 )
1121 )
1122 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1123 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1124 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1125 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1126 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1127 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1128 (i32.const 0x2000)
1129 )
1130 )
1131 "#;
1132
1133 #[test]
1134 fn host_get_announce_rate_found() {
1135 let engine = MockEngineCustom {
1137 announce_rate_val: Some(1500),
1138 link_state_val: None,
1139 };
1140 let mgr = make_manager();
1141 let mut prog = mgr
1142 .compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
1143 .unwrap();
1144 let ctx = HookContext::Tick;
1145 let exec = mgr
1146 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1147 .unwrap();
1148 assert!(exec.hook_result.unwrap().is_drop());
1149 }
1150
1151 #[test]
1152 fn host_get_announce_rate_not_found() {
1153 let engine = MockEngineCustom {
1155 announce_rate_val: None,
1156 link_state_val: None,
1157 };
1158 let mgr = make_manager();
1159 let mut prog = mgr
1160 .compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
1161 .unwrap();
1162 let ctx = HookContext::Tick;
1163 let exec = mgr
1164 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1165 .unwrap();
1166 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
1167 }
1168
1169 const WAT_LINK_STATE: &str = r#"
1172 (module
1173 (import "env" "host_get_link_state" (func $link_state (param i32) (result i32)))
1174 (memory (export "memory") 1)
1175 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1176 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
1177 (if (i32.eq (call $link_state (i32.const 0x3000)) (i32.const 2))
1178 (then
1179 (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
1180 )
1181 (else
1182 (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
1183 )
1184 )
1185 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1186 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1187 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1188 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1189 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1190 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1191 (i32.const 0x2000)
1192 )
1193 )
1194 "#;
1195
1196 #[test]
1197 fn host_get_link_state_active() {
1198 let engine = MockEngineCustom {
1200 announce_rate_val: None,
1201 link_state_val: Some(2),
1202 };
1203 let mgr = make_manager();
1204 let mut prog = mgr
1205 .compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
1206 .unwrap();
1207 let ctx = HookContext::Tick;
1208 let exec = mgr
1209 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1210 .unwrap();
1211 assert!(exec.hook_result.unwrap().is_drop());
1212 }
1213
1214 #[test]
1215 fn host_get_link_state_not_found() {
1216 let engine = MockEngineCustom {
1218 announce_rate_val: None,
1219 link_state_val: None,
1220 };
1221 let mgr = make_manager();
1222 let mut prog = mgr
1223 .compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
1224 .unwrap();
1225 let ctx = HookContext::Tick;
1226 let exec = mgr
1227 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
1228 .unwrap();
1229 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
1230 }
1231
1232 const WAT_INJECT_ACTION: &str = r#"
1237 (module
1238 (import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))
1239 (memory (export "memory") 1)
1240 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1241 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
1242 ;; Write the data payload at 0x3100
1243 (i32.store8 (i32.const 0x3100) (i32.const 0xDE))
1244 (i32.store8 (i32.const 0x3101) (i32.const 0xAD))
1245 (i32.store8 (i32.const 0x3102) (i32.const 0xBE))
1246 (i32.store8 (i32.const 0x3103) (i32.const 0xEF))
1247
1248 ;; Write ActionWire at 0x3000:
1249 ;; byte 0: tag = 0 (SendOnInterface)
1250 (i32.store8 (i32.const 0x3000) (i32.const 0))
1251 ;; bytes 1-8: interface = 1 (u64 LE)
1252 (i64.store (i32.const 0x3001) (i64.const 1))
1253 ;; bytes 9-12: data_offset = 0x3100 (u32 LE)
1254 (i32.store (i32.const 0x3009) (i32.const 0x3100))
1255 ;; bytes 13-16: data_len = 4 (u32 LE)
1256 (i32.store (i32.const 0x300D) (i32.const 4))
1257
1258 ;; Call inject: ptr=0x3000, len=17 (1 + 8 + 4 + 4)
1259 (drop (call $inject (i32.const 0x3000) (i32.const 17)))
1260
1261 ;; Return Continue
1262 (i32.store (i32.const 0x2000) (i32.const 0))
1263 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1264 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1265 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1266 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1267 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1268 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1269 (i32.const 0x2000)
1270 )
1271 )
1272 "#;
1273
1274 #[test]
1275 fn host_inject_action_send() {
1276 let mgr = make_manager();
1277 let mut prog = mgr
1278 .compile("inject".into(), WAT_INJECT_ACTION.as_bytes(), 0)
1279 .unwrap();
1280 let ctx = HookContext::Tick;
1281 let exec = mgr
1282 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1283 .unwrap();
1284 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
1285 assert_eq!(exec.injected_actions.len(), 1);
1286 match &exec.injected_actions[0] {
1287 crate::wire::ActionWire::SendOnInterface { interface, raw } => {
1288 assert_eq!(*interface, 1);
1289 assert_eq!(raw, &[0xDE, 0xAD, 0xBE, 0xEF]);
1290 }
1291 other => panic!("expected SendOnInterface, got {:?}", other),
1292 }
1293 }
1294
1295 const WAT_MODIFY: &str = r#"
1297 (module
1298 (memory (export "memory") 1)
1299 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1300 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
1301 ;; Write modified data at 0x2100
1302 (i32.store8 (i32.const 0x2100) (i32.const 0xAA))
1303 (i32.store8 (i32.const 0x2101) (i32.const 0xBB))
1304 (i32.store8 (i32.const 0x2102) (i32.const 0xCC))
1305 (i32.store8 (i32.const 0x2103) (i32.const 0xDD))
1306
1307 ;; verdict = 2 (Modify)
1308 (i32.store (i32.const 0x2000) (i32.const 2))
1309 ;; modified_data_offset = 0x2100
1310 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x2100))
1311 ;; modified_data_len = 4
1312 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
1313 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1314 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1315 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1316 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1317 (i32.const 0x2000)
1318 )
1319 )
1320 "#;
1321
1322 #[test]
1323 fn modify_extracts_data() {
1324 let mgr = make_manager();
1325 let mut prog = mgr.compile("mod".into(), WAT_MODIFY.as_bytes(), 0).unwrap();
1326 let ctx = HookContext::Tick;
1327 let exec = mgr
1328 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1329 .unwrap();
1330 let r = exec.hook_result.unwrap();
1331 assert_eq!(r.verdict, Verdict::Modify as u32);
1332 let data = exec.modified_data.unwrap();
1333 assert_eq!(data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
1334 }
1335
1336 #[test]
1337 fn chain_accumulates_injected_actions() {
1338 let mgr = make_manager();
1341 let injector = mgr
1342 .compile("injector".into(), WAT_INJECT_ACTION.as_bytes(), 100)
1343 .unwrap();
1344 let dropper = mgr
1345 .compile("dropper".into(), WAT_DROP.as_bytes(), 0)
1346 .unwrap();
1347 let mut programs = vec![injector, dropper];
1348 programs.sort_by_key(|program| std::cmp::Reverse(program.priority));
1349
1350 let ctx = HookContext::Tick;
1351 let exec = mgr
1352 .run_chain(&mut programs, &ctx, &NullEngine, 0.0)
1353 .unwrap();
1354 assert!(exec.hook_result.unwrap().is_drop());
1356 assert_eq!(exec.injected_actions.len(), 1);
1358 }
1359
1360 const WAT_COUNTER: &str = r#"
1366 (module
1367 (memory (export "memory") 1)
1368 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1369 (global $counter (mut i32) (i32.const 0))
1370 (func (export "on_hook") (param i32) (result i32)
1371 ;; Increment counter
1372 (global.set $counter (i32.add (global.get $counter) (i32.const 1)))
1373 ;; Write counter value at 0x3000 (scratch area)
1374 (i32.store (i32.const 0x3000) (global.get $counter))
1375 ;; Return Continue with the counter stashed in modified_data region
1376 ;; verdict = 2 (Modify) so we can extract the counter via modified_data
1377 (i32.store (i32.const 0x2000) (i32.const 2))
1378 ;; modified_data_offset = 0x3000
1379 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x3000))
1380 ;; modified_data_len = 4
1381 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
1382 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1383 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1384 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1385 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1386 (i32.const 0x2000)
1387 )
1388 )
1389 "#;
1390
1391 fn extract_counter(exec: &ExecuteResult) -> u32 {
1392 let data = exec.modified_data.as_ref().expect("no modified data");
1393 assert_eq!(data.len(), 4);
1394 u32::from_le_bytes([data[0], data[1], data[2], data[3]])
1395 }
1396
1397 #[test]
1398 fn instance_persistence_counter() {
1399 let mgr = make_manager();
1400 let mut prog = mgr
1401 .compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
1402 .unwrap();
1403 let ctx = HookContext::Tick;
1404
1405 let exec1 = mgr
1407 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1408 .unwrap();
1409 assert_eq!(extract_counter(&exec1), 1);
1410
1411 let exec2 = mgr
1412 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1413 .unwrap();
1414 assert_eq!(extract_counter(&exec2), 2);
1415
1416 let exec3 = mgr
1417 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1418 .unwrap();
1419 assert_eq!(extract_counter(&exec3), 3);
1420 }
1421
1422 #[test]
1423 fn instance_persistence_resets_on_drop_cache() {
1424 let mgr = make_manager();
1425 let mut prog = mgr
1426 .compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
1427 .unwrap();
1428 let ctx = HookContext::Tick;
1429
1430 mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1432 .unwrap();
1433 let exec2 = mgr
1434 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1435 .unwrap();
1436 assert_eq!(extract_counter(&exec2), 2);
1437
1438 prog.drop_cache();
1440
1441 let exec3 = mgr
1443 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1444 .unwrap();
1445 assert_eq!(extract_counter(&exec3), 1);
1446 }
1447
1448 const WAT_NO_ABI_VERSION: &str = r#"
1452 (module
1453 (memory (export "memory") 1)
1454 (func (export "on_hook") (param i32) (result i32)
1455 (i32.store (i32.const 0x2000) (i32.const 0))
1456 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1457 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1458 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1459 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1460 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1461 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1462 (i32.const 0x2000)
1463 )
1464 )
1465 "#;
1466
1467 const WAT_WRONG_ABI_VERSION: &str = r#"
1469 (module
1470 (memory (export "memory") 1)
1471 (func (export "__rns_abi_version") (result i32) (i32.const 9999))
1472 (func (export "on_hook") (param i32) (result i32)
1473 (i32.store (i32.const 0x2000) (i32.const 0))
1474 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1475 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1476 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1477 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1478 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1479 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1480 (i32.const 0x2000)
1481 )
1482 )
1483 "#;
1484
1485 #[test]
1486 fn rejects_missing_abi_version() {
1487 let mgr = make_manager();
1488 let result = mgr.compile("no_abi".into(), WAT_NO_ABI_VERSION.as_bytes(), 0);
1489 match result {
1490 Err(HookError::AbiVersionMismatch {
1491 hook_name,
1492 expected,
1493 found,
1494 }) => {
1495 assert_eq!(hook_name, "no_abi");
1496 assert_eq!(expected, HOST_ABI_VERSION);
1497 assert_eq!(found, None);
1498 }
1499 other => panic!(
1500 "expected AbiVersionMismatch with found=None, got {:?}",
1501 other.err()
1502 ),
1503 }
1504 }
1505
1506 #[test]
1507 fn rejects_wrong_abi_version() {
1508 let mgr = make_manager();
1509 let result = mgr.compile("bad_abi".into(), WAT_WRONG_ABI_VERSION.as_bytes(), 0);
1510 match result {
1511 Err(HookError::AbiVersionMismatch {
1512 hook_name,
1513 expected,
1514 found,
1515 }) => {
1516 assert_eq!(hook_name, "bad_abi");
1517 assert_eq!(expected, HOST_ABI_VERSION);
1518 assert_eq!(found, Some(9999));
1519 }
1520 other => panic!(
1521 "expected AbiVersionMismatch with found=Some(9999), got {:?}",
1522 other.err()
1523 ),
1524 }
1525 }
1526
1527 #[test]
1528 fn accepts_correct_abi_version() {
1529 let mgr = make_manager();
1530 let result = mgr.compile("good_abi".into(), WAT_CONTINUE.as_bytes(), 0);
1531 assert!(
1532 result.is_ok(),
1533 "compile should succeed with correct ABI version"
1534 );
1535 }
1536
1537 #[test]
1538 fn host_emit_event_collects_provider_event() {
1539 let mgr = make_manager();
1540 let wat = r#"
1541 (module
1542 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1543 (memory (export "memory") 1)
1544 (data (i32.const 0x3000) "packet")
1545 (data (i32.const 0x3010) "\01\02\03")
1546 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1547 (func (export "on_hook") (param i32) (result i32)
1548 (drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
1549 (i32.store (i32.const 0x2000) (i32.const 0))
1550 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1551 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1552 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1553 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1554 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1555 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1556 (i32.const 0x2000)
1557 )
1558 )
1559 "#;
1560 let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
1561 let mut prog = LoadedProgram::new("emit".into(), module, 0);
1562 let ctx = HookContext::Tick;
1563
1564 let exec = mgr
1565 .execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, true, None)
1566 .unwrap();
1567 assert_eq!(exec.provider_events.len(), 1);
1568 assert_eq!(exec.provider_events[0].hook_name, "emit");
1569 assert_eq!(exec.provider_events[0].payload_type, "packet");
1570 assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
1571 }
1572
1573 #[test]
1574 fn host_emit_event_is_ignored_when_disabled() {
1575 let mgr = make_manager();
1576 let wat = r#"
1577 (module
1578 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1579 (memory (export "memory") 1)
1580 (data (i32.const 0x3000) "packet")
1581 (data (i32.const 0x3010) "\01\02\03")
1582 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1583 (func (export "on_hook") (param i32) (result i32)
1584 (drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
1585 (i32.store (i32.const 0x2000) (i32.const 0))
1586 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1587 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1588 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1589 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1590 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1591 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1592 (i32.const 0x2000)
1593 )
1594 )
1595 "#;
1596 let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
1597 let mut prog = LoadedProgram::new("emit".into(), module, 0);
1598 let ctx = HookContext::Tick;
1599
1600 let exec = mgr
1601 .execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, false, None)
1602 .unwrap();
1603 assert!(exec.provider_events.is_empty());
1604 }
1605
1606 #[test]
1607 fn run_chain_returns_continue_only_provider_events() {
1608 let manager = make_manager();
1609 let wasm = wat::parse_str(
1610 r#"(module
1611 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1612 (memory (export "memory") 1)
1613 (data (i32.const 4096) "\00\00\00\00")
1614 (data (i32.const 8192) "packet")
1615 (data (i32.const 8208) "\01\02\03")
1616 (func (export "__rns_abi_version") (result i32) i32.const 1)
1617 (func (export "on_hook") (param i32) (result i32)
1618 i32.const 8192
1619 i32.const 6
1620 i32.const 8208
1621 i32.const 3
1622 call $emit
1623 drop
1624 i32.const 4096
1625 )
1626 )"#,
1627 )
1628 .unwrap();
1629
1630 let mut programs = vec![manager.compile("emit".into(), &wasm, 0).unwrap()];
1631 let pkt_ctx = crate::PacketContext {
1632 flags: 0,
1633 hops: 0,
1634 destination_hash: [0; 16],
1635 context: 0,
1636 packet_hash: [0; 32],
1637 interface_id: 1,
1638 data_offset: 0,
1639 data_len: 0,
1640 };
1641 let ctx = crate::HookContext::Packet {
1642 ctx: &pkt_ctx,
1643 raw: &[],
1644 };
1645
1646 let exec = manager
1647 .run_chain_with_provider_events(&mut programs, &ctx, &NullEngine, 0.0, true)
1648 .expect("expected provider event result");
1649 assert!(exec.hook_result.is_none());
1650 assert!(exec.injected_actions.is_empty());
1651 assert_eq!(exec.provider_events.len(), 1);
1652 assert_eq!(exec.provider_events[0].payload_type, "packet");
1653 assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
1654 }
1655
1656 fn hook_module(imports: &str, pages: u32, body: &str, out_len: u32) -> String {
1660 let verdict = if out_len == 0 { 0 } else { 2 };
1661 format!(
1662 r#"(module
1663 {imports}
1664 (memory (export "memory") {pages})
1665 (global $calls (mut i32) (i32.const 0))
1666 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1667 (func (export "on_hook") (param i32) (result i32)
1668 (local $i i32)
1669 (global.set $calls (i32.add (global.get $calls) (i32.const 1)))
1670 {body}
1671 (i32.store (i32.const 0x2000) (i32.const {verdict}))
1672 (i32.store (i32.const 0x2004) (i32.const 0x3000))
1673 (i32.store (i32.const 0x2008) (i32.const {out_len}))
1674 (i32.store (i32.const 0x200c) (i32.const 0))
1675 (i32.store (i32.const 0x2010) (i32.const 0))
1676 (i32.store (i32.const 0x2014) (i32.const 0))
1677 (i32.store (i32.const 0x2018) (i32.const 0))
1678 (i32.const 0x2000)))"#
1679 )
1680 }
1681
1682 fn read_i32s(data: &[u8]) -> Vec<i32> {
1683 data.chunks_exact(4)
1684 .map(|c| i32::from_le_bytes(c.try_into().unwrap()))
1685 .collect()
1686 }
1687
1688 fn cached_store(prog: &LoadedProgram) -> &Store<StoreData> {
1689 match &prog.backend {
1690 ProgramBackend::Wasm(wasm) => &wasm.cached.as_ref().expect("cached store").0,
1691 _ => panic!("expected a wasm program"),
1692 }
1693 }
1694
1695 struct AnsweringEngine;
1696 impl EngineAccess for AnsweringEngine {
1697 fn has_path(&self, _: &[u8; 16]) -> bool {
1698 true
1699 }
1700 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
1701 Some(2)
1702 }
1703 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
1704 Some([7; 16])
1705 }
1706 fn is_blackholed(&self, _: &[u8; 16]) -> bool {
1707 true
1708 }
1709 fn interface_name(&self, _: u64) -> Option<String> {
1710 Some("eth0".into())
1711 }
1712 fn interface_mode(&self, _: u64) -> Option<u8> {
1713 Some(1)
1714 }
1715 fn identity_hash(&self) -> Option<[u8; 16]> {
1716 Some([9; 16])
1717 }
1718 fn announce_rate(&self, _: u64) -> Option<i32> {
1719 Some(1)
1720 }
1721 fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
1722 Some(2)
1723 }
1724 }
1725
1726 const ALL_MEMORY_HOST_FNS: &str = r#"
1727 (import "env" "host_log" (func $log (param i32 i32)))
1728 (import "env" "host_has_path" (func $has_path (param i32) (result i32)))
1729 (import "env" "host_get_hops" (func $hops (param i32) (result i32)))
1730 (import "env" "host_get_next_hop" (func $next_hop (param i32 i32) (result i32)))
1731 (import "env" "host_is_blackholed" (func $blackholed (param i32) (result i32)))
1732 (import "env" "host_get_interface_name" (func $ifname (param i64 i32 i32) (result i32)))
1733 (import "env" "host_get_transport_identity" (func $identity (param i32) (result i32)))
1734 (import "env" "host_get_link_state" (func $link (param i32) (result i32)))
1735 (import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))
1736 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1737 "#;
1738
1739 #[test]
1740 fn host_fns_reject_out_of_range_guest_pointers() {
1741 let mgr = make_manager();
1742 let body = r#"
1743 (call $log (i32.const -16) (i32.const 0x7fffffff))
1744 (call $log (i32.const 0x100) (i32.const -1))
1745 (i32.store (i32.const 0x3000) (call $has_path (i32.const -16)))
1746 (i32.store (i32.const 0x3004) (call $hops (i32.const -16)))
1747 (i32.store (i32.const 0x3008) (call $next_hop (i32.const 0x100) (i32.const -16)))
1748 (i32.store (i32.const 0x300c) (call $next_hop (i32.const -16) (i32.const 0x100)))
1749 (i32.store (i32.const 0x3010) (call $blackholed (i32.const -16)))
1750 (i32.store (i32.const 0x3014)
1751 (call $ifname (i64.const 1) (i32.const -16) (i32.const 0x7fffffff)))
1752 (i32.store (i32.const 0x3018) (call $identity (i32.const -16)))
1753 (i32.store (i32.const 0x301c) (call $link (i32.const -16)))
1754 (i32.store (i32.const 0x3020) (call $inject (i32.const -16) (i32.const 0x7fffffff)))
1755 (i32.store (i32.const 0x3024)
1756 (call $emit (i32.const -16) (i32.const 16) (i32.const 0) (i32.const 0)))
1757 (i32.store (i32.const 0x3028)
1758 (call $emit (i32.const 0) (i32.const 0) (i32.const -16) (i32.const 16)))
1759 (i32.store (i32.const 0x302c) (call $next_hop (i32.const 0x100) (i32.const 0x200)))
1760 "#;
1761 let wat = hook_module(ALL_MEMORY_HOST_FNS, 1, body, 48);
1762 let mut prog = mgr.compile("bounds".into(), wat.as_bytes(), 0).unwrap();
1763 let expected = vec![0, -1, 0, 0, 0, -1, 0, -1, -1, -1, -1, 1];
1764
1765 for _ in 0..2 {
1766 let exec = mgr
1767 .execute_program_with_provider_events(
1768 &mut prog,
1769 &HookContext::Tick,
1770 &AnsweringEngine,
1771 0.0,
1772 true,
1773 None,
1774 )
1775 .expect("hook keeps running after out-of-range pointers");
1776 assert_eq!(read_i32s(&exec.modified_data.unwrap()), expected);
1777 assert!(exec.injected_actions.is_empty());
1778 assert!(exec.provider_events.is_empty());
1779 }
1780 assert_eq!(prog.consecutive_traps, 0);
1781 }
1782
1783 #[test]
1784 fn result_offset_near_end_of_address_space_is_rejected() {
1785 let mgr = make_manager();
1786 let wat = r#"(module
1787 (memory (export "memory") 1)
1788 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1789 (func (export "on_hook") (param i32) (result i32) (i32.const -4)))"#;
1790 let mut prog = mgr.compile("offset".into(), wat.as_bytes(), 0).unwrap();
1791 assert!(mgr
1792 .execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
1793 .is_none());
1794 assert_eq!(prog.consecutive_traps, 1);
1795
1796 let mut valid = mgr
1797 .compile("valid".into(), WAT_CONTINUE.as_bytes(), 0)
1798 .unwrap();
1799 assert!(mgr
1800 .execute_program(&mut valid, &HookContext::Tick, &NullEngine, 0.0, None)
1801 .is_some());
1802 }
1803
1804 #[test]
1805 fn host_panic_is_contained_and_program_recovers() {
1806 struct PanickingEngine;
1807 impl EngineAccess for PanickingEngine {
1808 fn has_path(&self, _: &[u8; 16]) -> bool {
1809 panic!("engine lookup failed")
1810 }
1811 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
1812 None
1813 }
1814 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
1815 None
1816 }
1817 fn is_blackholed(&self, _: &[u8; 16]) -> bool {
1818 false
1819 }
1820 fn interface_name(&self, _: u64) -> Option<String> {
1821 None
1822 }
1823 fn interface_mode(&self, _: u64) -> Option<u8> {
1824 None
1825 }
1826 fn identity_hash(&self) -> Option<[u8; 16]> {
1827 None
1828 }
1829 fn announce_rate(&self, _: u64) -> Option<i32> {
1830 None
1831 }
1832 fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
1833 None
1834 }
1835 }
1836
1837 let mgr = make_manager();
1838 let mut prog = mgr
1839 .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
1840 .unwrap();
1841 assert!(mgr
1842 .execute_program(&mut prog, &HookContext::Tick, &PanickingEngine, 0.0, None)
1843 .is_none());
1844 assert_eq!(prog.consecutive_traps, 1);
1845
1846 let exec = mgr
1847 .execute_program(&mut prog, &HookContext::Tick, &AnsweringEngine, 0.0, None)
1848 .expect("program runs again after a contained host panic");
1849 assert!(exec.hook_result.unwrap().is_drop());
1850 assert_eq!(prog.consecutive_traps, 0);
1851 }
1852
1853 #[test]
1854 fn memory_grow_past_limit_traps_and_host_continues() {
1855 let mgr = make_manager();
1856 let body = r#"
1857 (if (i32.eq (global.get $calls) (i32.const 1))
1858 (then (drop (memory.grow (i32.const 8192)))))
1859 (i32.store (i32.const 0x3000) (memory.size))
1860 "#;
1861 let wat = hook_module("", 1, body, 4);
1862 let mut prog = mgr.compile("grow".into(), wat.as_bytes(), 0).unwrap();
1863 assert!(mgr
1864 .execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
1865 .is_none());
1866 assert_eq!(prog.consecutive_traps, 1);
1867
1868 let exec = mgr
1869 .execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
1870 .expect("program runs after a refused grow");
1871 assert_eq!(read_i32s(&exec.modified_data.unwrap()), vec![1]);
1872 }
1873
1874 #[test]
1875 fn memory_grow_within_limit_succeeds() {
1876 let mgr = make_manager();
1877 let body = r#"
1878 (i32.store (i32.const 0x3000) (memory.grow (i32.const 100)))
1879 (i32.store (i32.const 0x3004) (memory.size))
1880 "#;
1881 let wat = hook_module("", 1, body, 8);
1882 let mut prog = mgr.compile("grow".into(), wat.as_bytes(), 0).unwrap();
1883 let exec = mgr
1884 .execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
1885 .unwrap();
1886 assert_eq!(read_i32s(&exec.modified_data.unwrap()), vec![1, 101]);
1887 }
1888
1889 #[test]
1890 fn rejects_memory64_modules() {
1891 let mgr = make_manager();
1892 let wat = r#"(module
1893 (memory (export "memory") i64 1)
1894 (func (export "__rns_abi_version") (result i32) (i32.const 1)))"#;
1895 let err = mgr
1896 .compile("m64".into(), wat.as_bytes(), 0)
1897 .err()
1898 .expect("memory64 module is rejected");
1899 assert!(err.to_string().contains("memory64"), "{}", err);
1900 }
1901
1902 #[test]
1903 fn rejects_declared_minimum_memory_above_limit() {
1904 let mgr = make_manager();
1905 let exported = r#"(module
1906 (memory (export "memory") 1024)
1907 (func (export "__rns_abi_version") (result i32) (i32.const 1)))"#;
1908 let err = mgr
1909 .compile("big".into(), exported.as_bytes(), 0)
1910 .err()
1911 .expect("module above the memory limit is rejected");
1912 assert!(err.to_string().contains("minimum memory"), "{}", err);
1913
1914 let internal = r#"(module
1915 (memory 1024)
1916 (func (export "__rns_abi_version") (result i32) (i32.const 1)))"#;
1917 assert!(mgr.compile("big".into(), internal.as_bytes(), 0).is_err());
1918 }
1919
1920 #[test]
1921 fn module_at_memory_limit_runs() {
1922 let mgr = make_manager();
1923 let pages = (crate::runtime::DEFAULT_MAX_MEMORY / 65536) as u32;
1924 let body = r#"(i32.store (i32.const 0x3000) (memory.size))"#;
1925 let wat = hook_module("", pages, body, 4);
1926 let mut prog = mgr.compile("full".into(), wat.as_bytes(), 0).unwrap();
1927 let exec = mgr
1928 .execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
1929 .unwrap();
1930 assert_eq!(read_i32s(&exec.modified_data.unwrap()), vec![pages as i32]);
1931 }
1932
1933 #[test]
1934 fn host_log_forwards_at_most_the_per_call_cap() {
1935 let mgr = make_manager();
1936 let imports = r#"(import "env" "host_log" (func $log (param i32 i32)))"#;
1937 let body = r#"
1938 (local.set $i (i32.const 0))
1939 (block $done (loop $next
1940 (br_if $done (i32.ge_u (local.get $i) (i32.const 100)))
1941 (call $log (i32.const 0x4000) (i32.const 4096))
1942 (local.set $i (i32.add (local.get $i) (i32.const 1)))
1943 (br $next)))
1944 "#;
1945 let wat = hook_module(imports, 1, body, 0);
1946 let mut prog = mgr.compile("log".into(), wat.as_bytes(), 0).unwrap();
1947 for _ in 0..2 {
1948 mgr.execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
1949 .unwrap();
1950 let data = cached_store(&prog).data();
1951 assert_eq!(
1952 data.log_messages_forwarded,
1953 crate::runtime::MAX_LOG_MESSAGES_PER_CALL
1954 );
1955 assert_eq!(
1956 data.log_messages_dropped,
1957 100 - crate::runtime::MAX_LOG_MESSAGES_PER_CALL
1958 );
1959 }
1960 }
1961
1962 fn inject_repeatedly(count: u32, payload_len: u32) -> String {
1966 let imports =
1967 r#"(import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))"#;
1968 let body = format!(
1969 r#"
1970 (i32.store8 (i32.const 0x3100) (i32.const 0))
1971 (i64.store (i32.const 0x3101) (i64.const 1))
1972 (i32.store (i32.const 0x3109) (i32.const 0x10000))
1973 (i32.store (i32.const 0x310d) (i32.const {payload_len}))
1974 (i32.store (i32.const 0x3000) (i32.const 0))
1975 (local.set $i (i32.const 0))
1976 (block $done (loop $next
1977 (br_if $done (i32.ge_u (local.get $i) (i32.const {count})))
1978 (i32.store (i32.const 0x3004) (call $inject (i32.const 0x3100) (i32.const 17)))
1979 (if (i32.eqz (i32.load (i32.const 0x3004)))
1980 (then (i32.store (i32.const 0x3000)
1981 (i32.add (i32.load (i32.const 0x3000)) (i32.const 1)))))
1982 (local.set $i (i32.add (local.get $i) (i32.const 1)))
1983 (br $next)))
1984 "#
1985 );
1986 hook_module(imports, 3, &body, 8)
1987 }
1988
1989 #[test]
1990 fn host_inject_action_refuses_actions_beyond_count_cap() {
1991 let mgr = make_manager();
1992 let cap = crate::runtime::MAX_INJECTED_ACTIONS_PER_CALL;
1993 let wat = inject_repeatedly(cap as u32 + 6, 4);
1994 let mut prog = mgr.compile("inject".into(), wat.as_bytes(), 0).unwrap();
1995 let exec = mgr
1996 .execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
1997 .unwrap();
1998 assert_eq!(
1999 read_i32s(&exec.modified_data.unwrap()),
2000 vec![cap as i32, host_fns::LIMIT_REACHED]
2001 );
2002 assert_eq!(exec.injected_actions.len(), cap);
2003 }
2004
2005 #[test]
2006 fn host_inject_action_refuses_actions_beyond_byte_cap() {
2007 let mgr = make_manager();
2008 let wat = inject_repeatedly(5, 65536);
2009 let mut prog = mgr.compile("inject".into(), wat.as_bytes(), 0).unwrap();
2010 let exec = mgr
2011 .execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
2012 .unwrap();
2013 assert_eq!(
2015 read_i32s(&exec.modified_data.unwrap()),
2016 vec![3, host_fns::LIMIT_REACHED]
2017 );
2018 assert_eq!(exec.injected_actions.len(), 3);
2019 for action in &exec.injected_actions {
2020 match action {
2021 crate::wire::ActionWire::SendOnInterface { raw, .. } => {
2022 assert_eq!(raw.len(), 65536)
2023 }
2024 other => panic!("expected SendOnInterface, got {:?}", other),
2025 }
2026 }
2027 }
2028
2029 #[test]
2030 fn host_emit_event_refuses_events_beyond_count_cap() {
2031 let mgr = make_manager();
2032 let cap = crate::runtime::MAX_PROVIDER_EVENTS_PER_CALL;
2033 let imports =
2034 r#"(import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))"#;
2035 let body = format!(
2036 r#"
2037 (i32.store (i32.const 0x3000) (i32.const 0))
2038 (local.set $i (i32.const 0))
2039 (block $done (loop $next
2040 (br_if $done (i32.ge_u (local.get $i) (i32.const {count})))
2041 (i32.store (i32.const 0x3004)
2042 (call $emit (i32.const 0x3100) (i32.const 4) (i32.const 0x3200) (i32.const 8)))
2043 (if (i32.eqz (i32.load (i32.const 0x3004)))
2044 (then (i32.store (i32.const 0x3000)
2045 (i32.add (i32.load (i32.const 0x3000)) (i32.const 1)))))
2046 (local.set $i (i32.add (local.get $i) (i32.const 1)))
2047 (br $next)))
2048 "#,
2049 count = cap + 10
2050 );
2051 let wat = hook_module(imports, 1, &body, 8);
2052 let mut prog = mgr.compile("emit".into(), wat.as_bytes(), 0).unwrap();
2053 let exec = mgr
2054 .execute_program_with_provider_events(
2055 &mut prog,
2056 &HookContext::Tick,
2057 &NullEngine,
2058 0.0,
2059 true,
2060 None,
2061 )
2062 .unwrap();
2063 assert_eq!(
2064 read_i32s(&exec.modified_data.unwrap()),
2065 vec![cap as i32, host_fns::LIMIT_REACHED]
2066 );
2067 assert_eq!(exec.provider_events.len(), cap);
2068 }
2069
2070 #[test]
2071 fn host_copies_consume_fuel_per_byte() {
2072 let mgr = make_manager();
2073 let imports =
2074 r#"(import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))"#;
2075 let fuel_used = |payload_len: u32| {
2076 let body = format!(
2077 r#"(drop (call $emit (i32.const 0x100) (i32.const 4)
2078 (i32.const 0x4000) (i32.const {payload_len})))"#
2079 );
2080 let wat = hook_module(imports, 2, &body, 0);
2081 let mut prog = mgr.compile("fuel".into(), wat.as_bytes(), 0).unwrap();
2082 let exec = mgr
2083 .execute_program_with_provider_events(
2084 &mut prog,
2085 &HookContext::Tick,
2086 &NullEngine,
2087 0.0,
2088 true,
2089 None,
2090 )
2091 .unwrap();
2092 assert_eq!(exec.provider_events[0].payload.len(), payload_len as usize);
2093 mgr.runtime.fuel() - cached_store(&prog).get_fuel().unwrap()
2094 };
2095 let small = fuel_used(0);
2096 let large = fuel_used(100_000);
2097 assert!(
2098 large - small >= 100_000,
2099 "fuel used: {} without payload, {} with 100000 bytes",
2100 small,
2101 large
2102 );
2103 }
2104}