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