vault: replace unwrap() on RwLock with poisoned-lock recovery (task: vault/poisoned-lock-recovery)

Drift item #2: replace all .read().unwrap()/.write().unwrap() calls in
VaultServiceHandle with .unwrap_or_else(|e| e.into_inner()) to recover from
poisoned locks instead of bricking the vault. Added test_poisoned_lock_recovery
that poisons the lock via a panicking thread and verifies the vault remains
usable.

Refs: docs/architecture/crates/vault/README.md drift #2
Implements: ADR-025

# Conflicts:
#	crates/alknet-vault/src/service.rs
This commit is contained in:
2026-06-23 13:35:53 +00:00

View File

@@ -130,7 +130,7 @@ impl VaultServiceHandle {
/// The passphrase is the BIP39 password (may be empty string for none). /// The passphrase is the BIP39 password (may be empty string for none).
/// After unlocking, derive and encrypt/decrypt operations are available. /// After unlocking, derive and encrypt/decrypt operations are available.
pub fn unlock(&self, phrase: &str, passphrase: Option<&str>) -> Result<(), VaultServiceError> { 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 { if inner.unlocked {
return Err(VaultServiceError::AlreadyUnlocked); return Err(VaultServiceError::AlreadyUnlocked);
} }
@@ -149,7 +149,7 @@ impl VaultServiceHandle {
/// Returns the generated mnemonic phrase. Store this phrase securely — /// Returns the generated mnemonic phrase. Store this phrase securely —
/// it is the root of trust for all derived keys. /// it is the root of trust for all derived keys.
pub fn unlock_new(&self, word_count: usize) -> Result<Zeroizing<String>, VaultServiceError> { pub fn unlock_new(&self, word_count: usize) -> Result<Zeroizing<String>, VaultServiceError> {
let mut inner = self.inner.write().unwrap(); let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
if inner.unlocked { if inner.unlocked {
return Err(VaultServiceError::AlreadyUnlocked); return Err(VaultServiceError::AlreadyUnlocked);
} }
@@ -170,7 +170,7 @@ impl VaultServiceHandle {
/// until `unlock` is called again. Calls `zeroize()` on all sensitive /// until `unlock` is called again. Calls `zeroize()` on all sensitive
/// material per ADR-038. /// material per ADR-038.
pub fn lock(&self) { 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.cache.clear();
inner.seed = None; inner.seed = None;
inner.mnemonic = None; inner.mnemonic = None;
@@ -179,12 +179,15 @@ impl VaultServiceHandle {
/// Check whether the service is currently unlocked. /// Check whether the service is currently unlocked.
pub fn is_unlocked(&self) -> bool { 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. /// Derive an Ed25519 keypair at the given path.
pub fn derive_ed25519(&self, path: &str) -> Result<DerivedKey, VaultServiceError> { pub fn derive_ed25519(&self, path: &str) -> Result<DerivedKey, VaultServiceError> {
let mut inner = self.inner.write().unwrap(); let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
if !inner.unlocked { if !inner.unlocked {
return Err(VaultServiceError::VaultLocked); return Err(VaultServiceError::VaultLocked);
} }
@@ -212,7 +215,7 @@ impl VaultServiceHandle {
/// Derive an AES-256-GCM encryption key at the given path. /// Derive an AES-256-GCM encryption key at the given path.
pub fn derive_encryption_key(&self, path: &str) -> Result<DerivedKey, VaultServiceError> { pub fn derive_encryption_key(&self, path: &str) -> Result<DerivedKey, VaultServiceError> {
let mut inner = self.inner.write().unwrap(); let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
if !inner.unlocked { if !inner.unlocked {
return Err(VaultServiceError::VaultLocked); return Err(VaultServiceError::VaultLocked);
} }
@@ -246,7 +249,7 @@ impl VaultServiceHandle {
pub fn derive_ethereum_key(&self, path: &str) -> Result<DerivedKey, VaultServiceError> { pub fn derive_ethereum_key(&self, path: &str) -> Result<DerivedKey, VaultServiceError> {
#[cfg(feature = "secp256k1")] #[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 { if !inner.unlocked {
return Err(VaultServiceError::VaultLocked); return Err(VaultServiceError::VaultLocked);
} }
@@ -289,7 +292,7 @@ impl VaultServiceHandle {
plaintext: &str, plaintext: &str,
key_version: u32, key_version: u32,
) -> Result<EncryptedData, VaultServiceError> { ) -> Result<EncryptedData, VaultServiceError> {
let mut inner = self.inner.write().unwrap(); let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
if !inner.unlocked { if !inner.unlocked {
return Err(VaultServiceError::VaultLocked); return Err(VaultServiceError::VaultLocked);
} }
@@ -313,7 +316,7 @@ impl VaultServiceHandle {
/// Decrypt an EncryptedData blob using the derived encryption key. /// Decrypt an EncryptedData blob using the derived encryption key.
pub fn decrypt(&self, encrypted: &EncryptedData) -> Result<String, VaultServiceError> { pub fn decrypt(&self, encrypted: &EncryptedData) -> Result<String, VaultServiceError> {
let mut inner = self.inner.write().unwrap(); let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
if !inner.unlocked { if !inner.unlocked {
return Err(VaultServiceError::VaultLocked); return Err(VaultServiceError::VaultLocked);
} }
@@ -404,6 +407,28 @@ mod tests {
assert!(service.derive_ed25519(PATHS::IDENTITY).is_err()); 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] #[test]
fn test_unlock_with_known_phrase() { fn test_unlock_with_known_phrase() {
let service = VaultServiceHandle::new(); let service = VaultServiceHandle::new();