{"record":{"id":"9895a7cdb18a195f","repo":"atuinsh/atuin","slug":"clone-pty-reader","errorCode":null,"errorMessage":"clone pty reader","messagePattern":"clone pty reader","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/atuin-lab-share/src/subshell.rs","lineNumber":121,"sourceCode":"    ///\n    /// The subshell owns its child outright, so `stop` kills it, and `wait`\n    /// applies the exit-code mapping the session has always used: the child's\n    /// own code when the wait succeeds (non-`i32` codes clamp to 1), 0 when\n    /// it fails. Everything else is the subshell's defaults: no bootstrap (a\n    /// fresh shell starts blank), synthetic query answers (the compositor\n    /// swallows its output, so nothing else would reply), and hub resizes\n    /// applied to the child PTY.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the reader cannot be cloned (the process is out of file\n    /// descriptors) or the writer was already taken — impossible on a freshly\n    /// spawned subshell, which is the only caller.\n    fn into_parts(self) -> crate::Result<SourceParts> {\n        let (reader, writer) = {\n            let master = self.master.lock().expect(\"master lock\");\n            (\n                master.try_clone_reader().expect(\"clone pty reader\"),\n                master.take_writer().expect(\"take pty writer\"),\n            )\n        };\n        let resizer = PtyResizer(self.master);\n        // Terminates the child without owning it, so the session can stop the\n        // child while `wait` runs on the blocking pool.\n        let mut killer = self.child.clone_killer();\n        let mut child = self.child;\n        Ok(SourceParts {\n            reader: Box::new(ByteReader(reader)),\n            writer,\n            resizer: Box::new(move |size| resizer.resize(size)),\n            stop: Box::new(move || {\n                // Best-effort, exactly as the session's kill switch always\n                // treated it: a failed kill still reaches `wait`'s mapping.\n                let _ = killer.kill();\n            }),\n            wait: Box::new(move || match child.wait() {","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/atuinsh/atuin/blob/15fe1318f1df51de604262eb50734c9883d48e7b/crates/atuin-lab-share/src/subshell.rs#L103-L139","documentation":"`into_parts` clones the PTY master's reader with `try_clone_reader().expect(\"clone pty reader\")` to build the subshell's event source. Cloning duplicates the underlying file descriptor, and it fails when the process has hit its descriptor limit (EMFILE/ENFILE) or the OS refuses the dup - the code's own doc comment states 'the process is out of file descriptors'.","triggerScenarios":"Spawning and tearing down many subshells (each PTY consumes several fds) until RLIMIT_NOFILE is exhausted; leaking readers from prior sessions; a low `ulimit -n` in containerized or daemonized environments.","commonSituations":"Docker/systemd services with a low `LimitNOFILE` default (often 1024); stress tests spawning hundreds of PTYs; long-running sessions that never drop `SourceParts.reader`.","solutions":["Raise the fd limit: `ulimit -n 65535`, `LimitNOFILE=` in systemd, `--ulimit nofile=` in Docker","Find and fix leaked PTY readers - make sure old sessions' `SourceParts` are dropped","Convert the expect into a `crate::Result` error so callers can report the failure and back off instead of crashing"],"exampleFix":"// before\nmaster.try_clone_reader().expect(\"clone pty reader\"),\n\n// after\nlet reader = master\n    .try_clone_reader()\n    .map_err(crate::Error::ClonePtyReader)?;","handlingStrategy":"validation","validationCode":"// Check fd headroom before spawning another PTY-owning subshell\nfn fd_headroom(min_free: usize) -> bool {\n    let open = std::fs::read_dir(\"/proc/self/fd\").map(|d| d.count()).unwrap_or(0);\n    let hard = 1024; // or read via rlimit crate: rlimit::getrlimit(rlimit::Resource::NOFILE)\n    (hard.saturating_sub(open)) >= min_free\n}\n\nif !fd_headroom(8) {\n    return Err(SessionError::FdExhaustion);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Raise RLIMIT_NOFILE (ulimit -n, LimitNOFILE=, --ulimit nofile=) in service and container environments","Drop PTY readers/writers promptly when sessions end; audit for leaks with ls /proc/self/fd","Keep a small fd budget check before spawning subshells in long-running processes"],"tags":["rust","pty","fd-exhaustion","emfile","panic","ulimit"],"backgroundTag":"too-many-open-files","analyzedSha":"15fe1318f1df51de604262eb50734c9883d48e7b","analyzedAt":"2026-08-19T08:56:57.719Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}