fix(messages-recv): use consistent transaction key for session operations

fix(messages-recv): use consistent transaction key for session operations
This commit is contained in:
Renato Alcara
2026-02-04 01:01:22 -03:00
committed by GitHub
5 changed files with 22 additions and 791 deletions
+6 -4
View File
@@ -465,10 +465,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
if (shouldRecreateSession) {
logger.debug({ fromJid, retryCount, reason: recreateReason, errorCode }, 'recreating session for retry')
// Delete existing session to force recreation
// CRITICAL: Wrap in transaction to prevent race with other session operations
// CRITICAL: Use same transaction key as encrypt/decrypt operations to prevent race
// Using meId ensures this delete serializes with sendMessage() and other session operations
await authState.keys.transaction(async () => {
await authState.keys.set({ session: { [sessionId]: null } })
}, `delete-session-${sessionId}`)
}, authState.creds.me?.id || 'session-operation')
forceIncludeKeys = true
}
} catch (error) {
@@ -1026,10 +1027,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
if (shouldRecreateSession) {
logger.debug({ participant, retryCount, reason: recreateReason, errorCode }, 'recreating session for outgoing retry')
// CRITICAL: Wrap in transaction to prevent race with other session operations
// CRITICAL: Use same transaction key as encrypt/decrypt operations to prevent race
// Using meId ensures this delete serializes with sendMessage() and other session operations
await authState.keys.transaction(async () => {
await authState.keys.set({ session: { [sessionId]: null } })
}, `delete-session-${sessionId}`)
}, authState.creds.me?.id || 'session-operation')
}
} catch (error) {
logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry')
+16 -5
View File
@@ -303,11 +303,6 @@ 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
@@ -322,6 +317,12 @@ export const addTransactionCapability = (
try {
return await mutex.runExclusive(async () => {
// CRITICAL: Check destroyed flag INSIDE mutex to prevent race condition
// This ensures atomic check-and-execute: if we acquire mutex, resources exist
if (destroyed) {
throw new Error('Transaction capability destroyed - cannot initiate new transactions')
}
const ctx: TransactionContext = {
cache: {},
mutations: {},
@@ -352,9 +353,19 @@ export const addTransactionCapability = (
/**
* Cleanup all resources (queues, managers, mutexes)
* Should be called during connection cleanup
*
* IMPORTANT BEHAVIOR:
* - Always sets destroyed=true to prevent NEW transactions
* - If mutexes are locked (active transactions), returns early WITHOUT destroying resources
* - This creates intentional temporary inconsistent state:
* * destroyed=true (new transactions rejected)
* * resources exist (active transactions complete safely)
* * resources cleaned up by GC after active transactions finish
* - If no locked mutexes, destroys resources immediately
*/
destroy: () => {
// CRITICAL: Set destroyed flag FIRST to prevent new transactions
// Note: Flag is set even if early return occurs (see doc above)
destroyed = true
logger.debug('🗑️ Cleaning up transaction capability resources')