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
- Find the FIRST panic in the process output - 'mutex poisoned' is the symptom; the earlier panic during log writing is the cause. Fix that
- Restart the process to clear the poisoned lock - nothing on disk is broken
- 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
- Read backwards in the log for the FIRST panic - poison is always secondary
- Keep tracing writer/MakeWriter code simple and total so it can never panic mid-write
- Guard feedback upload with catch_unwind - diagnostics capture must not be load-bearing
- A process restart clears the poison; no on-disk repair is needed
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
- current-time request failed: code={} message={}
- current-time request was canceled: {err}
- plugin cache root should be absolute: {err}
- generated input schema for {name} should parse: {err}
- generated skill tool schema should serialize: {err}
AI-assisted analysis of openai/codex@339751715c (2026-08-25).
Data as JSON: /api/errors/6a30a6ce8f9d47ea.
Report an issue: GitHub.