Skip to content

Commit 579ec9b

Browse files
bmartyclaude
andcommitted
Close the client used to log in before creating the client of the session
Since the login flows stopped handing their client over to RustMatrixClientFactory and let finalizeClientCreation() build a fresh one from the SessionData, two problems appeared. The client built by loginWithQrCode() was never owned by anyone: it is not assigned to currentClient, so clear() had nothing to close and the Rust Client stayed open for the whole app lifetime, holding the session stores of the freshly logged in account. It is now tracked in currentClient, and also disposed of when the QR login fails, which is safe because that flow always builds its own client. The client of the session was also built while the login client was still open. Both use the same session paths, so their state and crypto SQLite stores, and the search index when message search is enabled, were opened twice at the same time with no cross process lock to protect them. clear() now runs before the final client is created. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1f18a18 commit 579ec9b

5 files changed

Lines changed: 166 additions & 4 deletions

File tree

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ class RustMatrixAuthenticationService(
337337
sessionPaths = emptySessionPaths,
338338
qrCodeData = sdkQrCodeLoginData,
339339
)
340+
currentClient = client
340341
client.newLoginWithQrCodeHandler(
341342
oauthConfiguration = oAuthConfiguration,
342343
).use {
@@ -362,6 +363,8 @@ class RustMatrixAuthenticationService(
362363
else -> it
363364
}
364365
}.onFailure { throwable ->
366+
// A QR code login always builds its own client, so it can be disposed of on failure.
367+
clear()
365368
if (throwable is CancellationException) {
366369
throw throwable
367370
}
@@ -370,6 +373,10 @@ class RustMatrixAuthenticationService(
370373
}
371374

372375
private suspend fun finalizeClientCreation(sessionData: SessionData): SessionId {
376+
// Close the client which was used to perform the login before creating the final client.
377+
// Both use the same session paths, so their SQLite stores must never be opened at the same time.
378+
clear()
379+
373380
val matrixClient = restoreSession(sessionData)
374381
// Apply enterprise hooks to the newly created client as soon as possible
375382
clientEnterpriseHook(matrixClient)
@@ -379,9 +386,6 @@ class RustMatrixAuthenticationService(
379386
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
380387
sessionStore.addSession(sessionData)
381388

382-
// Clean up the strong reference held here since it's no longer necessary
383-
clear()
384-
385389
return SessionId(sessionData.userId)
386390
}
387391

libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,29 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService
1414
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
1515
import io.element.android.libraries.matrix.impl.ClientBuilderProvider
1616
import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider
17+
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
1718
import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory
1819
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
1920
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder
2021
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails
22+
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiLoginWithQrCodeHandler
23+
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData
2124
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
25+
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
26+
import io.element.android.libraries.matrix.test.A_USER_ID
2227
import io.element.android.libraries.matrix.test.auth.FakeOAuthRedirectUrlProvider
2328
import io.element.android.libraries.matrix.test.core.aBuildMeta
2429
import io.element.android.libraries.sessionstorage.api.SessionStore
2530
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
31+
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
2632
import io.element.android.tests.testutils.lambda.lambdaRecorder
2733
import io.element.android.tests.testutils.testCoroutineDispatchers
2834
import kotlinx.coroutines.test.TestScope
2935
import kotlinx.coroutines.test.runTest
3036
import org.junit.Test
37+
import org.matrix.rustcomponents.sdk.Client
38+
import org.matrix.rustcomponents.sdk.ClientBuilder
39+
import org.matrix.rustcomponents.sdk.HumanQrLoginException
3140
import java.io.File
3241

3342
class RustMatrixAuthenticationServiceTest {
@@ -74,8 +83,102 @@ class RustMatrixAuthenticationServiceTest {
7483
closeResult.assertions().isCalledOnce()
7584
}
7685

86+
@Test
87+
fun `login closes the client used to log in before building the client of the session`() = runTest {
88+
val events = mutableListOf<String>()
89+
val sut = createRustMatrixAuthenticationService(
90+
clientBuilderProvider = FakeSequentialClientBuilderProvider(
91+
{
92+
events.add("build login client")
93+
FakeFfiClient(
94+
homeserverLoginDetailsResult = { FakeFfiHomeserverLoginDetails() },
95+
loginResult = { _, _ -> },
96+
closeResult = { events.add("close login client") },
97+
)
98+
},
99+
{
100+
events.add("build session client")
101+
FakeFfiClient(withUtdHook = {})
102+
},
103+
),
104+
)
105+
106+
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
107+
assertThat(sut.login("alice", "password").getOrNull()).isEqualTo(A_USER_ID)
108+
109+
// The two clients share the same session paths, so the login one must be closed first.
110+
assertThat(events).containsExactly("build login client", "close login client", "build session client").inOrder()
111+
}
112+
113+
@Test
114+
fun `loginWithQrCode closes the client used to log in before building the client of the session`() = runTest {
115+
val events = mutableListOf<String>()
116+
val sut = createRustMatrixAuthenticationService(
117+
clientBuilderProvider = FakeSequentialClientBuilderProvider(
118+
{
119+
events.add("build login client")
120+
FakeFfiClient(
121+
newLoginWithQrCodeHandlerResult = { FakeFfiLoginWithQrCodeHandler() },
122+
closeResult = { events.add("close login client") },
123+
)
124+
},
125+
{
126+
events.add("build session client")
127+
FakeFfiClient(withUtdHook = {})
128+
},
129+
),
130+
)
131+
132+
val result = sut.loginWithQrCode(aSdkQrCodeLoginData()) {}
133+
134+
assertThat(result.getOrNull()).isEqualTo(A_USER_ID)
135+
assertThat(events).containsExactly("build login client", "close login client", "build session client").inOrder()
136+
}
137+
138+
@Test
139+
fun `loginWithQrCode closes the client it created when the login fails`() = runTest {
140+
val closeResult = lambdaRecorder<Unit> {}
141+
val sut = createRustMatrixAuthenticationService(
142+
clientBuilderProvider = FakeClientBuilderProvider(
143+
provideResult = {
144+
FakeFfiClientBuilder(
145+
buildResult = {
146+
FakeFfiClient(
147+
newLoginWithQrCodeHandlerResult = {
148+
FakeFfiLoginWithQrCodeHandler(
149+
scanResult = { throw HumanQrLoginException.Unknown() },
150+
)
151+
},
152+
closeResult = closeResult,
153+
)
154+
},
155+
)
156+
},
157+
),
158+
)
159+
160+
assertThat(sut.loginWithQrCode(aSdkQrCodeLoginData()) {}.isFailure).isTrue()
161+
closeResult.assertions().isCalledOnce()
162+
}
163+
164+
private fun aSdkQrCodeLoginData() = SdkQrCodeLoginData(
165+
FakeFfiQrCodeData(
166+
baseUrlResult = { A_HOMESERVER_URL },
167+
)
168+
)
169+
170+
/**
171+
* A [ClientBuilderProvider] handing out one [Client] per call, in order.
172+
*/
173+
private class FakeSequentialClientBuilderProvider(
174+
private vararg val clients: () -> Client,
175+
) : ClientBuilderProvider {
176+
private var index = 0
177+
override fun provide(): ClientBuilder = FakeFfiClientBuilder(buildResult = clients[index++])
178+
}
179+
77180
private fun TestScope.createRustMatrixAuthenticationService(
78-
sessionStore: SessionStore = InMemorySessionStore(),
181+
sessionStore: SessionStore = InMemorySessionStore(updateUserProfileResult = { _, _, _ -> }),
79182
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
80183
enterpriseService: EnterpriseService = FakeEnterpriseService(),
81184
): RustMatrixAuthenticationService {
@@ -85,6 +188,7 @@ class RustMatrixAuthenticationServiceTest {
85188
cacheDirectory = cacheDirectory,
86189
sessionStore = sessionStore,
87190
clientBuilderProvider = clientBuilderProvider,
191+
workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}),
88192
)
89193
return RustMatrixAuthenticationService(
90194
sessionPathsFactory = SessionPathsFactory(baseDirectory, cacheDirectory),

libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ import org.matrix.rustcomponents.sdk.Encryption
2525
import org.matrix.rustcomponents.sdk.HomeserverCapabilities
2626
import org.matrix.rustcomponents.sdk.HomeserverLoginDetails
2727
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
28+
import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler
2829
import org.matrix.rustcomponents.sdk.NoHandle
2930
import org.matrix.rustcomponents.sdk.NotificationClient
3031
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
3132
import org.matrix.rustcomponents.sdk.NotificationSettings
33+
import org.matrix.rustcomponents.sdk.OAuthConfiguration
3234
import org.matrix.rustcomponents.sdk.ProfileListener
3335
import org.matrix.rustcomponents.sdk.PusherIdentifiers
3436
import org.matrix.rustcomponents.sdk.PusherKind
@@ -57,6 +59,8 @@ class FakeFfiClient(
5759
private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() },
5860
private val getProfileResult: (String) -> UserProfile = { aRustUserProfile() },
5961
private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() },
62+
private val loginResult: (String, String) -> Unit = { _, _ -> lambdaError() },
63+
private val newLoginWithQrCodeHandlerResult: () -> LoginWithQrCodeHandler = { lambdaError() },
6064
private val getStoreSizesResult: () -> StoreSizes = { lambdaError() },
6165
private val createRoomResult: (CreateRoomParameters) -> String = { lambdaError() },
6266
private val homeserverCapabilities: HomeserverCapabilities = FakeFfiHomeserverCapabilities(),
@@ -122,6 +126,14 @@ class FakeFfiClient(
122126
return homeserverLoginDetailsResult()
123127
}
124128

129+
override suspend fun login(username: String, password: String, initialDeviceName: String?, deviceId: String?) {
130+
loginResult(username, password)
131+
}
132+
133+
override fun newLoginWithQrCodeHandler(oauthConfiguration: OAuthConfiguration): LoginWithQrCodeHandler {
134+
return newLoginWithQrCodeHandlerResult()
135+
}
136+
125137
override suspend fun setMediaRetentionPolicy(policy: MediaRetentionPolicy) {}
126138

127139
override suspend fun getStoreSizes(): StoreSizes {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2026 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.matrix.impl.fixtures.fakes
9+
10+
import org.matrix.rustcomponents.sdk.GeneratedQrLoginProgressListener
11+
import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler
12+
import org.matrix.rustcomponents.sdk.NoHandle
13+
import org.matrix.rustcomponents.sdk.QrCodeData
14+
import org.matrix.rustcomponents.sdk.QrLoginProgress
15+
import org.matrix.rustcomponents.sdk.QrLoginProgressListener
16+
17+
class FakeFfiLoginWithQrCodeHandler(
18+
private val generateResult: suspend () -> Unit = {},
19+
private val scanResult: suspend (QrCodeData) -> Unit = {},
20+
) : LoginWithQrCodeHandler(NoHandle) {
21+
private var scanProgressListener: QrLoginProgressListener? = null
22+
23+
override suspend fun generate(progressListener: GeneratedQrLoginProgressListener) {
24+
generateResult()
25+
}
26+
27+
override suspend fun scan(qrCodeData: QrCodeData, progressListener: QrLoginProgressListener) {
28+
scanProgressListener = progressListener
29+
scanResult(qrCodeData)
30+
}
31+
32+
fun emitScanProgress(progress: QrLoginProgress) {
33+
scanProgressListener?.onUpdate(progress)
34+
}
35+
36+
override fun close() = Unit
37+
}

libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiQrCodeData.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ import org.matrix.rustcomponents.sdk.QrCodeData
1414

1515
class FakeFfiQrCodeData(
1616
private val serverNameResult: () -> String? = { lambdaError() },
17+
private val baseUrlResult: () -> String? = { lambdaError() },
1718
private val toBytesResult: () -> ByteArray = { lambdaError() },
1819
) : QrCodeData(NoHandle) {
1920
override fun serverName(): String? {
2021
return serverNameResult()
2122
}
2223

24+
override fun baseUrl(): String? {
25+
return baseUrlResult()
26+
}
27+
2328
override fun toBytes(): ByteArray {
2429
return toBytesResult()
2530
}

0 commit comments

Comments
 (0)