fix(auth-utils): prevent transactions after destroy with destroyed flag

CRITICAL FIX: Adds destroyed flag to transaction capability to prevent
use-after-free crashes when transactions are initiated after socket.end()
is called.

Changes:
- Add destroyed flag set to true at start of destroy()
- Check destroyed flag in transaction() and throw error if set
- Reorder destroy() to check locked mutexes BEFORE destroying resources
- Skip resource destruction if any mutexes are locked (prevents corrupted state)

This prevents 4 critical race conditions:
1. sendMessage() after end() (messages-send.ts:868)
2. sendRetryRequest() after end() (messages-recv.ts:498)
3. resyncAppState() after end() (chats.ts:476, 769)
4. LID mapping operations after end() (lid-mapping.ts:417)

Timeline before fix:
T0: sendMessage() called
T1: end() sets closed=true
T2: end() awaits uploadPreKeysPromise (5s)
T3: sendMessage() calls keys.transaction() ← NO GUARD
T4: keys.destroy() executes
T5: Transaction crashes (resources destroyed)

Timeline after fix:
T0: sendMessage() called
T1: end() → destroy() sets destroyed=true
T2: sendMessage() → transaction() → throws error ✓

https://claude.ai/code/session_VMxqX
This commit is contained in:
Claude
2026-02-04 03:11:26 +00:00
parent 78f130d49b
commit 08cad354a7
+38 -20
View File
@@ -129,6 +129,9 @@ export const addTransactionCapability = (
// Pre-key manager for specialized operations
const preKeyManager = new PreKeyManager(state, logger)
// Destroyed flag to prevent operations after cleanup
let destroyed = false
/**
* Get or create a queue for a specific key type
*/
@@ -300,6 +303,11 @@ export const addTransactionCapability = (
isInTransaction,
transaction: async (work, key) => {
// CRITICAL: Prevent transactions after destroy to avoid use-after-free
if (destroyed) {
throw new Error('Transaction capability destroyed - socket closed')
}
const existing = txStorage.getStore()
// Nested transaction - reuse existing context
@@ -346,7 +354,36 @@ export const addTransactionCapability = (
* Should be called during connection cleanup
*/
destroy: () => {
// CRITICAL: Set destroyed flag FIRST to prevent new transactions
destroyed = true
logger.debug('🗑️ Cleaning up transaction capability resources')
// Count locked mutexes BEFORE destroying resources
let clearedCount = 0
let lockedCount = 0
const lockedKeys: string[] = []
txMutexes.forEach((mutex, key) => {
if (!mutex.isLocked()) {
txMutexes.delete(key)
txMutexRefCounts.delete(key)
clearedCount++
} else {
lockedCount++
lockedKeys.push(key)
}
})
// If there are locked mutexes, log error and skip resource destruction
if (lockedCount > 0) {
logger.error(
{ lockedCount, lockedKeys },
'❌ Cannot destroy resources - transactions still active! Resources will be cleaned up by GC.'
)
return
}
// Safe to destroy resources (no active transactions)
preKeyManager.destroy()
// Clear all key queues
@@ -357,26 +394,7 @@ export const addTransactionCapability = (
})
keyQueues.clear()
// Clear transaction mutexes and reference counts
// 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')
logger.debug({ clearedCount }, 'Transaction capability cleanup completed')
}
}
}