{"record":{"id":"4ec522d9eb3147a4","repo":"GraphiteEditor/Graphite","slug":"uuid-mutex-poisoned","errorCode":null,"errorMessage":"UUID mutex poisoned","messagePattern":"UUID mutex poisoned","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"node-graph/libraries/core-types/src/uuid.rs","lineNumber":66,"sourceCode":"\n\tstatic RNG: Mutex<Option<ChaCha20Rng>> = Mutex::new(None);\n\tthread_local! {\n\t\tpub static UUID_SEED: Cell<Option<u64>> = const { Cell::new(None) };\n\t}\n\n\tpub fn set_uuid_seed(random_seed: u64) {\n\t\tUUID_SEED.with(|seed| seed.set(Some(random_seed)))\n\t}\n\n\tpub fn generate_uuid() -> u64 {\n\t\tlet Ok(mut lock) = RNG.lock() else { panic!(\"UUID mutex poisoned\") };\n\t\tif lock.is_none() {\n\t\t\tUUID_SEED.with(|seed| {\n\t\t\t\tlet random_seed = seed.get().unwrap_or(42);\n\t\t\t\t*lock = Some(ChaCha20Rng::seed_from_u64(random_seed));\n\t\t\t})\n\t\t}\n\t\tlock.as_mut().map(ChaCha20Rng::next_u64).expect(\"UUID mutex poisoned\")\n\t}\n}\n\n#[repr(transparent)]\n#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, PartialOrd, Ord, DynAny)]\n#[cfg_attr(feature = \"wasm\", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\npub struct NodeId(pub u64);\n\nimpl NodeId {\n\tpub fn new() -> Self {\n\t\tSelf(generate_uuid())\n\t}\n}\n\nimpl std::fmt::Display for NodeId {\n\tfn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n\t\twrite!(f, \"{}\", self.0)","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/node-graph/libraries/core-types/src/uuid.rs#L48-L84","documentation":"generate_uuid() in core-types guards a process-global static RNG (Mutex<Option<ChaCha20Rng>>) and panics with \"UUID mutex poisoned\" when lock() returns Err. A Rust Mutex is poisoned only after some thread panicked while holding it, so this message is always a secondary failure: the real bug is the earlier panic that left the lock poisoned. Once poisoned, every later NodeId::new() and generate_uuid() call in the entire process panics immediately.","triggerScenarios":"Any earlier panic on any thread while the RNG mutex guard is alive (the critical section in generate_uuid) poisons the lock; every subsequent call, e.g. NodeId::new() during node creation, document load, or paste, then panics with this message instead of producing an ID.","commonSituations":"A graph-evaluation worker thread hits a different .expect panic (GPU error, encoding error) and the process keeps running, after which every ID allocation dies; parallel test runs where one failing test poisons globals and cascading failures report \"poisoned\" instead of the root cause; hosts that use catch_unwind to survive panics but keep reusing the same process.","solutions":["Treat this as a symptom: find the FIRST panic in the logs (it precedes the poisoning) and fix that root cause","Restart the process or test worker to clear the poisoned mutex; the global RNG state cannot be repaired from user code","Make generate_uuid() poison-tolerant: the RNG has no cross-call invariants, so recover the guard with RNG.lock().unwrap_or_else(|poisoned| poisoned.into_inner())","Longer term, replace the global Mutex<Option<Rng>> with a lock-free atomic counter or per-thread RNGs so poisoning is structurally impossible"],"exampleFix":"// before (uuid.rs)\nlet Ok(mut lock) = RNG.lock() else { panic!(\"UUID mutex poisoned\") };\n\n// after: RNG state carries no cross-call invariants, so recovering from poisoning is safe\nlet mut lock = RNG.lock().unwrap_or_else(|poisoned| poisoned.into_inner());","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"let ids = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n    (0..count).map(|_| NodeId::new()).collect::<Vec<_>>()\n}));\nif ids.is_err() {\n    // mutex poisoned (or another ID panic): locate the ORIGINAL panic in logs, then restart this worker/process\n}","preventionTips":["Always log the first panic with a backtrace; a poisoned-mutex report later is only a symptom","Run graph evaluation on worker threads that can be torn down and recreated after a panic instead of reusing the poisoned process","Avoid code that can panic while holding global locks; prefer Result-based error handling in those critical sections"],"tags":["rust","mutex","poisoning","concurrency","uuid","panic"],"backgroundTag":"mutex-poisoned","analyzedSha":"c507b356453361e31638b8bff8f6d46b6da2961e","analyzedAt":"2026-08-16T21:57:18.596Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}