jsdet-core 0.1.0

Core WASM-sandboxed JavaScript detonation engine
Documentation
/// Nested WASM execution — the novel capability.
///
/// When JavaScript inside `QuickJS` calls `new WebAssembly.Module(bytes)`,
/// the host intercepts this and creates a SECOND wasmtime instance.
///
/// ```text
/// ┌────────────────────────────────────┐
/// │ Host (Rust + wasmtime)             │
/// │                                    │
/// │  ┌──────────────────────┐          │
/// │  │ Outer WASM instance  │          │
/// │  │ (QuickJS engine)     │          │
/// │  │                      │          │
/// │  │  JS calls:           │          │
/// │  │  new WebAssembly     │          │
/// │  │    .Module(bytes)    │          │
/// │  │        │             │          │
/// │  └────────┼─────────────┘          │
/// │           │ host import            │
/// │           ▼                        │
/// │  ┌──────────────────────┐          │
/// │  │ Inner WASM instance  │          │
/// │  │ (user's WASM module) │          │
/// │  │                      │          │
/// │  │ - own linear memory  │          │
/// │  │ - own fuel budget    │          │
/// │  │ - no host imports    │          │
/// │  │   (fully sealed)     │          │
/// │  └──────────────────────┘          │
/// └────────────────────────────────────┘
/// ```
///
/// The inner instance:
/// - Has its own linear memory (cannot read outer's memory)
/// - Has its own fuel budget (cannot `DoS` the outer instance)
/// - Gets NO host imports (fully sealed — cannot call bridge APIs)
/// - Exports are callable from the outer instance via host trampolines
///
/// This means: even if the WASM module is a crypto miner, it runs
/// inside our fuel budget and memory cap, then stops. We observe
/// exactly what it imports, exports, and how much compute it uses.
///
use wasmtime::{Engine, Module, Store};

use crate::config::SandboxConfig;
use crate::error::{Error, Result};
use crate::observation::Observation;

/// Metadata about a nested WASM instantiation.
#[derive(Debug, Clone)]
pub struct NestedWasmInfo {
    /// Size of the WASM module bytes.
    pub module_size: usize,
    /// Names of functions the module imports (expects the host to provide).
    pub import_names: Vec<String>,
    /// Names of functions the module exports.
    pub export_names: Vec<String>,
    /// Whether the module was actually instantiated (vs just analyzed).
    pub instantiated: bool,
    /// Fuel consumed during execution (if instantiated).
    pub fuel_consumed: u64,
}

/// Analyze and optionally execute a WASM module provided by JavaScript.
///
/// Called when JS code does `new WebAssembly.Module(bytes)`.
///
/// # Errors
///
/// Returns an error if the initial metadata compilation fails.
pub fn handle_wasm_instantiation(
    engine: &Engine,
    wasm_bytes: &[u8],
    config: &SandboxConfig,
) -> Result<(NestedWasmInfo, Observation)> {
    // Compile the module to extract metadata.
    let module = Module::new(engine, wasm_bytes)
        .map_err(|e| Error::Internal(format!("nested WASM compilation failed: {e}")))?;

    let import_names: Vec<String> = module
        .imports()
        .map(|imp| format!("{}.{}", imp.module(), imp.name()))
        .collect();

    let export_names: Vec<String> = module.exports().map(|exp| exp.name().to_string()).collect();

    let mut info = NestedWasmInfo {
        module_size: wasm_bytes.len(),
        import_names: import_names.clone(),
        export_names: export_names.clone(),
        instantiated: false,
        fuel_consumed: 0,
    };

    // Only instantiate if configured to do so AND the module has no imports
    // (a module with imports expects host functions we don't provide).
    if config.allow_nested_wasm && import_names.is_empty() {
        let mut store = Store::new(engine, ());
        if config.nested_wasm_max_fuel > 0 {
            store
                .set_fuel(config.nested_wasm_max_fuel)
                .map_err(|e| Error::Internal(format!("nested fuel: {e}")))?;
        }

        // Instantiate with empty imports (module has none).
        let linker = wasmtime::Linker::new(engine);
        if let Ok(_instance) = linker.instantiate(&mut store, &module) {
            info.instantiated = true;
            if config.nested_wasm_max_fuel > 0 {
                let remaining = store.get_fuel().unwrap_or(0);
                info.fuel_consumed = config.nested_wasm_max_fuel.saturating_sub(remaining);
            }
        } else {
            // Instantiation failed — trap, OOM, etc. Record it.
            info.instantiated = false;
            return Ok((
                info.clone(),
                Observation::WasmInstantiation {
                    module_size: info.module_size,
                    import_names: info.import_names,
                    export_names: info.export_names,
                },
            ));
        }
    }

    let observation = Observation::WasmInstantiation {
        module_size: info.module_size,
        import_names: info.import_names.clone(),
        export_names: info.export_names.clone(),
    };

    Ok((info, observation))
}