Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

plugin/host/src/wasm.rs

//! The wasmtime host for holger's ABI v1 package handlers: load a `.wasm`, and
//! answer holger's verbs through it.
//!
//! The mechanics — verify before compile, the census digest, both CPU bounds
//! at the engine level, a fresh bounded `Store` per call, `alloc`/`result_len`
//! for the buffer protocol, a trap turned into the bound that caused it — are
//! `guest_host::module::LoadedModule` (edda) since 2026-09-22, shared with the
//! meter host below and with monetize's product host. What stays here is
//! holger's: the v1 export set, the manifest and its ABI-version check, the
//! four `host_store_*` imports over a [`SharedStore`], and the frozen
//! `holger-plugin-<n>` code on every refusal (`named.rs`).
//!
//! Error handling is the point, as it always was: a failure is a named `Err`
//! all the way out, because a repository backend that answers "no such
//! artifact" to a trapped module would be a silent 404 over real bytes.

use std::path::Path;
use std::sync::Arc;

use guest_host::module::{
    read_mem, read_str, write_to_wasm, GuestState, Instantiated, LoadedModule,
};
use guest_host::BoundedLimiter;
use holger_plugin_abi::{
    exports, imports, wire, WireArtifactEntry, WireArtifactId, WireHttpRequest, WireHttpResponse,
    WireManifest, ABI_VERSION,
};
use wasmtime::{Caller, Linker};

use crate::backend::{HandlerRuntime, SharedStore};
use crate::limits::PluginLimits;
use crate::named::{call_refusal, load_refusal};
use crate::PluginPolicy;

/// What the host lends a module for the length of one call.
struct HostState {
    store: SharedStore,
    /// Every failure a host import hit, in order. An import can only signal
    /// failure to wasm as a `0`, which a module is free to ignore — so the real
    /// reason is kept here and surfaced by the host after the call returns.
    faults: Vec<String>,
    /// The memory/table ceiling for this call, and whether it was hit.
    limits: BoundedLimiter,
}

impl GuestState for HostState {
    fn limiter(&mut self) -> &mut BoundedLimiter {
        &mut self.limits
    }
    fn faults(&mut self) -> &mut Vec<String> {
        &mut self.faults
    }
}

/// A `.wasm` package handler, loaded and ready to answer.
///
/// One `Store` per call (`LoadedModule::instantiate`), so a call cannot observe
/// another call's linear memory — which is what makes a `&self` verb sound on
/// a shared backend.
pub struct WasmHandler {
    inner: LoadedModule,
    store: SharedStore,
    manifest: WireManifest,
}

impl WasmHandler {
    /// Load a module and read its manifest.
    ///
    /// Every failure names the module path and what was wrong with it. In
    /// particular a **missing export** is named individually: a module that is
    /// half-built is otherwise indistinguishable from one that simply answers
    /// nothing.
    pub fn load(path: impl AsRef<Path>, store: SharedStore) -> Result<Self, String> {
        Self::load_with(path, store, &PluginPolicy::default())
    }

    /// [`WasmHandler::load`] under an explicit policy: what the module must prove
    /// before it is compiled, and what it may spend once it runs.
    ///
    /// **Verification happens before compilation**, over the bytes on disk —
    /// compiling an unverified module is already running the cranelift frontend
    /// over attacker-supplied input.
    pub fn load_with(
        path: impl AsRef<Path>,
        store: SharedStore,
        policy: &PluginPolicy,
    ) -> Result<Self, String> {
        let path = path.as_ref().to_path_buf();
        let limits = policy.limits.clone();
        crate::limits::validate(&limits)?;

        let inner = LoadedModule::load(&path, &limits, &policy.trust, exports::ALL)
            .map_err(load_refusal)?;

        let mut handler = Self {
            inner,
            store,
            // Placeholder: replaced immediately below. Never observable —
            // `read_manifest` does not consult it.
            manifest: WireManifest {
                abi_version: 0,
                handler: String::new(),
                format: String::new(),
                writable: false,
                identity: None,
            },
        };
        handler.manifest = handler.read_manifest()?;

        if handler.manifest.abi_version != ABI_VERSION {
            return Err(format!(
                "{}: plugin ABI version {} does not match this holger's {}. \
                 A mismatched ABI decodes one field's bytes as another's, so the module \
                 is refused rather than loaded. Rebuild the plugin against \
                 holger-plugin-abi {}.",
                path.display(),
                handler.manifest.abi_version,
                ABI_VERSION,
                ABI_VERSION
            ));
        }
        if handler.manifest.handler.trim().is_empty() {
            return Err(format!(
                "{}: plugin manifest declares an empty handler name",
                path.display()
            ));
        }
        Ok(handler)
    }

