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/meter_wasm.rs

//! **A Meter guest loaded from a `.wasm`.**
//!
//! Everything [`crate::wasm::WasmHandler`] is bounded by applies here unchanged —
//! fuel, the memory and table ceilings, the epoch deadline, no WASI, and the
//! detached CMS signature checked over the module's blake3 digest *before*
//! `Module::from_binary` compiles a byte. This file adds one thing and it is the
//! point of the whole lane:
//!
//! **The linker defines only the imports the operator granted.** An ungranted
//! capability is therefore not merely refused at a call — the import is not
//! defined, so the module does not instantiate. That is a property of *this
//! boot* rather than of *this build*, which is what makes it a usable
//! replacement for a symbol-table proof.
//!
//! # The load order, and why it is this order
//!
//! 1. Read the module's **static import list**, which needs no instantiation.
//! 2. Refuse an import outside holger's whole vocabulary, by name.
//! 3. Refuse an import whose capability was not granted, by name — **before**
//!    anything is instantiated, so an ungranted import is never even defined.
//! 4. Define exactly the granted imports, instantiate, read `plugin_abi`.
//! 5. Negotiate the ABI against this host's range.
//! 6. Read the TLV manifest; refuse an unknown REQUIRED tag.
//! 7. Gate A: everything the manifest declares must be granted. This is the gate
//!    that catches `principal.read` and `digest.read`, which are FIELDS and have
//!    no import to inspect in step 3.
//! 8. Gate B: every real import must be unlocked by something the manifest
//!    declares — a manifest that understates the module is refused.
//! 9. The kind's floor.

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, CallError};
use holger_plugin_abi::v2::{
    check_grants, exports, imports, negotiate, ops, payload, AbiRange, Capability, GuestKind,
    WireAuditRecord, WireCallContext, WireCapacity, WireFactSummary, WireManifest2, WireMeterDecl,
    WireMeterReading, WireSelftest, WireVerdict,
};
use wasmtime::{Caller, Linker};

use crate::backend::SharedStore;
use crate::limits::narrowed_to;
use crate::meter::{redact_context, MeterHostServices, MeterRuntime};
use crate::named::{call_refusal, load_refusal};
use crate::PluginPolicy;

/// Why a module in the plugin directory did not become a meter.
///
/// The three are kept apart because they mean different things to an operator:
/// the first is somebody else's module sitting in the same directory, the second
/// is a guest of a kind this loader does not load, and only the third is a
/// problem with the module itself.
#[derive(Debug)]
pub enum MeterLoadError {
    /// No `plugin_abi` export: an ABI v1 package handler, or not a holger guest
    /// at all. Skipped by the meter catalog rather than refused.
    NotAV2Guest { module: String },
    /// A v2 guest of another kind.
    WrongKind { module: String, kind: &'static str },
    /// A meter that could not be verified, granted, negotiated or read.
    Refused { module: String, reason: String },
}

impl std::fmt::Display for MeterLoadError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MeterLoadError::NotAV2Guest { module } => {
                write!(
                    f,
                    "{module}: exports no `{}`, so it is not an ABI v2 guest",
                    exports::PLUGIN_ABI
                )
            }
            MeterLoadError::WrongKind { module, kind } => {
                write!(
                    f,
                    "{module}: declares kind `{kind}`, which this loader does not load"
                )
            }
            MeterLoadError::Refused { module, reason } => write!(f, "{module}: {reason}"),
        }
    }
}

/// What the host lends a meter module for the length of one call.
struct MeterState {
    store: SharedStore,
    services: Arc<dyn MeterHostServices>,
    /// Every host-import failure, 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 after the call returns.
    faults: Vec<String>,
    limits: BoundedLimiter,
}

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

