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_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));
        }
    }
}