herdrdev/herdr · error
poll encountered invalid PTY actor fd
Error message
poll encountered invalid PTY actor fd
What it means
poll_pty_and_wake polls the PTY fd and the actor's wake fd with libc::poll; if either returns POLLNVAL, the fd is invalid (closed or never opened) and the function fails fast with ErrorKind::BrokenPipe instead of spinning on a dead fd. POLLNVAL means the fd does not refer to an open file.
Source
Thrown at src/pty/fd.rs:199
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
let Some(deadline) = deadline else {
continue;
};
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Ok(PtyWakeReadiness::default());
}
remaining_timeout_ms = remaining.as_millis().clamp(1, i32::MAX as u128) as i32;
continue;
}
return Err(err);
}
let pty_revents = if poll_pty { poll_fds[0].revents } else { 0 };
let wake_revents = poll_fds[1].revents;
if (pty_revents | wake_revents) & libc::POLLNVAL != 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"poll encountered invalid PTY actor fd",
));
}
if pty_revents & libc::POLLERR != 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"poll encountered PTY fd error",
));
}
return Ok(PtyWakeReadiness {
pty_read_ready: pty_revents & (libc::POLLIN | libc::POLLHUP) != 0,
pty_write_ready: pty_revents & (libc::POLLOUT | libc::POLLHUP) != 0,
wake_ready: wake_revents & (libc::POLLIN | libc::POLLHUP | libc::POLLERR) != 0,
});
}
}View on GitHub (pinned to f457cff4f2)
Solutions
- Audit fd ownership: ensure poll_pty_and_wake is never called after the PTY or wake fd is closed (check actor state before polling)
- Fix the ordering so the actor loop exits before the fds are closed, or use a guard/flag marking fds invalid
- Reproduce under a race detector or with shutdown logging to find who closes the fd early
- If using owned fds, switch to a type that clears the RawFd on close so stale fds can't be polled
Example fix
// before
let readiness = fd::poll_pty_and_wake(pty_fd, wake_fd, timeout)?; // may poll closed fd
// after
if actor.is_released() { return Ok(Default::default()); }
let readiness = fd::poll_pty_and_wake(actor.pty_fd()?, actor.wake_fd()?, timeout)?; Defensive patterns
Strategy: try-catch
Validate before calling
if !actor.has_valid_fds() { return Ok(()); } // skip poll after close Try / catch
match fd::poll_pty_and_wake(pty, wake, ms) {
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => { actor.shutdown_from_fd_error(); Ok(()) }
other => other,
} Prevention
- Never poll fds after release/close; check actor state first
- Clear RawFd values when their owner closes so stale fds can't be polled
- Order shutdown so the actor loop exits before fd teardown
When it happens
Trigger: Calling poll_pty_and_wake with a PTY fd or wake fd that was already closed, dup'd incorrectly, or never initialized — e.g. polling after release/close, or using a stale RawFd stored past the fd's lifetime.
Common situations: Use-after-close races in the PTY actor loop (release during shutdown), a child exiting and the master being closed by another thread, or fd lifecycle bugs after handoff/detach rework.
Related errors
- poll encountered PTY fd error
- PTY actor was released before handoff quiesce
- timed out draining PTY writes before handoff
- PTY closed while draining writes before handoff
- failed to create private herdr ssh config directory
AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28).
Data as JSON: /api/errors/29d4a96c9d7eb541.
Report an issue: GitHub.