/// A `.wasm` meter guest, loaded, granted and ready to answer.
///
/// The verified, digested, compiled and ticking module is a
/// `guest_host::LoadedModule`; the fuel measurement (?17's ceiling is a
/// measurement, not a guess, because wasmtime knows what a call consumed and
/// the loader asks it) lives there too, read through [`WasmMeter::last_call_fuel`]
/// and [`WasmMeter::peak_call_fuel`].
pub struct WasmMeter {
    inner: LoadedModule,
    store: SharedStore,
    services: Arc<dyn MeterHostServices>,
    manifest: WireManifest2,
    granted: Vec<Capability>,
    /// Exactly the import names defined on every linker this meter makes. The
    /// linker census reads this.
    granted_imports: Vec<&'static str>,
    /// The negotiated ABI, echoed into every context this meter is handed.
    abi: u32,
}

impl WasmMeter {
    pub fn load_with(
        path: impl AsRef<Path>,
        store: SharedStore,
        services: Arc<dyn MeterHostServices>,
        policy: &PluginPolicy,
        granted: &[Capability],
    ) -> Result<Self, MeterLoadError> {
        let path = path.as_ref().to_path_buf();
        let module_name = path
            .file_name()
            .map(|s| s.to_string_lossy().to_string())
            .unwrap_or_else(|| path.display().to_string());
        let refuse = |reason: String| MeterLoadError::Refused {
            module: module_name.clone(),
            reason,
        };
        let limits = policy.limits.clone();
        crate::limits::validate(&limits).map_err(&refuse)?;
        // ── ?17: THE KIND'S CEILING, APPLIED HERE ────────────────────────────
        //
        // `policy.limits` is the package handler's budget — 1e9 fuel, 30 s —
        // because `PluginPolicy::default()` is written for the kind that had the
        // loader first. A meter is a different job with a different shape, so
        // the Meter kind carries its own numbers on its `KindSpec`, and this is
        // where they bind. `narrowed_to` takes the TIGHTER of the two on each
        // axis: an operator may bring a guest further down, never up past the
        // kind's ceiling, and a deployment that configured nothing gets the
        // kind's number rather than the handler's.
        let limits = narrowed_to(&limits, GuestKind::Meter);
        crate::limits::validate(&limits).map_err(&refuse)?;

        // Provenance BEFORE compilation, the census digest over the same bytes,
        // both CPU bounds at the engine level, the ticker: all in the loader.
        // No export is required at this point, because the classification
        // below has to see a module with no `plugin_abi` as "not a v2 guest"
        // rather than as a refusal.
        let inner = LoadedModule::load(&path, &limits, &policy.trust, &[])
            .map_err(|e| refuse(load_refusal(e)))?;

        // The classification, before anything else is checked: a module with no
        // `plugin_abi` export is an ABI v1 package handler or not a holger guest
        // at all, and it belongs to the other catalog.
        let present = inner.export_names();
        if !present.iter().any(|e| e == exports::PLUGIN_ABI) {
            return Err(MeterLoadError::NotAV2Guest {
                module: module_name,
            });
        }

        // Step 1 — the static import list.
        inner
            .refuse_foreign_imports(imports::MODULE)
            .map_err(|e| refuse(load_refusal(e)))?;
        let real_imports: Vec<String> = inner.imports_from(imports::MODULE);

        // Steps 2 and 3 — refuse before instantiating.
        let mut needed: Vec<Capability> = Vec::new();
        for imp in &real_imports {
            let cap = Capability::for_import(imp).ok_or_else(|| {
                refuse(format!(
                    "imports `env::{imp}`, which is not a holger host function at all"
                ))
            })?;
            if !needed.contains(&cap) {
                needed.push(cap);
            }
        }
        for cap in &needed {
            if !granted.contains(cap) {
                return Err(refuse(format!(
                    "the module imports `{}`, which needs capability `{}`. This deployment \
                     granted it [{}]. The import is therefore NOT DEFINED on the linker, and \
                     the module would not instantiate.",
                    cap.imports().join(", "),
                    cap.name(),
                    granted
                        .iter()
                        .map(|c| c.name())
                        .collect::<Vec<_>>()
                        .join(", ")
                )));
            }
        }

        // Exports, checked before instantiating so the error names the symbol.
        let missing: Vec<&str> = exports::ALL_V2
            .iter()
            .copied()
            .filter(|want| !present.iter().any(|p| p == want))
            .collect();
        if !missing.is_empty() {
            return Err(refuse(format!(
                "not an ABI v2 guest — missing export(s): {}.\nA v2 module must export all \
                 of: {}",
                missing.join(", "),
                exports::ALL_V2.join(", ")
            )));
        }

        // Only the granted imports, and only those the module actually asked
        // for: defining an import a module does not use is harmless, but
        // defining one it does and was not granted is the whole failure this
        // design exists to prevent.
        let granted_imports: Vec<&'static str> = needed
            .iter()
            .flat_map(|c| c.imports().iter().copied())
            .collect();

        let mut meter = Self {
            inner,
            store,
            services,
            granted: granted.to_vec(),
            granted_imports,
            abi: 0,
            manifest: WireManifest2 {
                abi: AbiRange::new(0, 0),
                kind: GuestKind::Meter,
                name: String::new(),
                requires: Vec::new(),
                handler: None,
                meter: None,
                identity: None,
                unknown_optional: Vec::new(),
            },
        };

        // Steps 4 and 5.
        let guest_range = AbiRange::unpack(meter.call_abi().map_err(&refuse)?);
        meter.abi = negotiate(AbiRange::host(), guest_range).map_err(&refuse)?;

        // Step 6.
        let raw = meter.call_manifest().map_err(&refuse)?;
        meter.manifest = WireManifest2::from_bytes(&raw).map_err(|e| refuse(e.to_string()))?;
        if meter.manifest.name.trim().is_empty() {
            return Err(refuse(
                "the manifest declares an empty module name".to_string(),
            ));
        }
        if meter.manifest.kind != GuestKind::Meter {
            return Err(MeterLoadError::WrongKind {
                module: module_name,
                kind: meter.manifest.kind.name(),
            });
        }
        // A module whose `abi.max` is past `HOST_ABI_MAX` speaks more than this
        // host does; negotiation already picked a shape they share.

        // Steps 7, 8 and 9, all three in one call, all three refusing by name.
        check_grants(&module_name, &meter.manifest, &real_imports, granted)
            .map_err(|e| refuse(e.to_string()))?;

        Ok(meter)
    }

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

