Skip to content

Keep epoll interest alive while a duplicate of the registered fd survives - #1230

Open
Will Portnoy (willportnoy) wants to merge 3 commits into
mainfrom
wportnoy/epoll-dup-survival
Open

Keep epoll interest alive while a duplicate of the registered fd survives#1230
Will Portnoy (willportnoy) wants to merge 3 commits into
mainfrom
wportnoy/epoll-dup-survival

Conversation

@willportnoy

Copy link
Copy Markdown
Member

Problem

Linux epoll(7) removes a descriptor from an interest list only after every descriptor referring to the underlying open file description (OFD) has been closed. The shim instead anchored each interest to the per-descriptor TypedFd, so closing the registered descriptor dropped the interest even when a dup referring to the same OFD was still open — EpollEntry::poll bailed on a dead Weak<TypedFd> and readiness was never delivered.

Fix

Anchor epoll interest to the open file description:

  • Add WeakEntryHandle to litebox::fd — a durable, dup-surviving weak reference to a descriptor's shared entry, plus EntryHandle::downgrade/as_ptr/with_shared_metadata.
  • Re-point epoll's DescriptorRef at a per-subsystem WeakEntryHandle, key interests by the OFD's stable address, and re-poll through the shared entry (eventfd/unix/pipe via the entry's IOPollable, socket/file via aliased metadata). Observer registration is unchanged; it already targets the shared pollable.
  • Expose with_iopollable on the pipe entry so a pipe can be polled without a live per-descriptor PipeFd.

Covers all six epoll-able fd types on main (eventfd, unix, pipe, socket, file, and the pre-existing epoll-on-epoll unimplemented!()), via exhaustive matches.

Test

tests/epoll_dup.c: register an eventfd, dup it, close the original, and verify the interest still delivers events and survives re-arming. Passes natively, fails without this change under Litebox, and passes with it.

…red fd survives

Linux epoll(7) removes a file descriptor from an interest list only after
every descriptor referring to the underlying open file description has
been closed. The shim instead anchored each interest to the
per-descriptor `TypedFd`, so closing the registered descriptor dropped
the interest even when a `dup` referring to the same open file
description remained open: `EpollEntry::poll` bailed on a dead
`Weak<TypedFd>` and the readiness was never delivered.

Anchor epoll interest to the open file description instead:

- Add `WeakEntryHandle` to `litebox::fd`: a durable, dup-surviving weak
  reference to a descriptor's shared entry, plus `EntryHandle::downgrade`,
  `as_ptr`, and shared (open-file-description-level) metadata access.
- Re-point epoll's `DescriptorRef` at a per-subsystem `WeakEntryHandle`,
  key interests by the open file description's stable address, and re-poll
  through the shared entry (eventfd/unix/pipe via the entry's `IOPollable`,
  socket/file via aliased metadata). Observer registration is unchanged;
  it already targets the shared pollable.
- Expose `with_iopollable` on the pipe entry so a pipe can be polled
  without a live per-descriptor `PipeFd`.

Add tests/epoll_dup.c: register an eventfd, dup it, close the original,
and verify the interest still delivers events and survives re-arming. It
passes natively, fails without this change under Litebox, and passes with
it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1494e366-b3cf-4196-91a0-1430cb9d5cc8
Comment thread litebox/src/fd/mod.rs
…criptor

`close_and_duplicate_if_shared` checked `Arc::strong_count == 1` and then
took ownership with `Arc::into_inner(...).unwrap()`. With durable
`WeakEntryHandle`s now held by epoll interests, another thread can
`upgrade()` the shared entry lock-free (bypassing the descriptor-table
lock) between the count check and the unwrap, transiently raising the
strong count and panicking the unwrap.

Take ownership with `Arc::try_unwrap` instead and, on the error path,
put the entry back and return `CloseResult::Duplicated` so the
descriptor is closed once the transient reference drops — the same
self-healing path already used when the entry is genuinely shared.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1494e366-b3cf-4196-91a0-1430cb9d5cc8
…rvival

# Conflicts:
#	litebox_shim_linux/src/syscalls/epoll.rs
@github-actions

Copy link
Copy Markdown

🤖 SemverChecks 🤖 No breaking API changes detected

Note: this does not mean API is unchanged, or even that there are no breaking changes; simply, none of the detections triggered.

@jaybosamiya-ms Jay Bosamiya (Microsoft) (jaybosamiya-ms) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks Will, I reviewed the changes in the litebox core, since this is changing our API surface. I think some changes are needed to make this more compatible with future evolution. The base of the changes is mostly reasonable, just needs some tweaks.

The changes should be fairly straightforward, but do let me know if you'd prefer I open up a PR with the changes I have in mind to the core, instead of tweaking those things over this PR.

Oh also, please do make sure to keep the PR description human-written, not AI-generated-human-reviewed.

Comment thread litebox/src/fd/mod.rs
Comment on lines +596 to +604
/// 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()
}

Choose a reason for hiding this comment

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

I am not sure I agree with exposing a *const () here, over-broadens our interface and guarantees (if we introduce this, now we are promising that we will always give out the address specifically). The correct move is probably to expose an opaque PartialEq+Eq+Hash+Clone+Copy EntryStableKey. That would allow more stable utilization.

Choose a reason for hiding this comment

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

Also another issue with it being a *const () is that, it would prevent us from making the stronger guarantee in the future that re-allocations even at the same address would be caught. If we make it an opaque EntryStableKey we can strengthen things without breaking any API contract, and automatically things get better.

Comment thread litebox/src/fd/mod.rs
Comment on lines +583 to +594
/// 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)
}

Choose a reason for hiding this comment

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

Nit: doc comment + function naming could be better for consistency:

Suggested change
/// 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)
}
/// Apply `f` on metadata at the entry, if it exists.
///
/// In contrast to [`Descriptors::with_metadata`], this obtains entry-level metadata.
/// For FD-specific metadata, one necessarily needs the specific FD.
pub fn with_entry_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)
}

Comment thread litebox/src/fd/mod.rs
Comment on lines +606 to +614
/// 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)
}

Choose a reason for hiding this comment

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

Nit: doc comment, it is not about surviving dup, it is about getting a non-owning WeakEntryHandle, that's all. Also, we do not need to document upgrade here.

Comment thread litebox/src/fd/mod.rs
Comment on lines +617 to +624
/// 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).

Choose a reason for hiding this comment

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

Similarly, this is not dup-surviving, it is simply a weak reference to a descriptor entry. This is too much superfluous documentation. The upgrade stuff also is literally the obvious thing for any weak pointer, there is no reason to state that it works as long as something strong still exists. The whole doc comment here could just be:

Suggested change
/// 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).
/// A weak-reference to a descriptor entry.

Comment thread litebox/src/fd/mod.rs
/// interest, per Linux `epoll(7)` semantics).
pub struct WeakEntryHandle<Platform: RawSyncPrimitivesProvider, Subsystem: FdEnabledSubsystem>(
Weak<SharedEntry<Platform>>,
PhantomData<fn(Subsystem) -> Subsystem>,

Choose a reason for hiding this comment

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

Why is this inconsistent with EntryHandle's PhantomData?

Comment thread litebox/src/fd/mod.rs
Comment on lines +642 to +652
/// 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()
}

Choose a reason for hiding this comment

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

Similarly, this should switch to EntryStableKey

Comment thread litebox/src/pipes.rs
Comment on lines +735 to +739
///
/// 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.

Choose a reason for hiding this comment

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

This explanation is unnecessary to keep here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants