astrid-runtime/astrid · critical
device revocation map poisoned during startup hydration
Error message
device revocation map poisoned during startup hydration
What it means
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.
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.
Example fix
// before
*self.revoked_key_ids.write().expect("device revocation map poisoned during startup hydration") = devices;
// after
let mut guard = self
.revoked_key_ids
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = devices; Defensive patterns
Strategy: try-catch
Validate before calling
// Rust: check poison before installing device revocations
let guard = self.revoked_key_ids.write();
if guard.is_err() { log::error!("revoked_key_ids poisoned; fixing root cause required"); } Try / catch
// Rust: recover-by-overwrite when hydration data is authoritative
*self.revoked_key_ids.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = devices; Prevention
- 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.
When it happens
Trigger: Calling `hydrate_revocations` when the `revoked_key_ids` `RwLock` returns `Err(PoisonError)` because a prior thread panicked while holding it.
Common situations: 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.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- revocation map poisoned during startup hydration
- an incomplete capsule authority update exists at
- corpus input changed while its baseline snapshot was…
- durable capsule package
- inspect capsule projection
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/6100f5956851c428.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-gateway/src/state.rs:400
pub async fn hydrate_revocations(&self) -> anyhow::Result<()> {
let Some(store) = self.storage_kv.as_deref() else {
if crate::revocations::legacy_file_exists()? {
anyhow::bail!(
"gateway revocation storage is unavailable while a legacy revocation file exists"
);
}
return Ok(());
};
let _ = crate::revocations::migrate_legacy_file(store).await?;
let (principals, devices) = crate::revocations::load_from_store(store).await?;
*self
.revoked_at
.write()
.expect("revocation map poisoned during startup hydration") = principals;
*self
.revoked_key_ids
.write()
.expect("device revocation map poisoned during startup hydration") = devices;
Ok(())
}
/// Build a bus-direct admin client bound to `caller`. Routes
/// hosted in this same process talk to the kernel over the
/// shared event bus rather than the Unix socket — bypasses the
/// `astrid-capsule-cli` proxy entirely and removes the 19 RPS
/// admin-throughput ceiling the socket path imposes.
///
/// # Errors
/// Returns an internal error if the state was built without a
/// live event bus (the standalone tests-only constructor). In
/// production the daemon always wires it up.
pub fn admin_client(
&self,
caller: astrid_core::PrincipalId,
) -> Result<crate::bus_admin::BusAdminClient, crate::error::GatewayError> {
let bus = self.event_bus.clone().ok_or_else(|| {View on GitHub (pinned to affd8760f4)