fix(io): deregister a stopped actor's fd wait on the unwind path
A stopped actor unwinding out of wait_fd's park leaked its waiters entry and the kernel-side EPOLLONESHOT registration; the stale entry then failed every future wait_*() on that fd with AlreadyExists (the defensive bare DEL in epoll_register sits behind the contains_key check, so it never ran). Fix where the invariant breaks: a drop guard in wait_fd, armed after a successful register and forgotten on the normal wake (where FdReady already removed + DEL'd). On unwind it cleans up iff the entry is still this wait's (pid, epoch) — an entry consumed by a racing FdReady means the fd may carry another actor's fresh registration, which must be left alone. Closes the v0.2 fd-hygiene TODO.
This commit is contained in:
@@ -44,11 +44,13 @@
|
||||
//!
|
||||
//! Fd hygiene
|
||||
//! ==========
|
||||
//! If an actor dies while waiting on an fd, the registration is leaked
|
||||
//! (the fd stays in the epollfd, armed). EPOLLONESHOT bounds the damage:
|
||||
//! at most one stale wakeup, after which the kernel disarms. The stale
|
||||
//! wakeup hits a dead pid in `waiters` and is dropped. Acceptable for v0.2;
|
||||
//! a future pass should DEL on actor death.
|
||||
//! An actor stopped while waiting on an fd unwinds out of `wait_fd`'s park;
|
||||
//! a drop guard there (armed after a successful register, forgotten on a
|
||||
//! normal wake) removes the `waiters` entry iff it is still that wait's
|
||||
//! `(pid, epoch)` and only then `EPOLL_CTL_DEL`s the fd — an entry already
|
||||
//! consumed by a racing `FdReady` means the fd may carry someone else's
|
||||
//! fresh registration, which must be left alone. `epoll_register` keeps a
|
||||
//! defensive bare DEL before ADD as belt-and-braces.
|
||||
//!
|
||||
//! Buffers used with `read`/`write` should be on fds opened with
|
||||
//! `O_NONBLOCK`. If they aren't, the syscall may block the scheduler
|
||||
@@ -282,10 +284,10 @@ impl IoThread {
|
||||
));
|
||||
}
|
||||
|
||||
// Defensive cleanup: if a previous actor died while waiting on this
|
||||
// fd, the kernel-side registration was leaked (we don't walk all
|
||||
// waiters on actor death). A bare DEL is harmless if the fd isn't
|
||||
// registered (ENOENT), and removes any leak.
|
||||
// Belt-and-braces: the unwind guard in `wait_fd` is responsible for
|
||||
// cleaning up a stopped waiter's registration, but a bare DEL is
|
||||
// harmless if the fd isn't registered (ENOENT) and removes any leak
|
||||
// a path we haven't thought of might leave behind.
|
||||
unsafe {
|
||||
libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_DEL, fd, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user