plugin/host/tests/the_grant_is_the_linker.rs
//! **An ungranted capability is not refused — it is not linkable.**
//!
//! This is the running-process replacement for gunnar's symbol-table proof. 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
//! capability grant is where the strength is recovered, because the linker
//! defines only what was granted, so a module reaching for anything else fails
//! INSTANTIATION.
//!
//! Every assertion here runs against the REAL `holger_plugin_allocation_wasm.wasm`
//! built by `cargo xtask build-wasm-plugins`. Nothing is mocked and nothing
//! skips: a test that quietly passed because there was no module to load would
//! be worth nothing.
mod common;
use std::sync::Arc;
use holger_plugin_abi::v2::{
imports, Capability, ClockSource, Enforcement, Meter, MeterHost, MeterScope, ReadingValue,
WireAuditRecord, WireCallContext, WireInstant, WireMeterReading, WireVerb, WireVerdict,
};
use holger_plugin_allocation::AllocationMeter;
use holger_plugin_host::backend::SharedStore;
use holger_plugin_host::meter::{GrantedHost, LinkedMeter, MeterHostServices, MeterRuntime};
use holger_plugin_host::meter_wasm::WasmMeter;
use holger_plugin_host::{DirStore, PluginPolicy};
const MODULE: &str = "holger_plugin_allocation_wasm.wasm";
/// A host that counts, so the two runtimes are compared over the same numbers.
struct Counting;
fn reading(meter: &str, v: u64) -> WireMeterReading {
WireMeterReading {
meter: meter.into(),
scope: MeterScope::Product,
enforcement: Enforcement::Measured,
value: ReadingValue::Counted(v),
}
}
impl MeterHostServices for Counting {
fn usage_read(&self, repository: &str) -> Result<Vec<WireMeterReading>, String> {
use holger_plugin_abi::v2::meters;
if repository.is_empty() {
Ok(vec![
reading(meters::STORAGE_TOTAL_BYTES, 4096),
reading(meters::STORAGE_SERVABLE_BYTES, 3000),
reading(meters::STORAGE_USED_BYTES, 1096),
])
} else {
Ok(vec![
reading(meters::ARTIFACT_BYTES, 17),
WireMeterReading {
meter: meters::ARTIFACTS.into(),
scope: MeterScope::Tenant,
enforcement: Enforcement::Measured,
// The whole reason `ReadingValue` exists: this backend cannot
// enumerate, and says so instead of answering zero.
value: ReadingValue::Refused {
reason: "znippy-go cannot enumerate".into(),
},
},
])
}
}
fn audit_write(&self, _record: &WireAuditRecord) -> Result<(), String> {
Ok(())
}
fn clock(&self) -> Result<WireInstant, String> {
Ok(WireInstant {
unix_nanos: 1,
source: ClockSource::MonotonicSinceBoot,
trusted_until_unix_nanos: 0,
})
}
}
fn store_with(files: &[(&str, usize)]) -> (tempdirish::Scratch, SharedStore) {
let dir = tempdirish::Scratch::new("meter-store");
for (name, size) in files {
std::fs::write(dir.path().join(name), vec![7u8; *size]).expect("write probe file");
}
let store: SharedStore = Arc::new(DirStore::new(dir.path()));
(dir, store)
}
/// A scratch directory that removes itself. Local because the crate has no
/// tempfile dependency and this test must not add one.
mod tempdirish {
use std::path::{Path, PathBuf};
pub struct Scratch(PathBuf);
impl Scratch {
pub fn new(name: &str) -> Self {
let p = std::env::temp_dir().join(format!(
"holger-{name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).expect("scratch dir");
Self(p)
}
pub fn path(&self) -> &Path {
&self.0
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
}
fn load(
granted: &[Capability],
store: SharedStore,
) -> Result<WasmMeter, holger_plugin_host::meter_wasm::MeterLoadError> {
let dir = common::plugin_dir_with_modules(&[MODULE]);
WasmMeter::load_with(
dir.join(MODULE),
store,
Arc::new(Counting),
&PluginPolicy::default(),
granted,
)
}
const GRANT: &[Capability] = &[Capability::UsageRead, Capability::StoreList];
#[test]
fn the_linker_defines_exactly_the_granted_imports_and_nothing_else() {
let (_d, store) = store_with(&[]);
let m = load(GRANT, store).expect("the guest loads under its own grant");
let defined = m.linker_defines();
assert_eq!(defined.len(), 2, "defined: {defined:?}");
assert!(defined.contains(&imports::USAGE_READ));
assert!(defined.contains(&imports::STORE_LIST));
// The four that make a metering plugin dangerous are simply not there.
for absent in [
imports::AUDIT_WRITE,
imports::CLOCK,
imports::STORE_PUT,
imports::STORE_GET,
imports::STORE_SIZE,
] {
assert!(
!defined.contains(&absent),
"{absent} is defined on this boot's linker"
);
}
}
#[test]
fn a_grant_that_stops_short_refuses_the_module_by_name_before_it_is_instantiated() {
let (_d, store) = store_with(&[]);
let err = load(&[Capability::UsageRead], store)
.expect_err("store.list was not granted")
.to_string();
assert!(err.contains("store.list"), "{err}");
assert!(err.contains("host_store_list"), "{err}");
assert!(err.contains("NOT DEFINED"), "{err}");
}
#[test]
fn granting_more_than_the_module_asks_for_still_defines_only_what_it_uses() {
// An operator who over-grants does not widen the module's reach: the linker
// is built from what the module imports, intersected with the grant.
let (_d, store) = store_with(&[]);
let m = load(
&[
Capability::UsageRead,
Capability::StoreList,
Capability::AuditWrite,
Capability::ClockRead,
Capability::StoreWrite,
],
store,
)
.expect("an over-grant still loads");
assert_eq!(m.linker_defines().len(), 2, "{:?}", m.linker_defines());
assert!(!m.linker_defines().contains(&imports::AUDIT_WRITE));
}
#[test]
fn the_module_declares_the_two_capabilities_it_imports_and_no_others() {
let (_d, store) = store_with(&[]);
let m = load(GRANT, store).unwrap();
assert_eq!(m.manifest().name, "allocation");
assert_eq!(
m.manifest().requires,
vec![Capability::UsageRead, Capability::StoreList]
);
assert_eq!(m.abi(), 2);
assert!(!m.manifest().meter.as_ref().unwrap().accepts_facts);
}
#[test]
fn native_and_wasm_answer_identically() {
let (_d, store) = store_with(&[("one", 100), ("two", 300)]);
let linked = match LinkedMeter::new(
"allocation",
AllocationMeter,
Arc::clone(&store),
Arc::new(Counting),
GRANT,
) {
Ok(m) => m,
Err(e) => panic!("{e}"),
};
let loaded = load(GRANT, Arc::clone(&store)).expect("the module loads");
assert_eq!(linked.meters().unwrap(), loaded.meters().unwrap());
assert_eq!(linked.capacity().unwrap(), loaded.capacity().unwrap());
assert_eq!(
linked.read("releases").unwrap(),
loaded.read("releases").unwrap()
);
let a = linked.selftest().unwrap();
let b = loaded.selftest().unwrap();
assert_eq!(a, b);
assert!(a.passed, "{}", a.detail);
// The selftest measured the store for itself: 400 bytes over two keys.
assert!(a.detail.contains("400 bytes"), "{}", a.detail);
assert!(a.detail.contains("2 stored key"), "{}", a.detail);
let ctx = WireCallContext {
abi: 2,
repository: "releases".into(),
verb: WireVerb::Put,
principal: None,
artifact: None,
now: None,
};
assert_eq!(linked.evaluate(&ctx).unwrap(), WireVerdict::Allow);
assert_eq!(loaded.evaluate(&ctx).unwrap(), WireVerdict::Allow);
// Both decline a signed fact the same way, so nothing has to infer support
// from an error string.
assert_eq!(linked.accept_fact(b"x").unwrap_err(), "UNSUPPORTED");
assert_eq!(loaded.accept_fact(b"x").unwrap_err(), "UNSUPPORTED");
}
#[test]
fn an_uncounted_meter_survives_the_wasm_round_trip_as_a_refusal() {
let (_d, store) = store_with(&[]);
let loaded = load(GRANT, store).unwrap();
let rows = loaded.read("releases").unwrap();
let artifacts = rows
.iter()
.find(|r| r.meter == "artifacts")
.expect("the artifacts meter");
match &artifacts.value {
ReadingValue::Refused { reason } => {
assert!(reason.contains("cannot enumerate"), "{reason}")
}
other => panic!("a refusal became {other:?} across the ABI"),
}
assert_eq!(artifacts.value.counted(), None);
}
#[test]
fn the_host_bridge_refuses_an_ungranted_capability_on_the_native_path_too() {
// The wasm path is protected by the linker. The native path has no linker,
// so the same grant is enforced on every method of the bridge.
let (_d, store) = store_with(&[]);
let h = GrantedHost::new(store, Arc::new(Counting), GRANT);
assert!(h.usage_read("releases").is_ok());
assert!(h.store_list("").is_ok());
assert!(h
.audit_write(&WireAuditRecord::default())
.unwrap_err()
.contains("audit.write"));
assert!(h.clock().unwrap_err().contains("clock.read"));
assert!(h.store_get("k").unwrap_err().contains("store.read"));
assert!(h.store_put("k", b"v").unwrap_err().contains("store.write"));
}
#[test]
fn the_guest_never_learns_a_capability_it_was_not_granted_even_natively() {
// Belt and braces: the native guest, handed a bridge with nothing granted,
// still answers `evaluate` — and still cannot read anything.
struct Nothing;
impl MeterHost for Nothing {}
let ctx = WireCallContext {
abi: 2,
repository: "releases".into(),
verb: WireVerb::Delete,
principal: None,
artifact: None,
now: None,
};
assert_eq!(
AllocationMeter.evaluate(&Nothing, &ctx).unwrap(),
WireVerdict::Allow
);
assert!(AllocationMeter.capacity(&Nothing).is_err());
}