{"record":{"id":"6100f5956851c428","repo":"astrid-runtime/astrid","slug":"device-revocation-map-poisoned-during-startup-hydr","errorCode":null,"errorMessage":"device revocation map poisoned during startup hydration","messagePattern":"device revocation map poisoned during startup hydration","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/astrid-gateway/src/state.rs","lineNumber":400,"sourceCode":"    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(\n        &self,\n        caller: astrid_core::PrincipalId,\n    ) -> Result<crate::bus_admin::BusAdminClient, crate::error::GatewayError> {\n        let bus = self.event_bus.clone().ok_or_else(|| {","sourceCodeStart":382,"sourceCodeEnd":418,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/state.rs#L382-L418","documentation":"Same poisoning scenario as the `revoked_at` map, but for the `revoked_key_ids` map (per-device key revocations). `hydrate_revocations` panics if that `RwLock` is poisoned when installing the device revocation list loaded from the store. The device map must be populated before the gateway serves requests, so failure is fatal.","triggerScenarios":"Calling `hydrate_revocations` when the `revoked_key_ids` `RwLock` returns `Err(PoisonError)` because a prior thread panicked while holding it.","commonSituations":"A panic in code that iterates device key revocations (e.g. while verifying a request key id) leaves the lock poisoned; a later startup hydration or reload then fails with this message.","solutions":["Fix the upstream panic in code that holds the `revoked_key_ids` lock.","Make lock-protected mutation paths infallible or return errors instead of panicking under the lock.","Use `unwrap_or_else(PoisonError::into_inner)` in hydration if overwriting with fresh store data is verified safe.","Sequence hydration before request-handling threads start so nothing else can poison the lock first."],"exampleFix":"// before\n*self.revoked_key_ids.write().expect(\"device revocation map poisoned during startup hydration\") = devices;\n// after\nlet mut guard = self\n    .revoked_key_ids\n    .write()\n    .unwrap_or_else(std::sync::PoisonError::into_inner);\n*guard = devices;","handlingStrategy":"try-catch","validationCode":"// Rust: check poison before installing device revocations\nlet guard = self.revoked_key_ids.write();\nif guard.is_err() { log::error!(\"revoked_key_ids poisoned; fixing root cause required\"); }","typeGuard":null,"tryCatchPattern":"// Rust: recover-by-overwrite when hydration data is authoritative\n*self.revoked_key_ids.write()\n    .unwrap_or_else(std::sync::PoisonError::into_inner) = devices;","preventionTips":["Keep critical sections under revocation locks panic-free (no unwrap on untrusted data).","Hydrate device revocations before any key-verification code runs.","Consider Mutex from a no-poison crate (e.g. parking_lot) for these maps.","Audit any code path that indexes or unwraps while holding the lock."],"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"}