diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index d499a88c..60acb489 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -465,7 +465,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { if (shouldRecreateSession) { logger.debug({ fromJid, retryCount, reason: recreateReason, errorCode }, 'recreating session for retry') // Delete existing session to force recreation - await authState.keys.set({ session: { [sessionId]: null } }) + // CRITICAL: Wrap in transaction to prevent race with other session operations + await authState.keys.transaction(async () => { + await authState.keys.set({ session: { [sessionId]: null } }) + }, `delete-session-${sessionId}`) forceIncludeKeys = true } } catch (error) { @@ -1023,7 +1026,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { if (shouldRecreateSession) { logger.debug({ participant, retryCount, reason: recreateReason, errorCode }, 'recreating session for outgoing retry') - await authState.keys.set({ session: { [sessionId]: null } }) + // CRITICAL: Wrap in transaction to prevent race with other session operations + await authState.keys.transaction(async () => { + await authState.keys.set({ session: { [sessionId]: null } }) + }, `delete-session-${sessionId}`) } } catch (error) { logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry') diff --git a/src/Socket/socket.ts b/src/Socket/socket.ts index 7467e8f0..64bea367 100644 --- a/src/Socket/socket.ts +++ b/src/Socket/socket.ts @@ -766,12 +766,12 @@ export const makeSocket = (config: SocketConfig) => { logger.error({ error }, '🔑 PreKey auto-sync failed') } finally { isRunning = false - } - // PROTECTION 3: Prevent timer accumulation and post-cleanup rescheduling - // Check cleanedUp flag atomically to prevent race condition - if (!closed && !cleanedUp && ws.isOpen) { - syncTimer = setTimeout(syncLoop, SYNC_INTERVAL) + // PROTECTION 3: Prevent timer accumulation and post-cleanup rescheduling + // Check cleanedUp flag atomically INSIDE finally to minimize race window + if (!closed && !cleanedUp && ws.isOpen) { + syncTimer = setTimeout(syncLoop, SYNC_INTERVAL) + } } } diff --git a/src/Utils/auth-utils.ts b/src/Utils/auth-utils.ts index a65cf403..715e2ea9 100644 --- a/src/Utils/auth-utils.ts +++ b/src/Utils/auth-utils.ts @@ -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') } } }