⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/process/
session.rs

1//! Session management for multi-user support
2//!
3//! Provides session groups, virtual terminal assignment, session isolation,
4//! and login/logout lifecycle management. Each session represents a user's
5//! login context with its own process group, VT assignment, and state.
6//!
7//! Sessions are identified by a `SessionId` (u64) and tracked by a global
8//! `SessionManager` protected by a spin mutex. A maximum of 8 concurrent
9//! sessions is enforced.
10
11#![allow(dead_code)]
12
13#[cfg(feature = "alloc")]
14extern crate alloc;
15
16#[cfg(feature = "alloc")]
17use alloc::{string::String, vec::Vec};
18use core::sync::atomic::{AtomicU64, Ordering};
19
20use spin::Mutex;
21
22use crate::{error::KernelError, process::ProcessId};
23
24// ---------------------------------------------------------------------------
25// Constants
26// ---------------------------------------------------------------------------
27
28/// Maximum number of concurrent sessions.
29const MAX_SESSIONS: usize = 8;
30
31/// Base VT number for graphical sessions (session 0 -> VT7, session 1 -> VT8,
32/// ...).
33const VT_BASE: u8 = 7;
34
35// ---------------------------------------------------------------------------
36// SessionId
37// ---------------------------------------------------------------------------
38
39/// Unique session identifier.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub struct SessionId(pub u64);
42
43/// Monotonic session ID allocator.
44static NEXT_SESSION_ID: AtomicU64 = AtomicU64::new(1);
45
46fn alloc_session_id() -> SessionId {
47    SessionId(NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed))
48}
49
50// ---------------------------------------------------------------------------
51// SessionState
52// ---------------------------------------------------------------------------
53
54/// Runtime state of a session.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum SessionState {
57    /// Session is the foreground (active) session receiving input.
58    Active,
59    /// Session is locked -- requires re-authentication to resume.
60    Locked,
61    /// Session is running but in the background (another session is active).
62    Background,
63    /// Session is in the process of logging out.
64    LoggingOut,
65}
66
67// ---------------------------------------------------------------------------
68// Session
69// ---------------------------------------------------------------------------
70
71/// A user session containing process groups, VT assignment, and metadata.
72#[cfg(feature = "alloc")]
73#[derive(Debug, Clone)]
74pub struct Session {
75    /// Unique session identifier.
76    pub id: SessionId,
77    /// User ID of the session owner.
78    pub user_id: u64,
79    /// Username of the session owner.
80    pub username: String,
81    /// Virtual terminal number assigned to this session.
82    pub vt_number: u8,
83    /// Current state.
84    pub state: SessionState,
85    /// Process group ID that currently owns the terminal (foreground).
86    pub foreground_group: u64,
87    /// Background process group IDs.
88    pub background_groups: Vec<u64>,
89    /// TSC tick count at login time.
90    pub login_time: u64,
91    /// Process IDs belonging to this session.
92    pub process_ids: Vec<u64>,
93}
94
95#[cfg(feature = "alloc")]
96impl Session {
97    /// Create a new session.
98    fn new(id: SessionId, user_id: u64, username: &str, vt_number: u8) -> Self {
99        Self {
100            id,
101            user_id,
102            username: String::from(username),
103            vt_number,
104            state: SessionState::Active,
105            foreground_group: 0,
106            background_groups: Vec::new(),
107            login_time: 0,
108            process_ids: Vec::new(),
109        }
110    }
111
112    /// Check whether the given process belongs to this session.
113    pub fn contains_process(&self, pid: u64) -> bool {
114        self.process_ids.contains(&pid)
115    }
116
117    /// Return the number of processes in this session.
118    pub fn process_count(&self) -> usize {
119        self.process_ids.len()
120    }
121}
122
123// ---------------------------------------------------------------------------
124// SessionManager
125// ---------------------------------------------------------------------------
126
127/// Manages all active sessions.
128#[cfg(feature = "alloc")]
129pub struct SessionManager {
130    /// Active sessions indexed by position (max `MAX_SESSIONS`).
131    sessions: Vec<Session>,
132    /// Currently active (foreground) session ID.
133    active_session: Option<SessionId>,
134}
135
136#[cfg(feature = "alloc")]
137impl Default for SessionManager {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143#[cfg(feature = "alloc")]
144impl SessionManager {
145    /// Create a new, empty session manager.
146    pub const fn new() -> Self {
147        Self {
148            sessions: Vec::new(),
149            active_session: None,
150        }
151    }
152
153    /// Create a new session for a user.
154    ///
155    /// Assigns the next available virtual terminal starting at `VT_BASE`.
156    /// Returns `Err` if the maximum number of sessions is reached.
157    pub fn create_session(
158        &mut self,
159        user_id: u64,
160        username: &str,
161        vt: u8,
162    ) -> Result<SessionId, KernelError> {
163        if self.sessions.len() >= MAX_SESSIONS {
164            return Err(KernelError::InvalidState {
165                expected: "fewer than 8 sessions",
166                actual: "max sessions reached",
167            });
168        }
169
170        let id = alloc_session_id();
171        let vt_number = if vt > 0 {
172            vt
173        } else {
174            VT_BASE + self.sessions.len() as u8
175        };
176
177        let mut session = Session::new(id, user_id, username, vt_number);
178
179        // Read TSC as login time (or 0 if unavailable).
180        #[cfg(target_arch = "x86_64")]
181        {
182            session.login_time = unsafe { core::arch::x86_64::_rdtsc() };
183        }
184        #[cfg(not(target_arch = "x86_64"))]
185        {
186            session.login_time = 0;
187        }
188
189        // If no session is active, make this one active.
190        if self.active_session.is_none() {
191            session.state = SessionState::Active;
192            self.active_session = Some(id);
193        } else {
194            session.state = SessionState::Background;
195        }
196
197        self.sessions.push(session);
198        Ok(id)
199    }
200
201    /// Destroy a session and remove it from tracking.
202    pub fn destroy_session(&mut self, id: SessionId) {
203        self.sessions.retain(|s| s.id != id);
204        if self.active_session == Some(id) {
205            // Promote the first remaining session to active.
206            self.active_session = self.sessions.first().map(|s| s.id);
207            if let Some(new_active) = self.active_session {
208                if let Some(s) = self.sessions.iter_mut().find(|s| s.id == new_active) {
209                    s.state = SessionState::Active;
210                }
211            }
212        }
213    }
214
215    /// Get a reference to a session by ID.
216    pub fn get_session(&self, id: SessionId) -> Option<&Session> {
217        self.sessions.iter().find(|s| s.id == id)
218    }
219
220    /// Get a mutable reference to a session by ID.
221    pub fn get_session_mut(&mut self, id: SessionId) -> Option<&mut Session> {
222        self.sessions.iter_mut().find(|s| s.id == id)
223    }
224
225    /// Return the currently active (foreground) session ID.
226    pub fn get_active_session(&self) -> Option<SessionId> {
227        self.active_session
228    }
229
230    /// Switch to a different session.
231    ///
232    /// The previously active session is moved to `Background`; the new
233    /// session becomes `Active`.
234    pub fn switch_session(&mut self, id: SessionId) {
235        // Put current active session into background.
236        if let Some(current_id) = self.active_session {
237            if let Some(s) = self.sessions.iter_mut().find(|s| s.id == current_id) {
238                if s.state == SessionState::Active {
239                    s.state = SessionState::Background;
240                }
241            }
242        }
243
244        // Activate the target session.
245        if let Some(s) = self.sessions.iter_mut().find(|s| s.id == id) {
246            s.state = SessionState::Active;
247            self.active_session = Some(id);
248        }
249    }
250
251    /// Lock a session (requires re-authentication to resume).
252    pub fn lock_session(&mut self, id: SessionId) {
253        if let Some(s) = self.sessions.iter_mut().find(|s| s.id == id) {
254            s.state = SessionState::Locked;
255        }
256    }
257
258    /// Unlock a previously locked session, returning it to `Active`.
259    pub fn unlock_session(&mut self, id: SessionId) {
260        if let Some(s) = self.sessions.iter_mut().find(|s| s.id == id) {
261            if s.state == SessionState::Locked {
262                s.state = SessionState::Active;
263            }
264        }
265    }
266
267    /// Add a process to a session.
268    pub fn add_process(&mut self, session_id: SessionId, pid: ProcessId) {
269        if let Some(s) = self.sessions.iter_mut().find(|s| s.id == session_id) {
270            if !s.process_ids.contains(&pid.0) {
271                s.process_ids.push(pid.0);
272            }
273        }
274    }
275
276    /// Remove a process from a session.
277    pub fn remove_process(&mut self, session_id: SessionId, pid: ProcessId) {
278        if let Some(s) = self.sessions.iter_mut().find(|s| s.id == session_id) {
279            s.process_ids.retain(|&p| p != pid.0);
280        }
281    }
282
283    /// Find the session assigned to a given VT number.
284    pub fn get_session_for_vt(&self, vt: u8) -> Option<SessionId> {
285        self.sessions
286            .iter()
287            .find(|s| s.vt_number == vt)
288            .map(|s| s.id)
289    }
290
291    /// Return a list of all session IDs.
292    pub fn list_sessions(&self) -> Vec<SessionId> {
293        self.sessions.iter().map(|s| s.id).collect()
294    }
295
296    /// Number of active sessions.
297    pub fn session_count(&self) -> usize {
298        self.sessions.len()
299    }
300
301    /// Check whether a process in one session may signal a process in another.
302    ///
303    /// Session isolation: cross-session signaling is denied unless the
304    /// source process belongs to the same session as the target.
305    pub fn can_signal(&self, source_pid: u64, target_pid: u64) -> bool {
306        let source_session = self
307            .sessions
308            .iter()
309            .find(|s| s.process_ids.contains(&source_pid));
310        let target_session = self
311            .sessions
312            .iter()
313            .find(|s| s.process_ids.contains(&target_pid));
314
315        match (source_session, target_session) {
316            (Some(src), Some(tgt)) => src.id == tgt.id,
317            // If either process is not in any session, allow (kernel process).
318            _ => true,
319        }
320    }
321
322    /// Begin logout for a session: set state to `LoggingOut`.
323    pub fn begin_logout(&mut self, id: SessionId) {
324        if let Some(s) = self.sessions.iter_mut().find(|s| s.id == id) {
325            s.state = SessionState::LoggingOut;
326        }
327    }
328
329    /// Get all process IDs belonging to a session (for cleanup on logout).
330    pub fn get_session_processes(&self, id: SessionId) -> Vec<u64> {
331        self.sessions
332            .iter()
333            .find(|s| s.id == id)
334            .map(|s| s.process_ids.clone())
335            .unwrap_or_default()
336    }
337}
338
339// ---------------------------------------------------------------------------
340// Global session manager
341// ---------------------------------------------------------------------------
342
343/// Global session manager, protected by a spin mutex.
344#[cfg(feature = "alloc")]
345pub static SESSION_MANAGER: Mutex<SessionManager> = Mutex::new(SessionManager::new());
346
347/// Initialize the session subsystem.
348#[cfg(feature = "alloc")]
349pub fn init() {
350    // Force the lazy initialization of the session manager.
351    let _guard = SESSION_MANAGER.lock();
352    // Nothing else to do -- the manager starts empty.
353}
354
355// ---------------------------------------------------------------------------
356// Tests
357// ---------------------------------------------------------------------------
358
359#[cfg(test)]
360mod tests {
361    use alloc::vec;
362
363    use super::*;
364
365    #[test]
366    fn test_session_id_allocation() {
367        let id1 = alloc_session_id();
368        let id2 = alloc_session_id();
369        assert_ne!(id1, id2);
370        assert!(id2.0 > id1.0);
371    }
372
373    #[test]
374    fn test_create_session() {
375        let mut mgr = SessionManager::new();
376        let id = mgr.create_session(1000, "alice", 0).unwrap();
377        assert_eq!(mgr.session_count(), 1);
378        assert_eq!(mgr.get_active_session(), Some(id));
379
380        let session = mgr.get_session(id).unwrap();
381        assert_eq!(session.user_id, 1000);
382        assert_eq!(session.username, "alice");
383        assert_eq!(session.state, SessionState::Active);
384    }
385
386    #[test]
387    fn test_max_sessions() {
388        let mut mgr = SessionManager::new();
389        for i in 0..MAX_SESSIONS {
390            let name = alloc::format!("user{}", i);
391            mgr.create_session(i as u64, &name, 0).unwrap();
392        }
393        let result = mgr.create_session(100, "overflow", 0);
394        assert!(result.is_err());
395    }
396
397    #[test]
398    fn test_destroy_session() {
399        let mut mgr = SessionManager::new();
400        let id = mgr.create_session(1, "bob", 0).unwrap();
401        assert_eq!(mgr.session_count(), 1);
402        mgr.destroy_session(id);
403        assert_eq!(mgr.session_count(), 0);
404        assert_eq!(mgr.get_active_session(), None);
405    }
406
407    #[test]
408    fn test_switch_session() {
409        let mut mgr = SessionManager::new();
410        let id1 = mgr.create_session(1, "alice", 0).unwrap();
411        let id2 = mgr.create_session(2, "bob", 0).unwrap();
412
413        assert_eq!(mgr.get_active_session(), Some(id1));
414        assert_eq!(
415            mgr.get_session(id2).unwrap().state,
416            SessionState::Background
417        );
418
419        mgr.switch_session(id2);
420        assert_eq!(mgr.get_active_session(), Some(id2));
421        assert_eq!(
422            mgr.get_session(id1).unwrap().state,
423            SessionState::Background
424        );
425        assert_eq!(mgr.get_session(id2).unwrap().state, SessionState::Active);
426    }
427
428    #[test]
429    fn test_lock_unlock_session() {
430        let mut mgr = SessionManager::new();
431        let id = mgr.create_session(1, "charlie", 0).unwrap();
432
433        mgr.lock_session(id);
434        assert_eq!(mgr.get_session(id).unwrap().state, SessionState::Locked);
435
436        mgr.unlock_session(id);
437        assert_eq!(mgr.get_session(id).unwrap().state, SessionState::Active);
438    }
439
440    #[test]
441    fn test_add_remove_process() {
442        let mut mgr = SessionManager::new();
443        let id = mgr.create_session(1, "dave", 0).unwrap();
444
445        mgr.add_process(id, ProcessId(42));
446        mgr.add_process(id, ProcessId(43));
447        assert_eq!(mgr.get_session(id).unwrap().process_count(), 2);
448        assert!(mgr.get_session(id).unwrap().contains_process(42));
449
450        mgr.remove_process(id, ProcessId(42));
451        assert_eq!(mgr.get_session(id).unwrap().process_count(), 1);
452        assert!(!mgr.get_session(id).unwrap().contains_process(42));
453    }
454
455    #[test]
456    fn test_session_isolation() {
457        let mut mgr = SessionManager::new();
458        let id1 = mgr.create_session(1, "alice", 0).unwrap();
459        let id2 = mgr.create_session(2, "bob", 0).unwrap();
460
461        mgr.add_process(id1, ProcessId(10));
462        mgr.add_process(id2, ProcessId(20));
463
464        // Same session: allowed
465        mgr.add_process(id1, ProcessId(11));
466        assert!(mgr.can_signal(10, 11));
467
468        // Cross-session: denied
469        assert!(!mgr.can_signal(10, 20));
470    }
471
472    #[test]
473    fn test_get_session_for_vt() {
474        let mut mgr = SessionManager::new();
475        let id = mgr.create_session(1, "eve", 9).unwrap();
476        assert_eq!(mgr.get_session_for_vt(9), Some(id));
477        assert_eq!(mgr.get_session_for_vt(10), None);
478    }
479
480    #[test]
481    fn test_list_sessions() {
482        let mut mgr = SessionManager::new();
483        let id1 = mgr.create_session(1, "a", 0).unwrap();
484        let id2 = mgr.create_session(2, "b", 0).unwrap();
485        let list = mgr.list_sessions();
486        assert_eq!(list.len(), 2);
487        assert!(list.contains(&id1));
488        assert!(list.contains(&id2));
489    }
490
491    #[test]
492    fn test_begin_logout() {
493        let mut mgr = SessionManager::new();
494        let id = mgr.create_session(1, "frank", 0).unwrap();
495        mgr.add_process(id, ProcessId(100));
496        mgr.begin_logout(id);
497        assert_eq!(mgr.get_session(id).unwrap().state, SessionState::LoggingOut);
498
499        let pids = mgr.get_session_processes(id);
500        assert_eq!(pids, vec![100u64]);
501    }
502
503    #[test]
504    fn test_destroy_promotes_next() {
505        let mut mgr = SessionManager::new();
506        let id1 = mgr.create_session(1, "a", 0).unwrap();
507        let id2 = mgr.create_session(2, "b", 0).unwrap();
508        assert_eq!(mgr.get_active_session(), Some(id1));
509
510        mgr.destroy_session(id1);
511        // id2 should be promoted to active
512        assert_eq!(mgr.get_active_session(), Some(id2));
513        assert_eq!(mgr.get_session(id2).unwrap().state, SessionState::Active);
514    }
515}