Skip to main content

jsdet_core/
nested_wasm.rs

1/// Nested WASM execution — the novel capability.
2///
3/// When JavaScript inside `QuickJS` calls `new WebAssembly.Module(bytes)`,
4/// the host intercepts this and creates a SECOND wasmtime instance.
5///
6/// ```text
7/// ┌────────────────────────────────────┐
8/// │ Host (Rust + wasmtime)             │
9/// │                                    │
10/// │  ┌──────────────────────┐          │
11/// │  │ Outer WASM instance  │          │
12/// │  │ (QuickJS engine)     │          │
13/// │  │                      │          │
14/// │  │  JS calls:           │          │
15/// │  │  new WebAssembly     │          │
16/// │  │    .Module(bytes)    │          │
17/// │  │        │             │          │
18/// │  └────────┼─────────────┘          │
19/// │           │ host import            │
20/// │           ▼                        │
21/// │  ┌──────────────────────┐          │
22/// │  │ Inner WASM instance  │          │
23/// │  │ (user's WASM module) │          │
24/// │  │                      │          │
25/// │  │ - own linear memory  │          │
26/// │  │ - own fuel budget    │          │
27/// │  │ - no host imports    │          │
28/// │  │   (fully sealed)     │          │
29/// │  └──────────────────────┘          │
30/// └────────────────────────────────────┘
31/// ```
32///
33/// The inner instance:
34/// - Has its own linear memory (cannot read outer's memory)
35/// - Has its own fuel budget (cannot `DoS` the outer instance)
36/// - Gets NO host imports (fully sealed — cannot call bridge APIs)
37/// - Exports are callable from the outer instance via host trampolines
38///
39/// This means: even if the WASM module is a crypto miner, it runs
40/// inside our fuel budget and memory cap, then stops. We observe
41/// exactly what it imports, exports, and how much compute it uses.
42///
43use wasmtime::{Engine, Module, Store};
44
45use crate::config::SandboxConfig;
46use crate::error::{Error, Result};
47use crate::observation::Observation;
48
49/// Metadata about a nested WASM instantiation.
50#[derive(Debug, Clone)]
51pub struct NestedWasmInfo {
52    /// Size of the WASM module bytes.
53    pub module_size: usize,
54    /// Names of functions the module imports (expects the host to provide).
55    pub import_names: Vec<String>,
56    /// Names of functions the module exports.
57    pub export_names: Vec<String>,
58    /// Whether the module was actually instantiated (vs just analyzed).
59    pub instantiated: bool,
60    /// Fuel consumed during execution (if instantiated).
61    pub fuel_consumed: u64,
62}
63
64/// Analyze and optionally execute a WASM module provided by JavaScript.
65///
66/// Called when JS code does `new WebAssembly.Module(bytes)`.
67///
68/// # Errors
69///
70/// Returns an error if the initial metadata compilation fails.
71pub fn handle_wasm_instantiation(
72    engine: &Engine,
73    wasm_bytes: &[u8],
74    config: &SandboxConfig,
75) -> Result<(NestedWasmInfo, Observation)> {
76    // Compile the module to extract metadata.
77    let module = Module::new(engine, wasm_bytes)
78        .map_err(|e| Error::Internal(format!("nested WASM compilation failed: {e}")))?;
79
80    let import_names: Vec<String> = module
81        .imports()
82        .map(|imp| format!("{}.{}", imp.module(), imp.name()))
83        .collect();
84
85    let export_names: Vec<String> = module.exports().map(|exp| exp.name().to_string()).collect();
86
87    let mut info = NestedWasmInfo {
88        module_size: wasm_bytes.len(),
89        import_names: import_names.clone(),
90        export_names: export_names.clone(),
91        instantiated: false,
92        fuel_consumed: 0,
93    };
94
95    // Only instantiate if configured to do so AND the module has no imports
96    // (a module with imports expects host functions we don't provide).
97    if config.allow_nested_wasm && import_names.is_empty() {
98        let mut store = Store::new(engine, ());
99        if config.nested_wasm_max_fuel > 0 {
100            store
101                .set_fuel(config.nested_wasm_max_fuel)
102                .map_err(|e| Error::Internal(format!("nested fuel: {e}")))?;
103        }
104
105        // Instantiate with empty imports (module has none).
106        let linker = wasmtime::Linker::new(engine);
107        if let Ok(_instance) = linker.instantiate(&mut store, &module) {
108            info.instantiated = true;
109            if config.nested_wasm_max_fuel > 0 {
110                let remaining = store.get_fuel().unwrap_or(0);
111                info.fuel_consumed = config.nested_wasm_max_fuel.saturating_sub(remaining);
112            }
113        } else {
114            // Instantiation failed — trap, OOM, etc. Record it.
115            info.instantiated = false;
116            return Ok((
117                info.clone(),
118                Observation::WasmInstantiation {
119                    module_size: info.module_size,
120                    import_names: info.import_names,
121                    export_names: info.export_names,
122                },
123            ));
124        }
125    }
126
127    let observation = Observation::WasmInstantiation {
128        module_size: info.module_size,
129        import_names: info.import_names.clone(),
130        export_names: info.export_names.clone(),
131    };
132
133    Ok((info, observation))
134}