    pub fn manifest(&self) -> &WireManifest {
        &self.manifest
    }

    pub fn path(&self) -> &Path {
        self.inner.path()
    }

    fn read_manifest(&self) -> Result<WireManifest, String> {
        let raw = self.call_no_arg(exports::MANIFEST)?;
        WireManifest::from_bytes(&raw).map_err(|e| {
            format!(
                "{}: {} returned undecodable bytes: {e}",
                self.path().display(),
                exports::MANIFEST
            )
        })
    }

    /// A bounded store with the four store imports defined.
    fn instantiate(&self) -> Result<Instantiated<HostState>, String> {
        let mut linker = Linker::new(self.inner.engine());
        register_host_functions(&mut linker)
            .map_err(|e| format!("{}: registering host functions: {e}", self.path().display()))?;
        let state = HostState {
            store: Arc::clone(&self.store),
            faults: Vec::new(),
            limits: self.inner.limits().limiter(),
        };
        self.inner.instantiate(state, &linker).map_err(call_refusal)
    }

    fn call_no_arg(&self, export: &str) -> Result<Vec<u8>, String> {
        let mut inst = self.instantiate()?;
        self.inner.call_ptr(&mut inst, export).map_err(call_refusal)
    }

    /// Call `export(ptr, len) -> ptr` with `arg` copied into linear memory.
    fn call_with(&self, export: &str, arg: &[u8]) -> Result<Vec<u8>, String> {
        let mut inst = self.instantiate()?;
        self.inner
            .call_with_ptr(&mut inst, export, arg)
            .map_err(call_refusal)
    }
}

/// Prints WHERE the module came from, WHO signed it and WHAT it may spend —
/// never the engine or the module.
impl std::fmt::Debug for WasmHandler {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("WasmHandler")
            .field("path", &self.inner.path())
            .field("handler", &self.manifest.handler)
            .field("signer", &self.inner.signer())
            .field("limits", self.inner.limits())
            .finish()
    }
}

impl HandlerRuntime for WasmHandler {
    fn manifest(&self) -> &WireManifest {
        &self.manifest
    }

    fn fetch(&self, id: &WireArtifactId) -> Result<Option<Vec<u8>>, String> {
        let raw = self.call_with(exports::FETCH, &id.to_bytes())?;
        wire::decode_fetch_response(&raw)
            .map_err(|e| format!("{}: {}: {e}", self.path().display(), exports::FETCH))?
    }

    fn put(&self, id: &WireArtifactId, data: &[u8]) -> Result<(), String> {
        let req = wire::WirePutRequest {
            id: id.clone(),
            data: data.to_vec(),
        };
        let raw = self.call_with(exports::PUT, &req.to_bytes())?;
        wire::decode_unit_response(&raw)
            .map_err(|e| format!("{}: {}: {e}", self.path().display(), exports::PUT))?
    }

    fn list(
        &self,
        name_filter: Option<&str>,
        limit: usize,
    ) -> Result<Vec<WireArtifactEntry>, String> {
        let req = wire::WireListRequest {
            name_filter: name_filter.map(str::to_string),
            limit: limit.min(u32::MAX as usize) as u32,
        };
        let raw = self.call_with(exports::LIST, &req.to_bytes())?;
        wire::decode_list_response(&raw)
            .map_err(|e| format!("{}: {}: {e}", self.path().display(), exports::LIST))?
    }

    fn coordinate_for_path(&self, suburl: &str) -> Option<WireArtifactId> {
        // The trait returns `Option`, so a module failure cannot be reported
        // here. That is safe in exactly one direction: this is the serve-time
        // quarantine gate's lookup, and `None` means "not gated by coordinate",
        // which is the trait's own documented default. A failure therefore loses
        // a gate check, never serves bytes it should not have.
        let mut w = holger_plugin_abi::Writer::new();
        w.str(suburl);
        let raw = self
            .call_with(exports::COORDINATE_FOR_PATH, &w.finish())
            .ok()?;
        wire::decode_coordinate_response(&raw).ok().flatten()
    }

    fn http(&self, req: &WireHttpRequest) -> Result<WireHttpResponse, String> {
        let raw = self.call_with(exports::HTTP, &req.to_bytes())?;
        wire::decode_http_response(&raw)
            .map_err(|e| format!("{}: {}: {e}", self.path().display(), exports::HTTP))?
    }
}

// ─── Host imports ────────────────────────────────────────────────────

