plugin/host/src/meter_catalog.rs
//! **The meter catalog, and the census it can be asked for.**
//!
//! A meter mounts nothing. It has no repository name, no route and no place in
//! the route table, so this catalog is deliberately not `HandlerCatalog`: it
//! holds `Arc<dyn MeterRuntime>` and the row each one contributes to the census.
//!
//! # Why the census exists at all
//!
//! A cargo feature's absence is provable in the symbol table. An uninstalled
//! module's absence is only "a file that is not there", which is weaker. The
//! census is where that strength is recovered, on a RUNNING box:
//!
//! * [`MeterCatalog::census`] names every loaded module, with its digest and the
//! capabilities it was granted. There is no anonymous module, because a module
//! that loaded is a module with a row.
//! * [`MeterCatalog::granted_capabilities`] is the union of every grant in
//! force. Asserting that this set holds none of `principal.read`,
//! `digest.read`, `clock.read` or `audit.write` proves "and none can be", from
//! configuration, without looking at a directory at all.
//! * [`MeterCatalog::linker_defines`] is the union of the host functions
//! actually defined on this boot's linkers. A module wanting anything outside
//! it fails instantiation, because the import is not defined.
//!
//! `AdminService.ServerProfile` reports all three, because an operator on a
//! shell-less appliance has no other way to ask *"what is loaded here, and what
//! may it do"* — and neither has the test.
#[cfg(feature = "wasm-plugins")]
use std::path::Path;
use std::sync::Arc;
use holger_plugin_abi::v2::{Capability, GuestKind, Meter};
use crate::backend::SharedStore;
use crate::meter::{GrantTable, LinkedMeter, MeterHostServices, MeterRuntime, ModuleCensusRow};
/// A module in the plugin directory that is not a meter, or that is and could
/// not be loaded. Kept apart for the same reason the handler lane keeps them
/// apart: one of them is somebody else's module, the other is a broken one.
#[derive(Debug)]
pub enum MeterScanOutcome {
/// Loaded and granted.
Loaded { name: String },
/// Not an ABI v2 guest at all — a package handler, most likely. Skipped
/// without complaint, and named so a directory listing is still explicable.
NotAMeter { module: String },
/// A v2 guest of another kind.
WrongKind { module: String, kind: &'static str },
/// A meter that could not be loaded, granted or verified. **Never dropped**:
/// this is the only report the module makes.
Refused { module: String, reason: String },
}
impl std::fmt::Display for MeterScanOutcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MeterScanOutcome::Loaded { name } => write!(f, "{name}: loaded"),
MeterScanOutcome::NotAMeter { module } => {
write!(
f,
"{module}: not an ABI v2 guest; left to the package-handler catalog"
)
}
MeterScanOutcome::WrongKind { module, kind } => {
write!(f, "{module}: is a `{kind}` guest, not a meter")
}
MeterScanOutcome::Refused { module, reason } => {
write!(f, "{module}: REFUSED — {reason}")
}
}
}
}
/// The meters this boot has, and what each was allowed to do.
pub struct MeterCatalog {
meters: Vec<Arc<dyn MeterRuntime>>,
census: Vec<ModuleCensusRow>,
grants: GrantTable,
/// Every host function defined on any of this boot's meter linkers.
linker_defines: Vec<&'static str>,
/// How each loaded guest stood against the anti-rollback floor (W14).
floor_notes: Vec<String>,
}
impl MeterCatalog {
pub fn new(grants: GrantTable) -> Self {
Self {
meters: Vec::new(),
census: Vec::new(),
grants,
linker_defines: Vec::new(),
floor_notes: Vec::new(),
}
}
/// How each loaded guest stood against the anti-rollback floor, in load order.
pub fn floor_notes(&self) -> &[String] {
&self.floor_notes
}
/// The grant table in force, so a caller can report the effective table
/// even when nothing loaded.
pub fn grants(&self) -> &GrantTable {
&self.grants
}
pub fn meters(&self) -> &[Arc<dyn MeterRuntime>] {
&self.meters
}
pub fn census(&self) -> &[ModuleCensusRow] {
&self.census
}
pub fn is_empty(&self) -> bool {
self.meters.is_empty()
}
/// The union of every grant in force on this boot, whether the module that
/// holds it is linked or loaded.
pub fn granted_capabilities(&self) -> Vec<Capability> {
let mut out: Vec<Capability> = self
.meters
.iter()
.flat_map(|m| m.granted().iter().copied())
.collect();
out.sort();
out.dedup();
out
}
/// The union of the host functions actually defined on this boot's linkers.
pub fn linker_defines(&self) -> &[&'static str] {
&self.linker_defines
}
/// Add a meter compiled into this binary. It is held to the same grant a
/// loaded module is: the grant table is asked by the module's name, and a
/// linked meter with no entry gets nothing.
pub fn add_linked<M: Meter + Send + Sync + 'static>(
&mut self,
name: &str,
meter: M,
store: SharedStore,
services: Arc<dyn MeterHostServices>,
) -> Result<(), String> {
let granted = self.grants.for_module(name)?.to_vec();
let linked = LinkedMeter::new(name, meter, store, services, &granted)?;
self.census.push(ModuleCensusRow {
name: linked.manifest().name.clone(),
origin: "linked".to_string(),
kind: GuestKind::Meter.name(),
// A linked guest has no module bytes, so it has no digest. Empty is
// the honest answer and is distinguishable from a digest.
digest: String::new(),
receipt: None,
granted: granted.iter().map(|c| c.name()).collect(),
});
self.meters.push(Arc::new(linked));
Ok(())
}
}
#[cfg(feature = "wasm-plugins")]
impl MeterCatalog {
/// Load every meter guest in `dir`.
///
/// A module that is not an ABI v2 guest is **skipped, by name** — the same
/// directory holds package handlers, and they are the other catalog's. A
/// meter that fails its grant or its verification is an `Err`: a metering
/// plane that quietly holds fewer meters than the operator configured is a
/// box that stopped counting without saying so.
pub fn load_dir(
&mut self,
dir: impl AsRef<Path>,
store: SharedStore,
services: Arc<dyn MeterHostServices>,
policy: &crate::PluginPolicy,
) -> Result<Vec<MeterScanOutcome>, String> {
use crate::meter_wasm::{MeterLoadError, WasmMeter};
let dir = dir.as_ref();
let modules = crate::registry::module_paths(dir).map_err(|e| e.to_string())?;
let mut outcomes = Vec::new();
for path in modules {
let module_name = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| path.display().to_string());
let granted = self.grants.for_module(&module_name)?.to_vec();
match WasmMeter::load_with(
&path,
Arc::clone(&store),
Arc::clone(&services),
policy,
&granted,
) {
Ok(m) => {
// ── W14: the anti-rollback floor, placed then activated ──
// Refused BY NAME below the lane's floor; a meter that
// quietly rolled back would be a metering plane that
// stopped enforcing without saying so.
let admission = match policy.floor.store() {
Some(store) => store
.admit(&path, m.manifest().identity.as_ref())
.map_err(|reason| format!("{module_name}: {reason}"))?,
None => crate::floor::Admission::NotKept {
module: module_name.clone(),
},
};
if let (Some(store), Some(identity)) =
(policy.floor.store(), admission.identity())
{
store
.activate(&path, identity)
.map_err(|reason| format!("{module_name}: {reason}"))?;
}
eprintln!("{module_name}: {admission}");
self.floor_notes.push(format!("{module_name}: {admission}"));
for imp in m.linker_defines() {
if !self.linker_defines.contains(imp) {
self.linker_defines.push(imp);
}
}
self.census.push(ModuleCensusRow {
name: m.manifest().name.clone(),
origin: module_name.clone(),
kind: GuestKind::Meter.name(),
digest: m.digest_hex().to_string(),
receipt: m.signer().map(|s| s.to_string()),
granted: granted.iter().map(|c| c.name()).collect(),
});
outcomes.push(MeterScanOutcome::Loaded {
name: m.manifest().name.clone(),
});
self.meters.push(Arc::new(m));
}
Err(MeterLoadError::NotAV2Guest { module }) => {
outcomes.push(MeterScanOutcome::NotAMeter { module })
}
Err(MeterLoadError::WrongKind { module, kind }) => {
outcomes.push(MeterScanOutcome::WrongKind { module, kind })
}
Err(MeterLoadError::Refused { module, reason }) => {
return Err(format!("{module}: {reason}"))
}
}
}
self.linker_defines.sort();
Ok(outcomes)
}
}
#[cfg(test)]
mod tests {
use super::*;
use holger_plugin_abi::v2::{MeterHost, WireAuditRecord, WireInstant, WireMeterReading};
use holger_plugin_abi::BlobStore;
struct NoStore;
impl BlobStore for NoStore {
fn get(&self, _k: &str) -> Result<Option<Vec<u8>>, String> {
Ok(None)
}
fn put(&self, _k: &str, _d: &[u8]) -> Result<(), String> {
Ok(())
}
fn list(&self, _p: &str) -> Result<Vec<(String, u64)>, String> {
Ok(Vec::new())
}
}
struct Svc;
impl MeterHostServices for Svc {
fn usage_read(&self, _r: &str) -> Result<Vec<WireMeterReading>, String> {
Ok(Vec::new())
}
fn audit_write(&self, _r: &WireAuditRecord) -> Result<(), String> {
Ok(())
}
fn clock(&self) -> Result<WireInstant, String> {
Err("no clock".into())
}
}
/// The smallest meter that is not the open guest, so the catalog is tested
/// without depending on any particular guest crate.
struct Tiny;
impl Meter for Tiny {
fn manifest(&self) -> holger_plugin_abi::v2::WireManifest2 {
holger_plugin_abi::v2::WireManifest2 {
abi: holger_plugin_abi::v2::AbiRange::new(2, 2),
kind: GuestKind::Meter,
name: "tiny".into(),
requires: vec![Capability::UsageRead],
handler: None,
meter: Some(holger_plugin_abi::v2::MeterFacts {
accepts_facts: false,
}),
identity: None,
unknown_optional: Vec::new(),
}
}
fn meters(&self) -> Result<Vec<holger_plugin_abi::v2::WireMeterDecl>, String> {
Ok(Vec::new())
}
fn read(&self, _h: &dyn MeterHost, _r: &str) -> Result<Vec<WireMeterReading>, String> {
Ok(Vec::new())
}
fn capacity(
&self,
_h: &dyn MeterHost,
) -> Result<holger_plugin_abi::v2::WireCapacity, String> {
Err("tiny".into())
}
fn selftest(
&self,
_h: &dyn MeterHost,
) -> Result<holger_plugin_abi::v2::WireSelftest, String> {
Err("tiny".into())
}
fn evaluate(
&self,
_h: &dyn MeterHost,
_c: &holger_plugin_abi::v2::WireCallContext,
) -> Result<holger_plugin_abi::v2::WireVerdict, String> {
Ok(holger_plugin_abi::v2::WireVerdict::Allow)
}
}
#[test]
fn a_linked_meter_gets_a_census_row_with_no_digest() {
let grants = GrantTable::from_pairs([("tiny", vec!["usage.read"])]).unwrap();
let mut cat = MeterCatalog::new(grants);
cat.add_linked("tiny", Tiny, Arc::new(NoStore), Arc::new(Svc))
.unwrap();
assert_eq!(cat.census().len(), 1);
let row = &cat.census()[0];
assert_eq!(row.name, "tiny");
assert_eq!(row.origin, "linked");
assert_eq!(row.kind, "meter");
assert!(
row.digest.is_empty(),
"a linked guest has no module bytes to digest"
);
assert_eq!(row.granted, vec!["usage.read"]);
}
#[test]
fn a_linked_meter_with_no_grant_entry_is_refused_by_name() {
let mut cat = MeterCatalog::new(GrantTable::default());
let err = cat
.add_linked("tiny", Tiny, Arc::new(NoStore), Arc::new(Svc))
.unwrap_err();
assert!(err.contains("usage.read"), "{err}");
assert!(cat.census().is_empty());
}
#[test]
fn an_unreadable_grant_table_stops_a_linked_meter_too() {
let mut cat = MeterCatalog::new(GrantTable::unreadable("/data/holger.ron: EACCES"));
let err = cat
.add_linked("tiny", Tiny, Arc::new(NoStore), Arc::new(Svc))
.unwrap_err();
assert!(err.contains("EACCES"), "{err}");
}
#[test]
fn the_granted_census_is_the_union_and_holds_nothing_it_was_not_given() {
let grants = GrantTable::from_pairs([("tiny", vec!["usage.read"])]).unwrap();
let mut cat = MeterCatalog::new(grants);
cat.add_linked("tiny", Tiny, Arc::new(NoStore), Arc::new(Svc))
.unwrap();
assert_eq!(cat.granted_capabilities(), vec![Capability::UsageRead]);
for forbidden in [
Capability::PrincipalRead,
Capability::DigestRead,
Capability::ClockRead,
Capability::AuditWrite,
] {
assert!(!cat.granted_capabilities().contains(&forbidden));
}
}
}