{"record":{"id":"29d4a96c9d7eb541","repo":"herdrdev/herdr","slug":"poll-encountered-invalid-pty-actor-fd","errorCode":null,"errorMessage":"poll encountered invalid PTY actor fd","messagePattern":"poll encountered invalid PTY actor fd","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/pty/fd.rs","lineNumber":199,"sourceCode":"            let err = std::io::Error::last_os_error();\n            if err.kind() == std::io::ErrorKind::Interrupted {\n                let Some(deadline) = deadline else {\n                    continue;\n                };\n                let remaining = deadline.saturating_duration_since(Instant::now());\n                if remaining.is_zero() {\n                    return Ok(PtyWakeReadiness::default());\n                }\n                remaining_timeout_ms = remaining.as_millis().clamp(1, i32::MAX as u128) as i32;\n                continue;\n            }\n            return Err(err);\n        }\n\n        let pty_revents = if poll_pty { poll_fds[0].revents } else { 0 };\n        let wake_revents = poll_fds[1].revents;\n        if (pty_revents | wake_revents) & libc::POLLNVAL != 0 {\n            return Err(std::io::Error::new(\n                std::io::ErrorKind::BrokenPipe,\n                \"poll encountered invalid PTY actor fd\",\n            ));\n        }\n        if pty_revents & libc::POLLERR != 0 {\n            return Err(std::io::Error::new(\n                std::io::ErrorKind::BrokenPipe,\n                \"poll encountered PTY fd error\",\n            ));\n        }\n\n        return Ok(PtyWakeReadiness {\n            pty_read_ready: pty_revents & (libc::POLLIN | libc::POLLHUP) != 0,\n            pty_write_ready: pty_revents & (libc::POLLOUT | libc::POLLHUP) != 0,\n            wake_ready: wake_revents & (libc::POLLIN | libc::POLLHUP | libc::POLLERR) != 0,\n        });\n    }\n}","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/herdrdev/herdr/blob/f457cff4f2648eee85d176f8a41861241d4e8428/src/pty/fd.rs#L181-L217","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet readiness = fd::poll_pty_and_wake(pty_fd, wake_fd, timeout)?; // may poll closed fd\n\n// after\nif actor.is_released() { return Ok(Default::default()); }\nlet readiness = fd::poll_pty_and_wake(actor.pty_fd()?, actor.wake_fd()?, timeout)?;","handlingStrategy":"try-catch","validationCode":"if !actor.has_valid_fds() { return Ok(()); } // skip poll after close","typeGuard":null,"tryCatchPattern":"match fd::poll_pty_and_wake(pty, wake, ms) {\n    Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => { actor.shutdown_from_fd_error(); Ok(()) }\n    other => other,\n}","preventionTips":["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"],"tags":["pty","unix","poll","invalid-fd","fd-lifecycle"],"backgroundTag":"poll-invalid-fd","analyzedSha":"f457cff4f2648eee85d176f8a41861241d4e8428","analyzedAt":"2026-08-28T15:41:09.197Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}