Keep epoll interest alive while a duplicate of the registered fd survives - #1230
Keep epoll interest alive while a duplicate of the registered fd survives#1230Will Portnoy (willportnoy) wants to merge 3 commits into
Conversation
…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
| pub fn upgrade(&self) -> Option<EntryHandle<Platform, Subsystem>> { | ||
| self.0 | ||
| .upgrade() | ||
| .map(|entry| EntryHandle(entry, PhantomData)) | ||
| } |
There was a problem hiding this comment.
One subtle issue with this lies on
Lines 133 to 153 in e18e65a
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.
There was a problem hiding this comment.
AI-authored, human-reviewed.
Good catch — fixed in fb43794. You're right the Arc::strong_count == 1 check followed by Arc::into_inner(...).unwrap() is racy now: a WeakEntryHandle::upgrade() (which this PR newly lets epoll interests do) runs lock-free, so it can transiently bump the strong count between the check and the unwrap and panic it.
Switched to Arc::try_unwrap; on the error path it puts the entry back and returns CloseResult::Duplicated, so the descriptor is closed once the transient reference drops — the same self-healing queued-closure path already taken when the entry is genuinely shared.
I also checked the sibling remove(): it uses Arc::into_inner(...).map(...) (no unwrap), and its socket callers discard the entry via let _ =, so there's no analogous panic or orderly-close regression there.
…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
|
🤖 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. |
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-descriptorTypedFd, so closing the registered descriptor dropped the interest even when adupreferring to the same OFD was still open —EpollEntry::pollbailed on a deadWeak<TypedFd>and readiness was never delivered.Fix
Anchor epoll interest to the open file description:
WeakEntryHandletolitebox::fd— a durable,dup-surviving weak reference to a descriptor's shared entry, plusEntryHandle::downgrade/as_ptr/with_shared_metadata.DescriptorRefat a per-subsystemWeakEntryHandle, key interests by the OFD's stable address, and re-poll through the shared entry (eventfd/unix/pipe via the entry'sIOPollable, socket/file via aliased metadata). Observer registration is unchanged; it already targets the shared pollable.with_iopollableon the pipe entry so a pipe can be polled without a live per-descriptorPipeFd.Covers all six epoll-able fd types on
main(eventfd, unix, pipe, socket, file, and the pre-existing epoll-on-epollunimplemented!()), via exhaustive matches.Test
tests/epoll_dup.c: register an eventfd,dupit, 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.