    pub fn signer(&self) -> Option<&str> {
        self.inner.signer()
    }

    /// The negotiated ABI for this module.
    pub fn abi(&self) -> u32 {
        self.abi
    }

    /// Hex blake3-256 of the module bytes, as the CMS signature covers them.
    /// What the module census names the module by: a file name is a label an
    /// operator chose, a digest is what was loaded.
    pub fn digest_hex(&self) -> &str {
        self.inner.digest_hex()
    }

    /// Exactly the host functions this module's linker defines. The linker
    /// census in the open-build proof reads this and compares it to the grant.
    pub fn linker_defines(&self) -> &[&'static str] {
        &self.granted_imports
    }

    /// A bounded store whose linker defines ONLY the granted imports.
    fn instantiate(&self) -> Result<Instantiated<MeterState>, String> {
        let mut linker = Linker::new(self.inner.engine());
        register_granted(&mut linker, &self.granted_imports)
            .map_err(|e| format!("{}: registering host functions: {e}", self.path().display()))?;
        let state = MeterState {
            store: Arc::clone(&self.store),
            services: Arc::clone(&self.services),
            faults: Vec::new(),
            limits: self.inner.limits().limiter(),
        };
        self.inner.instantiate(state, &linker).map_err(|e| match e {
            CallError::Shape { .. } => format!(
                "{e}. This host defines exactly [{}] on the linker; an import outside that set \
                 cannot be satisfied.",
                self.granted_imports.join(", ")
            ),
            other => call_refusal(other),
        })
    }

    fn call_abi(&self) -> Result<u64, String> {
        let mut inst = self.instantiate()?;
        self.inner
            .call_u64(&mut inst, exports::PLUGIN_ABI)
            .map_err(call_refusal)
    }

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

