Skip to main content

rns_hooks/
manager.rs

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/// ABI version the host expects from compiled hook modules.
18#[cfg(feature = "wasm")]
19const HOST_ABI_VERSION: i32 = rns_hooks_abi::ABI_VERSION;
20
21/// Central manager for hook execution.
22///
23/// Owns any enabled backend runtime state. Programs are stored in `HookSlot`s
24/// (one per hook point); the manager provides execution.
25pub 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    /// Compile WASM bytes into a LoadedProgram.
79    ///
80    /// Validates that the module exports `__rns_abi_version` returning the
81    /// expected ABI version before accepting it.
82    pub fn compile(
83        &self,
84        name: String,
85        bytes: &[u8],
86        priority: i32,
87    ) -> Result<LoadedProgram, HookError> {
88        #[cfg(not(feature = "wasm"))]
89        {
90            let _ = (name, bytes, priority);
91            return Err(HookError::CompileError(
92                "WASM hook backend not enabled".to_string(),
93            ));
94        }
95        #[cfg(feature = "wasm")]
96        {
97            let module = self
98                .runtime
99                .compile(bytes)
100                .map_err(|e| HookError::CompileError(format!("{:#}", e)))?;
101            self.validate_abi_version(&name, &module)?;
102            Ok(LoadedProgram::new(name, module, priority))
103        }
104    }
105
106    /// Check that the module exports `__rns_abi_version() -> i32` and that
107    /// the returned value matches [`HOST_ABI_VERSION`].
108    #[cfg(feature = "wasm")]
109    fn validate_abi_version(&self, name: &str, module: &wasmtime::Module) -> Result<(), HookError> {
110        // Check if the export exists in the module's type information.
111        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        // Instantiate the module to call the function and read the version.
121        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    /// Compile a WASM file from disk.
157    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    /// Execute a single program against a hook context. Returns an `ExecuteResult`
207    /// containing the hook result, any injected actions, and modified data (all
208    /// extracted from WASM memory before the store is dropped). Returns `None`
209    /// on trap/fuel exhaustion (fail-open).
210    ///
211    /// If `data_override` is provided (from a previous Modify verdict in a chain),
212    /// it replaces the packet data region in the arena after writing the context.
213    ///
214    /// The store and instance are cached in the program for cross-call state
215    /// persistence (WASM linear memory survives across invocations). On each call
216    /// we reset fuel and per-call StoreData fields but keep the WASM globals and
217    /// memory intact.
218    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            return 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        // Safety: transmute erases the lifetime on the fat pointer. The pointer
369        // is only dereferenced during this function call, while the borrow is valid.
370        let engine_access_ptr: *const dyn EngineAccess =
371            unsafe { std::mem::transmute(engine_access as *const dyn EngineAccess) };
372
373        // Take the cached store+instance out of program (or create fresh).
374        // We take ownership to avoid borrow-checker conflicts with program.record_*().
375        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            // Reset per-call state: fuel, engine_access, injected_actions, log_messages
384            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        // Write context into guest memory
428        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 a previous hook in the chain returned Modify, override the packet data
446        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                // Non-fatal: continue with original data
454            }
455        }
456
457        // Call the exported hook function
458        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                // A host function panicked while serving this program. The
486                // store may hold partial state, so drop it instead of caching
487                // it; the next call instantiates a fresh one.
488                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                // Fail-open: trap or fuel exhaustion → continue
505                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        // Read result from guest memory
521        // Guest pointers are unsigned 32-bit offsets.
522        let ret = match arena::read_result(&memory, &store, result_offset as u32 as usize) {
523            Ok(result) => {
524                program.record_success();
525
526                // Extract modified data from WASM memory
527                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                // Extract injected actions from the store
534                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        // Put the store+instance back for next call
559        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    /// Run a chain of programs. Stops on Drop or Halt, continues on Continue or Modify.
579    /// Returns an `ExecuteResult` accumulating all injected actions across the chain
580    /// and the last meaningful hook result (Drop/Halt/Modify), or None if all continued.
581    ///
582    /// When a hook returns Modify with modified data, subsequent hooks in the chain
583    /// receive the modified data (only applicable to Packet contexts).
584    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                        _ => {} // Continue → keep going
648                    }
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/// Run a call into guest code, converting a panic raised by a host function
670/// into an error so a single program cannot unwind through the driver thread.
671#[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    /// WAT module that returns Continue (verdict=0).
705    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    /// WAT module that returns Drop (verdict=1).
731    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    /// WAT module that traps immediately.
749    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    /// WAT module with infinite loop (will exhaust fuel).
760    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    /// Exhausts fuel on the first call, then succeeds from the same cached
772    /// instance. This verifies both store recovery and per-call fuel reset.
773    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    /// Exercises Wasmtime's fuel-accounted bulk-memory implementation and
794    /// returns the copied bytes through the hook ABI.
795    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    /// WAT module that calls host_has_path and drops if path exists.
814    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        // Continue → Some with verdict=0
852        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        // Should fail-open (fuel exhausted = trap)
902        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        // Programs sorted by priority desc: high first
954        let mut programs = vec![high, low];
955        // Sort descending by priority (as attach would do)
956        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        // High priority drops → chain stops
961        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        // Sorted descending: beta(20) before alpha(10)
991        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        // MockEngine.has_path returns true → WASM drops
1050        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        // NullEngine.has_path returns false → WASM continues
1058        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    // --- New Phase 2 tests ---
1070
1071    /// Configurable mock engine for testing host functions.
1072    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    /// WAT: calls host_get_announce_rate(42), if result >= 0 → Drop, else Continue.
1108    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        // announce_rate returns Some(1500) (1.5 Hz * 1000) → Drop
1136        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        // announce_rate returns None → -1 → Continue
1154        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    /// WAT: calls host_get_link_state with 16-byte hash at 0x3000.
1170    /// If state == 2 (Active) → Drop, else Continue.
1171    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        // link_state returns Some(2) (Active) → Drop
1199        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        // link_state returns None → -1, which != 2 → Continue
1217        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    /// WAT: writes a SendOnInterface ActionWire at 0x3000 and calls host_inject_action.
1233    /// Binary: tag=0 (SendOnInterface), interface=1 (u64 LE), data_offset=0x3100, data_len=4
1234    /// Data at 0x3100: [0xDE, 0xAD, 0xBE, 0xEF]
1235    /// Returns Continue.
1236    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    /// WAT: returns Modify (verdict=2) with modified data at 0x2100 (4 bytes).
1296    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        // Chain: inject_action module (Continue) + Drop module
1339        // Both should contribute to the result; injected actions should be accumulated
1340        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        // Chain should drop (second hook)
1355        assert!(exec.hook_result.unwrap().is_drop());
1356        // But injected action from first hook should be present
1357        assert_eq!(exec.injected_actions.len(), 1);
1358    }
1359
1360    // --- Instance persistence tests ---
1361
1362    /// WAT module with a mutable global counter. Each call increments it and
1363    /// writes the counter value into the verdict field (abusing it as an integer).
1364    /// This lets us verify that the global persists across calls.
1365    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        // Call 3 times — counter should increment across calls
1406        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        // Increment twice
1431        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        // Drop cache (simulates reload)
1439        prog.drop_cache();
1440
1441        // Counter should restart at 1
1442        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    // --- ABI version validation tests ---
1449
1450    /// WAT module without __rns_abi_version export.
1451    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    /// WAT module with wrong ABI version (9999).
1468    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    /// Build a hook module whose `on_hook` runs `body` and then returns a
1657    /// Modify verdict exposing `out_len` bytes at 0x3000 (or Continue when
1658    /// `out_len` is 0).
1659    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    /// Write a SendOnInterface action at 0x3100 whose payload is
1963    /// `payload_len` bytes at 0x10000, then inject it `count` times. Stores
1964    /// the number of accepted calls at 0x3000 and the last return code at 0x3004.
1965    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        // Each action carries 17 + 65536 bytes; three fit in 256 KiB.
2014        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}