atuinsh/atuin · error

master lock

Error message

master lock

What it means

`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.

Source

Thrown at crates/atuin-lab-share/src/subshell.rs:119

    /// boxed PTY master survives inside the resizer closure; the reader and
    /// writer carry their own dups of the master fd.
    ///
    /// The subshell owns its child outright, so `stop` kills it, and `wait`
    /// applies the exit-code mapping the session has always used: the child's
    /// own code when the wait succeeds (non-`i32` codes clamp to 1), 0 when
    /// it fails. Everything else is the subshell's defaults: no bootstrap (a
    /// fresh shell starts blank), synthetic query answers (the compositor
    /// swallows its output, so nothing else would reply), and hub resizes
    /// applied to the child PTY.
    ///
    /// # Panics
    ///
    /// Panics if the reader cannot be cloned (the process is out of file
    /// descriptors) or the writer was already taken — impossible on a freshly
    /// spawned subshell, which is the only caller.
    fn into_parts(self) -> crate::Result<SourceParts> {
        let (reader, writer) = {
            let master = self.master.lock().expect("master lock");
            (
                master.try_clone_reader().expect("clone pty reader"),
                master.take_writer().expect("take pty writer"),
            )
        };
        let resizer = PtyResizer(self.master);
        // Terminates the child without owning it, so the session can stop the
        // child while `wait` runs on the blocking pool.
        let mut killer = self.child.clone_killer();
        let mut child = self.child;
        Ok(SourceParts {
            reader: Box::new(ByteReader(reader)),
            writer,
            resizer: Box::new(move |size| resizer.resize(size)),
            stop: Box::new(move || {
                // Best-effort, exactly as the session's kill switch always
                // treated it: a failed kill still reaches `wait`'s mapping.
                let _ = killer.kill();

View on GitHub (pinned to 15fe1318f1)

Solutions

  1. Fix the first panic: resolve the fd exhaustion or error that poisoned the lock (check `ulimit -n`, count entries in /proc/self/fd)
  2. Recover instead of panicking: `self.master.lock().unwrap_or_else(|e| e.into_inner())` - the PTY state is still usable
  3. Audit every code path that locks the master so no `.expect`/panic can fire while it is held

Example fix

// before
let master = self.master.lock().expect("master lock");

// after
let master = self.master
    .lock()
    .unwrap_or_else(|poisoned| poisoned.into_inner());
Defensive patterns

Strategy: fallback

Validate before calling

// Detect poisoning before it panics, and log the root cause
if let Ok(master) = self.master.try_lock() {
    // healthy path
    drop(master);
} else if self.master.is_poisoned() {
    log::warn!("master lock poisoned by an earlier panic; recovering");
}

Type guard

fn master_usable(master: &Mutex<Master>) -> bool {
    !master.is_poisoned()
}

Try / catch

// Recover the guarded data instead of propagating the panic:
// the PTY state protected by the mutex is still valid
let master = self
    .master
    .lock()
    .unwrap_or_else(|poisoned| poisoned.into_inner());

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of atuinsh/atuin@15fe1318f1 (2026-08-19). Data as JSON: /api/errors/26e438a0536832b8. Report an issue: GitHub.