Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions litebox/src/fd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
)]

use alloc::sync::Arc;
use alloc::sync::Weak;
use alloc::vec;
use alloc::vec::Vec;
use core::marker::PhantomData;
Expand Down Expand Up @@ -556,6 +557,77 @@ impl<Platform: RawSyncPrimitivesProvider, Subsystem: FdEnabledSubsystem>
pub fn with_entry_mut<R>(&self, f: impl FnOnce(&mut Subsystem::Entry) -> R) -> R {
f(self.0.entry.write().as_subsystem_mut::<Subsystem>())
}

/// 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<T, R>(&self, f: impl FnOnce(&T) -> R) -> Option<R>
where
T: core::any::Any + Clone + Send + Sync,
{
self.0.entry.read().metadata.get::<T>().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<Platform, Subsystem> {
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<Platform: RawSyncPrimitivesProvider, Subsystem: FdEnabledSubsystem>(
Weak<SharedEntry<Platform>>,
PhantomData<fn(Subsystem) -> Subsystem>,
);

impl<Platform: RawSyncPrimitivesProvider, Subsystem: FdEnabledSubsystem>
WeakEntryHandle<Platform, Subsystem>
{
/// 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<EntryHandle<Platform, Subsystem>> {
self.0
.upgrade()
.map(|entry| EntryHandle(entry, PhantomData))
}
Comment on lines +614 to +618

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One subtle issue with this lies on

pub(crate) fn close_and_duplicate_if_shared<
Subsystem: FdEnabledSubsystem,
F: FnOnce(&Subsystem::Entry) -> bool,
>(
&mut self,
fd: &TypedFd<Subsystem>,
can_close_immediately: F,
) -> Option<CloseResult<Subsystem>> {
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.entry.read().as_subsystem::<Subsystem>()) {
fd.x.mark_as_closed();
let entry = Arc::into_inner(old.x)
.map(|shared| RwLock::into_inner(shared.entry))
.map(DescriptorEntry::into_subsystem_entry::<Subsystem>)
.unwrap();
Some(CloseResult::Closed(entry))

Now Arc::into_inner(old.x)...unwrap() may panic. Closing a socket cannot be done simply via Drop. It needs to take the ownership of the entry. One potential fix suggested by copilot is to replace it with Arc::try_unwrap and return CloseResult::Duplicated on the error path.


/// 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
Expand Down
15 changes: 15 additions & 0 deletions litebox/src/pipes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,3 +729,18 @@ crate::fd::enable_fds_for_subsystem! {
PipeEnd<Platform>;
-> PipeFd<Platform>;
}

impl<Platform: RawSyncPrimitivesProvider + TimeProvider> DescriptorEntry<Platform> {
/// 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<R>(&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()),
}
}
}
81 changes: 81 additions & 0 deletions litebox_runner_linux_userland/tests/epoll_dup.c
Original file line number Diff line number Diff line change
@@ -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 <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <unistd.h>

#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;
}
Loading