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(())
}