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:
@@ -396,7 +396,39 @@ fn wait_fd(fd: std::os::fd::RawFd, readable: bool, writable: bool) -> std::io::R
|
||||
let mut io = inner.io.lock().unwrap();
|
||||
io.as_mut().expect("io thread not started").epoll_register(fd, me, epoch, readable, writable)
|
||||
})?;
|
||||
|
||||
// If a terminal stop unwinds us out of the park below, the registration
|
||||
// must not outlive us: a stale `waiters` entry fails every future
|
||||
// `wait_*` on this fd with AlreadyExists, and the kernel-side ADD leaks
|
||||
// until the fd happens to be reused. Clean up iff the entry is still
|
||||
// ours — a `FdReady` racing the stop may have consumed it already (it
|
||||
// removes + DELs under the io lock), after which the fd may even carry
|
||||
// ANOTHER actor's fresh registration; in that case touch nothing.
|
||||
struct Dereg {
|
||||
fd: std::os::fd::RawFd,
|
||||
me: Pid,
|
||||
epoch: u32,
|
||||
}
|
||||
impl Drop for Dereg {
|
||||
fn drop(&mut self) {
|
||||
with_runtime(|inner| {
|
||||
let mut io = inner.io.lock().unwrap();
|
||||
if let Some(io) = io.as_mut() {
|
||||
if io.waiters.get(&self.fd) == Some(&(self.me, self.epoch)) {
|
||||
io.waiters.remove(&self.fd);
|
||||
io.epoll_deregister(self.fd);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let guard = Dereg { fd, me, epoch };
|
||||
park_current();
|
||||
// Normal wake: the FdReady path removed the entry and DEL'd the fd
|
||||
// before the unpark, so the guard's check would be a guaranteed no-op —
|
||||
// skip the io lock on the hot path. (Dereg owns nothing; forget leaks
|
||||
// no resource.)
|
||||
std::mem::forget(guard);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user