From 9045dd83d3d5fc620209ff721c32af29e7974d06 Mon Sep 17 00:00:00 2001 From: "glm-5.2" Date: Tue, 23 Jun 2026 13:33:00 +0000 Subject: [PATCH] vault: replace RwLock unwrap with poisoned-lock recovery Replace all .read().unwrap() and .write().unwrap() calls in VaultServiceHandle methods with .unwrap_or_else(|e| e.into_inner()) so a panic while holding the lock does not brick the vault for all subsequent operations. Add unit test that poisons the lock and verifies the next call recovers. --- crates/alknet-vault/src/service.rs | 45 +++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/crates/alknet-vault/src/service.rs b/crates/alknet-vault/src/service.rs index d365d41..5b773ee 100644 --- a/crates/alknet-vault/src/service.rs +++ b/crates/alknet-vault/src/service.rs @@ -132,7 +132,7 @@ impl VaultServiceHandle { /// The passphrase is the BIP39 password (may be empty string for none). /// After unlocking, derive and encrypt/decrypt operations are available. pub fn unlock(&self, phrase: &str, passphrase: Option<&str>) -> Result<(), VaultServiceError> { - let mut inner = self.inner.write().unwrap(); + let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner()); if inner.unlocked { return Err(VaultServiceError::AlreadyUnlocked); } @@ -151,7 +151,7 @@ impl VaultServiceHandle { /// Returns the generated mnemonic phrase. Store this phrase securely — /// it is the root of trust for all derived keys. pub fn unlock_new(&self, word_count: usize) -> Result { - let mut inner = self.inner.write().unwrap(); + let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner()); if inner.unlocked { return Err(VaultServiceError::AlreadyUnlocked); } @@ -172,7 +172,7 @@ impl VaultServiceHandle { /// until `unlock` is called again. Calls `zeroize()` on all sensitive /// material per ADR-038. pub fn lock(&self) { - let mut inner = self.inner.write().unwrap(); + let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner()); inner.cache.clear(); inner.seed = None; inner.mnemonic = None; @@ -181,12 +181,15 @@ impl VaultServiceHandle { /// Check whether the service is currently unlocked. pub fn is_unlocked(&self) -> bool { - self.inner.read().unwrap().unlocked + self.inner + .read() + .unwrap_or_else(|e| e.into_inner()) + .unlocked } /// Derive an Ed25519 keypair at the given path. pub fn derive_ed25519(&self, path: &str) -> Result { - let mut inner = self.inner.write().unwrap(); + let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner()); if !inner.unlocked { return Err(VaultServiceError::VaultLocked); } @@ -214,7 +217,7 @@ impl VaultServiceHandle { /// Derive an AES-256-GCM encryption key at the given path. pub fn derive_encryption_key(&self, path: &str) -> Result { - let mut inner = self.inner.write().unwrap(); + let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner()); if !inner.unlocked { return Err(VaultServiceError::VaultLocked); } @@ -248,7 +251,7 @@ impl VaultServiceHandle { pub fn derive_ethereum_key(&self, path: &str) -> Result { #[cfg(feature = "secp256k1")] { - let mut inner = self.inner.write().unwrap(); + let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner()); if !inner.unlocked { return Err(VaultServiceError::VaultLocked); } @@ -284,7 +287,7 @@ impl VaultServiceHandle { } pub fn derive_password(&self, path: &str, length: usize) -> Result, VaultServiceError> { - let inner = self.inner.read().unwrap(); + let inner = self.inner.read().unwrap_or_else(|e| e.into_inner()); if !inner.unlocked { return Err(VaultServiceError::VaultLocked); } @@ -314,7 +317,7 @@ impl VaultServiceHandle { plaintext: &str, key_version: u32, ) -> Result { - let mut inner = self.inner.write().unwrap(); + let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner()); if !inner.unlocked { return Err(VaultServiceError::VaultLocked); } @@ -338,7 +341,7 @@ impl VaultServiceHandle { /// Decrypt an EncryptedData blob using the derived encryption key. pub fn decrypt(&self, encrypted: &EncryptedData) -> Result { - let mut inner = self.inner.write().unwrap(); + let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner()); if !inner.unlocked { return Err(VaultServiceError::VaultLocked); } @@ -429,6 +432,28 @@ mod tests { assert!(service.derive_ed25519(PATHS::IDENTITY).is_err()); } + #[test] + fn test_poisoned_lock_recovery() { + let service = VaultServiceHandle::new(); + service.unlock_new(24).unwrap(); + + let inner_arc = service.inner.clone(); + std::thread::spawn(move || { + let _guard = inner_arc.write().unwrap(); + panic!("simulated panic while holding write lock"); + }) + .join() + .expect_err("thread must panic to poison the lock"); + + assert!( + service.is_unlocked(), + "vault must remain usable after a poisoned lock" + ); + + let key = service.derive_ed25519(PATHS::IDENTITY).unwrap(); + assert!(!key.private_key.is_empty()); + } + #[test] fn test_unlock_with_known_phrase() { let service = VaultServiceHandle::new();