openai/codex · critical

mutex poisoned

Error message

mutex poisoned

What it means

CodexFeedback::snapshot (codex-rs/feedback/src/lib.rs:246-265) locks two std::sync::Mutexes - the log ring buffer and the tags map - with .expect("mutex poisoned"). A std Mutex becomes poisoned when a thread panics while holding it, and the same ring mutex is locked by the tracing writer path (FeedbackMakeWriter). So this panic means some earlier panic happened inside log writing while holding the lock; snapshot() merely detonates the leftover poison. The original panic appears earlier in the process output.

Source

Thrown at codex-rs/feedback/src/lib.rs:249

    /// Returns a [`tracing_subscriber`] layer that collects structured metadata for feedback.
    ///
    /// Events with `target: "feedback_tags"` are treated as key/value tags to attach to feedback
    /// uploads later.
    pub fn metadata_layer<S>(&self) -> impl Layer<S> + Send + Sync + 'static
    where
        S: tracing::Subscriber + for<'a> LookupSpan<'a>,
    {
        FeedbackMetadataLayer {
            inner: self.inner.clone(),
        }
        .with_filter(Targets::new().with_target(FEEDBACK_TAGS_TARGET, Level::TRACE))
    }

    pub fn snapshot(&self, session_id: Option<ThreadId>) -> FeedbackSnapshot {
        let bytes = {
            #[allow(clippy::expect_used)]
            let guard = self.inner.ring.lock().expect("mutex poisoned");
            guard.snapshot_bytes()
        };
        let tags = {
            #[allow(clippy::expect_used)]
            let guard = self.inner.tags.lock().expect("mutex poisoned");
            guard.clone()
        };
        FeedbackSnapshot {
            bytes,
            tags,
            feedback_diagnostics: FeedbackDiagnostics::collect_from_env(),
            thread_id: session_id
                .map(|id| id.to_string())
                .unwrap_or("no-active-thread-".to_string() + &ThreadId::new().to_string()),
        }
    }
}

View on GitHub (pinned to 339751715c)

Solutions

  1. Find the FIRST panic in the process output - 'mutex poisoned' is the symptom; the earlier panic during log writing is the cause. Fix that
  2. Restart the process to clear the poisoned lock - nothing on disk is broken
  3. If you wrap this API, recover the lock with lock().unwrap_or_else(std::sync::PoisonError::into_inner) (the buffer contents are still readable) or wrap snapshot() in catch_unwind so feedback upload cannot take the host down

Example fix

// before
let snapshot = feedback.snapshot(session_id); // panics: mutex poisoned

// after - feedback upload must never crash the host
let snapshot = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    feedback.snapshot(session_id)
}))
.ok()
.inspect_err(|_| tracing::warn!("feedback snapshot unavailable (poisoned lock)"));
Defensive patterns

Strategy: try-catch

Try / catch

let snapshot = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    feedback.snapshot(session_id)
}));
let snapshot = match snapshot {
    Ok(s) => Some(s),
    Err(_) => {
        tracing::warn!("feedback snapshot poisoned; skipping upload");
        None
    }
};

Prevention

When it happens

Trigger: Any panic on a thread holding inner.ring or inner.tags - typically inside the tracing-subscriber writer while formatting a log record into the ring buffer - after which every snapshot() call (feedback upload, tests) panics with 'mutex poisoned'.

Common situations: A formatting or ring-buffer bug panicking on a large or odd log line; test code panicking while the feedback subscriber is installed; any other panic in the process that happened mid-write. Restarting clears it - poison state is in-memory only.

Related errors


AI-assisted analysis of openai/codex@339751715c (2026-08-25). Data as JSON: /api/errors/6a30a6ce8f9d47ea. Report an issue: GitHub.