Skip to main content

confium_sandbox_wasm/
sandbox.rs

1//! The sandbox abstraction.
2//!
3//! [`Sandbox`] is the trait every plugin-runtime impl satisfies (WASM
4//! in-process via wasmtime here, out-of-process via IPC in
5//! `confium-sandbox-process` later — see `TODO.roadmap/16`). Every
6//! instance runs inside the sandbox's capability envelope: a plugin
7//! cannot reach a host import it has not been granted.
8//!
9//! See `TODO.roadmap/15-wasm-sandboxing.md` for the design.
10
11use std::path::PathBuf;
12
13use crate::Result;
14
15/// A capability a sandboxed plugin may be granted.
16///
17/// Capabilities are explicit, granular, and revocable. The sandbox
18/// runtime checks every host-import invocation against the instance's
19/// current capability set; a denied call traps with
20/// [`Error::CapabilityDenied`](crate::Error::CapabilityDenied).
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub enum Capability {
23    /// Plugin may call the `cfm_<name>_*` host-import family
24    /// (e.g. `InterfaceAccess { name: "hash" }` enables `cfm_hash_*`).
25    InterfaceAccess { name: String },
26    /// Plugin may talk to this network endpoint.
27    NetworkEndpoint { url: String },
28    /// Plugin may reference this key (read/use, never reveal bytes).
29    KeyAccess { key_id: String },
30    /// Plugin may read/write this filesystem path.
31    FilesystemPath { path: PathBuf, mode: FilesystemMode },
32}
33
34/// Access mode for a [`Capability::FilesystemPath`].
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum FilesystemMode {
37    ReadOnly,
38    ReadWrite,
39}
40
41/// A value passed across the sandbox boundary. Intentionally a small
42/// set — structured data crosses via linear-memory copy through a
43/// host import rather than being encoded into the type system.
44#[derive(Debug, Clone, PartialEq)]
45pub enum Value {
46    I32(i32),
47    I64(i64),
48    F32(f32),
49    F64(f64),
50    /// Opaque byte slice, copied into the guest's linear memory.
51    Bytes(Vec<u8>),
52}
53
54/// A loaded, capability-bound plugin.
55///
56/// Instances are mutable: capabilities can be granted and revoked
57/// between calls. A revoked capability takes effect immediately on
58/// the next host-import invocation.
59pub trait SandboxInstance: Send + Sync {
60    /// Invoke `function` with `args`. Returns the function's results.
61    fn call(&mut self, function: &str, args: &[Value]) -> Result<Vec<Value>>;
62
63    /// Grant `cap` to this instance. Grants are idempotent.
64    fn grant_capability(&mut self, cap: Capability) -> Result<()>;
65
66    /// Revoke the matching capability. Revokes are idempotent.
67    fn revoke_capability(&mut self, cap: &Capability) -> Result<()>;
68}
69
70/// A plugin runtime.
71///
72/// Implementations are cheap to clone: they share the underlying
73/// engine and compile cache. Each [`load_module`](Sandbox::load_module)
74/// produces an independent [`SandboxInstance`] with its own linear
75/// memory and an empty capability set.
76pub trait Sandbox: Send + Sync {
77    /// Compile and link a plugin from raw WASM (or WAT, depending on
78    /// the impl) bytes.
79    fn load_module(&self, bytes: &[u8]) -> Result<Box<dyn SandboxInstance>>;
80
81    /// Human-readable runtime name (e.g. `"wasmtime"`).
82    fn name(&self) -> &'static str;
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn capability_equality() {
91        let a = Capability::InterfaceAccess {
92            name: "hash".into(),
93        };
94        let b = Capability::InterfaceAccess {
95            name: "hash".into(),
96        };
97        assert_eq!(a, b);
98
99        let c = Capability::InterfaceAccess {
100            name: "sign".into(),
101        };
102        assert_ne!(a, c);
103    }
104
105    #[test]
106    fn capability_clone_preserves_fields() {
107        let cap = Capability::NetworkEndpoint {
108            url: "https://example.com".into(),
109        };
110        let cloned = cap.clone();
111        assert_eq!(cap, cloned);
112    }
113
114    #[test]
115    fn filesystem_mode_distinct() {
116        assert_ne!(FilesystemMode::ReadOnly, FilesystemMode::ReadWrite);
117    }
118}