From 7eaf4ddb11fcbd268641bf051367002226dbe04e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 3 Apr 2026 09:12:42 -0700 Subject: [PATCH 1/4] Foundation types: platform traits, event extensions, sync changes, AnyMap clone support Layer 0 of the core litebox crate changes for multi-process support: Platform traits: - AddressSpaceProvider trait for per-process address spaces - RawMessageProvider for direct guest-broker byte channels - StdioProvider expansion (terminal attrs, window size, nonblocking stdin, cancel, host TTY device info) - SendError::Io, ReceiveError::ProtocolError/Eof, StdioReadError::WouldBlock - SystemInfoProvider::current_processor_number() - Provider supertrait: + RawMessageProvider + AddressSpaceProvider Event/sync: - IOPollable::needs_host_poll(), should_block_read() defaults - Observer::prune_dead_observers() to GC stale Weak refs on register - Waker::ptr_eq() utility - FutexManager wait/wake: address_space_id parameter for multi-process - trace_fs feature gate for RawSyncPrimitivesProvider Utilities: - AnyMap: Clone bound on insert, type-erased Clone impl - PageManagementProvider::allocate_pages: noreserve parameter - copy_pages: fix chunk-length bug (was using old_range.len() per chunk) Caller updates across all platform crates and litebox_shim_linux. --- litebox/Cargo.toml | 1 + litebox/src/event/mod.rs | 14 ++ litebox/src/event/observer.rs | 64 +++++ litebox/src/event/wait.rs | 5 + litebox/src/fd/mod.rs | 4 +- litebox/src/fs/devices.rs | 1 + litebox/src/mm/linux.rs | 1 + litebox/src/mm/tests.rs | 1 + litebox/src/net/phy.rs | 6 +- litebox/src/platform/address_space.rs | 151 +++++++++++ litebox/src/platform/mock.rs | 58 ++++- litebox/src/platform/mod.rs | 280 ++++++++++++++++++++- litebox/src/platform/page_mgmt.rs | 7 +- litebox/src/sync/futex.rs | 56 ++++- litebox/src/sync/mod.rs | 16 +- litebox/src/utilities/anymap.rs | 39 ++- litebox_platform_linux_kernel/src/lib.rs | 7 + litebox_platform_linux_userland/src/lib.rs | 12 + litebox_platform_lvbs/src/lib.rs | 9 + litebox_shim_linux/src/lib.rs | 2 + litebox_shim_linux/src/syscalls/net.rs | 4 +- litebox_shim_linux/src/syscalls/process.rs | 4 +- 22 files changed, 712 insertions(+), 30 deletions(-) create mode 100644 litebox/src/platform/address_space.rs diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index 9a8b2e401..994e1ea06 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -33,6 +33,7 @@ windows-sys = { version = "0.60.2", features = [ lock_tracing = ["dep:arrayvec", "spin/mutex"] panic_on_unclosed_fd_drop = [] enforce_singleton_litebox_instance = [] +trace_fs = [] [lints] workspace = true diff --git a/litebox/src/event/mod.rs b/litebox/src/event/mod.rs index 24d5b6832..ce6f2688a 100644 --- a/litebox/src/event/mod.rs +++ b/litebox/src/event/mod.rs @@ -48,6 +48,20 @@ pub trait IOPollable { /// calls are what notify observers. This particular function itself however _may_ be used to /// essentially get "the current status" of events for the system. fn check_io_events(&self) -> Events; + + /// Returns `true` if this pollable cannot deliver asynchronous observer + /// notifications (e.g. host-backed stdin). Callers should use periodic + /// polling instead of blocking indefinitely on observer wakeups. + fn needs_host_poll(&self) -> bool { + false + } + + /// Returns `true` if reads on this pollable should block when no data is + /// available. Returns `false` for fds whose callers use epoll/poll and + /// expect EAGAIN to be returned immediately (e.g. PTY master). + fn should_block_read(&self) -> bool { + true + } } impl IOPollable for alloc::sync::Arc { diff --git a/litebox/src/event/observer.rs b/litebox/src/event/observer.rs index 4e42a304e..33b19c9d4 100644 --- a/litebox/src/event/observer.rs +++ b/litebox/src/event/observer.rs @@ -93,9 +93,21 @@ impl, Platform: RawSyncPrimitivesProvider> Subject, F>) { + observers.retain(|observer, _| { + if observer.upgrade().is_some() { + true + } else { + self.nums.fetch_sub(1, Ordering::Relaxed); + false + } + }); + } + /// Register an observer with the given filter. pub fn register_observer(&self, observer: Weak>, filter: F) { let mut observers = self.observers.lock(); + self.prune_dead_observers(&mut observers); if observers .insert(ObserverKey::new(observer), filter) .is_none() @@ -132,3 +144,55 @@ impl, Platform: RawSyncPrimitivesProvider> Subject for TestObserver { + fn on_events(&self, _events: &Events) { + self.notifications.fetch_add(1, Ordering::Relaxed); + } + } + + #[test] + fn register_observer_prunes_dead_entries() { + let subject = Subject::::new(); + + let stale = Arc::new(TestObserver { + notifications: AtomicUsize::new(0), + }); + subject.register_observer(Arc::downgrade(&stale) as _, Events::IN); + assert_eq!(subject.nums.load(Ordering::Relaxed), 1); + assert_eq!(subject.observers.lock().len(), 1); + drop(stale); + + let fresh = Arc::new(TestObserver { + notifications: AtomicUsize::new(0), + }); + subject.register_observer(Arc::downgrade(&fresh) as _, Events::OUT); + { + let observers = subject.observers.lock(); + let registered = observers + .keys() + .next() + .and_then(super::ObserverKey::upgrade) + .expect("dead observer should be pruned during registration"); + let fresh_observer: Arc> = fresh.clone(); + assert!(Arc::ptr_eq(®istered, &fresh_observer)); + assert_eq!(subject.nums.load(Ordering::Relaxed), 1); + assert_eq!(observers.len(), 1); + } + subject.notify_observers(Events::OUT); + + assert_eq!(fresh.notifications.load(Ordering::Relaxed), 1); + } +} diff --git a/litebox/src/event/wait.rs b/litebox/src/event/wait.rs index eb879c363..a2bdd1b17 100644 --- a/litebox/src/event/wait.rs +++ b/litebox/src/event/wait.rs @@ -73,6 +73,11 @@ impl Waker { pub fn wake(&self) { self.0.wake(); } + + /// Returns `true` if both wakers refer to the same thread's wait state. + pub fn ptr_eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } } impl WaitState { diff --git a/litebox/src/fd/mod.rs b/litebox/src/fd/mod.rs index aeef757dd..d93da35b1 100644 --- a/litebox/src/fd/mod.rs +++ b/litebox/src/fd/mod.rs @@ -477,7 +477,7 @@ impl Descriptors { ) -> Option where Subsystem: FdEnabledSubsystem, - T: core::any::Any + Send + Sync, + T: core::any::Any + Clone + Send + Sync, { self.entries[fd.x.as_usize()?] .as_ref() @@ -506,7 +506,7 @@ impl Descriptors { ) -> Option where Subsystem: FdEnabledSubsystem, - T: core::any::Any + Send + Sync, + T: core::any::Any + Clone + Send + Sync, { self.entries[fd.x.as_usize()?] .as_mut() diff --git a/litebox/src/fs/devices.rs b/litebox/src/fs/devices.rs index 90e715230..9d58272b7 100644 --- a/litebox/src/fs/devices.rs +++ b/litebox/src/fs/devices.rs @@ -254,6 +254,7 @@ impl< .read_from_stdin(buf) .map_err(|e| match e { StdioReadError::Closed => unimplemented!(), + StdioReadError::WouldBlock => unimplemented!(), }) } diff --git a/litebox/src/mm/linux.rs b/litebox/src/mm/linux.rs index f33094971..389bf9694 100644 --- a/litebox/src/mm/linux.rs +++ b/litebox/src/mm/linux.rs @@ -509,6 +509,7 @@ impl + 'static, const ALIGN: usize> Vmem MemoryRegionPermissions::from_bits(permissions).unwrap(), vma.flags.contains(VmFlags::VM_GROWSDOWN), populate_pages_immediately, + false, platform_fixed_address_behavior, ) .map_err(|err| match err { diff --git a/litebox/src/mm/tests.rs b/litebox/src/mm/tests.rs index 3142971ef..a6e9f772e 100644 --- a/litebox/src/mm/tests.rs +++ b/litebox/src/mm/tests.rs @@ -43,6 +43,7 @@ impl crate::platform::PageManagementProvider for DummyVmemBackend { initial_permissions: crate::platform::page_mgmt::MemoryRegionPermissions, can_grow_down: bool, populate_pages_immediately: bool, + _noreserve: bool, fixed_address_behavior: crate::platform::page_mgmt::FixedAddressBehavior, ) -> Result, crate::platform::page_mgmt::AllocationError> { Ok(TransparentMutPtr::from_usize(suggested_range.start)) diff --git a/litebox/src/net/phy.rs b/litebox/src/net/phy.rs index ff4d85c6e..e8ebc4beb 100644 --- a/litebox/src/net/phy.rs +++ b/litebox/src/net/phy.rs @@ -51,7 +51,11 @@ impl smoltcp::phy::Device for Device None, + Err( + platform::ReceiveError::WouldBlock + | platform::ReceiveError::ProtocolError + | platform::ReceiveError::Eof, + ) => None, } } diff --git a/litebox/src/platform/address_space.rs b/litebox/src/platform/address_space.rs new file mode 100644 index 000000000..adb88915a --- /dev/null +++ b/litebox/src/platform/address_space.rs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Address-space management types and traits for multi-process support. +//! +//! The [`AddressSpaceProvider`] trait is an **optional** South interface that +//! platforms implement to manage per-process address spaces. On kernel platforms +//! (e.g., LVBS) each process gets its own page table; on userland platforms the +//! single host address space is partitioned into non-overlapping VA sub-ranges. +//! +//! See the multi-process design document at +//! `docs/multi-process-single-address-space.md` §4 for the full +//! rationale. + +use core::ops::Range; +use thiserror::Error; + +/// The result of forking an address space. +/// +/// The variant tells the caller what kind of copy was created so it can adjust +/// its behavior (e.g., whether to copy page contents or share them). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ForkedAddressSpace { + /// Kernel platforms: independent copy-on-write copy with the full address + /// range. The child has its own page tables; CoW faults are resolved by + /// the platform. + Independent(Id), + /// Userland platforms: a new VA-range partition is assigned to the child. + /// Parent memory is shared (vfork-style semantics); the shim is + /// responsible for copying pages as needed. + SharedWithParent(Id), +} + +/// Errors that can occur during address-space operations. +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum AddressSpaceError { + /// No free address-space slots or VA ranges available. + #[error("no address space slots available")] + NoSpace, + /// The given address-space ID is not valid (already destroyed, never + /// created, etc.). + #[error("invalid address space id")] + InvalidId, + /// The platform does not support this operation. + #[error("operation not supported by this platform")] + NotSupported, +} + +/// A provider for managing per-process address spaces. +/// +/// This is an **optional** trait — platforms that do not yet support +/// multi-process may leave all methods at the default (which returns +/// [`AddressSpaceError::NotSupported`]). +/// +/// # Associated Type +/// +/// `AddressSpaceId` is an opaque, lightweight handle that identifies one +/// address space. It must be `Copy + Eq + Send + Sync` so it can be stored +/// inside `ProcessContext` and passed across threads. +pub trait AddressSpaceProvider { + /// Opaque identifier for an address space. + type AddressSpaceId: Copy + Eq + Send + Sync + core::fmt::Debug; + + /// Create a new, empty address space. + /// + /// * Kernel platforms: allocate a top-level page table (e.g., PML4) and + /// copy kernel entries. + /// * Userland platforms: claim a free VA-range partition. + fn create_address_space(&self) -> Result { + Err(AddressSpaceError::NotSupported) + } + + /// Destroy an address space, releasing all associated resources. + /// + /// After this call, `id` is invalid and must not be reused. + fn destroy_address_space(&self, id: Self::AddressSpaceId) -> Result<(), AddressSpaceError> { + let _ = id; + Err(AddressSpaceError::NotSupported) + } + + /// Fork an address space from `parent`. + /// + /// Returns a [`ForkedAddressSpace`] indicating what kind of fork was + /// performed: + /// + /// * [`Independent`](ForkedAddressSpace::Independent) — kernel: full CoW + /// copy. + /// * [`SharedWithParent`](ForkedAddressSpace::SharedWithParent) — userland: + /// new VA partition, parent pages shared. + fn fork_address_space( + &self, + parent: Self::AddressSpaceId, + ) -> Result, AddressSpaceError> { + let _ = parent; + Err(AddressSpaceError::NotSupported) + } + + /// Make `id` the active address space for the current CPU / thread. + /// + /// * Kernel: switch CR3 (or equivalent). + /// * Userland: typically a no-op. + fn activate_address_space(&self, id: Self::AddressSpaceId) -> Result<(), AddressSpaceError> { + let _ = id; + Err(AddressSpaceError::NotSupported) + } + + /// Execute `f` with the given address space active, then restore the + /// previously active address space. + /// + /// On kernel platforms implementations **must** restore the prior address + /// space even if `f` panics (use a guard / RAII pattern). Userland + /// platforms may keep this as a thin wrapper. + /// + /// The default returns [`AddressSpaceError::NotSupported`]. Platforms that + /// implement [`activate_address_space`](Self::activate_address_space) should + /// also override this method with a proper save/restore sequence. + fn with_address_space( + &self, + id: Self::AddressSpaceId, + f: impl FnOnce() -> R, + ) -> Result { + let _ = (id, f); + Err(AddressSpaceError::NotSupported) + } + + /// Whether the platform requires eager copy-on-write snapshots during + /// vfork instead of lazy page-fault-driven CoW. + /// + /// When `true`, the shim eagerly copies all writable guest pages before + /// spawning the vfork child and restores them after the child execs or + /// exits. When `false` (the default), the shim marks writable pages + /// read-only and lazily snapshots individual pages on first write fault. + /// + /// Platforms where the exception/fault handler shares the guest address + /// space (e.g., Windows userland) must set this to `true` because a + /// CoW fault inside the handler itself would be fatal. + const EAGER_COW_FOR_VFORK: bool = false; + + /// Return the VA range available to the given address space. + /// + /// * Kernel: the full `TASK_ADDR_MIN..TASK_ADDR_MAX`. + /// * Userland: the sub-range partition assigned to this address space. + fn address_space_range( + &self, + id: Self::AddressSpaceId, + ) -> Result, AddressSpaceError> { + let _ = id; + Err(AddressSpaceError::NotSupported) + } +} diff --git a/litebox/src/platform/mock.rs b/litebox/src/platform/mock.rs index 3a3297aa6..cf1d60f7f 100644 --- a/litebox/src/platform/mock.rs +++ b/litebox/src/platform/mock.rs @@ -59,6 +59,14 @@ impl MockPlatform { impl Provider for MockPlatform {} +impl RawMessageProvider for MockPlatform {} + +impl AddressSpaceProvider for MockPlatform { + // All methods default to `Err(NotSupported)`, which is correct for the + // mock platform (single-process only). + type AddressSpaceId = u32; +} + pub(crate) struct MockRawMutex { inner: AtomicU32, internal_state: std::sync::RwLock, @@ -210,7 +218,7 @@ impl IPInterfaceProvider for MockPlatform { #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct MockInstant { - time: u64, + pub(crate) time: u64, } impl Instant for MockInstant { @@ -230,7 +238,7 @@ impl Instant for MockInstant { } pub(crate) struct MockSystemTime { - time: u64, + pub(crate) time: u64, } impl SystemTime for MockSystemTime { @@ -290,6 +298,9 @@ impl RawPointerProvider for MockPlatform { impl StdioProvider for MockPlatform { fn read_from_stdin(&self, buf: &mut [u8]) -> Result { + if buf.is_empty() { + return Ok(0); + } let Some(front) = self.stdin_queue.write().unwrap().pop_front() else { return Err(StdioReadError::Closed); }; @@ -318,6 +329,26 @@ impl StdioProvider for MockPlatform { fn is_a_tty(&self, _stream: StdioStream) -> bool { false } + + fn get_terminal_input_bytes(&self, stream: StdioStream) -> Result { + match stream { + StdioStream::Stdin => { + let len = self + .stdin_queue + .read() + .unwrap() + .iter() + .map(std::vec::Vec::len) + .sum::(); + Ok(u32::try_from(len).unwrap_or(u32::MAX)) + } + StdioStream::Stdout | StdioStream::Stderr => Err(StdioIoctlError::NotATerminal), + } + } + + fn poll_stdin_readable(&self) -> bool { + self.stdin_queue.read().unwrap().front().is_some() + } } impl CrngProvider for MockPlatform { @@ -333,6 +364,29 @@ impl CrngProvider for MockPlatform { } } +#[cfg(test)] +mod tests { + use super::{MockPlatform, StdioProvider}; + + #[test] + fn nonblocking_stdin_reads_queued_input() { + let platform = MockPlatform::new(); + platform + .stdin_queue + .write() + .unwrap() + .push_back(b"ready".to_vec()); + + let mut buf = [0u8; 8]; + let read = platform + .read_from_stdin_nonblocking(&mut buf) + .expect("queued stdin should not block"); + + assert_eq!(read, 5); + assert_eq!(&buf[..read], b"ready"); + } +} + std::thread_local! { static MOCK_TLS: core::cell::Cell<*mut()> = const { core::cell::Cell::new(core::ptr::null_mut()) }; } diff --git a/litebox/src/platform/mod.rs b/litebox/src/platform/mod.rs index 983f20c84..b87d8937d 100644 --- a/litebox/src/platform/mod.rs +++ b/litebox/src/platform/mod.rs @@ -7,6 +7,7 @@ //! trait is merely a collection of subtraits that could be composed independently from various //! other crates that implement them upon various types. +pub mod address_space; pub mod common_providers; pub mod page_mgmt; pub mod trivial_providers; @@ -18,16 +19,19 @@ use either::Either; use thiserror::Error; use zerocopy::{FromBytes, IntoBytes}; +pub use address_space::AddressSpaceProvider; pub use page_mgmt::PageManagementProvider; #[macro_export] macro_rules! log_println { ($platform:expr, $s:expr) => {{ + #[allow(unused_imports)] use $crate::platform::DebugLogProvider as _; $platform.debug_log_print($s); }}; ($platform:expr, $($tt:tt)*) => {{ use core::fmt::Write as _; + #[allow(unused_imports)] use $crate::platform::DebugLogProvider as _; let mut t: arrayvec::ArrayString<8192> = arrayvec::ArrayString::new(); writeln!(t, $($tt)*).unwrap(); @@ -43,10 +47,12 @@ macro_rules! log_println { pub trait Provider: RawMutexProvider + IPInterfaceProvider + + RawMessageProvider + TimeProvider + PunchthroughProvider + DebugLogProvider + RawPointerProvider + + AddressSpaceProvider { } @@ -385,7 +391,11 @@ pub trait IPInterfaceProvider { /// A non-exhaustive list of errors that can be thrown by [`IPInterfaceProvider::send_ip_packet`]. #[derive(Error, Debug)] #[non_exhaustive] -pub enum SendError {} +pub enum SendError { + /// The underlying device returned an I/O error. The packet was not sent. + #[error("I/O error on send: errno {0}")] + Io(i32), +} /// A non-exhaustive list of errors that can be thrown by [`IPInterfaceProvider::receive_ip_packet`]. #[derive(Error, Debug)] @@ -393,6 +403,36 @@ pub enum SendError {} pub enum ReceiveError { #[error("Receive operation would block")] WouldBlock, + #[error("IPC protocol error: oversized frame")] + ProtocolError, + #[error("Channel closed (EOF)")] + Eof, +} + +/// A raw byte-stream channel for direct message passing between the guest and +/// the broker (bypassing the IP network stack). +/// +/// When available, this provides a fast path for protocols like 9P that would +/// otherwise pay the overhead of traversing two smoltcp stacks. +/// +/// The default implementation returns [`ReceiveError::WouldBlock`] / +/// [`SendError::Io`], indicating the channel is not available. Platforms that +/// support direct messaging override these methods. +pub trait RawMessageProvider { + /// Send bytes to the broker over the raw channel. + /// + /// Returns `Ok(n)` with the number of bytes sent, or an error. + fn send_raw_message(&self, _data: &[u8]) -> Result { + Err(SendError::Io(0)) + } + + /// Receive bytes from the broker over the raw channel. + /// + /// Returns `Ok(n)` with the number of bytes read into `buf`, or + /// [`ReceiveError::WouldBlock`] if no data is available yet. + fn recv_raw_message(&self, _buf: &mut [u8]) -> Result { + Err(ReceiveError::WouldBlock) + } } /// An interface to understanding time. @@ -611,6 +651,8 @@ where pub enum StdioReadError { #[error("input stream has been closed")] Closed, + #[error("input would block")] + WouldBlock, } /// A non-exhaustive list of errors that can be thrown by [`StdioProvider::write_to`]. @@ -641,16 +683,244 @@ pub enum StdioStream { Stderr = 2, } +/// A non-exhaustive list of errors from terminal operations on [`StdioProvider`]. +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum StdioIoctlError { + /// The fd is not a terminal (ENOTTY). + #[error("not a terminal")] + NotATerminal, + /// The operation failed with an OS error code (errno on Linux, mapped + /// equivalent on other platforms). + #[error("ioctl failed: {0}")] + OsError(i32), +} + +/// Platform-agnostic terminal attributes, mirroring the fields of Linux +/// `struct termios`. +/// +/// The guest always runs Linux binaries and speaks the Linux terminal ABI. +/// Platform implementations fill this struct using their native APIs (e.g., +/// direct ioctl forwarding on Linux, `GetConsoleMode`/`SetConsoleMode` on +/// Windows). +#[derive(Debug, Clone)] +pub struct TerminalAttributes { + /// Input mode flags. + pub c_iflag: u32, + /// Output mode flags. + pub c_oflag: u32, + /// Control mode flags. + pub c_cflag: u32, + /// Local mode flags. + pub c_lflag: u32, + /// Line discipline (typically `0` for `N_TTY`). + pub c_line: u8, + /// Control characters. + pub c_cc: [u8; 19], +} + +// Terminal attribute flag constants. +const TERMATTR_ECHO: u32 = 0x0008; +const TERMATTR_ICRNL: u32 = 0x0100; +const TERMATTR_OPOST: u32 = 0x0001; +const TERMATTR_ONLCR: u32 = 0x0004; + +impl TerminalAttributes { + /// Default terminal attributes matching a freshly opened Linux PTY. + /// + /// These are realistic values that satisfy terminal detection in programs + /// such as Node.js Ink. **All-zero termios causes such programs to reject + /// the terminal silently.** + pub fn new_default() -> Self { + Self { + c_iflag: 0x6d02, // ICRNL | IXON | IXANY | IMAXBEL | IUTF8 + c_oflag: 0x0005, // OPOST | ONLCR + c_cflag: 0x04bf, // CS8 | CREAD | CLOCAL | B38400 + c_lflag: 0x8a3b, // ECHO | ECHOE | ECHOK | ISIG | ICANON | IEXTEN | ECHOCTL | ECHOKE + c_line: 0, // N_TTY + c_cc: [ + 0x03, 0x1c, 0x7f, 0x15, 0x04, 0x00, 0x01, 0x00, 0x11, 0x13, 0x1a, 0xff, 0x12, 0x0f, + 0x17, 0x16, 0xff, 0x00, 0x00, + ], + } + } + + /// Returns `true` if the `ECHO` local flag is set. + pub fn echo_enabled(&self) -> bool { + self.c_lflag & TERMATTR_ECHO != 0 + } + + /// Returns `true` if the `ICRNL` input flag is set. + pub fn icrnl_enabled(&self) -> bool { + self.c_iflag & TERMATTR_ICRNL != 0 + } + + /// Returns `true` if output post-processing with newline translation + /// (`OPOST | ONLCR`) is enabled. + pub fn onlcr_enabled(&self) -> bool { + (self.c_oflag & TERMATTR_OPOST != 0) && (self.c_oflag & TERMATTR_ONLCR != 0) + } +} + +/// Platform-agnostic terminal window size. +#[derive(Debug, Clone, Copy)] +pub struct WindowSize { + /// Number of rows (height in characters). + pub rows: u16, + /// Number of columns (width in characters). + pub cols: u16, + /// Horizontal size in pixels (informational, often zero). + pub xpixel: u16, + /// Vertical size in pixels (informational, often zero). + pub ypixel: u16, +} + +/// When to apply terminal attribute changes, corresponding to POSIX +/// `tcsetattr()` actions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SetTermiosWhen { + /// Apply immediately (Linux `TCSETS`). + Now, + /// Drain output first, then apply (Linux `TCSETSW`). + AfterDrain, + /// Drain output first, flush pending input, then apply (Linux `TCSETSF`). + AfterDrainFlushInput, +} + /// A provider of standard input/output functionality. pub trait StdioProvider { /// Read from standard input. Returns number of bytes read. fn read_from_stdin(&self, buf: &mut [u8]) -> Result; + /// Read from standard input without blocking. + /// + /// Platforms with exact nonblocking stdin support should override this + /// instead of emulating it with a separate readiness probe. + fn read_from_stdin_nonblocking(&self, buf: &mut [u8]) -> Result { + if buf.is_empty() { + return Ok(0); + } + if !self.poll_stdin_readable() { + return Err(StdioReadError::WouldBlock); + } + self.read_from_stdin(buf) + } + /// Write to stdout/stderr. Returns number of bytes written. fn write_to(&self, stream: StdioOutStream, buf: &[u8]) -> Result; /// Check if a stream is connected to a TTY. fn is_a_tty(&self, stream: StdioStream) -> bool; + + /// Get the terminal attributes for a stdio stream. + /// + /// On Linux, this forwards `TCGETS` to the host kernel. On Windows, this + /// returns stored attributes (initialized with realistic defaults). + /// + /// The default implementation returns [`StdioIoctlError::NotATerminal`]. + fn get_terminal_attributes( + &self, + _stream: StdioStream, + ) -> Result { + Err(StdioIoctlError::NotATerminal) + } + + /// Set the terminal attributes for a stdio stream. + /// + /// On Linux, this forwards `TCSETS`/`TCSETSW`/`TCSETSF` to the host + /// kernel. On Windows, this stores the attributes and translates key flags + /// (e.g., `ECHO`, `ICANON`) to `SetConsoleMode` calls. + /// + /// The default implementation returns [`StdioIoctlError::NotATerminal`]. + fn set_terminal_attributes( + &self, + _stream: StdioStream, + _attrs: &TerminalAttributes, + _when: SetTermiosWhen, + ) -> Result<(), StdioIoctlError> { + Err(StdioIoctlError::NotATerminal) + } + + /// Get the terminal window size for a stdio stream. + /// + /// On Linux, this forwards `TIOCGWINSZ` to the host kernel. On Windows, + /// this queries `GetConsoleScreenBufferInfo` or returns a stored override. + /// + /// The default implementation returns [`StdioIoctlError::NotATerminal`]. + fn get_window_size(&self, _stream: StdioStream) -> Result { + Err(StdioIoctlError::NotATerminal) + } + + /// Get the number of input bytes currently readable from a terminal stream. + /// + /// On Linux, this forwards `FIONREAD` to the host kernel. Platforms that + /// do not support terminal input-queue queries may return + /// [`StdioIoctlError::NotATerminal`]. + fn get_terminal_input_bytes(&self, _stream: StdioStream) -> Result { + Err(StdioIoctlError::NotATerminal) + } + + /// Set the terminal window size for a stdio stream. + /// + /// On Linux, this forwards `TIOCSWINSZ` to the host kernel. On other + /// platforms, this stores the size so that subsequent `get_window_size` + /// calls return the stored value (the actual console is not resized). + /// + /// The default implementation returns [`StdioIoctlError::NotATerminal`]. + fn set_window_size( + &self, + _stream: StdioStream, + _size: &WindowSize, + ) -> Result<(), StdioIoctlError> { + Err(StdioIoctlError::NotATerminal) + } + + /// Check if stdin has data available for reading without blocking. + /// + /// Returns `true` if a `read()` on stdin would return data immediately. + /// Used by epoll/poll to report stdin readability. The default returns + /// `false`. + fn poll_stdin_readable(&self) -> bool { + false + } + + /// Cancel any pending `read_from_stdin()` call, causing it to return + /// [`StdioReadError::Closed`]. Used during process exit to unblock + /// threads waiting on stdin. The default is a no-op. + fn cancel_stdin(&self) {} + + /// Returns the host terminal device identity for stdin, if it is + /// connected to a real terminal (e.g., a PTY slave like `/dev/pts/156`). + /// + /// Used to report correct device info in guest-visible `fstat()` and + /// `readlink("/proc/self/fd/0")`, so that runtimes like Bun/libuv can + /// discover and reopen the controlling terminal by its actual device path. + /// + /// The returned `st_dev`, `st_ino`, and `st_rdev` must match what the + /// host kernel returns for `stat(path)` on the device path, because + /// glibc `ttyname_r` verifies all three fields via `is_mytty()`. + /// + /// Returns `None` when stdin is not a terminal (pipes, files) or on + /// platforms that do not expose PTY device paths (Windows). + fn host_stdin_tty_device_info(&self) -> Option { + None + } +} + +/// Host terminal device identity, returned by +/// [`StdioProvider::host_stdin_tty_device_info`]. +#[derive(Debug, Clone)] +pub struct HostTtyDeviceInfo { + /// Device path on the host, e.g., `/dev/pts/156`. + pub path: alloc::string::String, + /// `st_rdev` from `fstat()` on the host stdin fd, encoding the + /// major/minor device numbers (e.g., `0x889c` for major 136, minor 156). + pub rdev: u64, + /// `st_dev` from `fstat()` on the host stdin fd (devpts superblock + /// device number). + pub dev: u64, + /// `st_ino` from `fstat()` on the host stdin fd (inode within devpts). + pub ino: u64, } /// A provider for system information. @@ -666,6 +936,14 @@ pub trait SystemInfoProvider { /// Return `Some(address)` if the VDSO is available on the platform, or `None` /// if the platform does not support or provide a VDSO. fn get_vdso_address(&self) -> Option; + + /// Returns the current processor number exposed to guest compatibility features. + /// + /// Platforms that do not expose a stable processor identifier, or that + /// virtualize CPU topology, may return `0`. + fn current_processor_number(&self) -> u32 { + 0 + } } /// A provider for thread-local storage. diff --git a/litebox/src/platform/page_mgmt.rs b/litebox/src/platform/page_mgmt.rs index c4fca057a..21f8fe197 100644 --- a/litebox/src/platform/page_mgmt.rs +++ b/litebox/src/platform/page_mgmt.rs @@ -49,6 +49,8 @@ pub trait PageManagementProvider: RawPointerProvider { /// a page fault. /// - `populate_pages_immediately`: If `true`, the pages are populated immediately; otherwise, /// they are populated lazily. + /// - `noreserve`: If `true`, request a sparse reservation that avoids reserving swap/commit + /// upfront when the platform supports it. /// - `fixed_address_behavior`: Specifies the required semantics of `suggested_range`. /// /// # Returns @@ -64,6 +66,7 @@ pub trait PageManagementProvider: RawPointerProvider { initial_permissions: MemoryRegionPermissions, can_grow_down: bool, populate_pages_immediately: bool, + noreserve: bool, fixed_address_behavior: FixedAddressBehavior, ) -> Result, AllocationError>; @@ -108,6 +111,7 @@ pub trait PageManagementProvider: RawPointerProvider { temp_permissions, false, true, + false, FixedAddressBehavior::NoReplace, ) .map_err(|e| match e { @@ -135,12 +139,13 @@ pub trait PageManagementProvider: RawPointerProvider { let total_len = old_range.len(); let mut offset = 0; while offset < total_len { + let chunk_len = (total_len - offset).min(ALIGN); let old_ptr = ::RawConstPointer::from_usize(old_range.start + offset); new_ptr .write_slice_at_offset( isize::try_from(offset).unwrap(), - &old_ptr.to_owned_slice(old_range.len()).unwrap(), + &old_ptr.to_owned_slice(chunk_len).unwrap(), ) .unwrap(); offset += ALIGN; diff --git a/litebox/src/sync/futex.rs b/litebox/src/sync/futex.rs index 7785b116f..bfaf8651d 100644 --- a/litebox/src/sync/futex.rs +++ b/litebox/src/sync/futex.rs @@ -25,8 +25,13 @@ use thiserror::Error; /// A manager of all available futexes. /// -/// Note: currently, this only supports "private" futexes, since it assumes only a single process. -/// In the future, this may be expanded to support multi-process futexes. +/// Supports both private and shared futexes. On userland platforms where all +/// processes share a single flat virtual address space (with non-overlapping VA +/// partitions), both private and shared futexes naturally work with +/// `address_space_id = 0` because the same virtual address always refers to +/// the same backing memory. On kernel platforms, the `address_space_id` +/// discriminator prevents false aliasing between processes that might have +/// overlapping virtual address ranges. pub struct FutexManager { /// Chaining hash table to map from futex address to waiter lists. table: alloc::boxed::Box<[LoanList>; HASH_TABLE_ENTRIES]>, @@ -41,6 +46,10 @@ const HASH_TABLE_ENTRIES: usize = 256; struct FutexEntry { addr: usize, + /// Discriminator to prevent false aliasing across address spaces. + /// On userland (non-overlapping VA partitions) this is always 0. + /// On kernel platforms each process passes its AddressSpaceId as u64. + address_space_id: u64, waker: Waker, bitset: u32, done: AtomicBool, @@ -62,9 +71,16 @@ impl } } - /// Returns the hash table bucket for the given futex address. - fn bucket(&self, addr: usize) -> &LoanList> { - let hash: usize = self.hash_builder.hash_one(addr).truncate(); + /// Returns the hash table bucket for the given futex key. + fn bucket( + &self, + addr: usize, + address_space_id: u64, + ) -> &LoanList> { + let hash: usize = self + .hash_builder + .hash_one((addr, address_space_id)) + .truncate(); &self.table[hash % HASH_TABLE_ENTRIES] } @@ -80,12 +96,18 @@ impl /// If `bitset` is `Some`, then the waiter is only woken if the wake call's /// `bitset` has a non-zero intersection with the waiter's mask. Specifying /// `None` is equivalent to setting all bits in the mask. + /// + /// `address_space_id` is an opaque discriminator that distinguishes futexes + /// at the same virtual address in different address spaces. On userland + /// platforms (non-overlapping VA partitions) pass `0`. On kernel platforms + /// pass the process's `AddressSpaceId` converted to `u64`. pub fn wait( &self, cx: &WaitContext<'_, Platform>, futex_addr: Platform::RawMutPointer, expected_value: u32, bitset: Option, + address_space_id: u64, ) -> Result<(), FutexError> { let bitset = bitset.unwrap_or(ALL_BITS).get(); let addr = futex_addr.as_usize(); @@ -93,9 +115,10 @@ impl return Err(FutexError::NotAligned); } - let bucket = self.bucket(addr); + let bucket = self.bucket(addr, address_space_id); let mut entry = pin!(LoanListEntry::new(FutexEntry { addr, + address_space_id, waker: cx.waker().clone(), bitset, done: AtomicBool::new(false), @@ -131,12 +154,16 @@ impl /// (subject to the `num_to_wake` limit). If `bitset` is `None`, then all /// waiters are eligible to be woken. /// + /// `address_space_id` must match the value passed to the corresponding + /// [`wait`](Self::wait) call. + /// /// Returns the number of waiters that were woken up. pub fn wake( &self, futex_addr: Platform::RawMutPointer, num_to_wake_up: NonZeroU32, bitset: Option, + address_space_id: u64, ) -> Result { let addr = futex_addr.as_usize(); if !addr.is_multiple_of(align_of::()) { @@ -144,10 +171,13 @@ impl } let bitset = bitset.unwrap_or(ALL_BITS).get(); let mut woken = 0; - let bucket = self.bucket(addr); + let bucket = self.bucket(addr, address_space_id); // Extract matching entries from the bucket until we've woken enough. let entries = bucket.extract_if(|entry| { - if entry.addr != addr || entry.bitset & bitset == 0 { + if entry.addr != addr + || entry.address_space_id != address_space_id + || entry.bitset & bitset == 0 + { return core::ops::ControlFlow::Continue(false); } woken += 1; @@ -218,7 +248,7 @@ mod tests { barrier_clone.wait(); // Sync with main thread // Wait for value 0 - futex_manager_clone.wait(&WaitState::new(platform).context(), futex_addr, 0, None) + futex_manager_clone.wait(&WaitState::new(platform).context(), futex_addr, 0, None, 0) }); barrier.wait(); // Wait for waiter to be ready @@ -231,7 +261,7 @@ mod tests { futex_word.as_ptr() as usize, ); let woken = futex_manager - .wake(futex_addr, NonZeroU32::new(1).unwrap(), None) + .wake(futex_addr, NonZeroU32::new(1).unwrap(), None, 0) .unwrap(); // Wait for waiter thread to complete @@ -270,6 +300,7 @@ mod tests { futex_addr, 0, None, + 0, ) }); @@ -283,7 +314,7 @@ mod tests { futex_word.as_ptr() as usize, ); let woken = futex_manager - .wake(futex_addr, NonZeroU32::new(1).unwrap(), None) + .wake(futex_addr, NonZeroU32::new(1).unwrap(), None, 0) .unwrap(); // Wait for waiter thread to complete @@ -324,6 +355,7 @@ mod tests { futex_addr, 0, None, + 0, ) }); waiters.push(waiter); @@ -339,7 +371,7 @@ mod tests { futex_word.as_ptr() as usize, ); let woken = futex_manager - .wake(futex_addr, NonZeroU32::new(u32::MAX).unwrap(), None) + .wake(futex_addr, NonZeroU32::new(u32::MAX).unwrap(), None, 0) .unwrap(); // Wait for all waiter threads to complete diff --git a/litebox/src/sync/mod.rs b/litebox/src/sync/mod.rs index 0778d6d15..9e3324eb8 100644 --- a/litebox/src/sync/mod.rs +++ b/litebox/src/sync/mod.rs @@ -29,15 +29,27 @@ pub use rwlock::{ MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard, RwLockWriteGuard, }; -#[cfg(not(feature = "lock_tracing"))] +#[cfg(not(any(feature = "lock_tracing", feature = "trace_fs")))] /// A convenience name for specific requirements from the platform pub trait RawSyncPrimitivesProvider: platform::RawMutexProvider + Sync + 'static {} -#[cfg(not(feature = "lock_tracing"))] +#[cfg(not(any(feature = "lock_tracing", feature = "trace_fs")))] impl RawSyncPrimitivesProvider for Platform where Platform: platform::RawMutexProvider + Sync + 'static { } +#[cfg(all(feature = "trace_fs", not(feature = "lock_tracing")))] +/// A convenience name for specific requirements from the platform +pub trait RawSyncPrimitivesProvider: + platform::RawMutexProvider + platform::DebugLogProvider + Sync + 'static +{ +} +#[cfg(all(feature = "trace_fs", not(feature = "lock_tracing")))] +impl RawSyncPrimitivesProvider for Platform where + Platform: platform::RawMutexProvider + platform::DebugLogProvider + Sync + 'static +{ +} + #[cfg(feature = "lock_tracing")] /// A convenience name for specific requirements from the platform pub trait RawSyncPrimitivesProvider: diff --git a/litebox/src/utilities/anymap.rs b/litebox/src/utilities/anymap.rs index 68af3f8a6..5eb4b21c6 100644 --- a/litebox/src/utilities/anymap.rs +++ b/litebox/src/utilities/anymap.rs @@ -18,14 +18,25 @@ use alloc::boxed::Box; use core::any::{Any, TypeId}; use hashbrown::HashMap; +/// Type-erased clone function stored alongside each value. +type CloneFn = fn(&(dyn Any + Send + Sync)) -> Box; + /// A safe store of exactly one value of any type `T`. pub(crate) struct AnyMap { // Invariant: the value at a particular typeid is guaranteed to be the correct type boxed up. - storage: HashMap>, + storage: HashMap, CloneFn)>, } const GUARANTEED: &str = "guaranteed correct type by invariant"; +/// Create a clone function for a specific concrete type. +fn make_clone_fn() -> CloneFn { + |val: &(dyn Any + Send + Sync)| -> Box { + let concrete = val.downcast_ref::().expect(GUARANTEED); + Box::new(concrete.clone()) + } +} + impl AnyMap { /// Create a new empty `AnyMap` pub(crate) fn new() -> Self { @@ -35,20 +46,22 @@ impl AnyMap { } /// Insert `v`, replacing and returning the old value if one existed already. - pub(crate) fn insert(&mut self, v: T) -> Option { - let old = self.storage.insert(TypeId::of::(), Box::new(v))?; - Some(*old.downcast().expect(GUARANTEED)) + pub(crate) fn insert(&mut self, v: T) -> Option { + let old = self + .storage + .insert(TypeId::of::(), (Box::new(v), make_clone_fn::()))?; + Some(*old.0.downcast().expect(GUARANTEED)) } /// Get a reference to a value of type `T` if it exists. pub(crate) fn get(&self) -> Option<&T> { - let v = self.storage.get(&TypeId::of::())?; + let v = &self.storage.get(&TypeId::of::())?.0; Some(v.downcast_ref().expect(GUARANTEED)) } /// Get a mutable reference to a value of type `T` if it exists. pub(crate) fn get_mut(&mut self) -> Option<&mut T> { - let v = self.storage.get_mut(&TypeId::of::())?; + let v = &mut self.storage.get_mut(&TypeId::of::())?.0; Some(v.downcast_mut().expect(GUARANTEED)) } @@ -58,7 +71,19 @@ impl AnyMap { )] /// Remove and return the value of type `T` if it exists. pub(crate) fn remove(&mut self) -> Option { - let v = self.storage.remove(&TypeId::of::())?; + let v = self.storage.remove(&TypeId::of::())?.0; Some(*v.downcast().expect(GUARANTEED)) } } + +impl Clone for AnyMap { + fn clone(&self) -> Self { + Self { + storage: self + .storage + .iter() + .map(|(&type_id, (val, clone_fn))| (type_id, (clone_fn(val.as_ref()), *clone_fn))) + .collect(), + } + } +} diff --git a/litebox_platform_linux_kernel/src/lib.rs b/litebox_platform_linux_kernel/src/lib.rs index 12f6bc2d1..b069ac663 100644 --- a/litebox_platform_linux_kernel/src/lib.rs +++ b/litebox_platform_linux_kernel/src/lib.rs @@ -85,6 +85,12 @@ impl<'a, Host: HostInterface> PunchthroughToken for LinuxPunchthroughToken<'a, H impl Provider for LinuxKernel {} +impl litebox::platform::RawMessageProvider for LinuxKernel {} + +impl litebox::platform::AddressSpaceProvider for LinuxKernel { + type AddressSpaceId = u32; +} + // TODO: implement pointer validation to ensure the pointers are in user space. type UserConstPtr = litebox::platform::common_providers::userspace_pointers::UserConstPtr< litebox::platform::common_providers::userspace_pointers::NoValidation, @@ -424,6 +430,7 @@ impl PageManagementProvider for initial_permissions: litebox::platform::page_mgmt::MemoryRegionPermissions, can_grow_down: bool, populate_pages_immediately: bool, + _noreserve: bool, fixed_address_behavior: FixedAddressBehavior, ) -> Result, litebox::platform::page_mgmt::AllocationError> { let range = PageRange::new(suggested_range.start, suggested_range.end) diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index 871c4ccdd..1e34d53ad 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -445,6 +445,12 @@ impl LinuxUserland { impl litebox::platform::Provider for LinuxUserland {} +impl litebox::platform::RawMessageProvider for LinuxUserland {} + +impl litebox::platform::AddressSpaceProvider for LinuxUserland { + type AddressSpaceId = u32; +} + impl litebox::platform::SignalProvider for LinuxUserland { type Signal = litebox_common_linux::signal::Signal; @@ -1728,6 +1734,7 @@ impl litebox::platform::PageManagementProvider for Li initial_permissions: MemoryRegionPermissions, can_grow_down: bool, populate_pages_immediately: bool, + noreserve: bool, fixed_address_behavior: FixedAddressBehavior, ) -> Result, litebox::platform::page_mgmt::AllocationError> { let flags = MapFlags::MAP_PRIVATE @@ -1746,6 +1753,11 @@ impl litebox::platform::PageManagementProvider for Li MapFlags::MAP_POPULATE } else { MapFlags::empty() + } + | if noreserve { + MapFlags::MAP_NORESERVE + } else { + MapFlags::empty() }; let r = unsafe { syscalls::syscall6( diff --git a/litebox_platform_lvbs/src/lib.rs b/litebox_platform_lvbs/src/lib.rs index 2487086d7..d636aa3ca 100644 --- a/litebox_platform_lvbs/src/lib.rs +++ b/litebox_platform_lvbs/src/lib.rs @@ -1228,6 +1228,7 @@ impl PageManagementProvider for initial_permissions: litebox::platform::page_mgmt::MemoryRegionPermissions, can_grow_down: bool, populate_pages_immediately: bool, + _noreserve: bool, fixed_address_behavior: FixedAddressBehavior, ) -> Result, litebox::platform::page_mgmt::AllocationError> { let range = PageRange::new(suggested_range.start, suggested_range.end) @@ -1351,6 +1352,14 @@ impl litebox::platform::SystemInfoProvider for LinuxKernel< } } +impl litebox::platform::RawMessageProvider for LinuxKernel {} + +impl litebox::platform::AddressSpaceProvider for LinuxKernel { + // All methods default to `Err(NotSupported)` — real implementation comes + // when LVBS multi-process (separate page tables) is added. + type AddressSpaceId = u32; +} + #[cfg(feature = "optee_syscall")] /// Checks whether the given physical addresses are contiguous with respect to ALIGN. fn is_contiguous(addrs: &[PhysPageAddr]) -> bool { diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 38f96b535..59c10023f 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -345,9 +345,11 @@ fn default_fs( } // Special override so that `GETFL` can return stdio-specific flags +#[derive(Clone)] pub(crate) struct StdioStatusFlags(litebox::fs::OFlags); /// Status flags for pipes +#[derive(Clone)] pub(crate) struct PipeStatusFlags(pub litebox::fs::OFlags); impl syscalls::file::FilesState { diff --git a/litebox_shim_linux/src/syscalls/net.rs b/litebox_shim_linux/src/syscalls/net.rs index d1b04e894..9192971c1 100644 --- a/litebox_shim_linux/src/syscalls/net.rs +++ b/litebox_shim_linux/src/syscalls/net.rs @@ -155,7 +155,7 @@ impl SocketAddress { } } -#[derive(Default)] +#[derive(Default, Clone)] pub(super) struct SocketOptions { pub(super) reuse_address: bool, pub(super) keep_alive: bool, @@ -171,7 +171,9 @@ pub(super) struct SocketOptions { pub(super) linger_timeout: Option, } +#[derive(Clone)] pub(crate) struct SocketOFlags(pub OFlags); +#[derive(Clone)] pub(crate) struct SocketProxy(pub Arc>); pub(super) enum SocketOptionValue { diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index 419afb09c..a6605a409 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -1292,7 +1292,7 @@ impl Task { let Some(count) = core::num::NonZeroU32::new(count) else { return Ok(0); }; - self.global.futex_manager.wake(addr, count, None)? as usize + self.global.futex_manager.wake(addr, count, None, 0)? as usize } FutexArgs::Wait { addr, @@ -1307,6 +1307,7 @@ impl Task { addr, val, None, + 0, )?; 0 } @@ -1334,6 +1335,7 @@ impl Task { addr, val, core::num::NonZeroU32::new(bitmask), + 0, )?; 0 } From 8c99c99c8512983fe38678b73c2cb7b9e75e3296 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 3 Apr 2026 13:02:01 -0700 Subject: [PATCH 2/4] Fix Windows build: add missing trait impls and noreserve param Add RawMessageProvider and AddressSpaceProvider implementations for WindowsUserland, and add the _noreserve parameter to allocate_pages. These are required by the Provider supertrait expansion and PageManagementProvider signature change in the foundation types PR. --- litebox_platform_windows_userland/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index b43f2f790..8c2de74e9 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -331,6 +331,12 @@ impl WindowsUserland { } } +impl litebox::platform::RawMessageProvider for WindowsUserland {} + +impl litebox::platform::AddressSpaceProvider for WindowsUserland { + type AddressSpaceId = u32; +} + impl litebox::platform::Provider for WindowsUserland {} impl litebox::platform::SignalProvider for WindowsUserland { @@ -1661,6 +1667,7 @@ impl litebox::platform::PageManagementProvider for Wi initial_permissions: MemoryRegionPermissions, can_grow_down: bool, populate_pages_immediately: bool, + _noreserve: bool, fixed_address_behavior: FixedAddressBehavior, ) -> Result, AllocationError> { debug_assert!(ALIGN.is_multiple_of(self.sys_info.read().unwrap().dwPageSize as usize)); From f9f769e8c713a553653e0f9450b536512220de67 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 3 Apr 2026 14:42:27 -0700 Subject: [PATCH 3/4] Remove unused Waker::ptr_eq (defer to layer that introduces callers) --- litebox/src/event/wait.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/litebox/src/event/wait.rs b/litebox/src/event/wait.rs index a2bdd1b17..eb879c363 100644 --- a/litebox/src/event/wait.rs +++ b/litebox/src/event/wait.rs @@ -73,11 +73,6 @@ impl Waker { pub fn wake(&self) { self.0.wake(); } - - /// Returns `true` if both wakers refer to the same thread's wait state. - pub fn ptr_eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.0, &other.0) - } } impl WaitState { From 2480a403c55080c1c9703feb194f99a1a60bb818 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 3 Apr 2026 13:57:24 -0700 Subject: [PATCH 4/4] File descriptor layer: dup/close hooks, fork support, WeakEntryHandle Refactor the Descriptors table to support dup/close lifecycle hooks, close_and_remove semantics, fork-aware cloning, and WeakEntryHandle. Remove DescriptorObjectId in favor of identity_addr() (Arc pointer identity). --- litebox/src/fd/mod.rs | 411 ++++++++++++++++++++++++++++------------ litebox/src/fd/tests.rs | 52 +++++ litebox/src/net/mod.rs | 32 +--- 3 files changed, 345 insertions(+), 150 deletions(-) diff --git a/litebox/src/fd/mod.rs b/litebox/src/fd/mod.rs index d93da35b1..f242a0ac0 100644 --- a/litebox/src/fd/mod.rs +++ b/litebox/src/fd/mod.rs @@ -8,12 +8,11 @@ reason = "still under development, remove before merging PR" )] -use alloc::sync::Arc; +use alloc::sync::{Arc, Weak}; use alloc::vec; use alloc::vec::Vec; use core::marker::PhantomData; use core::sync::atomic::AtomicBool; -use hashbrown::HashMap; use thiserror::Error; use crate::sync::{RawSyncPrimitivesProvider, RwLock}; @@ -66,6 +65,41 @@ impl Descriptors { } } + /// Duplicate an entry given only its raw index, returning a new [`OwnedFd`] + /// that independently tracks its closed state. + /// + /// This is used by [`RawDescriptorStorage::clone_for_fork`] so that parent + /// and child processes get independent `OwnedFd` instances that share the + /// underlying file description (via `Arc`). + /// + /// Per-fd metadata (e.g. `FD_CLOEXEC`) is cloned from the source entry, + /// matching POSIX fork semantics where the child inherits the parent's + /// per-fd flags. + /// + /// Returns `None` if the fd is already closed. + pub(crate) fn duplicate_raw_fd(&mut self, raw: &OwnedFd) -> Option { + let idx = self + .entries + .iter() + .position(Option::is_none) + .unwrap_or_else(|| { + self.entries.push(None); + self.entries.len() - 1 + }); + let (shared_entry, metadata) = { + let src = self.entries[raw.as_usize()?].as_ref().unwrap(); + src.x.read().entry.on_dup(); + (Arc::clone(&src.x), src.metadata.clone()) + }; + let new_ind_entry = IndividualEntry { + x: shared_entry, + metadata, + }; + let old = self.entries[idx].replace(new_ind_entry); + assert!(old.is_none()); + Some(OwnedFd::new(idx)) + } + /// Create a duplicate of the provided `fd`. /// /// This newly-created FD shares all behavior with the existing FD, including (for example) @@ -92,9 +126,12 @@ impl Descriptors { self.entries.push(None); self.entries.len() - 1 }); - let new_ind_entry = IndividualEntry::new(Arc::clone( - &self.entries[fd.x.as_usize()?].as_ref().unwrap().x, - )); + let shared_entry = { + let src = self.entries[fd.x.as_usize()?].as_ref().unwrap(); + src.x.read().entry.on_dup(); + Arc::clone(&src.x) + }; + let new_ind_entry = IndividualEntry::new(shared_entry); let old = self.entries[idx].replace(new_ind_entry); assert!(old.is_none()); Some(TypedFd { @@ -116,19 +153,42 @@ impl Descriptors { let Some(old) = self.entries[fd.x.as_usize()?].take() else { unreachable!(); }; + old.x.read().entry.on_close(); fd.x.mark_as_closed(); Arc::into_inner(old.x) .map(RwLock::into_inner) .map(DescriptorEntry::into_subsystem_entry::) } - /// Close the provided `fd`, and remove the corresponding entry if it is unique. - /// If not unique, duplicate the `fd` for future closure. + /// Close the provided `fd`, removing the entry from the descriptor table. + /// + /// # Design + /// + /// This method **always frees the descriptor table slot** for `fd`, regardless of whether + /// other references (from `dup` or `fork`) exist. Resource lifetime is managed by `Arc`: + /// when the last `Arc` reference is dropped, the underlying resource is torn down. + /// + /// This avoids the need for polling-based deferred cleanup. Callers do not need to track + /// "queued for closure" fds or retry close on each event loop tick — slot removal is + /// immediate, and `Arc` drop semantics handle the rest. + /// + /// # Results + /// + /// - `Closed(entry)`: This was the last reference. The caller receives ownership of the + /// entry and is responsible for tearing down the underlying resource (e.g., removing a + /// socket from the socket set). + /// - `Released`: The slot was freed, but other `Arc` references still hold the entry. + /// No teardown is needed — the last closer will get `Closed`. + /// - `Deferred`: This is the last reference, but `can_close_immediately` returned false + /// (e.g., pending data to flush). The entry stays in the slot for the caller's polling + /// loop (e.g., `close_pending_sockets`) to retry later. + /// + /// # Parameters /// - /// This method takes a closure `can_close_immediately` that is called with the entry to determine - /// whether the file descriptor can be closed immediately. This allows the caller to implement - /// custom logic (e.g., checking for pending data) before allowing the close to proceed. - pub(crate) fn close_and_duplicate_if_shared< + /// `can_close_immediately` is only consulted when this is the **last** reference. If other + /// references exist, the slot is always released without checking — those other holders + /// will service any pending work. + pub(crate) fn close_and_remove< Subsystem: FdEnabledSubsystem, F: FnOnce(&Subsystem::Entry) -> bool, >( @@ -137,99 +197,33 @@ impl Descriptors { can_close_immediately: F, ) -> Option> { let idx = fd.x.as_usize()?; - let Some(old) = self.entries[idx].take() else { - unreachable!(); - }; - if Arc::strong_count(&old.x) == 1 { - // Unique, so we can just return it if allowed. - if can_close_immediately(old.x.read().as_subsystem::()) { - fd.x.mark_as_closed(); - let entry = Arc::into_inner(old.x) - .map(RwLock::into_inner) - .map(DescriptorEntry::into_subsystem_entry::) - .unwrap(); - Some(CloseResult::Closed(entry)) - } else { - // Put it back - let old = self.entries[idx].replace(old); - assert!(old.is_none()); - Some(CloseResult::Deferred) - } - } else { - fd.x.mark_as_closed(); - // Shared, so we need to duplicate it. - let old = self.entries[idx].replace(old); - assert!(old.is_none()); - Some(CloseResult::Duplicated(TypedFd { - _phantom: PhantomData, - x: OwnedFd::new(idx), - })) + let entry = self.entries[idx].as_ref().unwrap(); + + // Only check pending data when we're the last reference. + // If other refs exist (dup or fork), they'll service the socket after we release our slot. + if Arc::strong_count(&entry.x) == 1 + && !can_close_immediately(entry.x.read().as_subsystem::()) + { + // Last ref, pending data — keep entry in slot for close_pending_sockets to poll. + return Some(CloseResult::Deferred); } - } - /// Drain all entries that are fully accounted for by the `fds`, removing those FDs from `fd`s, - /// and returning their corresponding entries. - /// - /// This is similar to [`Self::remove`] except it allows draining a whole collection of FDs, - /// which is helpful if there are duplicated FDs in the mix. This is particularly useful if one - /// is unsure if there are ongoing operations on some entries in the FD, and thus wants to delay - /// some sort of `close` operation. - /// - /// No ordering guarantees are provided by this function; the resulting entries can be - /// arbitrarily ordered. - /// - /// If an FD remains in `fds` after this function finishes running, then it is guaranteed to - /// have at least one other duplicate floating around and still accessing an entry somewhere - /// outside of `fds`; if an entry is returned, then all possible FDs to it have been removed - /// removed from `fds` (and no other operation was concurrently accessing an entry). - pub(crate) fn drain_entries_full_covered_by( - &mut self, - fds: &mut Vec>, - ) -> Vec { - // Each FD corresponds to an `IndividualEntry`, which has an Arc to a `DescriptorEntry`. If - // we have the same number of FDs as matching to the strong-count of a descriptor entry, - // then it must be the case that we have everything needed to close the entries out. - let removable_entries: Vec<*const RwLock<_, _>> = { - let mut strong_count_and_count = HashMap::<*const _, (usize, usize)>::new(); - for fd in fds.iter() { - let entry = &self.entries[fd.x.as_usize().unwrap()]; - // It would not be "incorrect" to see a closed out entry, but as it currently stands, I - // believe that we'll only see alive entries, so this `unwrap` is confirming that; if we - // need to expand it out, we'd simply have a `continue` here. - let entry = entry.as_ref().unwrap(); - strong_count_and_count - .entry(Arc::as_ptr(&entry.x)) - .or_insert((Arc::strong_count(&entry.x), 0)) - .1 += 1; + // Always remove the slot. + let old = self.entries[idx].take().unwrap(); + old.x.read().entry.on_close(); + fd.x.mark_as_closed(); + + match Arc::into_inner(old.x) { + Some(rwlock) => { + // Last reference — extract entry for teardown. + let entry = RwLock::into_inner(rwlock).into_subsystem_entry::(); + Some(CloseResult::Closed(entry)) } - strong_count_and_count - .into_iter() - .filter(|(_ptr, (sc, c))| sc == c) - .map(|(ptr, _)| ptr) - .collect() - }; - // Now we can actually go and remove every single such FD. - let entries: Vec = { - let mut entries = vec![]; - fds.retain(|fd: &TypedFd| { - let entry = &self.entries[fd.x.as_usize().unwrap()]; - let entry = entry.as_ref().unwrap(); - let entry_ptr = Arc::as_ptr(&entry.x); - if !removable_entries.contains(&entry_ptr) { - return true; - } - // This FD is removable - let entry = self.remove(fd); - if let Some(entry) = entry { - // This is the last of the individual entries that were holding a ref to this. - entries.push(entry); - } - false - }); - entries - }; - debug_assert_eq!(entries.len(), removable_entries.len()); - entries + None => { + // Other references exist (dup or fork). Slot freed, done. + Some(CloseResult::Released) + } + } } /// An iterator of descriptors and entries for a subsystem @@ -336,7 +330,28 @@ impl Descriptors { // the first place, none of this should panic---if it does, someone has done a bad cast // somewhere. let entry = self.entries[fd.x.as_usize()?].as_ref()?; - Some(EntryHandle(Arc::clone(&entry.x), PhantomData)) + Some(EntryHandle { + entry: Arc::clone(&entry.x), + _phantom: PhantomData, + }) + } + + /// Handles to all currently alive entries in a subsystem. + pub fn entry_handles( + &self, + ) -> impl Iterator> + '_ { + self.entries.iter().filter_map(|entry| { + let entry = entry.as_ref()?; + let guard = entry.read(); + if !guard.matches_subsystem::() { + return None; + } + drop(guard); + Some(EntryHandle { + entry: entry.x.clone(), + _phantom: PhantomData, + }) + }) } /// Use the entry at `internal_fd` as mutably. @@ -518,28 +533,78 @@ impl Descriptors { /// A handle to a descriptor entry (via [`Descriptors::entry_handle`]) that can be used without /// maintaining access to the descriptor table itself. -pub struct EntryHandle( - Arc>, - PhantomData, -); +pub struct EntryHandle { + entry: Arc>, + _phantom: PhantomData, +} +impl Clone + for EntryHandle +{ + fn clone(&self) -> Self { + Self { + entry: Arc::clone(&self.entry), + _phantom: PhantomData, + } + } +} impl EntryHandle { + pub fn identity_addr(&self) -> usize { + Arc::as_ptr(&self.entry).addr() + } + pub fn with_entry(&self, f: impl FnOnce(&Subsystem::Entry) -> R) -> R { - f(self.0.read().as_subsystem::()) + f(self.entry.read().as_subsystem::()) } pub fn with_entry_mut(&self, f: impl FnOnce(&mut Subsystem::Entry) -> R) -> R { - f(self.0.write().as_subsystem_mut::()) + f(self.entry.write().as_subsystem_mut::()) + } + + pub fn downgrade(&self) -> WeakEntryHandle { + WeakEntryHandle { + entry: Arc::downgrade(&self.entry), + _phantom: PhantomData, + } } } -/// Result of a [`Descriptors::close_and_duplicate_if_shared`] operation +pub struct WeakEntryHandle { + entry: Weak>, + _phantom: PhantomData, +} +impl Clone + for WeakEntryHandle +{ + fn clone(&self) -> Self { + Self { + entry: self.entry.clone(), + _phantom: PhantomData, + } + } +} +impl + WeakEntryHandle +{ + pub fn identity_addr(&self) -> usize { + self.entry.as_ptr().addr() + } + + pub fn upgrade(&self) -> Option> { + Some(EntryHandle { + entry: self.entry.upgrade()?, + _phantom: PhantomData, + }) + } +} + +/// Result of a [`Descriptors::close_and_remove`] operation pub(crate) enum CloseResult { - /// The FD was the last reference and has been closed, returning the entry + /// The FD was the last reference and has been closed, returning the entry for teardown Closed(Subsystem::Entry), - /// There are other references, so a new duplicate was created for queued closure - Duplicated(TypedFd), + /// The slot was freed, but other references (dup or fork) still exist — no teardown needed + Released, /// The FD was unique but couldn't be closed immediately (e.g., due to pending data) Deferred, } @@ -555,10 +620,28 @@ pub struct RawDescriptorStorage { stored_fds: Vec>, } +/// An opaque token representing an fd duplicated for inter-process delivery +/// (e.g. via `SCM_RIGHTS`). Create one with +/// [`RawDescriptorStorage::duplicate_for_passing`] and install it on the +/// receiving side with [`RawDescriptorStorage::insert_passed_fd`]. +pub struct PassedFd { + owned: OwnedFd, + type_id: core::any::TypeId, +} + struct StoredFd { x: Arc, subsystem_entry_type_id: core::any::TypeId, } + +impl Clone for StoredFd { + fn clone(&self) -> Self { + Self { + x: Arc::clone(&self.x), + subsystem_entry_type_id: self.subsystem_entry_type_id, + } + } +} impl StoredFd { fn new(fd: TypedFd) -> Self { Self { @@ -579,6 +662,89 @@ impl RawDescriptorStorage { Self { stored_fds: vec![] } } + /// Clone the descriptor storage for `fork()`. + /// + /// Each stored fd is duplicated in the global `Descriptors` table so that + /// the child gets an independent `OwnedFd` (with its own `closed` flag). + /// The underlying file descriptions are shared via `Arc`, matching POSIX + /// shared-file-description semantics after fork. + /// + /// # Panics + /// + /// Panics if any stored fd has already been closed in `global_dt`. + #[must_use] + pub fn clone_for_fork( + &self, + global_dt: &mut Descriptors, + ) -> Self { + Self { + stored_fds: self + .stored_fds + .iter() + .map(|slot| { + slot.as_ref().map(|stored| { + let new_owned = global_dt + .duplicate_raw_fd(&stored.x) + .expect("fd should not be closed during fork"); + StoredFd { + x: Arc::new(new_owned), + subsystem_entry_type_id: stored.subsystem_entry_type_id, + } + }) + }) + .collect(), + } + } + + /// Duplicate a stored fd for inter-process passing (e.g. `SCM_RIGHTS`). + /// + /// Creates a new entry in the global descriptor table that shares the same + /// underlying file description as the fd at `raw_fd`. Returns a + /// [`PassedFd`] token that can be installed in another process's raw + /// descriptor store via [`Self::insert_passed_fd`]. + /// + /// Returns `None` if `raw_fd` is not occupied or the underlying owned fd + /// has already been closed. + pub fn duplicate_for_passing( + &self, + raw_fd: usize, + global_dt: &mut Descriptors, + ) -> Option { + let stored = self.stored_fds.get(raw_fd)?.as_ref()?; + let new_owned = global_dt.duplicate_raw_fd(&stored.x)?; + Some(PassedFd { + owned: new_owned, + type_id: stored.subsystem_entry_type_id, + }) + } + + /// Install an fd received via inter-process passing (e.g. `SCM_RIGHTS`). + /// + /// The caller supplies a [`PassedFd`] token (obtained from + /// [`Self::duplicate_for_passing`]). The fd is placed in the first + /// available slot and its raw integer is returned. + /// + /// # Panics + /// + /// Panics if the selected slot is unexpectedly occupied. + pub fn insert_passed_fd(&mut self, passed: PassedFd) -> usize { + let raw_fd = self + .stored_fds + .iter() + .position(Option::is_none) + .unwrap_or_else(|| { + self.stored_fds.push(None); + self.stored_fds.len() - 1 + }); + let stored = StoredFd { + x: Arc::new(passed.owned), + subsystem_entry_type_id: passed.type_id, + }; + let old = self.stored_fds[raw_fd].replace(stored); + assert!(old.is_none()); + raw_fd + } + /// Get the corresponding integer value of the provided `fd`. /// /// This explicitly consumes the `fd`. @@ -620,13 +786,6 @@ impl RawDescriptorStorage { ) -> bool { // TODO(jayb): Should we be storing things via a HashMap to make sure this operation cannot // be too expensive if someone tries to store into a large raw FD? - // - // If this assertion failure is hit in practice, we might need to be more defensive via the - // HashMap, rather than just silently allow big growth - assert!( - raw_fd < self.stored_fds.len() + 100, - "explicit upper bound restriction for now; see implementation details" - ); if self.stored_fds.get(raw_fd).is_some_and(Option::is_some) { // There's already something at this slot. return false; @@ -762,7 +921,17 @@ pub trait FdEnabledSubsystem: Sized { } /// A per-FD entry stored in the descriptor table for a specific [`FdEnabledSubsystem`] -pub trait FdEnabledSubsystemEntry: Send + Sync + core::any::Any {} +pub trait FdEnabledSubsystemEntry: Send + Sync + core::any::Any { + /// Called when this entry is duplicated (e.g. via `dup`/`dup2`/`fork`). + /// Subsystems that maintain reference counts (PTY open counts, pipe sender + /// counts) should increment them here. + fn on_dup(&self) {} + + /// Called when a file descriptor referencing this entry is closed. + /// Subsystems that maintain reference counts should decrement them here + /// and fire any resulting notifications (e.g. HUP on last PTY close). + fn on_close(&self) {} +} /// Possible errors from [`RawDescriptorStorage::fd_from_raw_integer`] and /// [`RawDescriptorStorage::fd_consume_raw_integer`]. @@ -878,7 +1047,7 @@ pub(crate) struct InternalFd { /// /// Note: this indicates ownership over the descriptor itself, but not necessarily the underlying /// entry, since there might be duplicates to the underlying entry. -struct OwnedFd { +pub(crate) struct OwnedFd { raw: u32, closed: AtomicBool, } diff --git a/litebox/src/fd/tests.rs b/litebox/src/fd/tests.rs index 04a482b44..39d03f65e 100644 --- a/litebox/src/fd/tests.rs +++ b/litebox/src/fd/tests.rs @@ -116,6 +116,31 @@ fn test_with_entry() { }); } +#[test] +fn test_duplicate_shares_identity() { + let litebox = litebox(); + let mut descriptors = litebox.descriptor_table_mut(); + + let entry = MockEntry { + data: "test".to_string(), + }; + let typed_fd: TypedFd = descriptors.insert(entry); + let dup_fd = descriptors + .duplicate(&typed_fd) + .expect("duplicate should succeed"); + + let handle_orig = descriptors + .entry_handle(&typed_fd) + .expect("entry handle should exist"); + let handle_dup = descriptors + .entry_handle(&dup_fd) + .expect("dup entry handle should exist"); + assert_eq!(handle_orig.identity_addr(), handle_dup.identity_addr()); + + let weak = handle_orig.downgrade(); + assert_eq!(weak.identity_addr(), handle_orig.identity_addr()); +} + #[test] fn test_fd_raw_integer() { let litebox = litebox(); @@ -147,3 +172,30 @@ fn test_fd_raw_integer() { .unwrap(); assert_eq!(data, "test"); } + +#[test] +fn test_clone_for_fork_shares_identity() { + let litebox = litebox(); + let mut descriptors = litebox.descriptor_table_mut(); + let mut parent_rds = super::RawDescriptorStorage::new(); + + let entry = MockEntry { + data: "test".to_string(), + }; + let typed_fd: TypedFd = descriptors.insert(entry); + let raw_fd = parent_rds.fd_into_raw_integer(typed_fd); + + let child_rds = parent_rds.clone_for_fork(&mut descriptors); + + let parent_fd = parent_rds + .fd_from_raw_integer::(raw_fd) + .expect("parent fd should still exist"); + let child_fd = child_rds + .fd_from_raw_integer::(raw_fd) + .expect("child fd should be cloned"); + + // Parent and child fds should share the same underlying entry (same Arc) + let parent_handle = descriptors.entry_handle(&parent_fd).unwrap(); + let child_handle = descriptors.entry_handle(&child_fd).unwrap(); + assert_eq!(parent_handle.identity_addr(), child_handle.identity_addr()); +} diff --git a/litebox/src/net/mod.rs b/litebox/src/net/mod.rs index 954162620..c0709fcf9 100644 --- a/litebox/src/net/mod.rs +++ b/litebox/src/net/mod.rs @@ -76,8 +76,6 @@ where local_port_allocator: LocalPortAllocator, /// Whether outside interaction is automatic or manual platform_interaction: PlatformInteraction, - /// FDs that are queued for eventual closure - queued_for_closure: Vec>, /// Sockets that are closing in the background closing_in_background: Vec, } @@ -121,7 +119,6 @@ where zero_time: litebox.x.platform.now(), local_port_allocator: LocalPortAllocator::new(), platform_interaction: PlatformInteraction::Automatic, - queued_for_closure: vec![], closing_in_background: vec![], } } @@ -481,7 +478,6 @@ where /// (Internal-only API) Actually perform the queued interactions with the outside world. fn internal_perform_platform_interaction(&mut self) -> smoltcp::iface::PollResult { - self.attempt_to_close_queued(); self.remove_dead_sockets(); self.close_pending_sockets(); @@ -834,7 +830,7 @@ where let mut dt = self.litebox.descriptor_table_mut(); // We close immediately if we can match dt - .close_and_duplicate_if_shared(fd, |entry| { + .close_and_remove(fd, |entry| { match behavior { CloseBehavior::Immediate => { let socket_handle = &entry.entry; @@ -869,11 +865,8 @@ where drop(dt); self.close_handle(socket_handle.entry); } - super::fd::CloseResult::Duplicated(dup_fd) => { - // It seems like there might be other duplicates around (e.g., due to `dup`), so we - // can't immediately close it out. - // We attempt to queue it for future closure and then just return. - self.queued_for_closure.push(dup_fd); + super::fd::CloseResult::Released => { + // Slot freed, other references (dup or fork) still hold the socket. Nothing to do. } super::fd::CloseResult::Deferred => { let Some(()) = dt.with_entry_mut(fd, |entry| entry.entry.consider_closed = true) @@ -886,25 +879,6 @@ where Ok(()) } - /// Attempt to close as many queued-to-close FDs as possible. Returns `true` iff any of them - /// were closed. - fn attempt_to_close_queued(&mut self) -> bool { - if self.queued_for_closure.is_empty() { - // fast path - return false; - } - let mut dt = self.litebox.descriptor_table_mut(); - let entries = dt.drain_entries_full_covered_by(&mut self.queued_for_closure); - drop(dt); - if entries.is_empty() { - return false; - } - for entry in entries { - self.close_handle(entry.entry); - } - true - } - /// Close the `socket_handle` fn close_handle(&mut self, socket_handle: SocketHandle) { let SocketHandle {