From 1a1b8d1009699d078ffff8cac3cb1c8ad5eb7638 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 11 May 2026 15:01:24 -0400 Subject: [PATCH] fix(cash): race between give transactor dispose and start on quick bg/fg When the user backgrounds and foregrounds quickly during an active give-bill flow, cancelAwaitForGrab() can dispose the transactor between with() and start(), nulling owner before start() reads it. Fix by making the transactor self-protecting: - Reorder dispose() to cancel the scope before nulling fields - Check scope.isActive at the top of start() to bail out if disposed Bugsnag: 6a021d748c3285d1a5987c43 Signed-off-by: Brandon McAnsh --- .../opencode/internal/transactors/GiveBillTransactor.kt | 6 +++++- .../com/getcode/opencode/managers/BillTransactionManager.kt | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactor.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactor.kt index 4299980af..9c90a8a82 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactor.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactor.kt @@ -112,6 +112,10 @@ internal class GiveBillTransactor( * @return the confirmed [TransactionMetadata.SendPublicPayment] on success. */ suspend fun start(): Result { + if (!scope.isActive) { + return logAndFail(GiveTransactorError.Other(message = "Transactor was disposed")) + } + val ownerKey = owner ?: return logAndFail(GiveTransactorError.Other(message = "No owner key. Did you call with() first?")) val desiredToken = token @@ -212,13 +216,13 @@ internal class GiveBillTransactor( /** Cancels the coroutine scope and clears all held state. */ fun dispose() { + scope.cancel() owner = null presentationData = BillPresentationData(emptyList(), emptyList()) rendezvousKey = null receivingAccount = null token = null providedVerifiedState = null - scope.cancel() } sealed class GiveTransactorError( diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/managers/BillTransactionManager.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/managers/BillTransactionManager.kt index 6f7857117..e6f3b7a44 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/managers/BillTransactionManager.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/managers/BillTransactionManager.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import java.util.Timer import java.util.TimerTask @@ -113,6 +114,10 @@ class BillTransactionManager @Inject constructor( present(transactor.presentationData) presentBillForGive(onTimeout) + // If cancelAwaitForGrab() fired between present() and here, + // bail out before start() reads fields that dispose() may have nulled. + ensureActive() + transactor.start() .onSuccess { childScope.cancel()