{"record":{"id":"26e438a0536832b8","repo":"atuinsh/atuin","slug":"master-lock","errorCode":null,"errorMessage":"master lock","messagePattern":"master lock","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/atuin-lab-share/src/subshell.rs","lineNumber":119,"sourceCode":"    /// boxed PTY master survives inside the resizer closure; the reader and\n    /// writer carry their own dups of the master fd.\n    ///\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();","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/atuinsh/atuin/blob/15fe1318f1df51de604262eb50734c9883d48e7b/crates/atuin-lab-share/src/subshell.rs#L101-L137","documentation":"`Subshell::into_parts` locks the PTY master's internal `Mutex` to split it into reader/writer/resizer parts, and `.expect(\"master lock\")` panics when the lock is poisoned. A std `Mutex` is poisoned when some thread panicked while holding it, so this panic is always a downstream symptom of an earlier panic elsewhere; the function's own doc comment names fd exhaustion in `try_clone_reader` as the realistic first failure.","triggerScenarios":"Any thread panicking while holding the master's lock - most plausibly `try_clone_reader` failing with EMFILE inside a prior `into_parts` call, or a panic inside a resize/write path that locks the same master - followed by another attempt to lock it.","commonSituations":"Long-lived lab-share sessions leaking file descriptors until `try_clone_reader` panics; a session torn down mid-setup; user code that locks the same `Master` and panics while holding it.","solutions":["Fix the first panic: resolve the fd exhaustion or error that poisoned the lock (check `ulimit -n`, count entries in /proc/self/fd)","Recover instead of panicking: `self.master.lock().unwrap_or_else(|e| e.into_inner())` - the PTY state is still usable","Audit every code path that locks the master so no `.expect`/panic can fire while it is held"],"exampleFix":"// before\nlet master = self.master.lock().expect(\"master lock\");\n\n// after\nlet master = self.master\n    .lock()\n    .unwrap_or_else(|poisoned| poisoned.into_inner());","handlingStrategy":"fallback","validationCode":"// Detect poisoning before it panics, and log the root cause\nif let Ok(master) = self.master.try_lock() {\n    // healthy path\n    drop(master);\n} else if self.master.is_poisoned() {\n    log::warn!(\"master lock poisoned by an earlier panic; recovering\");\n}","typeGuard":"fn master_usable(master: &Mutex<Master>) -> bool {\n    !master.is_poisoned()\n}","tryCatchPattern":"// Recover the guarded data instead of propagating the panic:\n// the PTY state protected by the mutex is still valid\nlet master = self\n    .master\n    .lock()\n    .unwrap_or_else(|poisoned| poisoned.into_inner());","preventionTips":["Never let a panic escape while holding a shared Mutex - convert expects into Results inside the critical section","Log the FIRST panic; a poisoned lock is always a symptom, not the disease","Keep critical sections around the PTY master minimal so fewer panics can fire inside them"],"tags":["rust","pty","mutex","panic","concurrency","portable-pty"],"backgroundTag":"mutex-poisoned","analyzedSha":"15fe1318f1df51de604262eb50734c9883d48e7b","analyzedAt":"2026-08-19T08:56:57.719Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}