Skip to main content

agentos_kernel/
fd_table.rs

1use std::collections::{btree_map::Values, BTreeMap, BTreeSet};
2use std::error::Error;
3use std::fmt;
4use std::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering};
5use std::sync::{Arc, Condvar, Mutex, MutexGuard};
6
7use crate::vfs::VirtualStat;
8
9pub const MAX_FDS_PER_PROCESS: usize = 256;
10
11pub const O_RDONLY: u32 = 0;
12pub const O_WRONLY: u32 = 1;
13pub const O_RDWR: u32 = 2;
14pub const O_CREAT: u32 = 0o100;
15pub const O_EXCL: u32 = 0o200;
16pub const O_TRUNC: u32 = 0o1000;
17pub const O_APPEND: u32 = 0o2000;
18pub const O_NONBLOCK: u32 = 0o4000;
19pub const O_DIRECT: u32 = 0o40000;
20pub const O_DIRECTORY: u32 = 0o200000;
21pub const O_NOFOLLOW: u32 = 0o400000;
22pub const F_DUPFD: u32 = 0;
23pub const F_GETFD: u32 = 1;
24pub const F_SETFD: u32 = 2;
25pub const F_GETFL: u32 = 3;
26pub const F_SETFL: u32 = 4;
27pub const FD_CLOEXEC: u32 = 1;
28pub const LOCK_SH: u32 = 1;
29pub const LOCK_EX: u32 = 2;
30pub const LOCK_NB: u32 = 4;
31pub const LOCK_UN: u32 = 8;
32const DEFAULT_MAX_RECORD_LOCKS: usize = 4096;
33
34pub const FILETYPE_UNKNOWN: u8 = 0;
35pub const FILETYPE_BLOCK_DEVICE: u8 = 1;
36pub const FILETYPE_CHARACTER_DEVICE: u8 = 2;
37pub const FILETYPE_DIRECTORY: u8 = 3;
38pub const FILETYPE_REGULAR_FILE: u8 = 4;
39pub const FILETYPE_SOCKET_DGRAM: u8 = 5;
40pub const FILETYPE_SOCKET_STREAM: u8 = 6;
41pub const FILETYPE_PIPE: u8 = FILETYPE_SOCKET_STREAM;
42pub const FILETYPE_SYMBOLIC_LINK: u8 = 7;
43
44pub type FdResult<T> = Result<T, FdTableError>;
45pub type SharedFileDescription = Arc<FileDescription>;
46
47// Every kernel subsystem keys pipes, PTYs, sockets, locks, and poll state by
48// open-file-description identity. Allocate that identity from one global
49// monotonic domain: per-subsystem counters can eventually overlap and make an
50// unrelated regular file look like a stale socket or pipe.
51static NEXT_FILE_DESCRIPTION_ID: AtomicU64 = AtomicU64::new(1);
52
53pub(crate) fn allocate_file_description_id() -> u64 {
54    NEXT_FILE_DESCRIPTION_ID
55        .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |id| id.checked_add(1))
56        .expect("open-file-description id space exhausted")
57}
58
59#[derive(Debug, Default)]
60pub struct AnonymousFileUsage {
61    bytes: AtomicU64,
62    inodes: AtomicUsize,
63}
64
65impl AnonymousFileUsage {
66    pub fn bytes(&self) -> u64 {
67        self.bytes.load(Ordering::SeqCst)
68    }
69
70    pub fn inodes(&self) -> usize {
71        self.inodes.load(Ordering::SeqCst)
72    }
73
74    fn add_file(&self, size: u64) {
75        self.bytes.fetch_add(size, Ordering::SeqCst);
76        self.inodes.fetch_add(1, Ordering::SeqCst);
77    }
78
79    fn remove_file(&self, size: u64) {
80        self.bytes.fetch_sub(size, Ordering::SeqCst);
81        self.inodes.fetch_sub(1, Ordering::SeqCst);
82    }
83
84    fn resize_file(&self, old_size: u64, new_size: u64) {
85        if new_size >= old_size {
86            self.bytes
87                .fetch_add(new_size.saturating_sub(old_size), Ordering::SeqCst);
88        } else {
89            self.bytes
90                .fetch_sub(old_size.saturating_sub(new_size), Ordering::SeqCst);
91        }
92    }
93}
94
95#[derive(Debug)]
96pub struct AnonymousFile {
97    pub data: Vec<u8>,
98    pub stat: VirtualStat,
99    usage: Arc<AnonymousFileUsage>,
100}
101
102impl AnonymousFile {
103    pub fn new(data: Vec<u8>, stat: VirtualStat, usage: Arc<AnonymousFileUsage>) -> Self {
104        usage.add_file(stat.size);
105        Self { data, stat, usage }
106    }
107}
108
109impl Drop for AnonymousFile {
110    fn drop(&mut self) {
111        self.usage.remove_file(self.stat.size);
112    }
113}
114
115pub type SharedAnonymousFile = Arc<Mutex<AnonymousFile>>;
116
117#[derive(Debug)]
118enum FileBacking {
119    Path(String),
120    LinkedAlias {
121        former_path: String,
122        live_path: String,
123    },
124    Anonymous {
125        former_path: String,
126        file: SharedAnonymousFile,
127    },
128    DetachedDirectory {
129        former_path: String,
130        stat: VirtualStat,
131    },
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct FdTableError {
136    code: &'static str,
137    message: String,
138}
139
140impl FdTableError {
141    pub fn code(&self) -> &'static str {
142        self.code
143    }
144
145    fn bad_file_descriptor(fd: u32) -> Self {
146        Self {
147            code: "EBADF",
148            message: format!("bad file descriptor {fd}"),
149        }
150    }
151
152    fn too_many_open_files() -> Self {
153        Self {
154            code: "EMFILE",
155            message: String::from("too many open files"),
156        }
157    }
158
159    fn no_memory(message: impl Into<String>) -> Self {
160        Self {
161            code: "ENOMEM",
162            message: message.into(),
163        }
164    }
165
166    fn invalid_argument(message: impl Into<String>) -> Self {
167        Self {
168            code: "EINVAL",
169            message: message.into(),
170        }
171    }
172
173    fn would_block(message: impl Into<String>) -> Self {
174        Self {
175            code: "EWOULDBLOCK",
176            message: message.into(),
177        }
178    }
179
180    fn deadlock(message: impl Into<String>) -> Self {
181        Self {
182            code: "EDEADLK",
183            message: message.into(),
184        }
185    }
186}
187
188impl fmt::Display for FdTableError {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        write!(f, "{}: {}", self.code, self.message)
191    }
192}
193
194impl Error for FdTableError {}
195
196#[derive(Debug)]
197pub struct FileDescription {
198    id: u64,
199    backing: Mutex<FileBacking>,
200    lock_target: Option<FileLockTarget>,
201    cursor: AtomicU64,
202    flags: AtomicU32,
203    ref_count: AtomicUsize,
204}
205
206impl FileDescription {
207    pub fn new(id: u64, path: impl Into<String>, flags: u32) -> Self {
208        Self::with_ref_count_and_lock(id, path, flags, 1, None)
209    }
210
211    pub fn new_with_lock(
212        id: u64,
213        path: impl Into<String>,
214        flags: u32,
215        lock_target: Option<FileLockTarget>,
216    ) -> Self {
217        Self::with_ref_count_and_lock(id, path, flags, 1, lock_target)
218    }
219
220    pub fn with_ref_count(id: u64, path: impl Into<String>, flags: u32, ref_count: usize) -> Self {
221        Self::with_ref_count_and_lock(id, path, flags, ref_count, None)
222    }
223
224    pub fn with_ref_count_and_lock(
225        id: u64,
226        path: impl Into<String>,
227        flags: u32,
228        ref_count: usize,
229        lock_target: Option<FileLockTarget>,
230    ) -> Self {
231        Self {
232            id,
233            backing: Mutex::new(FileBacking::Path(path.into())),
234            lock_target,
235            cursor: AtomicU64::new(0),
236            flags: AtomicU32::new(flags),
237            ref_count: AtomicUsize::new(ref_count),
238        }
239    }
240
241    pub fn id(&self) -> u64 {
242        self.id
243    }
244
245    pub fn path(&self) -> String {
246        match &*lock_or_recover(&self.backing) {
247            FileBacking::Path(path) => path.clone(),
248            FileBacking::LinkedAlias { live_path, .. } => live_path.clone(),
249            FileBacking::Anonymous { former_path, .. } => former_path.clone(),
250            FileBacking::DetachedDirectory { former_path, .. } => former_path.clone(),
251        }
252    }
253
254    /// Linux renders an unlinked open file description through procfs with a
255    /// ` (deleted)` suffix while the description itself remains usable.
256    pub fn proc_display_path(&self) -> String {
257        match &*lock_or_recover(&self.backing) {
258            FileBacking::Path(path) => path.clone(),
259            FileBacking::LinkedAlias { former_path, .. }
260            | FileBacking::Anonymous { former_path, .. }
261            | FileBacking::DetachedDirectory { former_path, .. } => {
262                format!("{former_path} (deleted)")
263            }
264        }
265    }
266
267    pub fn is_path_backed_by(&self, expected: &str) -> bool {
268        match &*lock_or_recover(&self.backing) {
269            FileBacking::Path(path) => path == expected,
270            FileBacking::LinkedAlias { live_path, .. } => live_path == expected,
271            FileBacking::Anonymous { .. } | FileBacking::DetachedDirectory { .. } => false,
272        }
273    }
274
275    pub fn rename_path_prefix(&self, old_path: &str, new_path: &str) {
276        let mut backing = lock_or_recover(&self.backing);
277        let path = match &mut *backing {
278            FileBacking::Path(path) => path,
279            FileBacking::LinkedAlias { live_path, .. } => live_path,
280            FileBacking::Anonymous { .. } | FileBacking::DetachedDirectory { .. } => return,
281        };
282        if path == old_path {
283            *path = new_path.to_string();
284        } else if let Some(suffix) = path
285            .strip_prefix(old_path)
286            .filter(|value| value.starts_with('/'))
287        {
288            *path = format!("{new_path}{suffix}");
289        }
290    }
291
292    pub fn detach_path(&self, expected: &str, file: SharedAnonymousFile) -> bool {
293        let mut backing = lock_or_recover(&self.backing);
294        let former_path = match &*backing {
295            FileBacking::Path(path) if path == expected => expected.to_string(),
296            FileBacking::LinkedAlias {
297                former_path,
298                live_path,
299            } if live_path == expected => former_path.clone(),
300            _ => return false,
301        };
302        *backing = FileBacking::Anonymous { former_path, file };
303        true
304    }
305
306    /// Keep an unlinked open description attached to the same inode through a
307    /// surviving hard-link name. The former name remains visible through
308    /// procfs, while descriptor operations use the live alias.
309    pub fn rebind_deleted_path(&self, expected: &str, live_path: &str) -> bool {
310        let mut backing = lock_or_recover(&self.backing);
311        let former_path = match &*backing {
312            FileBacking::Path(path) if path == expected => expected.to_string(),
313            FileBacking::LinkedAlias {
314                former_path,
315                live_path: current,
316            } if current == expected => former_path.clone(),
317            _ => return false,
318        };
319        *backing = FileBacking::LinkedAlias {
320            former_path,
321            live_path: live_path.to_string(),
322        };
323        true
324    }
325
326    pub fn detach_directory(&self, _expected: &str, stat: VirtualStat) -> bool {
327        let mut backing = lock_or_recover(&self.backing);
328        let FileBacking::Path(former_path) = &*backing else {
329            return false;
330        };
331        let former_path = former_path.clone();
332        *backing = FileBacking::DetachedDirectory { former_path, stat };
333        true
334    }
335
336    pub fn detached_directory_stat(&self) -> Option<VirtualStat> {
337        match &*lock_or_recover(&self.backing) {
338            FileBacking::DetachedDirectory { stat, .. } => Some(stat.clone()),
339            FileBacking::Path(_)
340            | FileBacking::LinkedAlias { .. }
341            | FileBacking::Anonymous { .. } => None,
342        }
343    }
344
345    pub fn anonymous_stat(&self) -> Option<VirtualStat> {
346        let file = match &*lock_or_recover(&self.backing) {
347            FileBacking::Anonymous { file, .. } => Arc::clone(file),
348            FileBacking::Path(_)
349            | FileBacking::LinkedAlias { .. }
350            | FileBacking::DetachedDirectory { .. } => return None,
351        };
352        let stat = lock_or_recover(&file).stat.clone();
353        Some(stat)
354    }
355
356    pub fn anonymous_pread(&self, offset: u64, length: usize) -> Option<Vec<u8>> {
357        let file = match &*lock_or_recover(&self.backing) {
358            FileBacking::Anonymous { file, .. } => Arc::clone(file),
359            FileBacking::Path(_)
360            | FileBacking::LinkedAlias { .. }
361            | FileBacking::DetachedDirectory { .. } => return None,
362        };
363        let file = lock_or_recover(&file);
364        let start = usize::try_from(offset).unwrap_or(usize::MAX);
365        if start >= file.data.len() {
366            return Some(Vec::new());
367        }
368        let end = start.saturating_add(length).min(file.data.len());
369        Some(file.data[start..end].to_vec())
370    }
371
372    pub fn anonymous_pwrite(&self, offset: u64, data: &[u8]) -> Option<FdResult<u64>> {
373        let file = match &*lock_or_recover(&self.backing) {
374            FileBacking::Anonymous { file, .. } => Arc::clone(file),
375            FileBacking::Path(_)
376            | FileBacking::LinkedAlias { .. }
377            | FileBacking::DetachedDirectory { .. } => return None,
378        };
379        let mut file = lock_or_recover(&file);
380        let Ok(start) = usize::try_from(offset) else {
381            return Some(Err(FdTableError::invalid_argument(
382                "anonymous file offset exceeds host address space",
383            )));
384        };
385        let Some(end) = start.checked_add(data.len()) else {
386            return Some(Err(FdTableError::invalid_argument(
387                "anonymous file write range overflows",
388            )));
389        };
390        if end > file.data.len() {
391            let additional = end - file.data.len();
392            if file.data.try_reserve(additional).is_err() {
393                return Some(Err(FdTableError::no_memory(
394                    "anonymous file write allocation failed",
395                )));
396            }
397            file.data.resize(end, 0);
398        }
399        file.data[start..end].copy_from_slice(data);
400        let old_size = file.stat.size;
401        file.stat.size = file.data.len() as u64;
402        file.stat.blocks = file.stat.size.div_ceil(512);
403        file.usage.resize_file(old_size, file.stat.size);
404        Some(Ok(file.stat.size))
405    }
406
407    pub fn anonymous_truncate(&self, length: u64) -> Option<FdResult<()>> {
408        let file = match &*lock_or_recover(&self.backing) {
409            FileBacking::Anonymous { file, .. } => Arc::clone(file),
410            FileBacking::Path(_)
411            | FileBacking::LinkedAlias { .. }
412            | FileBacking::DetachedDirectory { .. } => return None,
413        };
414        let Ok(length) = usize::try_from(length) else {
415            return Some(Err(FdTableError::invalid_argument(
416                "anonymous file length exceeds host address space",
417            )));
418        };
419        let mut file = lock_or_recover(&file);
420        let additional = length.saturating_sub(file.data.len());
421        if additional > 0 && file.data.try_reserve(additional).is_err() {
422            return Some(Err(FdTableError::no_memory(
423                "anonymous file truncate allocation failed",
424            )));
425        }
426        file.data.resize(length, 0);
427        let old_size = file.stat.size;
428        file.stat.size = length as u64;
429        file.stat.blocks = file.stat.size.div_ceil(512);
430        file.usage.resize_file(old_size, file.stat.size);
431        Some(Ok(()))
432    }
433
434    pub fn detached_chmod(&self, mode: u32) -> bool {
435        let mut backing = lock_or_recover(&self.backing);
436        match &mut *backing {
437            FileBacking::Anonymous { file, .. } => {
438                let mut file = lock_or_recover(file);
439                file.stat.mode = (file.stat.mode & !0o7777) | (mode & 0o7777);
440                true
441            }
442            FileBacking::DetachedDirectory { stat, .. } => {
443                stat.mode = (stat.mode & !0o7777) | (mode & 0o7777);
444                true
445            }
446            FileBacking::Path(_) | FileBacking::LinkedAlias { .. } => false,
447        }
448    }
449
450    pub fn detached_chown(&self, uid: u32, gid: u32, changed_mode: Option<u32>) -> bool {
451        let mut backing = lock_or_recover(&self.backing);
452        match &mut *backing {
453            FileBacking::Anonymous { file, .. } => {
454                let mut file = lock_or_recover(file);
455                file.stat.uid = uid;
456                file.stat.gid = gid;
457                if let Some(mode) = changed_mode {
458                    file.stat.mode = mode;
459                }
460                true
461            }
462            FileBacking::DetachedDirectory { stat, .. } => {
463                stat.uid = uid;
464                stat.gid = gid;
465                true
466            }
467            FileBacking::Path(_) | FileBacking::LinkedAlias { .. } => false,
468        }
469    }
470
471    pub fn lock_target(&self) -> Option<FileLockTarget> {
472        self.lock_target
473    }
474
475    pub fn cursor(&self) -> u64 {
476        self.cursor.load(Ordering::SeqCst)
477    }
478
479    pub fn set_cursor(&self, cursor: u64) {
480        self.cursor.store(cursor, Ordering::SeqCst);
481    }
482
483    pub fn flags(&self) -> u32 {
484        self.flags.load(Ordering::SeqCst)
485    }
486
487    pub fn update_flags(&self, mask: u32, flags: u32) -> u32 {
488        let mut current = self.flags();
489        loop {
490            let next = (current & !mask) | (flags & mask);
491            match self
492                .flags
493                .compare_exchange(current, next, Ordering::SeqCst, Ordering::SeqCst)
494            {
495                Ok(_) => return next,
496                Err(observed) => current = observed,
497            }
498        }
499    }
500
501    pub fn ref_count(&self) -> usize {
502        self.ref_count.load(Ordering::SeqCst)
503    }
504
505    pub fn increment_ref_count(&self) -> usize {
506        self.ref_count.fetch_add(1, Ordering::SeqCst) + 1
507    }
508
509    pub fn decrement_ref_count(&self) -> usize {
510        let mut current = self.ref_count.load(Ordering::SeqCst);
511        loop {
512            let next = current.saturating_sub(1);
513            match self
514                .ref_count
515                .compare_exchange(current, next, Ordering::SeqCst, Ordering::SeqCst)
516            {
517                Ok(_) => return next,
518                Err(observed) => current = observed,
519            }
520        }
521    }
522}
523
524#[derive(Debug, Clone)]
525pub struct FdEntry {
526    pub fd: u32,
527    pub description: SharedFileDescription,
528    pub status_flags: u32,
529    pub fd_flags: u32,
530    pub rights: u64,
531    pub filetype: u8,
532}
533
534#[derive(Debug)]
535pub struct TransferredFd {
536    description: SharedFileDescription,
537    status_flags: u32,
538    rights: u64,
539    filetype: u8,
540}
541
542impl Clone for TransferredFd {
543    fn clone(&self) -> Self {
544        self.description.increment_ref_count();
545        Self {
546            description: Arc::clone(&self.description),
547            status_flags: self.status_flags,
548            rights: self.rights,
549            filetype: self.filetype,
550        }
551    }
552}
553
554impl PartialEq for TransferredFd {
555    fn eq(&self, other: &Self) -> bool {
556        self.description.id() == other.description.id()
557            && self.status_flags == other.status_flags
558            && self.rights == other.rights
559            && self.filetype == other.filetype
560    }
561}
562
563impl Eq for TransferredFd {}
564
565impl Drop for TransferredFd {
566    fn drop(&mut self) {
567        self.description.decrement_ref_count();
568    }
569}
570
571impl TransferredFd {
572    pub(crate) fn description(&self) -> SharedFileDescription {
573        Arc::clone(&self.description)
574    }
575
576    pub fn description_id(&self) -> u64 {
577        self.description.id()
578    }
579
580    pub fn status_flags(&self) -> u32 {
581        self.status_flags
582    }
583
584    pub fn rights(&self) -> u64 {
585        self.rights
586    }
587
588    pub fn filetype(&self) -> u8 {
589        self.filetype
590    }
591}
592
593#[derive(Debug, Clone, Copy, PartialEq, Eq)]
594pub struct FdStat {
595    pub filetype: u8,
596    pub flags: u32,
597    pub rights: u64,
598}
599
600#[derive(Debug, Clone)]
601pub struct StdioOverride {
602    pub description: SharedFileDescription,
603    pub filetype: u8,
604}
605
606#[derive(Debug, Clone)]
607struct DescriptionFactory {}
608
609impl DescriptionFactory {
610    fn new(_starting_id: u64) -> Self {
611        Self {}
612    }
613
614    fn allocate(&self, path: &str, flags: u32) -> SharedFileDescription {
615        self.allocate_with_lock(path, flags, None)
616    }
617
618    fn allocate_with_lock(
619        &self,
620        path: &str,
621        flags: u32,
622        lock_target: Option<FileLockTarget>,
623    ) -> SharedFileDescription {
624        let next_id = allocate_file_description_id();
625        Arc::new(FileDescription::new_with_lock(
626            next_id,
627            path,
628            flags,
629            lock_target,
630        ))
631    }
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
635pub struct FileLockTarget {
636    dev: u64,
637    ino: u64,
638}
639
640impl FileLockTarget {
641    pub const fn new(dev: u64, ino: u64) -> Self {
642        Self { dev, ino }
643    }
644
645    pub const fn dev(self) -> u64 {
646        self.dev
647    }
648
649    pub const fn ino(self) -> u64 {
650        self.ino
651    }
652}
653
654#[derive(Debug, Clone, Copy, PartialEq, Eq)]
655enum FileLockMode {
656    Shared,
657    Exclusive,
658}
659
660#[derive(Debug, Clone, Copy, PartialEq, Eq)]
661pub enum RecordLockType {
662    Read,
663    Write,
664    Unlock,
665}
666
667#[derive(Debug, Clone, Copy, PartialEq, Eq)]
668pub struct RecordLock {
669    pub lock_type: RecordLockType,
670    pub start: u64,
671    /// Exclusive end offset. `None` means through EOF, including future growth.
672    pub end: Option<u64>,
673    pub pid: u32,
674}
675
676impl RecordLock {
677    pub fn new(lock_type: RecordLockType, start: u64, length: u64, pid: u32) -> FdResult<Self> {
678        let end =
679            if length == 0 {
680                None
681            } else {
682                Some(start.checked_add(length).ok_or_else(|| {
683                    FdTableError::invalid_argument("record lock range exceeds u64")
684                })?)
685            };
686        Ok(Self {
687            lock_type,
688            start,
689            end,
690            pid,
691        })
692    }
693
694    pub fn length(self) -> u64 {
695        self.end.map_or(0, |end| end - self.start)
696    }
697}
698
699#[derive(Debug, Clone, Copy, PartialEq, Eq)]
700pub enum FlockOperation {
701    Shared { nonblocking: bool },
702    Exclusive { nonblocking: bool },
703    Unlock,
704}
705
706impl FlockOperation {
707    pub fn from_bits(operation: u32) -> FdResult<Self> {
708        let nonblocking = operation & LOCK_NB != 0;
709        match operation & !LOCK_NB {
710            LOCK_SH => Ok(Self::Shared { nonblocking }),
711            LOCK_EX => Ok(Self::Exclusive { nonblocking }),
712            LOCK_UN => Ok(Self::Unlock),
713            _ => Err(FdTableError::invalid_argument(format!(
714                "invalid flock operation {operation:#x}"
715            ))),
716        }
717    }
718}
719
720#[derive(Debug, Clone)]
721pub struct ProcessFdTable {
722    entries: BTreeMap<u32, FdEntry>,
723    next_fd: u32,
724    alloc_desc: DescriptionFactory,
725    max_fds: usize,
726}
727
728impl ProcessFdTable {
729    fn new(alloc_desc: DescriptionFactory, max_fds: usize) -> Self {
730        Self {
731            entries: BTreeMap::new(),
732            next_fd: 3,
733            alloc_desc,
734            max_fds,
735        }
736    }
737
738    pub fn max_fds(&self) -> usize {
739        self.max_fds
740    }
741
742    pub fn available_fd_capacity(&self) -> usize {
743        self.max_fds.saturating_sub(self.entries.len())
744    }
745
746    pub fn init_stdio(
747        &mut self,
748        stdin_desc: SharedFileDescription,
749        stdout_desc: SharedFileDescription,
750        stderr_desc: SharedFileDescription,
751    ) {
752        self.entries.insert(
753            0,
754            FdEntry {
755                fd: 0,
756                description: stdin_desc,
757                status_flags: 0,
758                fd_flags: 0,
759                rights: 0,
760                filetype: FILETYPE_CHARACTER_DEVICE,
761            },
762        );
763        self.entries.insert(
764            1,
765            FdEntry {
766                fd: 1,
767                description: stdout_desc,
768                status_flags: 0,
769                fd_flags: 0,
770                rights: 0,
771                filetype: FILETYPE_CHARACTER_DEVICE,
772            },
773        );
774        self.entries.insert(
775            2,
776            FdEntry {
777                fd: 2,
778                description: stderr_desc,
779                status_flags: 0,
780                fd_flags: 0,
781                rights: 0,
782                filetype: FILETYPE_CHARACTER_DEVICE,
783            },
784        );
785    }
786
787    pub fn init_stdio_with_types(
788        &mut self,
789        stdin_desc: SharedFileDescription,
790        stdin_type: u8,
791        stdout_desc: SharedFileDescription,
792        stdout_type: u8,
793        stderr_desc: SharedFileDescription,
794        stderr_type: u8,
795    ) {
796        stdin_desc.increment_ref_count();
797        stdout_desc.increment_ref_count();
798        stderr_desc.increment_ref_count();
799        self.entries.insert(
800            0,
801            FdEntry {
802                fd: 0,
803                description: stdin_desc,
804                status_flags: 0,
805                fd_flags: 0,
806                rights: 0,
807                filetype: stdin_type,
808            },
809        );
810        self.entries.insert(
811            1,
812            FdEntry {
813                fd: 1,
814                description: stdout_desc,
815                status_flags: 0,
816                fd_flags: 0,
817                rights: 0,
818                filetype: stdout_type,
819            },
820        );
821        self.entries.insert(
822            2,
823            FdEntry {
824                fd: 2,
825                description: stderr_desc,
826                status_flags: 0,
827                fd_flags: 0,
828                rights: 0,
829                filetype: stderr_type,
830            },
831        );
832    }
833
834    pub fn open(&mut self, path: &str, flags: u32) -> FdResult<u32> {
835        self.open_with_details(path, flags, FILETYPE_REGULAR_FILE, None)
836    }
837
838    pub fn open_with_filetype(&mut self, path: &str, flags: u32, filetype: u8) -> FdResult<u32> {
839        self.open_with_details(path, flags, filetype, None)
840    }
841
842    pub fn open_with_details(
843        &mut self,
844        path: &str,
845        flags: u32,
846        filetype: u8,
847        lock_target: Option<FileLockTarget>,
848    ) -> FdResult<u32> {
849        let fd = self.allocate_fd()?;
850        let description =
851            self.alloc_desc
852                .allocate_with_lock(path, description_flags(flags), lock_target);
853        self.entries.insert(
854            fd,
855            FdEntry {
856                fd,
857                description,
858                status_flags: status_flags(flags),
859                fd_flags: 0,
860                rights: 0,
861                filetype,
862            },
863        );
864        Ok(fd)
865    }
866
867    pub fn open_with(
868        &mut self,
869        description: SharedFileDescription,
870        filetype: u8,
871        target_fd: Option<u32>,
872    ) -> FdResult<u32> {
873        let entry_status_flags = status_flags(description.flags());
874        let fd = match target_fd {
875            Some(fd) => {
876                self.validate_fd_bounds(fd)?;
877                if self.entries.contains_key(&fd) {
878                    self.close(fd);
879                }
880                fd
881            }
882            None => self.allocate_fd()?,
883        };
884        description.increment_ref_count();
885        self.entries.insert(
886            fd,
887            FdEntry {
888                fd,
889                description,
890                status_flags: entry_status_flags,
891                fd_flags: 0,
892                rights: 0,
893                filetype,
894            },
895        );
896        Ok(fd)
897    }
898
899    pub fn open_pair_with_details(
900        &mut self,
901        first_path: &str,
902        second_path: &str,
903        status_flags: u32,
904        fd_flags: u32,
905        filetype: u8,
906    ) -> FdResult<(u32, u32, SharedFileDescription, SharedFileDescription)> {
907        if self.entries.len().saturating_add(2) > self.max_fds {
908            return Err(FdTableError::too_many_open_files());
909        }
910        let first_fd = self.allocate_fd()?;
911        let second_fd = match self.allocate_fd() {
912            Ok(fd) => fd,
913            Err(error) => {
914                self.next_fd = first_fd;
915                return Err(error);
916            }
917        };
918        let first = self.alloc_desc.allocate(first_path, O_RDWR);
919        let second = self.alloc_desc.allocate(second_path, O_RDWR);
920        for (fd, description) in [
921            (first_fd, Arc::clone(&first)),
922            (second_fd, Arc::clone(&second)),
923        ] {
924            self.entries.insert(
925                fd,
926                FdEntry {
927                    fd,
928                    description,
929                    status_flags: status_flags & ENTRY_STATUS_FLAG_MASK,
930                    fd_flags: fd_flags & FD_CLOEXEC,
931                    rights: 0,
932                    filetype,
933                },
934            );
935        }
936        Ok((first_fd, second_fd, first, second))
937    }
938
939    pub fn transfer(&self, fd: u32) -> FdResult<TransferredFd> {
940        let entry = self
941            .entries
942            .get(&fd)
943            .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?;
944        entry.description.increment_ref_count();
945        Ok(TransferredFd {
946            description: Arc::clone(&entry.description),
947            status_flags: entry.status_flags,
948            rights: entry.rights,
949            filetype: entry.filetype,
950        })
951    }
952
953    /// Create an open-file-description transfer without consuming an fd slot.
954    ///
955    /// SCM_RIGHTS queues descriptions, not temporary descriptors in the
956    /// sending process. Keeping this separate from `open_with_details` avoids
957    /// spuriously returning EMFILE when the sender's descriptor table is full.
958    pub fn create_transfer(&self, path: &str, flags: u32, filetype: u8) -> TransferredFd {
959        TransferredFd {
960            description: self.alloc_desc.allocate(path, description_flags(flags)),
961            status_flags: status_flags(flags),
962            rights: 0,
963            filetype,
964        }
965    }
966
967    pub fn install_transferred(
968        &mut self,
969        transfers: &[TransferredFd],
970        close_on_exec: bool,
971    ) -> FdResult<Vec<u32>> {
972        if self.entries.len().saturating_add(transfers.len()) > self.max_fds {
973            return Err(FdTableError::too_many_open_files());
974        }
975        let mut candidates = Vec::with_capacity(transfers.len());
976        let mut cursor = self.next_fd;
977        for _ in transfers {
978            let start = usize::try_from(cursor).unwrap_or(0) % self.max_fds;
979            let candidate = (0..self.max_fds)
980                .map(|offset| ((start + offset) % self.max_fds) as u32)
981                .find(|fd| !self.entries.contains_key(fd) && !candidates.contains(fd))
982                .ok_or_else(FdTableError::too_many_open_files)?;
983            candidates.push(candidate);
984            cursor = candidate.saturating_add(1);
985        }
986        for (fd, transfer) in candidates.iter().copied().zip(transfers) {
987            transfer.description.increment_ref_count();
988            self.entries.insert(
989                fd,
990                FdEntry {
991                    fd,
992                    description: Arc::clone(&transfer.description),
993                    status_flags: transfer.status_flags,
994                    fd_flags: if close_on_exec { FD_CLOEXEC } else { 0 },
995                    rights: transfer.rights,
996                    filetype: transfer.filetype,
997                },
998            );
999        }
1000        self.next_fd = cursor;
1001        Ok(candidates)
1002    }
1003
1004    /// Install a transferred open file description at an exact descriptor.
1005    /// This is used by spawn staging, where the post-file-action descriptor
1006    /// numbers are already canonical and must not be allocated a second time.
1007    pub(crate) fn install_transferred_at(
1008        &mut self,
1009        transfer: &TransferredFd,
1010        fd: u32,
1011        fd_flags: u32,
1012    ) -> FdResult<()> {
1013        self.validate_fd_bounds(fd)?;
1014        if self.entries.contains_key(&fd) {
1015            self.close(fd);
1016        }
1017        transfer.description.increment_ref_count();
1018        self.entries.insert(
1019            fd,
1020            FdEntry {
1021                fd,
1022                description: Arc::clone(&transfer.description),
1023                status_flags: transfer.status_flags,
1024                fd_flags: fd_flags & FD_CLOEXEC,
1025                rights: transfer.rights,
1026                filetype: transfer.filetype,
1027            },
1028        );
1029        Ok(())
1030    }
1031
1032    pub fn get(&self, fd: u32) -> Option<&FdEntry> {
1033        self.entries.get(&fd)
1034    }
1035
1036    pub fn values(&self) -> Values<'_, u32, FdEntry> {
1037        self.entries.values()
1038    }
1039
1040    pub fn close(&mut self, fd: u32) -> bool {
1041        let Some(entry) = self.entries.remove(&fd) else {
1042            return false;
1043        };
1044        entry.description.decrement_ref_count();
1045        true
1046    }
1047
1048    /// File descriptors that must be closed when the current process image is
1049    /// replaced by exec(2). The caller performs the closes through the kernel
1050    /// so pipe/socket lifecycle accounting receives the same notifications as
1051    /// an explicit close(2).
1052    pub fn close_on_exec_fds(&self) -> Vec<u32> {
1053        self.entries
1054            .iter()
1055            .filter_map(|(fd, entry)| (entry.fd_flags & FD_CLOEXEC != 0).then_some(*fd))
1056            .collect()
1057    }
1058
1059    pub fn dup(&mut self, fd: u32) -> FdResult<u32> {
1060        self.dup_with_status_flags(fd, None)
1061    }
1062
1063    pub fn dup_with_status_flags(
1064        &mut self,
1065        fd: u32,
1066        status_flags_override: Option<u32>,
1067    ) -> FdResult<u32> {
1068        let entry = self
1069            .entries
1070            .get(&fd)
1071            .cloned()
1072            .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?;
1073        let new_fd = self.allocate_fd()?;
1074        self.duplicate_entry(
1075            &entry,
1076            new_fd,
1077            status_flags_override.unwrap_or(entry.status_flags),
1078            0,
1079        )
1080    }
1081
1082    pub fn dup2(&mut self, old_fd: u32, new_fd: u32) -> FdResult<()> {
1083        let entry = self
1084            .entries
1085            .get(&old_fd)
1086            .cloned()
1087            .ok_or_else(|| FdTableError::bad_file_descriptor(old_fd))?;
1088        self.validate_fd_bounds(new_fd)?;
1089        if old_fd == new_fd {
1090            return Ok(());
1091        }
1092
1093        if self.entries.contains_key(&new_fd) {
1094            self.close(new_fd);
1095        }
1096
1097        self.duplicate_entry(&entry, new_fd, entry.status_flags, 0)?;
1098        Ok(())
1099    }
1100
1101    pub fn stat(&self, fd: u32) -> FdResult<FdStat> {
1102        let entry = self
1103            .entries
1104            .get(&fd)
1105            .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?;
1106        Ok(FdStat {
1107            filetype: entry.filetype,
1108            flags: visible_fd_flags(entry.description.flags(), entry.status_flags),
1109            rights: entry.rights,
1110        })
1111    }
1112
1113    pub fn fcntl(&mut self, fd: u32, command: u32, arg: u32) -> FdResult<u32> {
1114        match command {
1115            F_DUPFD => {
1116                let entry = self
1117                    .entries
1118                    .get(&fd)
1119                    .cloned()
1120                    .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?;
1121                let min_fd = self.validate_fcntl_dup_min(arg)?;
1122                let new_fd = self.allocate_fd_from(min_fd)?;
1123                self.duplicate_entry(&entry, new_fd, entry.status_flags, 0)
1124            }
1125            F_GETFD => {
1126                let entry = self
1127                    .entries
1128                    .get(&fd)
1129                    .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?;
1130                Ok(entry.fd_flags & FD_CLOEXEC)
1131            }
1132            F_SETFD => {
1133                let entry = self
1134                    .entries
1135                    .get_mut(&fd)
1136                    .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?;
1137                entry.fd_flags = arg & FD_CLOEXEC;
1138                Ok(0)
1139            }
1140            F_GETFL => {
1141                let entry = self
1142                    .entries
1143                    .get(&fd)
1144                    .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?;
1145                Ok(visible_fd_flags(
1146                    entry.description.flags(),
1147                    entry.status_flags,
1148                ))
1149            }
1150            F_SETFL => {
1151                let entry = self
1152                    .entries
1153                    .get_mut(&fd)
1154                    .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?;
1155                entry.status_flags = arg & ENTRY_STATUS_FLAG_MASK;
1156                entry.description.update_flags(SHARED_STATUS_FLAG_MASK, arg);
1157                Ok(0)
1158            }
1159            _ => Err(FdTableError::invalid_argument(format!(
1160                "unsupported fcntl command {command}"
1161            ))),
1162        }
1163    }
1164
1165    pub fn fork(&self) -> Self {
1166        self.fork_with_cloexec(false)
1167    }
1168
1169    /// Clone this descriptor table at the fork half of a deferred exec.
1170    ///
1171    /// Linux keeps `FD_CLOEXEC` descriptors across `fork(2)` so spawn file
1172    /// actions can use them as sources. The exec half closes any descriptors
1173    /// that remain marked after those actions have run.
1174    pub fn fork_preserving_cloexec(&self) -> Self {
1175        self.fork_with_cloexec(true)
1176    }
1177
1178    fn fork_with_cloexec(&self, preserve_cloexec: bool) -> Self {
1179        let mut child = Self::new(self.alloc_desc.clone(), self.max_fds);
1180        child.next_fd = self.next_fd;
1181
1182        for (fd, entry) in &self.entries {
1183            // Kernel process creation is spawn (fork + exec combined), so
1184            // close-on-exec descriptors must not leak into the child. This
1185            // matters for pipe write ends: an inherited writer keeps the
1186            // pipe's writer refcount above zero forever, so a blocked reader
1187            // (for example a grandchild sharing the parent's stdin pipe)
1188            // would never observe EOF.
1189            if !preserve_cloexec && entry.fd_flags & FD_CLOEXEC != 0 {
1190                continue;
1191            }
1192            entry.description.increment_ref_count();
1193            child.entries.insert(
1194                *fd,
1195                FdEntry {
1196                    fd: *fd,
1197                    description: Arc::clone(&entry.description),
1198                    status_flags: entry.status_flags,
1199                    fd_flags: entry.fd_flags,
1200                    rights: entry.rights,
1201                    filetype: entry.filetype,
1202                },
1203            );
1204        }
1205
1206        child
1207    }
1208
1209    pub fn len_for_exec(&self) -> usize {
1210        self.entries
1211            .values()
1212            .filter(|entry| entry.fd_flags & FD_CLOEXEC == 0)
1213            .count()
1214    }
1215
1216    pub fn close_all(&mut self) {
1217        let fds: Vec<u32> = self.entries.keys().copied().collect();
1218        for fd in fds {
1219            self.close(fd);
1220        }
1221    }
1222
1223    pub fn len(&self) -> usize {
1224        self.entries.len()
1225    }
1226
1227    pub fn is_empty(&self) -> bool {
1228        self.entries.is_empty()
1229    }
1230
1231    pub fn iter(&self) -> Values<'_, u32, FdEntry> {
1232        self.entries.values()
1233    }
1234
1235    fn allocate_fd(&mut self) -> FdResult<u32> {
1236        if self.entries.len() >= self.max_fds {
1237            return Err(FdTableError::too_many_open_files());
1238        }
1239
1240        let start = usize::try_from(self.next_fd).unwrap_or(0) % self.max_fds;
1241        for offset in 0..self.max_fds {
1242            let candidate = ((start + offset) % self.max_fds) as u32;
1243            if !self.entries.contains_key(&candidate) {
1244                self.next_fd = candidate.saturating_add(1);
1245                return Ok(candidate);
1246            }
1247        }
1248
1249        Err(FdTableError::too_many_open_files())
1250    }
1251
1252    fn allocate_fd_from(&mut self, min_fd: u32) -> FdResult<u32> {
1253        if self.entries.len() >= self.max_fds {
1254            return Err(FdTableError::too_many_open_files());
1255        }
1256
1257        if min_fd as usize >= self.max_fds {
1258            return Err(FdTableError::invalid_argument(format!(
1259                "fd {min_fd} exceeds process fd limit"
1260            )));
1261        }
1262
1263        for candidate in min_fd..self.max_fds as u32 {
1264            if !self.entries.contains_key(&candidate) {
1265                self.next_fd = candidate.saturating_add(1);
1266                return Ok(candidate);
1267            }
1268        }
1269
1270        Err(FdTableError::too_many_open_files())
1271    }
1272
1273    fn duplicate_entry(
1274        &mut self,
1275        entry: &FdEntry,
1276        new_fd: u32,
1277        status_flags: u32,
1278        fd_flags: u32,
1279    ) -> FdResult<u32> {
1280        entry.description.increment_ref_count();
1281        self.entries.insert(
1282            new_fd,
1283            FdEntry {
1284                fd: new_fd,
1285                description: Arc::clone(&entry.description),
1286                status_flags,
1287                fd_flags,
1288                rights: entry.rights,
1289                filetype: entry.filetype,
1290            },
1291        );
1292        Ok(new_fd)
1293    }
1294
1295    fn validate_fd_bounds(&self, fd: u32) -> FdResult<()> {
1296        if fd as usize >= self.max_fds {
1297            return Err(FdTableError::bad_file_descriptor(fd));
1298        }
1299        Ok(())
1300    }
1301
1302    fn validate_fcntl_dup_min(&self, min_fd: u32) -> FdResult<u32> {
1303        if min_fd as usize >= self.max_fds {
1304            return Err(FdTableError::invalid_argument(format!(
1305                "fd {min_fd} exceeds process fd limit"
1306            )));
1307        }
1308        Ok(min_fd)
1309    }
1310}
1311
1312fn description_flags(flags: u32) -> u32 {
1313    flags & !status_flags(flags)
1314}
1315
1316fn status_flags(flags: u32) -> u32 {
1317    flags & ENTRY_STATUS_FLAG_MASK
1318}
1319
1320fn visible_fd_flags(description_flags: u32, entry_status_flags: u32) -> u32 {
1321    (description_flags & (0b11 | SHARED_STATUS_FLAG_MASK))
1322        | (entry_status_flags & ENTRY_STATUS_FLAG_MASK)
1323}
1324
1325const SHARED_STATUS_FLAG_MASK: u32 = O_APPEND;
1326const ENTRY_STATUS_FLAG_MASK: u32 = O_NONBLOCK;
1327
1328impl<'a> IntoIterator for &'a ProcessFdTable {
1329    type Item = &'a FdEntry;
1330    type IntoIter = Values<'a, u32, FdEntry>;
1331
1332    fn into_iter(self) -> Self::IntoIter {
1333        self.entries.values()
1334    }
1335}
1336
1337#[derive(Debug, Clone)]
1338pub struct FdTableManager {
1339    tables: BTreeMap<u32, ProcessFdTable>,
1340    alloc_desc: DescriptionFactory,
1341    max_fds: usize,
1342}
1343
1344impl Default for FdTableManager {
1345    fn default() -> Self {
1346        Self {
1347            tables: BTreeMap::new(),
1348            alloc_desc: DescriptionFactory::new(1),
1349            max_fds: MAX_FDS_PER_PROCESS,
1350        }
1351    }
1352}
1353
1354impl FdTableManager {
1355    pub fn new() -> Self {
1356        Self::default()
1357    }
1358
1359    pub fn with_max_fds(max_fds: usize) -> Self {
1360        Self {
1361            max_fds,
1362            ..Self::default()
1363        }
1364    }
1365
1366    pub fn create(&mut self, pid: u32) -> &mut ProcessFdTable {
1367        let mut table = ProcessFdTable::new(self.alloc_desc.clone(), self.max_fds);
1368        table.init_stdio(
1369            self.alloc_desc.allocate("/dev/stdin", O_RDONLY),
1370            self.alloc_desc.allocate("/dev/stdout", O_WRONLY),
1371            self.alloc_desc.allocate("/dev/stderr", O_WRONLY),
1372        );
1373        self.remove(pid);
1374        self.tables.insert(pid, table);
1375        self.tables
1376            .get_mut(&pid)
1377            .expect("newly created FD table should be stored")
1378    }
1379
1380    pub fn create_with_stdio(
1381        &mut self,
1382        pid: u32,
1383        stdin_override: Option<StdioOverride>,
1384        stdout_override: Option<StdioOverride>,
1385        stderr_override: Option<StdioOverride>,
1386    ) -> &mut ProcessFdTable {
1387        let mut table = ProcessFdTable::new(self.alloc_desc.clone(), self.max_fds);
1388        let stdin_desc = stdin_override
1389            .as_ref()
1390            .map(|entry| Arc::clone(&entry.description))
1391            .unwrap_or_else(|| self.alloc_desc.allocate("/dev/stdin", O_RDONLY));
1392        let stdout_desc = stdout_override
1393            .as_ref()
1394            .map(|entry| Arc::clone(&entry.description))
1395            .unwrap_or_else(|| self.alloc_desc.allocate("/dev/stdout", O_WRONLY));
1396        let stderr_desc = stderr_override
1397            .as_ref()
1398            .map(|entry| Arc::clone(&entry.description))
1399            .unwrap_or_else(|| self.alloc_desc.allocate("/dev/stderr", O_WRONLY));
1400
1401        table.init_stdio_with_types(
1402            stdin_desc,
1403            stdin_override
1404                .as_ref()
1405                .map(|entry| entry.filetype)
1406                .unwrap_or(FILETYPE_CHARACTER_DEVICE),
1407            stdout_desc,
1408            stdout_override
1409                .as_ref()
1410                .map(|entry| entry.filetype)
1411                .unwrap_or(FILETYPE_CHARACTER_DEVICE),
1412            stderr_desc,
1413            stderr_override
1414                .as_ref()
1415                .map(|entry| entry.filetype)
1416                .unwrap_or(FILETYPE_CHARACTER_DEVICE),
1417        );
1418        self.remove(pid);
1419        self.tables.insert(pid, table);
1420        self.tables
1421            .get_mut(&pid)
1422            .expect("newly created FD table should be stored")
1423    }
1424
1425    pub fn fork(&mut self, parent_pid: u32, child_pid: u32) -> &mut ProcessFdTable {
1426        self.fork_with_cloexec(parent_pid, child_pid, false)
1427    }
1428
1429    pub fn fork_preserving_cloexec(
1430        &mut self,
1431        parent_pid: u32,
1432        child_pid: u32,
1433    ) -> &mut ProcessFdTable {
1434        self.fork_with_cloexec(parent_pid, child_pid, true)
1435    }
1436
1437    fn fork_with_cloexec(
1438        &mut self,
1439        parent_pid: u32,
1440        child_pid: u32,
1441        preserve_cloexec: bool,
1442    ) -> &mut ProcessFdTable {
1443        if !self.tables.contains_key(&parent_pid) {
1444            return self.create(child_pid);
1445        }
1446
1447        let parent = self
1448            .tables
1449            .get(&parent_pid)
1450            .expect("parent table presence was checked");
1451        let child = if preserve_cloexec {
1452            parent.fork_preserving_cloexec()
1453        } else {
1454            parent.fork()
1455        };
1456        self.remove(child_pid);
1457        self.tables.insert(child_pid, child);
1458        self.tables
1459            .get_mut(&child_pid)
1460            .expect("forked FD table should be stored")
1461    }
1462
1463    pub fn get(&self, pid: u32) -> Option<&ProcessFdTable> {
1464        self.tables.get(&pid)
1465    }
1466
1467    pub fn get_mut(&mut self, pid: u32) -> Option<&mut ProcessFdTable> {
1468        self.tables.get_mut(&pid)
1469    }
1470
1471    pub fn has(&self, pid: u32) -> bool {
1472        self.tables.contains_key(&pid)
1473    }
1474
1475    pub fn len(&self) -> usize {
1476        self.tables.len()
1477    }
1478
1479    pub fn is_empty(&self) -> bool {
1480        self.tables.is_empty()
1481    }
1482
1483    pub fn total_open_fds(&self) -> usize {
1484        self.tables.values().map(ProcessFdTable::len).sum()
1485    }
1486
1487    pub fn pids(&self) -> Vec<u32> {
1488        self.tables.keys().copied().collect()
1489    }
1490
1491    pub fn remove(&mut self, pid: u32) {
1492        if let Some(mut table) = self.tables.remove(&pid) {
1493            table.close_all();
1494        }
1495    }
1496}
1497
1498#[derive(Debug, Clone, Default)]
1499pub struct FileLockManager {
1500    inner: Arc<FileLockManagerInner>,
1501}
1502
1503#[derive(Debug)]
1504struct FileLockManagerInner {
1505    state: Mutex<FileLockState>,
1506    wake: Condvar,
1507    max_record_locks: usize,
1508}
1509
1510impl Default for FileLockManagerInner {
1511    fn default() -> Self {
1512        Self {
1513            state: Mutex::new(FileLockState::default()),
1514            wake: Condvar::new(),
1515            max_record_locks: DEFAULT_MAX_RECORD_LOCKS,
1516        }
1517    }
1518}
1519
1520#[derive(Debug, Default)]
1521struct FileLockState {
1522    entries: BTreeMap<FileLockTarget, FileLockEntry>,
1523    record_locks: Vec<(FileLockTarget, RecordLock)>,
1524    record_lock_waits: BTreeMap<u32, RecordLockWait>,
1525    warned_near_record_lock_limit: bool,
1526    warned_near_record_lock_wait_limit: bool,
1527}
1528
1529#[derive(Debug, Clone)]
1530struct RecordLockWait {
1531    target: FileLockTarget,
1532    request: RecordLock,
1533    blockers: BTreeSet<u32>,
1534}
1535
1536#[derive(Debug, Default)]
1537struct FileLockEntry {
1538    shared: BTreeSet<u64>,
1539    exclusive: Option<u64>,
1540}
1541
1542impl FileLockManager {
1543    pub fn new() -> Self {
1544        Self::with_record_lock_limit(DEFAULT_MAX_RECORD_LOCKS)
1545    }
1546
1547    pub fn with_record_lock_limit(max_record_locks: usize) -> Self {
1548        Self {
1549            inner: Arc::new(FileLockManagerInner {
1550                state: Mutex::new(FileLockState::default()),
1551                wake: Condvar::new(),
1552                max_record_locks: max_record_locks.max(1),
1553            }),
1554        }
1555    }
1556
1557    pub fn apply(
1558        &self,
1559        owner_id: u64,
1560        target: FileLockTarget,
1561        operation: FlockOperation,
1562    ) -> FdResult<()> {
1563        match operation {
1564            FlockOperation::Shared { nonblocking } => {
1565                self.acquire(owner_id, target, FileLockMode::Shared, nonblocking)
1566            }
1567            FlockOperation::Exclusive { nonblocking } => {
1568                self.acquire(owner_id, target, FileLockMode::Exclusive, nonblocking)
1569            }
1570            FlockOperation::Unlock => {
1571                self.release_owner(owner_id);
1572                Ok(())
1573            }
1574        }
1575    }
1576
1577    pub fn release_owner(&self, owner_id: u64) -> bool {
1578        let mut state = lock_or_recover(&self.inner.state);
1579        let mut released = false;
1580        state.entries.retain(|_, entry| {
1581            let entry_changed = entry.shared.remove(&owner_id) || entry.exclusive == Some(owner_id);
1582            if entry.exclusive == Some(owner_id) {
1583                entry.exclusive = None;
1584            }
1585            released |= entry_changed;
1586            !entry.is_empty()
1587        });
1588        drop(state);
1589        if released {
1590            self.inner.wake.notify_all();
1591        }
1592        released
1593    }
1594
1595    pub fn query_record_lock(
1596        &self,
1597        target: FileLockTarget,
1598        request: RecordLock,
1599    ) -> Option<RecordLock> {
1600        let state = lock_or_recover(&self.inner.state);
1601        state
1602            .record_locks
1603            .iter()
1604            .filter_map(|(candidate_target, candidate)| {
1605                (*candidate_target == target
1606                    && candidate.pid != request.pid
1607                    && record_locks_conflict(*candidate, request))
1608                .then_some(*candidate)
1609            })
1610            .min_by_key(|candidate| (candidate.start, candidate.pid))
1611    }
1612
1613    pub fn set_record_lock(&self, target: FileLockTarget, request: RecordLock) -> FdResult<()> {
1614        self.set_record_lock_inner(target, request, false)
1615    }
1616
1617    pub fn set_blocking_record_lock(
1618        &self,
1619        target: FileLockTarget,
1620        request: RecordLock,
1621    ) -> FdResult<()> {
1622        self.set_record_lock_inner(target, request, true)
1623    }
1624
1625    fn set_record_lock_inner(
1626        &self,
1627        target: FileLockTarget,
1628        request: RecordLock,
1629        blocking: bool,
1630    ) -> FdResult<()> {
1631        let mut state = lock_or_recover(&self.inner.state);
1632        let blockers = if request.lock_type == RecordLockType::Unlock {
1633            BTreeSet::new()
1634        } else {
1635            conflicting_record_lock_pids(&state.record_locks, target, request)
1636        };
1637        if !blockers.is_empty() {
1638            if blocking {
1639                self.register_record_lock_wait(&mut state, target, request, blockers)?;
1640            } else {
1641                state.record_lock_waits.remove(&request.pid);
1642            }
1643            return Err(FdTableError::would_block(
1644                "POSIX record lock is held by another process",
1645            ));
1646        }
1647
1648        state.record_lock_waits.remove(&request.pid);
1649
1650        let mut next = Vec::with_capacity(state.record_locks.len().saturating_add(2));
1651        for (candidate_target, candidate) in state.record_locks.iter().copied() {
1652            if candidate_target != target
1653                || candidate.pid != request.pid
1654                || !record_ranges_overlap(candidate, request)
1655            {
1656                next.push((candidate_target, candidate));
1657                continue;
1658            }
1659
1660            if candidate.start < request.start {
1661                next.push((
1662                    target,
1663                    RecordLock {
1664                        end: Some(request.start),
1665                        ..candidate
1666                    },
1667                ));
1668            }
1669            if let Some(request_end) = request.end {
1670                if candidate
1671                    .end
1672                    .is_none_or(|candidate_end| candidate_end > request_end)
1673                {
1674                    next.push((
1675                        target,
1676                        RecordLock {
1677                            start: request_end,
1678                            ..candidate
1679                        },
1680                    ));
1681                }
1682            }
1683        }
1684
1685        if request.lock_type != RecordLockType::Unlock {
1686            next.push((target, request));
1687        }
1688        coalesce_record_locks(&mut next);
1689        let warning_threshold = (self.inner.max_record_locks.saturating_mul(4) / 5).max(1);
1690        if !state.warned_near_record_lock_limit && next.len() >= warning_threshold {
1691            state.warned_near_record_lock_limit = true;
1692            eprintln!(
1693                "[agentos] POSIX record lock usage {}/{} is near the limit derived from limits.resources.maxOpenFds; raise limits.resources.maxOpenFds if needed",
1694                next.len(), self.inner.max_record_locks
1695            );
1696        }
1697        if next.len() > self.inner.max_record_locks {
1698            return Err(FdTableError {
1699                code: "ENOLCK",
1700                message: format!(
1701                    "POSIX record lock table limit ({}) derived from limits.resources.maxOpenFds reached; raise limits.resources.maxOpenFds if needed",
1702                    self.inner.max_record_locks
1703                ),
1704            });
1705        }
1706        state.record_locks = next;
1707        refresh_record_lock_waits(&mut state);
1708        drop(state);
1709        self.inner.wake.notify_all();
1710        Ok(())
1711    }
1712
1713    pub fn cancel_record_lock_wait(&self, pid: u32) -> bool {
1714        let mut state = lock_or_recover(&self.inner.state);
1715        state.record_lock_waits.remove(&pid).is_some()
1716    }
1717
1718    /// POSIX process-associated locks are all discarded when the process
1719    /// closes any descriptor referring to the same file.
1720    pub fn release_process_target(&self, pid: u32, target: FileLockTarget) -> bool {
1721        let mut state = lock_or_recover(&self.inner.state);
1722        let previous = state.record_locks.len();
1723        state
1724            .record_locks
1725            .retain(|(candidate_target, lock)| *candidate_target != target || lock.pid != pid);
1726        let wait_cancelled = state.record_lock_waits.remove(&pid).is_some();
1727        let released = state.record_locks.len() != previous;
1728        if released || wait_cancelled {
1729            refresh_record_lock_waits(&mut state);
1730        }
1731        drop(state);
1732        if released || wait_cancelled {
1733            self.inner.wake.notify_all();
1734        }
1735        released || wait_cancelled
1736    }
1737
1738    pub fn release_process(&self, pid: u32) -> bool {
1739        let mut state = lock_or_recover(&self.inner.state);
1740        let previous = state.record_locks.len();
1741        state.record_locks.retain(|(_, lock)| lock.pid != pid);
1742        let wait_cancelled = state.record_lock_waits.remove(&pid).is_some();
1743        let released = state.record_locks.len() != previous;
1744        if released || wait_cancelled {
1745            refresh_record_lock_waits(&mut state);
1746        }
1747        drop(state);
1748        if released || wait_cancelled {
1749            self.inner.wake.notify_all();
1750        }
1751        released || wait_cancelled
1752    }
1753
1754    fn register_record_lock_wait(
1755        &self,
1756        state: &mut FileLockState,
1757        target: FileLockTarget,
1758        request: RecordLock,
1759        blockers: BTreeSet<u32>,
1760    ) -> FdResult<()> {
1761        let new_waiter = !state.record_lock_waits.contains_key(&request.pid);
1762        let waiter_count = state.record_lock_waits.len() + usize::from(new_waiter);
1763        if new_waiter && waiter_count > self.inner.max_record_locks {
1764            return Err(FdTableError {
1765                code: "ENOLCK",
1766                message: format!(
1767                    "POSIX record lock waiter limit ({}) derived from limits.resources.maxOpenFds reached; raise limits.resources.maxOpenFds if needed",
1768                    self.inner.max_record_locks
1769                ),
1770            });
1771        }
1772        let warning_threshold = (self.inner.max_record_locks.saturating_mul(4) / 5).max(1);
1773        if !state.warned_near_record_lock_wait_limit && waiter_count >= warning_threshold {
1774            state.warned_near_record_lock_wait_limit = true;
1775            eprintln!(
1776                "[agentos] POSIX record lock waiter usage {}/{} is near the limit derived from limits.resources.maxOpenFds; raise limits.resources.maxOpenFds if needed",
1777                waiter_count,
1778                self.inner.max_record_locks
1779            );
1780        }
1781        state.record_lock_waits.insert(
1782            request.pid,
1783            RecordLockWait {
1784                target,
1785                request,
1786                blockers,
1787            },
1788        );
1789        if record_lock_wait_cycle(state, request.pid) {
1790            state.record_lock_waits.remove(&request.pid);
1791            return Err(FdTableError::deadlock(
1792                "POSIX record lock wait would create a deadlock",
1793            ));
1794        }
1795        Ok(())
1796    }
1797
1798    fn acquire(
1799        &self,
1800        owner_id: u64,
1801        target: FileLockTarget,
1802        mode: FileLockMode,
1803        nonblocking: bool,
1804    ) -> FdResult<()> {
1805        let mut state = lock_or_recover(&self.inner.state);
1806        loop {
1807            let entry = state.entries.entry(target).or_default();
1808            if entry.can_grant(owner_id, mode) {
1809                entry.grant(owner_id, mode);
1810                return Ok(());
1811            }
1812
1813            if nonblocking {
1814                return Err(FdTableError::would_block(
1815                    "advisory file lock is unavailable",
1816                ));
1817            }
1818
1819            state = wait_or_recover(&self.inner.wake, state);
1820        }
1821    }
1822}
1823
1824fn record_ranges_overlap(left: RecordLock, right: RecordLock) -> bool {
1825    left.end.is_none_or(|end| right.start < end) && right.end.is_none_or(|end| left.start < end)
1826}
1827
1828fn record_locks_conflict(left: RecordLock, right: RecordLock) -> bool {
1829    record_ranges_overlap(left, right)
1830        && (left.lock_type == RecordLockType::Write || right.lock_type == RecordLockType::Write)
1831}
1832
1833fn conflicting_record_lock_pids(
1834    locks: &[(FileLockTarget, RecordLock)],
1835    target: FileLockTarget,
1836    request: RecordLock,
1837) -> BTreeSet<u32> {
1838    locks
1839        .iter()
1840        .filter_map(|(candidate_target, candidate)| {
1841            (*candidate_target == target
1842                && candidate.pid != request.pid
1843                && record_locks_conflict(*candidate, request))
1844            .then_some(candidate.pid)
1845        })
1846        .collect()
1847}
1848
1849fn refresh_record_lock_waits(state: &mut FileLockState) {
1850    let FileLockState {
1851        record_locks,
1852        record_lock_waits,
1853        ..
1854    } = state;
1855    for wait in record_lock_waits.values_mut() {
1856        wait.blockers = conflicting_record_lock_pids(record_locks, wait.target, wait.request);
1857    }
1858}
1859
1860fn record_lock_wait_cycle(state: &FileLockState, requester: u32) -> bool {
1861    let Some(wait) = state.record_lock_waits.get(&requester) else {
1862        return false;
1863    };
1864    let mut pending = wait.blockers.iter().copied().collect::<Vec<_>>();
1865    let mut visited = BTreeSet::new();
1866    while let Some(pid) = pending.pop() {
1867        if pid == requester {
1868            return true;
1869        }
1870        if !visited.insert(pid) {
1871            continue;
1872        }
1873        if let Some(wait) = state.record_lock_waits.get(&pid) {
1874            pending.extend(wait.blockers.iter().copied());
1875        }
1876    }
1877    false
1878}
1879
1880fn coalesce_record_locks(locks: &mut Vec<(FileLockTarget, RecordLock)>) {
1881    locks.sort_by_key(|(target, lock)| (*target, lock.pid, lock.lock_type as u8, lock.start));
1882    let mut merged: Vec<(FileLockTarget, RecordLock)> = Vec::with_capacity(locks.len());
1883    for (target, lock) in locks.drain(..) {
1884        if let Some((previous_target, previous)) = merged.last_mut() {
1885            let touches = previous.end.is_none_or(|end| lock.start <= end);
1886            if *previous_target == target
1887                && previous.pid == lock.pid
1888                && previous.lock_type == lock.lock_type
1889                && touches
1890            {
1891                previous.end = match (previous.end, lock.end) {
1892                    (None, _) | (_, None) => None,
1893                    (Some(left), Some(right)) => Some(left.max(right)),
1894                };
1895                continue;
1896            }
1897        }
1898        merged.push((target, lock));
1899    }
1900    *locks = merged;
1901}
1902
1903impl FileLockEntry {
1904    fn can_grant(&self, owner_id: u64, mode: FileLockMode) -> bool {
1905        match mode {
1906            FileLockMode::Shared => self.exclusive.is_none_or(|owner| owner == owner_id),
1907            FileLockMode::Exclusive => {
1908                self.exclusive.is_none_or(|owner| owner == owner_id)
1909                    && self.shared.iter().all(|owner| *owner == owner_id)
1910            }
1911        }
1912    }
1913
1914    fn grant(&mut self, owner_id: u64, mode: FileLockMode) {
1915        match mode {
1916            FileLockMode::Shared => {
1917                self.exclusive = None;
1918                self.shared.insert(owner_id);
1919            }
1920            FileLockMode::Exclusive => {
1921                self.shared.retain(|owner| *owner != owner_id);
1922                self.exclusive = Some(owner_id);
1923            }
1924        }
1925    }
1926
1927    fn is_empty(&self) -> bool {
1928        self.exclusive.is_none() && self.shared.is_empty()
1929    }
1930}
1931
1932fn lock_or_recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
1933    mutex
1934        .lock()
1935        .unwrap_or_else(|poisoned| poisoned.into_inner())
1936}
1937
1938fn wait_or_recover<'a, T>(condvar: &Condvar, guard: MutexGuard<'a, T>) -> MutexGuard<'a, T> {
1939    condvar
1940        .wait(guard)
1941        .unwrap_or_else(|poisoned| poisoned.into_inner())
1942}