    /// One `plugin_call`, with `arg` copied into linear memory and the answer
    /// read back out of it. What the op cost, including the `alloc` of its
    /// argument, is recorded by the loader: one store, one tank, one number.
    fn call_op(&self, op: u32, arg: &[u8]) -> Result<Vec<u8>, String> {
        let mut inst = self.instantiate()?;
        self.inner.call_op(&mut inst, op, arg).map_err(call_refusal)
    }

    /// Fuel burned by the most recent completed call into this module.
    pub fn last_call_fuel(&self) -> u64 {
        self.inner.last_call_fuel()
    }

    /// The most fuel any one call into this module has burned since it loaded.
    pub fn peak_call_fuel(&self) -> u64 {
        self.inner.peak_call_fuel()
    }

    /// The fuel ceiling in force for this module — the kind's ceiling narrowed
    /// by whatever the operator configured. What [`peak_call_fuel`] is to be
    /// read against.
    ///
    /// [`peak_call_fuel`]: WasmMeter::peak_call_fuel
    pub fn fuel_ceiling(&self) -> u64 {
        self.inner.fuel_ceiling()
    }
}

/// Prints the module path, its declared name and what it was granted -- never
/// the engine or the compiled module, which are not facts about the deployment.
impl std::fmt::Debug for WasmMeter {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("WasmMeter")
            .field("path", &self.inner.path())
            .field("name", &self.manifest.name)
            .field("abi", &self.abi)
            .field(
                "granted",
                &self.granted.iter().map(|c| c.name()).collect::<Vec<_>>(),
            )
            .field("linker_defines", &self.granted_imports)
            .finish()
    }
}

impl MeterRuntime for WasmMeter {
    fn manifest(&self) -> &WireManifest2 {
        &self.manifest
    }

    fn granted(&self) -> &[Capability] {
        &self.granted
    }

    fn meters(&self) -> Result<Vec<WireMeterDecl>, String> {
        let raw = self.call_op(ops::METERS, &[])?;
        payload::decode_meters(&raw).map_err(|e| e.message())?
    }

    fn read(&self, repository: &str) -> Result<Vec<WireMeterReading>, String> {
        let raw = self.call_op(ops::READ, &payload::encode_str_arg(repository))?;
        payload::decode_readings(&raw).map_err(|e| e.message())?
    }

    fn capacity(&self) -> Result<WireCapacity, String> {
        let raw = self.call_op(ops::CAPACITY, &[])?;
        payload::decode_capacity(&raw).map_err(|e| e.message())?
    }

    fn selftest(&self) -> Result<WireSelftest, String> {
        let raw = self.call_op(ops::SELFTEST, &[])?;
        payload::decode_selftest(&raw).map_err(|e| e.message())?
    }

    fn evaluate(&self, ctx: &WireCallContext) -> Result<WireVerdict, String> {
        let mut ctx = redact_context(ctx.clone(), &self.granted);
        // The negotiated number, never the caller's guess.
        ctx.abi = self.abi;
        let raw = self.call_op(ops::EVALUATE, &ctx.to_bytes())?;
        payload::decode_verdict(&raw).map_err(|e| e.message())?
    }

    fn accept_fact(&self, signed: &[u8]) -> Result<WireFactSummary, String> {
        let raw = self.call_op(ops::ACCEPT_FACT, &payload::encode_bytes_arg(signed))?;
        payload::decode_fact_summary(&raw).map_err(|e| e.message())?
    }

    fn describe_fact(&self, repository: &str) -> Result<WireFactSummary, String> {
        let raw = self.call_op(ops::DESCRIBE_FACT, &payload::encode_str_arg(repository))?;
        payload::decode_fact_summary(&raw).map_err(|e| e.message())?
    }
}

// ───────────────────────────── host functions ─────────────────────────────

