Skip to main content

lab_proxy_core/
session.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Session lifecycle: identity staleness, allowlist validation, and the
6//! exclusive mutation lease.
7//!
8//! Opening a session validates the caller's identity expectations against
9//! the proxy instance and the entire allowlist against the resources and
10//! ceiling. A stale expectation or rejected allowlist creates no session
11//! and performs no hardware operation.
12
13use crate::access_policy::{
14    AccessPolicy, AccessRule, Denial, MmioResource, ResourceCeiling, ResourceId,
15};
16use crate::config::{AccessLimitEnforcer, BASELINE_MAX_DEADLINE_NS};
17use std::collections::BTreeMap;
18
19/// Host-supplied run context recorded with each session. Values are
20/// diagnostic provenance, not authorization.
21#[derive(Clone, Debug, Default, PartialEq, Eq)]
22pub struct RunContext {
23    /// Host run identifier.
24    pub run_id: String,
25    /// Host case identifier.
26    pub case_id: String,
27    /// Canonical plan digest.
28    pub plan_digest: String,
29    /// Host tool version.
30    pub host_tool_version: String,
31}
32
33/// The proxy instance identity that session expectations are validated
34/// against. Also the shape of those expectations.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct ProxyIdentity {
37    /// Boot identity of the target.
38    pub boot_id: String,
39    /// Generation of this proxy instance; changes on driver restart.
40    pub proxy_generation: u64,
41    /// Whole-instance resource-description digest.
42    pub resource_digest: String,
43    /// Target-policy digest.
44    pub policy_digest: String,
45}
46
47pub use crate::access_policy::SessionMode;
48
49/// Why a session could not be opened.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub enum OpenError {
52    /// The proxy is stopping and accepts no new sessions.
53    NotAccepting,
54    /// The expected boot identity does not match.
55    StaleBootId,
56    /// The expected proxy generation does not match.
57    StaleProxyGeneration,
58    /// The expected resource digest does not match.
59    StaleResourceDigest,
60    /// The expected policy digest does not match.
61    StalePolicyDigest,
62    /// An allowlist rule failed narrowing validation; no session exists.
63    RejectedAllowlist {
64        /// The first offending rule.
65        rule: AccessRule,
66        /// Why it was rejected.
67        denial: Denial,
68    },
69    /// Another mutating session already holds the mutation lease.
70    MutationLeaseHeld,
71    /// Mutating sessions are not permitted by the target ceiling policy.
72    MutationNotPermitted,
73    /// The request set a reserved (phase 2) expectation field. Fail
74    /// closed rather than silently ignoring the field.
75    UnsupportedExpectation,
76}
77
78/// One open session.
79#[derive(Debug)]
80pub struct Session {
81    /// Session identifier, unique for the proxy instance's lifetime.
82    pub id: u64,
83    /// Session mode.
84    pub mode: SessionMode,
85    /// Host-supplied run context.
86    pub context: RunContext,
87    /// The validated per-session policy engine.
88    pub policy: AccessPolicy,
89    /// Enforcer for access rate and deadline limits (Spec 12.1 step 9).
90    pub limit_enforcer: AccessLimitEnforcer,
91}
92
93use std::sync::Arc;
94use std::sync::atomic::{AtomicBool, Ordering};
95
96/// Callback invoked when a mutating session acquires (`true`) or releases
97/// (`false`) the exclusive mutation lease (Spec Phase 2 Section 2.2).
98pub type QuiesceHook = Arc<dyn Fn(bool) + Send + Sync>;
99
100/// Tracks open sessions, identity staleness, and the mutation lease.
101pub struct SessionManager {
102    identity: ProxyIdentity,
103    resources: BTreeMap<ResourceId, MmioResource>,
104    ceiling: BTreeMap<ResourceId, ResourceCeiling>,
105    allow_mutating_sessions: bool,
106    next_id: u64,
107    sessions: BTreeMap<u64, Session>,
108    mutating: Option<u64>,
109    accepting: bool,
110    limit_enforcer: AccessLimitEnforcer,
111    quiesced: Arc<AtomicBool>,
112    quiesce_hook: Option<QuiesceHook>,
113}
114
115impl std::fmt::Debug for SessionManager {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("SessionManager")
118            .field("identity", &self.identity)
119            .field("resources", &self.resources)
120            .field("ceiling", &self.ceiling)
121            .field("allow_mutating_sessions", &self.allow_mutating_sessions)
122            .field("next_id", &self.next_id)
123            .field("sessions", &self.sessions)
124            .field("mutating", &self.mutating)
125            .field("accepting", &self.accepting)
126            .field("quiesced", &self.quiesced.load(Ordering::SeqCst))
127            .field("has_quiesce_hook", &self.quiesce_hook.is_some())
128            .finish()
129    }
130}
131
132impl SessionManager {
133    /// Creates a manager for one proxy instance.
134    pub fn new(
135        identity: ProxyIdentity,
136        resources: BTreeMap<ResourceId, MmioResource>,
137        ceiling: BTreeMap<ResourceId, ResourceCeiling>,
138        allow_mutating_sessions: bool,
139    ) -> Self {
140        Self::with_limits(
141            identity,
142            resources,
143            ceiling,
144            allow_mutating_sessions,
145            AccessLimitEnforcer::new(0, BASELINE_MAX_DEADLINE_NS),
146        )
147    }
148
149    /// Creates a manager with explicit access limit enforcer.
150    pub fn with_limits(
151        identity: ProxyIdentity,
152        resources: BTreeMap<ResourceId, MmioResource>,
153        ceiling: BTreeMap<ResourceId, ResourceCeiling>,
154        allow_mutating_sessions: bool,
155        limit_enforcer: AccessLimitEnforcer,
156    ) -> Self {
157        Self {
158            identity,
159            resources,
160            ceiling,
161            allow_mutating_sessions,
162            next_id: 1,
163            sessions: BTreeMap::new(),
164            mutating: None,
165            accepting: true,
166            limit_enforcer,
167            quiesced: Arc::new(AtomicBool::new(false)),
168            quiesce_hook: None,
169        }
170    }
171
172    /// Registers a synchronous quiesce callback invoked with `true` when a
173    /// mutating session opens and `false` when it closes.
174    pub fn set_quiesce_hook(&mut self, hook: QuiesceHook) {
175        self.quiesce_hook = Some(hook);
176    }
177
178    /// Returns whether the driver is currently quiesced due to an active
179    /// mutating session.
180    pub fn is_quiesced(&self) -> bool {
181        self.quiesced.load(Ordering::SeqCst)
182    }
183
184    /// Returns a cloneable handle to the atomic quiesce flag so driver
185    /// background loops can check quiesce state without locking.
186    pub fn quiesce_flag(&self) -> Arc<AtomicBool> {
187        self.quiesced.clone()
188    }
189
190    /// The identity `Describe` reports and expectations are checked
191    /// against.
192    pub fn identity(&self) -> &ProxyIdentity {
193        &self.identity
194    }
195
196    /// The resource descriptions offered by this instance.
197    pub fn resources(&self) -> &BTreeMap<ResourceId, MmioResource> {
198        &self.resources
199    }
200
201    /// Opens a session. Stale identity is rejected before allowlist
202    /// validation; a rejected allowlist creates no session.
203    pub fn open_session(
204        &mut self,
205        context: RunContext,
206        mode: SessionMode,
207        expectations: &ProxyIdentity,
208        allowlist: impl IntoIterator<Item = AccessRule>,
209    ) -> Result<u64, OpenError> {
210        if !self.accepting {
211            return Err(OpenError::NotAccepting);
212        }
213        if expectations.boot_id != self.identity.boot_id {
214            return Err(OpenError::StaleBootId);
215        }
216        if expectations.proxy_generation != self.identity.proxy_generation {
217            return Err(OpenError::StaleProxyGeneration);
218        }
219        if expectations.resource_digest != self.identity.resource_digest {
220            return Err(OpenError::StaleResourceDigest);
221        }
222        if expectations.policy_digest != self.identity.policy_digest {
223            return Err(OpenError::StalePolicyDigest);
224        }
225        if mode == SessionMode::Mutating {
226            if !self.allow_mutating_sessions {
227                return Err(OpenError::MutationNotPermitted);
228            }
229            if self.mutating.is_some() {
230                return Err(OpenError::MutationLeaseHeld);
231            }
232        }
233        let policy =
234            AccessPolicy::new(mode, self.resources.clone(), self.ceiling.clone(), allowlist)
235                .map_err(|(rule, denial)| OpenError::RejectedAllowlist { rule, denial })?;
236        let id = self.next_id;
237        self.next_id += 1;
238        if mode == SessionMode::Mutating {
239            self.mutating = Some(id);
240            self.quiesced.store(true, Ordering::SeqCst);
241            if let Some(hook) = &self.quiesce_hook {
242                hook(true);
243            }
244        }
245        self.sessions.insert(
246            id,
247            Session { id, mode, context, policy, limit_enforcer: self.limit_enforcer.clone() },
248        );
249        Ok(id)
250    }
251
252    /// Enforces access-class rate limit for `session_id`.
253    pub fn check_access(
254        &mut self,
255        session_id: u64,
256        class: crate::access_policy::AccessClass,
257        now_ns: i64,
258    ) -> Result<(), Denial> {
259        let session = self.sessions.get_mut(&session_id).ok_or(Denial::UnknownResource)?;
260        session.limit_enforcer.check_access(class, now_ns)
261    }
262
263    /// Enforces deadline limit for `session_id`.
264    pub fn check_deadline(&self, session_id: u64, requested_timeout_ns: i64) -> Result<(), Denial> {
265        let session = self.sessions.get(&session_id).ok_or(Denial::UnknownResource)?;
266        session.limit_enforcer.check_deadline(requested_timeout_ns)
267    }
268
269    /// The session with `id`, if open.
270    pub fn session(&self, id: u64) -> Option<&Session> {
271        self.sessions.get(&id)
272    }
273
274    /// Closes a session, releasing the mutation lease and unquiescing the
275    /// driver if it held the lease. Returns whether the session existed.
276    pub fn close_session(&mut self, id: u64) -> bool {
277        let existed = self.sessions.remove(&id).is_some();
278        if existed && self.mutating == Some(id) {
279            self.mutating = None;
280            self.quiesced.store(false, Ordering::SeqCst);
281            if let Some(hook) = &self.quiesce_hook {
282                hook(false);
283            }
284        }
285        existed
286    }
287
288    /// Stop path: reject all new sessions and release any active mutation
289    /// lease / quiesce state.
290    pub fn reject_new_sessions(&mut self) {
291        self.accepting = false;
292        if self.mutating.take().is_some() {
293            self.quiesced.store(false, Ordering::SeqCst);
294            if let Some(hook) = &self.quiesce_hook {
295                hook(false);
296            }
297        }
298    }
299
300    /// Whether new sessions are being accepted.
301    pub fn is_accepting(&self) -> bool {
302        self.accepting
303    }
304
305    /// Number of open sessions.
306    pub fn open_count(&self) -> usize {
307        self.sessions.len()
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::access_policy::{AccessClass, WIDTH32};
315
316    const CTRL: ResourceId = 1;
317
318    fn identity() -> ProxyIdentity {
319        ProxyIdentity {
320            boot_id: "boot-1".to_string(),
321            proxy_generation: 7,
322            resource_digest: "sha256:aa".to_string(),
323            policy_digest: "sha256:bb".to_string(),
324        }
325    }
326
327    fn manager() -> SessionManager {
328        let resources = BTreeMap::from([(
329            CTRL,
330            MmioResource {
331                name: "ctrl".to_string(),
332                kind: crate::access_policy::ResourceKind::Mmio,
333                logical_size: 0x100,
334                mapped_size: 0x100,
335            },
336        )]);
337        let ceiling = BTreeMap::from([(
338            CTRL,
339            ResourceCeiling {
340                hard_denied: vec![],
341                allow_unknown_reads: true,
342                allow_poll: false,
343                writable_registers: vec![],
344                protocol: None,
345                allow_interrupt: false,
346            },
347        )]);
348        SessionManager::new(identity(), resources, ceiling, true)
349    }
350
351    fn open(manager: &mut SessionManager, mode: SessionMode) -> Result<u64, OpenError> {
352        manager.open_session(RunContext::default(), mode, &identity(), [])
353    }
354
355    #[test]
356    fn open_and_close() {
357        let mut manager = manager();
358        let id = open(&mut manager, SessionMode::ReadOnly).unwrap();
359        assert_eq!(manager.open_count(), 1);
360        assert!(manager.session(id).is_some());
361        assert!(manager.close_session(id));
362        assert_eq!(manager.open_count(), 0);
363        assert!(!manager.close_session(id));
364    }
365
366    #[test]
367    fn session_ids_are_unique_and_increasing() {
368        let mut manager = manager();
369        let first = open(&mut manager, SessionMode::ReadOnly).unwrap();
370        let second = open(&mut manager, SessionMode::ReadOnly).unwrap();
371        assert!(second > first);
372    }
373
374    #[test]
375    fn each_stale_expectation_is_distinct() {
376        let mut manager = manager();
377
378        let mut stale = identity();
379        stale.boot_id = "boot-2".to_string();
380        assert_eq!(
381            manager.open_session(RunContext::default(), SessionMode::ReadOnly, &stale, []),
382            Err(OpenError::StaleBootId)
383        );
384
385        let mut stale = identity();
386        stale.proxy_generation = 8;
387        assert_eq!(
388            manager.open_session(RunContext::default(), SessionMode::ReadOnly, &stale, []),
389            Err(OpenError::StaleProxyGeneration)
390        );
391
392        let mut stale = identity();
393        stale.resource_digest = "sha256:cc".to_string();
394        assert_eq!(
395            manager.open_session(RunContext::default(), SessionMode::ReadOnly, &stale, []),
396            Err(OpenError::StaleResourceDigest)
397        );
398
399        let mut stale = identity();
400        stale.policy_digest = "sha256:dd".to_string();
401        assert_eq!(
402            manager.open_session(RunContext::default(), SessionMode::ReadOnly, &stale, []),
403            Err(OpenError::StalePolicyDigest)
404        );
405
406        assert_eq!(manager.open_count(), 0);
407    }
408
409    #[test]
410    fn rejected_allowlist_creates_no_session() {
411        let mut manager = manager();
412        let rule =
413            AccessRule { resource: 99, offset: 0, width: WIDTH32, class: AccessClass::ReadOnce };
414        let result =
415            manager.open_session(RunContext::default(), SessionMode::ReadOnly, &identity(), [rule]);
416        assert_eq!(
417            result,
418            Err(OpenError::RejectedAllowlist { rule, denial: Denial::UnknownResource })
419        );
420        assert_eq!(manager.open_count(), 0);
421    }
422
423    #[test]
424    fn mutation_lease_is_exclusive() {
425        let mut manager = manager();
426        let holder = open(&mut manager, SessionMode::Mutating).unwrap();
427        assert_eq!(open(&mut manager, SessionMode::Mutating), Err(OpenError::MutationLeaseHeld));
428        // Read-only sessions still coexist.
429        assert!(open(&mut manager, SessionMode::ReadOnly).is_ok());
430        // Closing the holder releases the lease.
431        assert!(manager.close_session(holder));
432        assert!(open(&mut manager, SessionMode::Mutating).is_ok());
433    }
434
435    #[test]
436    fn reject_new_sessions_fails_closed() {
437        let mut manager = manager();
438        manager.reject_new_sessions();
439        assert_eq!(open(&mut manager, SessionMode::ReadOnly), Err(OpenError::NotAccepting));
440    }
441
442    #[test]
443    fn mutating_session_rejected_when_not_permitted() {
444        let resources = BTreeMap::from([(
445            CTRL,
446            MmioResource {
447                name: "ctrl".to_string(),
448                kind: crate::access_policy::ResourceKind::Mmio,
449                logical_size: 0x100,
450                mapped_size: 0x100,
451            },
452        )]);
453        let ceiling = BTreeMap::from([(
454            CTRL,
455            ResourceCeiling {
456                hard_denied: vec![],
457                allow_unknown_reads: true,
458                allow_poll: false,
459                writable_registers: vec![],
460                protocol: None,
461                allow_interrupt: false,
462            },
463        )]);
464        let mut manager = SessionManager::new(identity(), resources, ceiling, false);
465        assert_eq!(open(&mut manager, SessionMode::Mutating), Err(OpenError::MutationNotPermitted));
466        // Read-only sessions still work.
467        assert!(open(&mut manager, SessionMode::ReadOnly).is_ok());
468    }
469
470    #[test]
471    fn session_rate_and_deadline_limits() {
472        let mut mgr = manager();
473        mgr.limit_enforcer = AccessLimitEnforcer::new(2, 500_000_000);
474        let id = open(&mut mgr, SessionMode::ReadOnly).unwrap();
475
476        let t0 = 1_000_000_000;
477        assert!(mgr.check_access(id, AccessClass::ReadOnce, t0).is_ok());
478        assert!(mgr.check_access(id, AccessClass::ReadOnce, t0 + 100).is_ok());
479        assert_eq!(
480            mgr.check_access(id, AccessClass::ReadOnce, t0 + 200),
481            Err(Denial::LimitExceeded)
482        );
483
484        // Deadline check
485        assert!(mgr.check_deadline(id, 400_000_000).is_ok());
486        assert_eq!(mgr.check_deadline(id, 600_000_000), Err(Denial::LimitExceeded));
487    }
488
489    #[test]
490    fn quiesce_hook_engages_on_mutating_session_and_releases_on_close_or_stop() {
491        let mut mgr = manager();
492        let events = Arc::new(std::sync::Mutex::new(Vec::new()));
493        let events_clone = events.clone();
494        mgr.set_quiesce_hook(Arc::new(move |paused| {
495            events_clone.lock().unwrap().push(paused);
496        }));
497        let flag = mgr.quiesce_flag();
498        assert!(!mgr.is_quiesced());
499        assert!(!flag.load(Ordering::SeqCst));
500
501        // Read-only session does not trigger quiesce
502        let ro_id = open(&mut mgr, SessionMode::ReadOnly).unwrap();
503        assert!(!mgr.is_quiesced());
504        assert!(events.lock().unwrap().is_empty());
505
506        // Mutating session engages quiesce
507        let mut_id = open(&mut mgr, SessionMode::Mutating).unwrap();
508        assert!(mgr.is_quiesced());
509        assert!(flag.load(Ordering::SeqCst));
510        assert_eq!(*events.lock().unwrap(), vec![true]);
511
512        // Second mutating session fails without re-firing quiesce
513        assert_eq!(open(&mut mgr, SessionMode::Mutating), Err(OpenError::MutationLeaseHeld));
514        assert_eq!(*events.lock().unwrap(), vec![true]);
515
516        // Closing read-only session does not release quiesce
517        assert!(mgr.close_session(ro_id));
518        assert!(mgr.is_quiesced());
519        assert_eq!(*events.lock().unwrap(), vec![true]);
520
521        // Closing mutating session releases quiesce
522        assert!(mgr.close_session(mut_id));
523        assert!(!mgr.is_quiesced());
524        assert!(!flag.load(Ordering::SeqCst));
525        assert_eq!(*events.lock().unwrap(), vec![true, false]);
526
527        // Re-opening mutating session and stopping driver also releases quiesce
528        let _mut_id2 = open(&mut mgr, SessionMode::Mutating).unwrap();
529        assert!(mgr.is_quiesced());
530        mgr.reject_new_sessions();
531        assert!(!mgr.is_quiesced());
532        assert_eq!(*events.lock().unwrap(), vec![true, false, true, false]);
533    }
534}