fix(auth-utils): prevent corrupted state from clearing locked mutexes

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
This commit is contained in:
Claude
2026-02-04 01:46:58 +00:00
parent 49a56e6c54
commit ac30dd3f96
+17 -4
View File
@@ -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')
}