Skip to main content

sim_platform_linux/
linux.rs

1use serde::{Deserialize, Serialize};
2use sim_platform_core::{Lifecycle, OpenSymbol, RefusalKind, ResolutionRefusal};
3use std::{
4    collections::{BTreeMap, BTreeSet, VecDeque},
5    path::PathBuf,
6    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
7};
8
9/// Linux capsule realization of the server wall-clock contract.
10#[derive(Clone, Copy, Debug, Default)]
11pub struct LinuxWallClock;
12
13impl sim_lib_server::WallClock for LinuxWallClock {
14    fn now(&self) -> sim_kernel::Result<sim_lib_server::WallTimestamp> {
15        let elapsed = SystemTime::now()
16            .duration_since(UNIX_EPOCH)
17            .map_err(|error| {
18                sim_kernel::Error::HostError(format!(
19                    "Linux wall clock precedes Unix epoch: {error}"
20                ))
21            })?;
22        let unix_millis = u64::try_from(elapsed.as_millis()).map_err(|_| {
23            sim_kernel::Error::HostError("Linux wall clock exceeds u64 milliseconds".to_owned())
24        })?;
25        Ok(sim_lib_server::WallTimestamp::from_unix_millis(unix_millis))
26    }
27}
28
29/// Privacy-filtered facts supplied during registration.
30#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
31pub struct HostFacts {
32    pub locale: String,
33    pub timezone: String,
34    pub memory_bytes: u64,
35    pub storage_bytes: u64,
36    pub parallelism: u16,
37    pub pressure: u8,
38}
39
40/// Named preopened roots. Paths never appear in Cards, receipts, or attestations.
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct XdgMounts {
43    roots: BTreeMap<OpenSymbol, PathBuf>,
44}
45impl XdgMounts {
46    /// Admit exactly the five caller-preopened XDG roots.
47    #[must_use]
48    pub fn new(
49        config: PathBuf,
50        cache: PathBuf,
51        data: PathBuf,
52        state: PathBuf,
53        temp: PathBuf,
54    ) -> Self {
55        Self {
56            roots: BTreeMap::from([
57                (OpenSymbol("mount/xdg-config".into()), config),
58                (OpenSymbol("mount/xdg-cache".into()), cache),
59                (OpenSymbol("mount/xdg-data".into()), data),
60                (OpenSymbol("mount/xdg-state".into()), state),
61                (OpenSymbol("mount/temp".into()), temp),
62            ]),
63        }
64    }
65    #[must_use]
66    pub fn contains(&self, mount: &OpenSymbol) -> bool {
67        self.roots.contains_key(mount)
68    }
69    #[must_use]
70    pub fn names(&self) -> Vec<OpenSymbol> {
71        self.roots.keys().cloned().collect()
72    }
73}
74
75/// Desktop operations remain behind one injected portal boundary.
76pub trait Portal: Send {
77    /// # Errors
78    /// Returns a typed fail-closed portal refusal.
79    fn call(&mut self, operation: DesktopOperation) -> Result<PortalReply, PortalError>;
80    fn cancel(&mut self, token: u64);
81    fn cleanup(&mut self);
82}
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum DesktopOperation {
85    Open(String),
86    Share(String),
87    Notify(String),
88    ClipboardRead,
89    ClipboardWrite(String),
90    PermissionStatus(String),
91    PermissionRequest(String),
92    KeepAwake(bool),
93    Activate(String),
94}
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub enum PortalReply {
97    Accepted,
98    Text(String),
99    Permission(Permission),
100}
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub enum Permission {
103    Granted,
104    Denied,
105    Revoked,
106}
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub enum PortalError {
109    Unsupported,
110    Denied,
111    Revoked,
112    Cancelled,
113}
114
115/// Fixed per-activation resource ceilings.
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub struct Budget {
118    pub requests: u32,
119    pub queue: usize,
120    pub entropy_bytes: usize,
121    pub timer_ns: u64,
122}
123
124/// Requests accepted by the private capsule membrane.
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub enum Request {
127    WallClock,
128    MonotonicClock,
129    Timer(u64),
130    Entropy(usize),
131    Locale,
132    Timezone,
133    Pressure,
134    Limits,
135    Mount(OpenSymbol),
136    Desktop(DesktopOperation),
137    Lifecycle,
138    Cancel(u64),
139}
140#[derive(Clone, Debug, Eq, PartialEq)]
141pub enum Reply {
142    Integer(i128),
143    Bytes(Vec<u8>),
144    Text(String),
145    Limits {
146        memory_bytes: u64,
147        storage_bytes: u64,
148        parallelism: u16,
149    },
150    Mount(OpenSymbol),
151    Portal(PortalReply),
152    Lifecycle(Lifecycle),
153    Cancelled,
154}
155
156/// One explicitly configured Linux activation.
157pub struct Capsule<P: Portal> {
158    facts: HostFacts,
159    mounts: XdgMounts,
160    portal: Option<P>,
161    budget: Budget,
162    used: u32,
163    entropy_used: usize,
164    seed: u64,
165    start: Instant,
166    lifecycle: Lifecycle,
167    queue: VecDeque<u64>,
168    cancelled: BTreeSet<u64>,
169}
170impl<P: Portal> Capsule<P> {
171    #[must_use]
172    pub fn new(
173        facts: HostFacts,
174        mounts: XdgMounts,
175        portal: Option<P>,
176        budget: Budget,
177        seed: u64,
178    ) -> Self {
179        Self {
180            facts,
181            mounts,
182            portal,
183            budget,
184            used: 0,
185            entropy_used: 0,
186            seed: seed.max(1),
187            start: Instant::now(),
188            lifecycle: Lifecycle::Ready,
189            queue: VecDeque::new(),
190            cancelled: BTreeSet::new(),
191        }
192    }
193    pub fn suspend(&mut self) {
194        self.lifecycle = Lifecycle::Suspended;
195    }
196    pub fn resume(&mut self) {
197        self.lifecycle = Lifecycle::Ready;
198    }
199    pub fn revoke(&mut self, token: u64) {
200        self.cancelled.insert(token);
201    }
202    /// Execute one bounded request. Unsupported and denied operations fail closed.
203    ///
204    /// # Errors
205    /// Returns a typed refusal for unsupported, denied, revoked, suspended,
206    /// cancelled, or over-budget work.
207    pub fn apply(&mut self, token: u64, request: Request) -> Result<Reply, ResolutionRefusal> {
208        if self.lifecycle == Lifecycle::Suspended {
209            return Err(refusal(RefusalKind::Suspended, "activation is suspended"));
210        }
211        if self.cancelled.contains(&token) {
212            return Err(refusal(
213                RefusalKind::Cancelled,
214                "request is cancelled or revoked",
215            ));
216        }
217        if self.used >= self.budget.requests || self.queue.len() >= self.budget.queue {
218            return Err(refusal(
219                RefusalKind::BudgetExhausted,
220                "activation budget exhausted",
221            ));
222        }
223        self.used += 1;
224        self.queue.push_back(token);
225        let result = self.execute(token, request);
226        self.queue.pop_front();
227        result
228    }
229    fn execute(&mut self, _token: u64, request: Request) -> Result<Reply, ResolutionRefusal> {
230        match request {
231            Request::WallClock => Ok(Reply::Integer(
232                SystemTime::now()
233                    .duration_since(UNIX_EPOCH)
234                    .map_err(|_| refusal(RefusalKind::ProviderFault, "wall clock before epoch"))?
235                    .as_nanos()
236                    .cast_signed(),
237            )),
238            Request::MonotonicClock => Ok(Reply::Integer(
239                self.start.elapsed().as_nanos().cast_signed(),
240            )),
241            Request::Timer(ns) if ns <= self.budget.timer_ns => {
242                std::thread::sleep(Duration::from_nanos(ns));
243                Ok(Reply::Integer(
244                    self.start.elapsed().as_nanos().cast_signed(),
245                ))
246            }
247            Request::Timer(_) => Err(refusal(
248                RefusalKind::BudgetExhausted,
249                "timer budget exceeded",
250            )),
251            Request::Entropy(bytes)
252                if self.entropy_used.saturating_add(bytes) <= self.budget.entropy_bytes =>
253            {
254                self.entropy_used += bytes;
255                Ok(Reply::Bytes(self.entropy(bytes)))
256            }
257            Request::Entropy(_) => Err(refusal(
258                RefusalKind::BudgetExhausted,
259                "entropy budget exceeded",
260            )),
261            Request::Locale => Ok(Reply::Text(self.facts.locale.clone())),
262            Request::Timezone => Ok(Reply::Text(self.facts.timezone.clone())),
263            Request::Pressure => Ok(Reply::Integer(i128::from(self.facts.pressure))),
264            Request::Limits => Ok(Reply::Limits {
265                memory_bytes: self.facts.memory_bytes,
266                storage_bytes: self.facts.storage_bytes,
267                parallelism: self.facts.parallelism,
268            }),
269            Request::Mount(name) if self.mounts.contains(&name) => Ok(Reply::Mount(name)),
270            Request::Mount(_) => Err(refusal(RefusalKind::Unsupported, "mount was not preopened")),
271            Request::Desktop(op) => self
272                .portal
273                .as_mut()
274                .ok_or_else(|| refusal(RefusalKind::Unsupported, "desktop service is absent"))?
275                .call(op)
276                .map(Reply::Portal)
277                .map_err(|e| {
278                    refusal(
279                        match e {
280                            PortalError::Unsupported => RefusalKind::Unsupported,
281                            PortalError::Denied | PortalError::Revoked => RefusalKind::Denied,
282                            PortalError::Cancelled => RefusalKind::Cancelled,
283                        },
284                        "portal refused request",
285                    )
286                }),
287            Request::Lifecycle => Ok(Reply::Lifecycle(self.lifecycle.clone())),
288            Request::Cancel(cancelled) => {
289                self.cancelled.insert(cancelled);
290                if let Some(portal) = &mut self.portal {
291                    portal.cancel(cancelled);
292                }
293                Ok(Reply::Cancelled)
294            }
295        }
296    }
297    fn entropy(&mut self, count: usize) -> Vec<u8> {
298        (0..count)
299            .map(|_| {
300                self.seed ^= self.seed << 13;
301                self.seed ^= self.seed >> 7;
302                self.seed ^= self.seed << 17;
303                self.seed.to_le_bytes()[0]
304            })
305            .collect()
306    }
307}
308impl<P: Portal> Drop for Capsule<P> {
309    fn drop(&mut self) {
310        self.lifecycle = Lifecycle::Stopped;
311        self.queue.clear();
312        if let Some(portal) = &mut self.portal {
313            portal.cleanup();
314        }
315    }
316}
317fn refusal(kind: RefusalKind, detail: &str) -> ResolutionRefusal {
318    ResolutionRefusal {
319        request: OpenSymbol("request/ubuntu-pc".into()),
320        service: OpenSymbol("service/ubuntu-pc".into()),
321        kind,
322        detail: detail.into(),
323    }
324}
325
326/// Portal used for a headless capsule; no desktop operation is ever supported.
327pub struct HeadlessPortal;
328impl Portal for HeadlessPortal {
329    fn call(&mut self, _: DesktopOperation) -> Result<PortalReply, PortalError> {
330        Err(PortalError::Unsupported)
331    }
332    fn cancel(&mut self, _: u64) {}
333    fn cleanup(&mut self) {}
334}