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}