From ac30dd3f9648fca7f2f7e70b3b22473b49e66e0a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Feb 2026 01:46:58 +0000 Subject: [PATCH] fix(auth-utils): prevent corrupted state from clearing locked mutexes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL FIX: Applies isLocked() verification before clearing transaction mutexes during destroy(), preventing corrupted state from premature cleanup. ## Problem Analysis (Protocolo de Blindagem) ### Cross-file Analysis: Traced all uploadPreKeysToServerIfRequired() call sites: 1. CB:success handler (socket.ts:1299) - NOT awaited 2. PreKey auto-sync (socket.ts:763) - can be in progress 3. Reactive uploads (messages-recv.ts:571) - can be in progress All three paths call keys.transaction() which acquires txMutexes. ### Data Flow Tracking: ``` Timeline of Race Condition: T0: CB:success → uploadPreKeys() → keys.transaction() → mutex.runExclusive() └─ Mutex LOCKED, transaction executing T1: Connection error → end() called └─ keys.destroy() → txMutexes.clear() ❌ CLEARS LOCKED MUTEX T2: Transaction tries to commit └─ Mutex was cleared → corrupted state / unhandled rejection ``` ### Pattern Matching: Found CORRECT pattern already in same file (lines 171-177): ```typescript // releaseTxMutexRef() - CORRECT IMPLEMENTATION if (count <= 0) { const mutex = txMutexes.get(key) if (mutex && !mutex.isLocked()) { // ✅ Checks lock txMutexes.delete(key) } } ``` But destroy() was doing: ```typescript // destroy() - INCORRECT (before fix) txMutexes.clear() // ❌ No lock check ``` ### Invariant Verification: **Violated Invariant**: "Must not destroy resources while in use" - Transactions can be active when end() is called - Clearing locked mutex breaks transaction isolation - Commit can fail with corrupted state ## Solution Applied ### Code Changes: ```typescript // BEFORE (UNSAFE): txMutexes.clear() txMutexRefCounts.clear() // AFTER (SAFE): txMutexes.forEach((mutex, key) => { if (!mutex.isLocked()) { txMutexes.delete(key) txMutexRefCounts.delete(key) clearedCount++ } else { lockedCount++ logger.warn({ key }, 'Mutex still locked during cleanup') } }) ``` ### Semantic Differentiation: - `.clear()` = unconditional removal (dangerous) - `.delete()` with check = safe conditional removal ### Safety Guarantees: ✅ Unlocked mutexes: Cleaned up (prevents leak) ✅ Locked mutexes: Preserved (prevents corruption) ✅ Observability: Logs warn if mutexes remain locked ✅ Graceful degradation: Memory leak preferable to corrupted state ## Impact Assessment **What Could Go Wrong (Before Fix)**: - Corrupted pre-key state in database - Unhandled promise rejections - Transaction isolation violated - Silent data loss **What Happens Now (After Fix)**: - Active transactions complete safely - Unlocked mutexes cleaned up (no leak in normal case) - Warning logged if cleanup happens during transaction - Graceful degradation if race occurs ## Testing Scenarios This fix handles: 1. ✅ Normal case: All transactions complete before destroy 2. ✅ Race case: CB:success running when connection closes 3. ✅ Edge case: PreKey sync active during disconnect 4. ✅ Multiple pending: Several transactions in flight ## Protocol de Blindagem Applied ✅ Cross-file Analysis: Traced all transaction() callers ✅ Pattern Matching: Found correct pattern in releaseTxMutexRef() ✅ Invariant Verification: "Don't destroy resources in use" ✅ Data Flow Tracking: Mapped end() → destroy() → mutex lifecycle ✅ Semantic Differentiation: clear() vs delete() with guards https://claude.ai/code/session_VMxqX --- src/Utils/auth-utils.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Utils/auth-utils.ts b/src/Utils/auth-utils.ts index 1e90ec8d..a65cf403 100644 --- a/src/Utils/auth-utils.ts +++ b/src/Utils/auth-utils.ts @@ -358,10 +358,23 @@ export const addTransactionCapability = ( keyQueues.clear() // Clear transaction mutexes and reference counts - // CRITICAL: Prevents memory leak from accumulated mutex objects - txMutexes.clear() - txMutexRefCounts.clear() - logger.debug('Transaction mutexes cleared') + // CRITICAL: Only delete unlocked mutexes to prevent corrupting active transactions + let clearedCount = 0 + let lockedCount = 0 + txMutexes.forEach((mutex, key) => { + if (!mutex.isLocked()) { + txMutexes.delete(key) + txMutexRefCounts.delete(key) + clearedCount++ + } else { + lockedCount++ + logger.warn( + { key }, + 'Transaction mutex still locked during cleanup - transaction may be in progress' + ) + } + }) + logger.debug({ clearedCount, lockedCount }, 'Transaction mutexes cleanup completed') logger.debug('Transaction capability cleanup completed') }