Skip to main content

secure_exec_bridge/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Shared bridge contracts between the secure-exec kernel and execution planes.
4
5pub mod queue_tracker;
6
7use std::collections::BTreeMap;
8use std::sync::OnceLock;
9use std::time::{Duration, SystemTime};
10
11use serde::Deserialize;
12
13/// Shared associated types for bridge implementations.
14pub trait BridgeTypes {
15    type Error;
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum FileKind {
20    File,
21    Directory,
22    SymbolicLink,
23    Other,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct FileMetadata {
28    pub mode: u32,
29    pub size: u64,
30    pub kind: FileKind,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct DirectoryEntry {
35    pub name: String,
36    pub kind: FileKind,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct PathRequest {
41    pub vm_id: String,
42    pub path: String,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct ReadFileRequest {
47    pub vm_id: String,
48    pub path: String,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct WriteFileRequest {
53    pub vm_id: String,
54    pub path: String,
55    pub contents: Vec<u8>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ReadDirRequest {
60    pub vm_id: String,
61    pub path: String,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct CreateDirRequest {
66    pub vm_id: String,
67    pub path: String,
68    pub recursive: bool,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct RenameRequest {
73    pub vm_id: String,
74    pub from_path: String,
75    pub to_path: String,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct SymlinkRequest {
80    pub vm_id: String,
81    pub target_path: String,
82    pub link_path: String,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct ChmodRequest {
87    pub vm_id: String,
88    pub path: String,
89    pub mode: u32,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct TruncateRequest {
94    pub vm_id: String,
95    pub path: String,
96    pub len: u64,
97}
98
99pub trait FilesystemBridge: BridgeTypes {
100    fn read_file(&mut self, request: ReadFileRequest) -> Result<Vec<u8>, Self::Error>;
101    fn write_file(&mut self, request: WriteFileRequest) -> Result<(), Self::Error>;
102    fn stat(&mut self, request: PathRequest) -> Result<FileMetadata, Self::Error>;
103    fn lstat(&mut self, request: PathRequest) -> Result<FileMetadata, Self::Error>;
104    fn read_dir(&mut self, request: ReadDirRequest) -> Result<Vec<DirectoryEntry>, Self::Error>;
105    fn create_dir(&mut self, request: CreateDirRequest) -> Result<(), Self::Error>;
106    fn remove_file(&mut self, request: PathRequest) -> Result<(), Self::Error>;
107    fn remove_dir(&mut self, request: PathRequest) -> Result<(), Self::Error>;
108    fn rename(&mut self, request: RenameRequest) -> Result<(), Self::Error>;
109    fn symlink(&mut self, request: SymlinkRequest) -> Result<(), Self::Error>;
110    fn read_link(&mut self, request: PathRequest) -> Result<String, Self::Error>;
111    fn chmod(&mut self, request: ChmodRequest) -> Result<(), Self::Error>;
112    fn truncate(&mut self, request: TruncateRequest) -> Result<(), Self::Error>;
113    fn exists(&mut self, request: PathRequest) -> Result<bool, Self::Error>;
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum PermissionVerdict {
118    Allow,
119    Deny,
120    Prompt,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct PermissionDecision {
125    pub verdict: PermissionVerdict,
126    pub reason: Option<String>,
127}
128
129impl PermissionDecision {
130    pub fn allow() -> Self {
131        Self {
132            verdict: PermissionVerdict::Allow,
133            reason: None,
134        }
135    }
136
137    pub fn deny(reason: impl Into<String>) -> Self {
138        Self {
139            verdict: PermissionVerdict::Deny,
140            reason: Some(reason.into()),
141        }
142    }
143
144    pub fn prompt(reason: impl Into<String>) -> Self {
145        Self {
146            verdict: PermissionVerdict::Prompt,
147            reason: Some(reason.into()),
148        }
149    }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum FilesystemAccess {
154    Read,
155    Write,
156    Stat,
157    ReadDir,
158    CreateDir,
159    Remove,
160    Rename,
161    Symlink,
162    ReadLink,
163    Chmod,
164    Truncate,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct FilesystemPermissionRequest {
169    pub vm_id: String,
170    pub path: String,
171    pub access: FilesystemAccess,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum NetworkAccess {
176    Fetch,
177    Http,
178    Dns,
179    Listen,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct NetworkPermissionRequest {
184    pub vm_id: String,
185    pub access: NetworkAccess,
186    pub resource: String,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct CommandPermissionRequest {
191    pub vm_id: String,
192    pub command: String,
193    pub args: Vec<String>,
194    pub cwd: Option<String>,
195    pub env: BTreeMap<String, String>,
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum EnvironmentAccess {
200    Read,
201    Write,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct EnvironmentPermissionRequest {
206    pub vm_id: String,
207    pub access: EnvironmentAccess,
208    pub key: String,
209    pub value: Option<String>,
210}
211
212pub trait PermissionBridge: BridgeTypes {
213    fn check_filesystem_access(
214        &mut self,
215        request: FilesystemPermissionRequest,
216    ) -> Result<PermissionDecision, Self::Error>;
217    fn check_network_access(
218        &mut self,
219        request: NetworkPermissionRequest,
220    ) -> Result<PermissionDecision, Self::Error>;
221    fn check_command_execution(
222        &mut self,
223        request: CommandPermissionRequest,
224    ) -> Result<PermissionDecision, Self::Error>;
225    fn check_environment_access(
226        &mut self,
227        request: EnvironmentPermissionRequest,
228    ) -> Result<PermissionDecision, Self::Error>;
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct FilesystemSnapshot {
233    pub format: String,
234    pub bytes: Vec<u8>,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct LoadFilesystemStateRequest {
239    pub vm_id: String,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct FlushFilesystemStateRequest {
244    pub vm_id: String,
245    pub snapshot: FilesystemSnapshot,
246}
247
248pub trait PersistenceBridge: BridgeTypes {
249    fn load_filesystem_state(
250        &mut self,
251        request: LoadFilesystemStateRequest,
252    ) -> Result<Option<FilesystemSnapshot>, Self::Error>;
253    fn flush_filesystem_state(
254        &mut self,
255        request: FlushFilesystemStateRequest,
256    ) -> Result<(), Self::Error>;
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
260pub struct ClockRequest {
261    pub vm_id: String,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct ScheduleTimerRequest {
266    pub vm_id: String,
267    pub delay: Duration,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq)]
271pub struct ScheduledTimer {
272    pub timer_id: String,
273    pub delay: Duration,
274}
275
276pub trait ClockBridge: BridgeTypes {
277    fn wall_clock(&mut self, request: ClockRequest) -> Result<SystemTime, Self::Error>;
278    fn monotonic_clock(&mut self, request: ClockRequest) -> Result<Duration, Self::Error>;
279    fn schedule_timer(
280        &mut self,
281        request: ScheduleTimerRequest,
282    ) -> Result<ScheduledTimer, Self::Error>;
283}
284
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct RandomBytesRequest {
287    pub vm_id: String,
288    pub len: usize,
289}
290
291pub trait RandomBridge: BridgeTypes {
292    fn fill_random_bytes(&mut self, request: RandomBytesRequest) -> Result<Vec<u8>, Self::Error>;
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296pub enum LogLevel {
297    Trace,
298    Debug,
299    Info,
300    Warn,
301    Error,
302}
303
304#[derive(Debug, Clone, PartialEq, Eq)]
305pub struct LogRecord {
306    pub vm_id: String,
307    pub level: LogLevel,
308    pub message: String,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
312pub struct DiagnosticRecord {
313    pub vm_id: String,
314    pub message: String,
315    pub fields: BTreeMap<String, String>,
316}
317
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub struct StructuredEventRecord {
320    pub vm_id: String,
321    pub name: String,
322    pub fields: BTreeMap<String, String>,
323}
324
325#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326pub enum LifecycleState {
327    Starting,
328    Ready,
329    Busy,
330    Terminated,
331}
332
333#[derive(Debug, Clone, PartialEq, Eq)]
334pub struct LifecycleEventRecord {
335    pub vm_id: String,
336    pub state: LifecycleState,
337    pub detail: Option<String>,
338}
339
340pub trait EventBridge: BridgeTypes {
341    fn emit_structured_event(&mut self, event: StructuredEventRecord) -> Result<(), Self::Error>;
342    fn emit_diagnostic(&mut self, event: DiagnosticRecord) -> Result<(), Self::Error>;
343    fn emit_log(&mut self, event: LogRecord) -> Result<(), Self::Error>;
344    fn emit_lifecycle(&mut self, event: LifecycleEventRecord) -> Result<(), Self::Error>;
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum GuestRuntime {
349    JavaScript,
350    WebAssembly,
351}
352
353#[derive(Debug, Clone, PartialEq, Eq)]
354pub struct CreateJavascriptContextRequest {
355    pub vm_id: String,
356    pub bootstrap_module: Option<String>,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct CreateWasmContextRequest {
361    pub vm_id: String,
362    pub module_path: Option<String>,
363}
364
365#[derive(Debug, Clone, PartialEq, Eq)]
366pub struct GuestContextHandle {
367    pub context_id: String,
368    pub runtime: GuestRuntime,
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct StartExecutionRequest {
373    pub vm_id: String,
374    pub context_id: String,
375    pub argv: Vec<String>,
376    pub env: BTreeMap<String, String>,
377    pub cwd: String,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq)]
381pub struct StartedExecution {
382    pub execution_id: String,
383}
384
385#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct ExecutionHandleRequest {
387    pub vm_id: String,
388    pub execution_id: String,
389}
390
391#[derive(Debug, Clone, PartialEq, Eq)]
392pub struct WriteExecutionStdinRequest {
393    pub vm_id: String,
394    pub execution_id: String,
395    pub chunk: Vec<u8>,
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub enum ExecutionSignal {
400    Terminate,
401    Interrupt,
402    Kill,
403}
404
405#[derive(Debug, Clone, PartialEq, Eq)]
406pub struct KillExecutionRequest {
407    pub vm_id: String,
408    pub execution_id: String,
409    pub signal: ExecutionSignal,
410}
411
412#[derive(Debug, Clone, PartialEq, Eq)]
413pub struct PollExecutionEventRequest {
414    pub vm_id: String,
415}
416
417#[derive(Debug, Clone, PartialEq, Eq)]
418pub struct OutputChunk {
419    pub vm_id: String,
420    pub execution_id: String,
421    pub chunk: Vec<u8>,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq)]
425pub struct ExecutionExited {
426    pub vm_id: String,
427    pub execution_id: String,
428    pub exit_code: i32,
429}
430
431#[derive(Debug, Clone, PartialEq, Eq)]
432pub struct GuestKernelCall {
433    pub vm_id: String,
434    pub execution_id: String,
435    pub operation: String,
436    pub payload: Vec<u8>,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq)]
440pub enum ExecutionEvent {
441    Stdout(OutputChunk),
442    Stderr(OutputChunk),
443    Exited(ExecutionExited),
444    GuestRequest(GuestKernelCall),
445}
446
447pub trait ExecutionBridge: BridgeTypes {
448    fn create_javascript_context(
449        &mut self,
450        request: CreateJavascriptContextRequest,
451    ) -> Result<GuestContextHandle, Self::Error>;
452    fn create_wasm_context(
453        &mut self,
454        request: CreateWasmContextRequest,
455    ) -> Result<GuestContextHandle, Self::Error>;
456    fn start_execution(
457        &mut self,
458        request: StartExecutionRequest,
459    ) -> Result<StartedExecution, Self::Error>;
460    fn write_stdin(&mut self, request: WriteExecutionStdinRequest) -> Result<(), Self::Error>;
461    fn close_stdin(&mut self, request: ExecutionHandleRequest) -> Result<(), Self::Error>;
462    fn kill_execution(&mut self, request: KillExecutionRequest) -> Result<(), Self::Error>;
463    fn poll_execution_event(
464        &mut self,
465        request: PollExecutionEventRequest,
466    ) -> Result<Option<ExecutionEvent>, Self::Error>;
467}
468
469pub trait HostBridge:
470    FilesystemBridge
471    + PermissionBridge
472    + PersistenceBridge
473    + ClockBridge
474    + RandomBridge
475    + EventBridge
476    + ExecutionBridge
477{
478}
479
480impl<T> HostBridge for T where
481    T: FilesystemBridge
482        + PermissionBridge
483        + PersistenceBridge
484        + ClockBridge
485        + RandomBridge
486        + EventBridge
487        + ExecutionBridge
488{
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
492#[serde(rename_all = "camelCase")]
493pub enum BridgeCallConvention {
494    Sync,
495    Async,
496    SyncPromise,
497}
498
499#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
500#[serde(rename_all = "camelCase")]
501pub struct BridgeContractGroup {
502    pub convention: BridgeCallConvention,
503    #[serde(default)]
504    pub argument_types: Vec<String>,
505    pub return_type: String,
506    pub names: Vec<String>,
507}
508
509#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
510#[serde(rename_all = "camelCase")]
511pub struct BridgeContract {
512    pub version: u32,
513    pub groups: Vec<BridgeContractGroup>,
514}
515
516static BRIDGE_CONTRACT: OnceLock<BridgeContract> = OnceLock::new();
517
518pub fn bridge_contract() -> &'static BridgeContract {
519    BRIDGE_CONTRACT.get_or_init(|| {
520        serde_json::from_str(include_str!("../bridge-contract.json"))
521            .expect("bridge-contract.json must be valid")
522    })
523}
524
525#[cfg(test)]
526mod tests {
527    use super::{bridge_contract, BridgeCallConvention};
528
529    #[test]
530    fn bridge_contract_has_version_and_unique_method_names() {
531        let contract = bridge_contract();
532        assert!(
533            contract.version > 0,
534            "bridge contract version must be positive"
535        );
536
537        let mut seen = std::collections::BTreeSet::new();
538        for group in &contract.groups {
539            assert!(
540                !group.names.is_empty(),
541                "every bridge contract group must list at least one method"
542            );
543            for name in &group.names {
544                assert!(
545                    seen.insert(name.clone()),
546                    "duplicate bridge contract method: {name}"
547                );
548            }
549        }
550    }
551
552    #[test]
553    fn bridge_contract_lists_each_convention() {
554        let contract = bridge_contract();
555        for convention in [
556            BridgeCallConvention::Sync,
557            BridgeCallConvention::Async,
558            BridgeCallConvention::SyncPromise,
559        ] {
560            assert!(
561                contract
562                    .groups
563                    .iter()
564                    .any(|group| group.convention == convention),
565                "missing bridge contract group for {convention:?}"
566            );
567        }
568    }
569
570    #[test]
571    fn bridge_contract_module_loading_signatures_match_runtime_calls() {
572        let contract = bridge_contract();
573
574        let find_group = |method: &str| {
575            contract
576                .groups
577                .iter()
578                .find(|group| group.names.iter().any(|name| name == method))
579                .unwrap_or_else(|| panic!("missing bridge contract method {method}"))
580        };
581
582        let resolve_group = find_group("_resolveModule");
583        assert_eq!(resolve_group.convention, BridgeCallConvention::SyncPromise);
584        assert_eq!(
585            resolve_group.argument_types,
586            vec![
587                "specifier: string",
588                "fromDir: string",
589                "mode?: \"require\" | \"import\""
590            ]
591        );
592        assert_eq!(
593            resolve_group.names,
594            vec!["_resolveModule", "_resolveModuleSync"]
595        );
596
597        let load_group = find_group("_loadFile");
598        assert_eq!(load_group.convention, BridgeCallConvention::SyncPromise);
599        assert_eq!(load_group.argument_types, vec!["path: string"]);
600        assert_eq!(load_group.names, vec!["_loadFile", "_loadFileSync"]);
601
602        let format_group = find_group("_moduleFormat");
603        assert_eq!(format_group.convention, BridgeCallConvention::SyncPromise);
604        assert_eq!(format_group.argument_types, vec!["filename: string"]);
605        assert_eq!(
606            format_group.return_type,
607            "\"module\" | \"commonjs\" | \"json\" | null"
608        );
609        assert_eq!(format_group.names, vec!["_moduleFormat"]);
610    }
611}