diff --git a/litebox/src/fd/mod.rs b/litebox/src/fd/mod.rs index 6114118c8..887a494b9 100644 --- a/litebox/src/fd/mod.rs +++ b/litebox/src/fd/mod.rs @@ -9,6 +9,7 @@ )] use alloc::sync::Arc; +use alloc::sync::Weak; use alloc::vec; use alloc::vec::Vec; use core::marker::PhantomData; @@ -556,6 +557,77 @@ impl pub fn with_entry_mut(&self, f: impl FnOnce(&mut Subsystem::Entry) -> R) -> R { f(self.0.entry.write().as_subsystem_mut::()) } + + /// Runs `f` with the aliased (open-file-description-level) metadata of type + /// `T`, if present. + /// + /// This reads the metadata stored via [`Descriptors::set_entry_metadata`], + /// which is shared by every descriptor referring to the same open file + /// description. Returns `None` if no such metadata exists. + pub fn with_shared_metadata(&self, f: impl FnOnce(&T) -> R) -> Option + where + T: core::any::Any + Clone + Send + Sync, + { + self.0.entry.read().metadata.get::().map(f) + } + + /// The address of the shared open file description this handle refers to. + /// + /// Duplicates of a descriptor share one open file description, so this + /// address is stable across `dup` and uniquely identifies the description + /// for as long as any duplicate keeps it alive. + #[must_use] + pub fn as_ptr(&self) -> *const () { + Arc::as_ptr(&self.0).cast() + } + + /// Downgrades to a [`WeakEntryHandle`] that survives `dup`. + /// + /// The resulting handle upgrades for as long as *any* descriptor referring + /// to the same open file description remains open, even after the specific + /// descriptor this handle was obtained from has been closed. + #[must_use] + pub fn downgrade(&self) -> WeakEntryHandle { + WeakEntryHandle(Arc::downgrade(&self.0), PhantomData) + } +} + +/// A durable, `dup`-surviving weak reference to a descriptor's open file +/// description. +/// +/// Unlike a [`TypedFd`], which is tied to one descriptor slot, this upgrades as +/// long as *any* descriptor referring to the same open file description is +/// open. It is the correct anchor for interest that must outlive the closure of +/// the specific descriptor it was registered against (for example, epoll +/// interest, per Linux `epoll(7)` semantics). +pub struct WeakEntryHandle( + Weak>, + PhantomData Subsystem>, +); + +impl + WeakEntryHandle +{ + /// Upgrades to a strong [`EntryHandle`] if the open file description is + /// still alive (i.e. at least one duplicate remains open). + #[must_use] + pub fn upgrade(&self) -> Option> { + self.0 + .upgrade() + .map(|entry| EntryHandle(entry, PhantomData)) + } + + /// The address of the shared open file description, stable across `dup`. + /// + /// This is safe to use as a durable identity key. A [`WeakEntryHandle`] + /// keeps the underlying allocation reserved even after the open file + /// description is closed (every strong reference dropped), so this address + /// is never recycled for a different open file description while this handle + /// exists. The pointer is only ever compared, never dereferenced. + #[must_use] + pub fn as_ptr(&self) -> *const () { + self.0.as_ptr().cast() + } } /// Result of a [`Descriptors::close_and_duplicate_if_shared`] operation diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index edda8b194..ce6605f11 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -729,3 +729,18 @@ crate::fd::enable_fds_for_subsystem! { PipeEnd; -> PipeFd; } + +impl DescriptorEntry { + /// Runs `f` with the [`IOPollable`] backing this pipe end. + /// + /// This lets a holder of a durable entry handle poll the pipe without a + /// live per-descriptor [`PipeFd`], which is required so that an epoll + /// interest survives closing the registered descriptor while a duplicate + /// referring to the same open file description remains open. + pub fn with_iopollable(&self, f: impl FnOnce(&dyn IOPollable) -> R) -> R { + match &self.entry { + PipeEnd::Receiver(receiver) => f(receiver.as_ref()), + PipeEnd::Sender(sender) => f(sender.as_ref()), + } + } +} diff --git a/litebox_runner_linux_userland/tests/epoll_dup.c b/litebox_runner_linux_userland/tests/epoll_dup.c new file mode 100644 index 000000000..2591dd846 --- /dev/null +++ b/litebox_runner_linux_userland/tests/epoll_dup.c @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Tests: an epoll interest survives closing the registered fd as long as a +// duplicate referring to the same open file description remains open, matching +// Linux epoll(7) semantics ("a file descriptor is removed from an interest +// list only after all the file descriptors referring to the underlying open +// file description have been closed"). + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#define TEST_ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (line %d): %s (errno=%d: %s)\n", \ + __func__, __LINE__, msg, errno, strerror(errno)); \ + return 1; \ + } \ + } while (0) + +int main(void) { + int efd = eventfd(0, EFD_CLOEXEC); + TEST_ASSERT(efd >= 0, "eventfd failed"); + + int epfd = epoll_create1(EPOLL_CLOEXEC); + TEST_ASSERT(epfd >= 0, "epoll_create1 failed"); + + struct epoll_event ev; + memset(&ev, 0, sizeof(ev)); + ev.events = EPOLLIN; + ev.data.u64 = 0x42; + TEST_ASSERT(epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev) == 0, + "epoll_ctl ADD failed"); + + // The duplicate shares the same open file description as efd. + int dupfd = dup(efd); + TEST_ASSERT(dupfd >= 0, "dup failed"); + + // Close the originally-registered fd. The interest must survive because + // dupfd still refers to the same open file description. + TEST_ASSERT(close(efd) == 0, "close original failed"); + + // Make the description readable through the surviving duplicate. + uint64_t one = 1; + TEST_ASSERT(write(dupfd, &one, sizeof(one)) == (ssize_t)sizeof(one), + "write via dup failed"); + + // The registration must still be reported, carrying its original data. + struct epoll_event out[4]; + memset(out, 0, sizeof(out)); + int n = epoll_wait(epfd, out, 4, 1000); + TEST_ASSERT(n == 1, "epoll_wait should report the surviving registration"); + TEST_ASSERT((out[0].events & EPOLLIN) != 0, "expected EPOLLIN"); + TEST_ASSERT(out[0].data.u64 == 0x42, "event data mismatch"); + + // The registration is durable: after draining and re-arming through the + // duplicate, a second wait still reports it. + uint64_t val = 0; + TEST_ASSERT(read(dupfd, &val, sizeof(val)) == (ssize_t)sizeof(val), + "read via dup failed"); + TEST_ASSERT(val == 1, "unexpected eventfd value"); + TEST_ASSERT(write(dupfd, &one, sizeof(one)) == (ssize_t)sizeof(one), + "second write via dup failed"); + memset(out, 0, sizeof(out)); + n = epoll_wait(epfd, out, 4, 1000); + TEST_ASSERT(n == 1, "epoll_wait should still report after re-arm"); + TEST_ASSERT(out[0].data.u64 == 0x42, "event data mismatch after re-arm"); + + TEST_ASSERT(close(dupfd) == 0, "close dup failed"); + TEST_ASSERT(close(epfd) == 0, "close epoll failed"); + + printf("epoll dup-survival: PASS\n"); + return 0; +} diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 37122005c..f2058a839 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -15,7 +15,7 @@ use litebox::{ polling::{Pollee, TryOpError}, wait::{WaitContext, WaitError, Waker}, }, - fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry, TypedFd}, + fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry, TypedFd, WeakEntryHandle}, utils::ReinterpretUnsignedExt, }; use litebox_common_linux::{EpollEvent, EpollOp, errno::Errno}; @@ -80,35 +80,108 @@ impl EpollDescriptor { } } +/// A durable, `dup`-surviving reference to an epoll target's open file +/// description. +/// +/// Each variant holds a [`WeakEntryHandle`], which upgrades for as long as any +/// descriptor referring to the same open file description remains open. This is +/// what lets an epoll interest outlive closing the descriptor it was registered +/// against, matching Linux `epoll(7)` semantics. enum DescriptorRef { - Eventfd(Weak>>), - Epoll(Weak>>), - File(Weak>), - Socket(Weak>), - Pipe(Weak>), - Unix(Weak>>), + Eventfd(WeakEntryHandle>), + Epoll(WeakEntryHandle>), + File(WeakEntryHandle), + Socket(WeakEntryHandle>), + Pipe(WeakEntryHandle>), + Unix(WeakEntryHandle>), } impl DescriptorRef { - fn from(value: &EpollDescriptor) -> Self { - match value { - EpollDescriptor::Eventfd(file) => Self::Eventfd(Arc::downgrade(file)), - EpollDescriptor::Epoll(file) => Self::Epoll(Arc::downgrade(file)), - EpollDescriptor::File(file) => Self::File(Arc::downgrade(file)), - EpollDescriptor::Socket(socket) => Self::Socket(Arc::downgrade(socket)), - EpollDescriptor::Pipe(pipe) => Self::Pipe(Arc::downgrade(pipe)), - EpollDescriptor::Unix(unix) => Self::Unix(Arc::downgrade(unix)), + /// Derives a durable reference to `desc`'s open file description. + /// + /// Returns `None` if the descriptor has already been closed. + fn new( + global: &GlobalState, + desc: &EpollDescriptor, + ) -> Option { + let dt = global.litebox.descriptor_table(); + Some(match desc { + EpollDescriptor::Eventfd(fd) => Self::Eventfd(dt.entry_handle(fd)?.downgrade()), + EpollDescriptor::Epoll(fd) => Self::Epoll(dt.entry_handle(fd)?.downgrade()), + EpollDescriptor::File(fd) => Self::File(dt.entry_handle(fd)?.downgrade()), + EpollDescriptor::Socket(fd) => Self::Socket(dt.entry_handle(fd)?.downgrade()), + EpollDescriptor::Pipe(fd) => Self::Pipe(dt.entry_handle(fd)?.downgrade()), + EpollDescriptor::Unix(fd) => Self::Unix(dt.entry_handle(fd)?.downgrade()), + }) + } + + /// The address of the shared open file description, stable across `dup`. + fn as_ptr(&self) -> usize { + match self { + DescriptorRef::Eventfd(handle) => handle.as_ptr().addr(), + DescriptorRef::Epoll(handle) => handle.as_ptr().addr(), + DescriptorRef::File(handle) => handle.as_ptr().addr(), + DescriptorRef::Socket(handle) => handle.as_ptr().addr(), + DescriptorRef::Pipe(handle) => handle.as_ptr().addr(), + DescriptorRef::Unix(handle) => handle.as_ptr().addr(), } } - fn upgrade(&self) -> Option> { + /// Whether the open file description is still alive (some duplicate remains + /// open). + fn is_alive(&self) -> bool { match self { - DescriptorRef::Eventfd(eventfd) => eventfd.upgrade().map(EpollDescriptor::Eventfd), - DescriptorRef::Epoll(epoll) => epoll.upgrade().map(EpollDescriptor::Epoll), - DescriptorRef::File(file) => file.upgrade().map(EpollDescriptor::File), - DescriptorRef::Socket(socket) => socket.upgrade().map(EpollDescriptor::Socket), - DescriptorRef::Pipe(pipe) => pipe.upgrade().map(EpollDescriptor::Pipe), - DescriptorRef::Unix(unix) => unix.upgrade().map(EpollDescriptor::Unix), + DescriptorRef::Eventfd(handle) => handle.upgrade().is_some(), + DescriptorRef::Epoll(handle) => handle.upgrade().is_some(), + DescriptorRef::File(handle) => handle.upgrade().is_some(), + DescriptorRef::Socket(handle) => handle.upgrade().is_some(), + DescriptorRef::Pipe(handle) => handle.upgrade().is_some(), + DescriptorRef::Unix(handle) => handle.upgrade().is_some(), + } + } + + /// Checks the currently-ready events, polling through the shared open file + /// description rather than a per-descriptor handle. + /// + /// Returns `None` once every descriptor referring to the open file + /// description has been closed. + fn poll(&self, _global: &GlobalState, mask: Events) -> Option { + let check = |iop: &dyn IOPollable| iop.check_io_events() & (mask | Events::ALWAYS_POLLED); + match self { + DescriptorRef::Eventfd(handle) => { + Some(handle.upgrade()?.with_entry(|entry| check(entry))) + } + DescriptorRef::Unix(handle) => Some(handle.upgrade()?.with_entry(|entry| check(entry))), + DescriptorRef::Pipe(handle) => Some( + handle + .upgrade()? + .with_entry(|entry| entry.with_iopollable(check)), + ), + DescriptorRef::Socket(handle) => { + let proxy = handle + .upgrade()? + .with_shared_metadata::, _>( + |crate::syscalls::net::SocketProxy(proxy)| proxy.clone(), + )?; + Some(check(&proxy)) + } + DescriptorRef::File(handle) => { + // File polling returns dummy events, distinguishing stdio enough + // for REPLs (mirrors `EpollDescriptor::poll`). + let handle = handle.upgrade()?; + let events = match handle + .with_shared_metadata::(|stream| *stream) + { + Some(litebox::platform::StdioStream::Stdin) => Events::IN, + Some( + litebox::platform::StdioStream::Stdout + | litebox::platform::StdioStream::Stderr, + ) + | None => Events::OUT, + }; + Some(events & mask) + } + DescriptorRef::Epoll(_handle) => unimplemented!(), } } } @@ -222,10 +295,9 @@ impl EpollFile { Err(Errno::EINVAL) } EpollOp::EpollCtlDel => { + let key = EpollEntryKey::new(global, fd, file).ok_or(Errno::EBADF)?; let mut interests = self.interests.lock(); - let _ = interests - .remove(&EpollEntryKey::new(fd, file)) - .ok_or(Errno::ENOENT)?; + let _ = interests.remove(&key).ok_or(Errno::ENOENT)?; Ok(()) } } @@ -238,10 +310,11 @@ impl EpollFile { file: &EpollDescriptor, event: EpollEvent, ) -> Result<(), Errno> { + let desc = DescriptorRef::new(global, file).ok_or(Errno::EBADF)?; + let key = EpollEntryKey(fd, desc.as_ptr()); let mut interests = self.interests.lock(); - let key = EpollEntryKey::new(fd, file); if let Some(entry) = interests.get(&key) - && entry.desc.upgrade().is_some() + && entry.desc.is_alive() { return Err(Errno::EEXIST); } @@ -250,7 +323,7 @@ impl EpollFile { let mask = Events::from_bits_truncate(event.events); let entry = EpollEntry::new( - DescriptorRef::from(file), + desc, mask, EpollFlags::from_bits_truncate(event.events), event.data, @@ -282,10 +355,10 @@ impl EpollFile { } let mut interests = self.interests.lock(); - let key = EpollEntryKey::new(fd, file); + let key = EpollEntryKey::new(global, fd, file).ok_or(Errno::EBADF)?; let entry = interests.get(&key).ok_or(Errno::ENOENT)?; - if entry.desc.upgrade().is_none() { - // The file descriptor is closed, remove the entry + if !entry.desc.is_alive() { + // The open file description is closed, remove the entry interests.remove(&key); return Err(Errno::ENOENT); } @@ -329,19 +402,17 @@ impl EpollFile { #[derive(PartialEq, Eq, PartialOrd, Ord)] struct EpollEntryKey(u32, usize); impl EpollEntryKey { + /// Builds the key `(fd, open-file-description address)`. + /// + /// The address is stable across `dup`, so an interest registered against one + /// descriptor is found again while any duplicate of its open file + /// description remains open. Returns `None` if the descriptor is closed. fn new( + global: &GlobalState, fd: u32, desc: &EpollDescriptor, - ) -> Self { - let ptr = match desc { - EpollDescriptor::Eventfd(file) => Arc::as_ptr(file).addr(), - EpollDescriptor::Epoll(file) => Arc::as_ptr(file).addr(), - EpollDescriptor::File(file) => Arc::as_ptr(file).addr(), - EpollDescriptor::Socket(socket_fd) => Arc::as_ptr(socket_fd).addr(), - EpollDescriptor::Pipe(pipe_fd) => Arc::as_ptr(pipe_fd).addr(), - EpollDescriptor::Unix(unix) => Arc::as_ptr(unix).addr(), - }; - Self(fd, ptr) + ) -> Option { + Some(Self(fd, DescriptorRef::new(global, desc)?.as_ptr())) } } @@ -379,7 +450,6 @@ impl EpollEntry { } fn poll(&self, global: &GlobalState) -> Option<(Option, bool)> { - let file = self.desc.upgrade()?; let inner = self.inner.lock(); if !self.is_enabled.load(core::sync::atomic::Ordering::Relaxed) { @@ -387,7 +457,7 @@ impl EpollEntry { return None; } - let events = file.poll(global, inner.mask, None)?; + let events = self.desc.poll(global, inner.mask)?; if events.is_empty() { Some((None, false)) } else {