/// **Define exactly the named imports and nothing else.**
///
/// This is the linker census's subject: whatever this function defined on this
/// boot is what a module can reach, and a module wanting anything else fails
/// instantiation rather than receiving a stub.
fn register_granted(linker: &mut Linker<MeterState>, names: &[&str]) -> anyhow::Result<()> {
    for name in names {
        match *name {
            imports::STORE_GET => {
                linker.func_wrap(
                    imports::MODULE,
                    imports::STORE_GET,
                    |mut caller: Caller<'_, MeterState>, 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 = holger_plugin_abi::wire::encode_store_get(&v);
                                write_to_wasm(&mut caller, &bytes)
                            }
                            Err(e) => {
                                caller.data_mut().faults.push(format!("get({key}): {e}"));
                                0
                            }
                        }
                    },
                )?;
            }
            imports::STORE_SIZE => {
                linker.func_wrap(
                    imports::MODULE,
                    imports::STORE_SIZE,
                    |mut caller: Caller<'_, MeterState>, 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) => {
                                let payload = v.map(|n| n.to_le_bytes().to_vec());
                                let bytes = holger_plugin_abi::wire::encode_store_get(&payload);
                                write_to_wasm(&mut caller, &bytes)
                            }
                            Err(e) => {
                                caller.data_mut().faults.push(format!("size({key}): {e}"));
                                0
                            }
                        }
                    },
                )?;
            }
            imports::STORE_PUT => {
                linker.func_wrap(
                    imports::MODULE,
                    imports::STORE_PUT,
                    |mut caller: Caller<'_, MeterState>,
                     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 = holger_plugin_abi::wire::encode_unit_response(&res);
                        write_to_wasm(&mut caller, &bytes)
                    },
                )?;
            }
            imports::STORE_LIST => {
                linker.func_wrap(
                    imports::MODULE,
                    imports::STORE_LIST,
                    |mut caller: Caller<'_, MeterState>, 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 = holger_plugin_abi::wire::encode_store_listing(&rows);
                                write_to_wasm(&mut caller, &bytes)
                            }
                            Err(e) => {
                                caller
                                    .data_mut()
                                    .faults
                                    .push(format!("list({prefix}): {e}"));
                                0
                            }
                        }
                    },
                )?;
            }
            imports::USAGE_READ => {
                linker.func_wrap(
                    imports::MODULE,
                    imports::USAGE_READ,
                    |mut caller: Caller<'_, MeterState>, ptr: u32, len: u32| -> u64 {
                        let Some(repo) = read_str(&mut caller, ptr, len, imports::USAGE_READ)
                        else {
                            return 0;
                        };
                        let res = caller.data().services.usage_read(&repo);
                        // A failed counter is NOT flattened into an empty
                        // reading list: the guest gets the Err, and the honest
                        // answer for one meter is `ReadingValue::Refused`.
                        let bytes = payload::encode_usage(&res);
                        write_to_wasm(&mut caller, &bytes)
                    },
                )?;
            }
            imports::AUDIT_WRITE => {
                linker.func_wrap(
                    imports::MODULE,
                    imports::AUDIT_WRITE,
                    |mut caller: Caller<'_, MeterState>, ptr: u32, len: u32| -> u64 {
                        let Some(raw) = read_mem(&mut caller, ptr, len) else {
                            caller.data_mut().faults.push(format!(
                                "{}: record outside linear memory",
                                imports::AUDIT_WRITE
                            ));
                            return 0;
                        };
                        let res = match WireAuditRecord::from_bytes(&raw) {
                            // The host stamps the time, the identity and the
                            // address; the record the guest wrote carries none
                            // of the three and cannot.
                            Ok(rec) => caller.data().services.audit_write(&rec),
                            Err(e) => Err(e.message()),
                        };
                        let bytes = holger_plugin_abi::wire::encode_unit_response(&res);
                        write_to_wasm(&mut caller, &bytes)
                    },
                )?;
            }
            imports::CLOCK => {
                linker.func_wrap(
                    imports::MODULE,
                    imports::CLOCK,
                    |mut caller: Caller<'_, MeterState>| -> u64 {
                        let res = caller.data().services.clock();
                        let bytes = payload::encode_clock(&res);
                        write_to_wasm(&mut caller, &bytes)
                    },
                )?;
            }
            other => {
                anyhow::bail!(
                    "`{other}` is not a holger host function; the vocabulary is: {}",
                    imports::ALL_V2.join(", ")
                )
            }
        }
    }
    Ok(())
}