fn register_host_functions(linker: &mut Linker<HostState>) -> anyhow::Result<()> {
    linker.func_wrap(
        imports::MODULE,
        imports::STORE_GET,
        |mut caller: Caller<'_, HostState>, ptr: u32, len: u32| -> u64 {
            let Some(key) = read_str(&mut caller, ptr, len, imports::STORE_GET) else {
                return 0;
            };
            match caller.data().store.get(&key) {
                Ok(v) => {
                    let bytes = wire::encode_store_get(&v);
                    write_to_wasm(&mut caller, &bytes)
                }
                Err(e) => {
                    // A store failure is recorded, and 0 is returned. It is NOT
                    // encoded as `None`: a module that read "absent" would turn
                    // an unreadable disk into a clean 404.
                    caller.data_mut().faults.push(format!("get({key}): {e}"));
                    0
                }
            }
        },
    )?;

    linker.func_wrap(
        imports::MODULE,
        imports::STORE_PUT,
        |mut caller: Caller<'_, HostState>,
         key_ptr: u32,
         key_len: u32,
         data_ptr: u32,
         data_len: u32|
         -> u64 {
            let Some(key) = read_str(&mut caller, key_ptr, key_len, imports::STORE_PUT) else {
                return 0;
            };
            let Some(data) = read_mem(&mut caller, data_ptr, data_len) else {
                caller.data_mut().faults.push(format!(
                    "{}: body outside linear memory",
                    imports::STORE_PUT
                ));
                return 0;
            };
            let res = caller.data().store.put(&key, &data);
            if let Err(e) = &res {
                caller.data_mut().faults.push(format!("put({key}): {e}"));
            }
            let bytes = wire::encode_unit_response(&res);
            write_to_wasm(&mut caller, &bytes)
        },
    )?;

    linker.func_wrap(
        imports::MODULE,
        imports::STORE_LIST,
        |mut caller: Caller<'_, HostState>, ptr: u32, len: u32| -> u64 {
            let Some(prefix) = read_str(&mut caller, ptr, len, imports::STORE_LIST) else {
                return 0;
            };
            match caller.data().store.list(&prefix) {
                Ok(rows) => {
                    let bytes = wire::encode_store_listing(&rows);
                    write_to_wasm(&mut caller, &bytes)
                }
                Err(e) => {
                    caller
                        .data_mut()
                        .faults
                        .push(format!("list({prefix}): {e}"));
                    0
                }
            }
        },
    )?;

    linker.func_wrap(
        imports::MODULE,
        imports::STORE_SIZE,
        |mut caller: Caller<'_, HostState>, ptr: u32, len: u32| -> u64 {
            let Some(key) = read_str(&mut caller, ptr, len, imports::STORE_SIZE) else {
                return 0;
            };
            match caller.data().store.size(&key) {
                Ok(v) => {
                    // Carried as `Option<Vec<u8>>` holding 8 little-endian bytes,
                    // so "absent" and "zero bytes" stay distinguishable.
                    let payload = v.map(|n| n.to_le_bytes().to_vec());
                    let bytes = wire::encode_store_get(&payload);
                    write_to_wasm(&mut caller, &bytes)
                }
                Err(e) => {
                    caller.data_mut().faults.push(format!("size({key}): {e}"));
                    0
                }
            }
        },
    )?;

    Ok(())
}

impl WasmHandler {
    /// Trait-object-safe helper so `WasmHandler` can be boxed into a
    /// [`crate::backend::HandlerBackend`] without the caller naming its type.
    pub fn into_runtime(self) -> Box<dyn HandlerRuntime> {
        Box::new(self)
    }

    /// The same compiled module, bound to a different store.
    ///
    /// Compiling is the expensive part of [`WasmHandler::load`]; binding a store
    /// is free. A catalog reads a plugin directory ONCE at boot and mounts the
    /// same handler under however many repositories name it — so this must not
    /// recompile, must not start a second epoch ticker, and must not re-read the
    /// manifest (a module whose manifest answered differently on a second read
    /// would be registered under one name and serve as another).
    pub fn with_store(&self, store: SharedStore) -> Self {
        Self {
            inner: self.inner.clone_shared(),
            store,
            manifest: self.manifest.clone(),
        }
    }

    /// The bounds this handler runs under.
    pub fn limits(&self) -> &PluginLimits {
        self.inner.limits()
    }

    /// The verified signer's common name, or `None` when no provenance was
    /// required. Never `None` because verification failed — that is an `Err` out
    /// of [`WasmHandler::load_with`], and the module is not here.
    pub fn signer(&self) -> Option<&str> {
        self.inner.signer()
    }

    /// Hex blake3-256 of the module bytes, as the signature covers them.
    pub fn digest_hex(&self) -> &str {
        self.inner.digest_hex()
    }
}