{"record":{"id":"bac0b762439089af","repo":"astrid-runtime/astrid","slug":"revocation-map-poisoned-during-startup-hydration","errorCode":null,"errorMessage":"revocation map poisoned during startup hydration","messagePattern":"revocation map poisoned during startup hydration","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/astrid-gateway/src/state.rs","lineNumber":396,"sourceCode":"    /// # Panics\n    ///\n    /// Panics if either in-memory revocation lock is poisoned, indicating an\n    /// earlier panic while mutating gateway security state.\n    pub async fn hydrate_revocations(&self) -> anyhow::Result<()> {\n        let Some(store) = self.storage_kv.as_deref() else {\n            if crate::revocations::legacy_file_exists()? {\n                anyhow::bail!(\n                    \"gateway revocation storage is unavailable while a legacy revocation file exists\"\n                );\n            }\n            return Ok(());\n        };\n        let _ = crate::revocations::migrate_legacy_file(store).await?;\n        let (principals, devices) = crate::revocations::load_from_store(store).await?;\n        *self\n            .revoked_at\n            .write()\n            .expect(\"revocation map poisoned during startup hydration\") = principals;\n        *self\n            .revoked_key_ids\n            .write()\n            .expect(\"device revocation map poisoned during startup hydration\") = devices;\n        Ok(())\n    }\n\n    /// Build a bus-direct admin client bound to `caller`. Routes\n    /// hosted in this same process talk to the kernel over the\n    /// shared event bus rather than the Unix socket — bypasses the\n    /// `astrid-capsule-cli` proxy entirely and removes the 19 RPS\n    /// admin-throughput ceiling the socket path imposes.\n    ///\n    /// # Errors\n    /// Returns an internal error if the state was built without a\n    /// live event bus (the standalone tests-only constructor). In\n    /// production the daemon always wires it up.\n    pub fn admin_client(","sourceCodeStart":378,"sourceCodeEnd":414,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/state.rs#L378-L414","documentation":"`hydrate_revocations` loads principals/ devices revocation lists from the store and swaps them into `RwLock`-guarded maps. The `expect` fires when the `revoked_at` lock is poisoned — i.e. another thread panicked while holding the write lock — so hydration cannot safely install the fresh maps. Panicking during startup hydration is deliberate: an unusable revocation state must not be silently ignored.","triggerScenarios":"Calling `hydrate_revocations` at startup when the `revoked_at` `RwLock` was previously poisoned by a panic in any earlier reader/writer (a `.write()` returns `Err(PoisonError)`).","commonSituations":"An earlier panic while mutating/reading revocation maps (e.g. malformed entry causing unwrap failure elsewhere), then a restart/hydration pass hits the poisoned lock; multi-threaded startup where one task panics before hydration runs.","solutions":["Find and fix the original panic that poisoned the `revoked_at` lock — poisoning is a symptom, not the root cause.","Avoid panicking while holding revocation-map locks; return `Result` from mutation paths instead.","If hydration must tolerate poison, use `unwrap_or_else(PoisonError::into_inner)` only after auditing the stale state is safe to overwrite.","Ensure hydration runs before other threads acquire these locks to eliminate the poisoning window."],"exampleFix":"// before\n*self.revoked_at.write().expect(\"revocation map poisoned during startup hydration\") = principals;\n// after\nlet mut guard = self\n    .revoked_at\n    .write()\n    .unwrap_or_else(std::sync::PoisonError::into_inner);\n*guard = principals;","handlingStrategy":"try-catch","validationCode":"// Rust: nothing to pre-validate; instead check lock state before hydration\nassert!(!is_poisoned(&self.revoked_at), \"revoked_at lock poisoned before hydration\");","typeGuard":null,"tryCatchPattern":"// Rust: tolerate poison when overwriting with fresh store data is safe\nlet mut guard = self.revoked_at.write()\n    .unwrap_or_else(std::sync::PoisonError::into_inner);\n*guard = principals;","preventionTips":["Never panic while holding a lock; return Result from mutations.","Run hydration single-threaded before request handlers start.","Add a CI test that mutates revocation maps concurrently to surface panics under the lock.","Fix root-cause panics, not the poisoning symptom."],"tags":["rust","concurrency","mutex-poisoning","startup","revocation"],"backgroundTag":"internal-invariant-violation","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}