GraphiteEditor/Graphite · error

Failed to lock internal overlay context

Error message

Failed to lock internal overlay context

What it means

OverlayContext stores its scene and drawing state behind Arc<Mutex<OverlayContextInternal>>. Clone::clone locks the mutex to snapshot visibility_settings, and .expect("Failed to lock internal overlay context") fires when lock() returns Err. In Rust that happens when the mutex is poisoned (a panic occurred on some thread while holding a guard), after which every later lock panics too — or when the same non-reentrant mutex is locked again on the same thread (reentrant deadlock; on single-threaded WASM this hangs rather than returning Err).

Source

Thrown at editor/src/messages/portfolio/document/overlays/utility_types_native.rs:180

	pub fn handles(&self) -> bool {
		self.all && self.anchors && self.handles
	}
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct OverlayContext {
	// Serde functionality isn't used but is required by the message system macros
	#[serde(skip)]
	internal: Arc<Mutex<OverlayContextInternal>>,
	pub viewport: ViewportMessageHandler,
	pub visibility_settings: OverlaysVisibilitySettings,
}

impl Clone for OverlayContext {
	fn clone(&self) -> Self {
		let internal = self.internal.lock().expect("Failed to lock internal overlay context");
		let visibility_settings = internal.visibility_settings;
		drop(internal); // Explicitly release the lock before cloning the Arc<Mutex<_>>
		Self {
			internal: self.internal.clone(),
			viewport: self.viewport,
			visibility_settings,
		}
	}
}

// Manual implementations since Scene doesn't implement PartialEq or Debug
impl PartialEq for OverlayContext {
	fn eq(&self, other: &Self) -> bool {
		self.viewport == other.viewport && self.visibility_settings == other.visibility_settings
	}
}

impl std::fmt::Debug for OverlayContext {

View on GitHub (pinned to c507b35645)

Solutions

  1. Find the FIRST panic in the crash sequence — poisoning is a secondary failure; fix the original panic that held the lock.
  2. Recover from poisoning instead of panicking: lock().unwrap_or_else(std::sync::PoisonError::into_inner), since the internal scene data is recoverable drawing state.
  3. Audit for reentrant locking (clone or draw while a guard is alive) and shorten guard lifetimes.
  4. If contention or reentrancy is structural, replace the Mutex with per-call state or a lock-free design.

Example fix

// before
let internal = self.internal.lock().expect("Failed to lock internal overlay context");

// after
use std::sync::PoisonError;
let internal = self.internal.lock().unwrap_or_else(PoisonError::into_inner);
Defensive patterns

Strategy: fallback

Try / catch

use std::sync::PoisonError;
let internal = match self.internal.lock() {
	Ok(guard) => guard,
	Err(poisoned) => {
		log::warn!("overlay context mutex poisoned; recovering");
		poisoned.into_inner()
	}
};

Prevention

When it happens

Trigger: Any panic anywhere between a lock() and guard drop elsewhere in overlay code poisons the mutex; the next OverlayContext::clone() then panics with this message. Alternatively, cloning from within a scope that already holds a guard deadlocks.

Common situations: A sibling overlay panic (drawing expect, take_scene, internal accessor) firing first and poisoning the mutex, turning every subsequent clone into this exact panic; error-path code cloning the context while iterating its internals.

Related errors


AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16). Data as JSON: /api/errors/e4254d4d7908ed63. Report an issue: GitHub.