Skip to content

Commit 03f4e84

Browse files
authored
Reusable continuation leak (#1858)
Detect suspendCancellableCoroutine right after suspendCancellableCoroutineReusable within the same state machine and properly cleanup its child handle when its block completes Fixes #1855
1 parent aff8202 commit 03f4e84

File tree

5 files changed

+37
-5
lines changed

5 files changed

+37
-5
lines changed

benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ package benchmarks.flow.scrabble
66

77
import kotlinx.coroutines.*
88
import kotlinx.coroutines.flow.*
9+
import kotlinx.coroutines.flow.Flow
910
import org.openjdk.jmh.annotations.*
10-
import java.lang.Long.*
1111
import java.util.*
1212
import java.util.concurrent.*
13+
import kotlin.math.*
1314

1415
@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS)
1516
@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS)

benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
package benchmarks.flow.scrabble
66

77
import kotlinx.coroutines.*
8+
import kotlinx.coroutines.flow.*
89
import org.openjdk.jmh.annotations.*
910
import java.lang.Long.*
1011
import java.util.*
11-
import java.util.concurrent.*
12+
import java.util.concurrent.TimeUnit
1213

1314
@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS)
1415
@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS)

kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,13 @@ internal open class CancellableContinuationImpl<in T>(
8585
// This method does nothing. Leftover for binary compatibility with old compiled code
8686
}
8787

88-
private fun isReusable(): Boolean = delegate is DispatchedContinuation<*> && delegate.isReusable
88+
private fun isReusable(): Boolean = delegate is DispatchedContinuation<*> && delegate.isReusable(this)
8989

9090
/**
9191
* Resets cancellability state in order to [suspendAtomicCancellableCoroutineReusable] to work.
9292
* Invariant: used only by [suspendAtomicCancellableCoroutineReusable] in [REUSABLE_CLAIMED] state.
9393
*/
94+
@JvmName("resetState") // Prettier stack traces
9495
internal fun resetState(): Boolean {
9596
assert { parentHandle !== NonDisposableHandle }
9697
val state = _state.value

kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,24 @@ internal class DispatchedContinuation<in T>(
6363
public val reusableCancellableContinuation: CancellableContinuationImpl<*>?
6464
get() = _reusableCancellableContinuation.value as? CancellableContinuationImpl<*>
6565

66-
public val isReusable: Boolean
67-
get() = _reusableCancellableContinuation.value != null
66+
public fun isReusable(requester: CancellableContinuationImpl<*>): Boolean {
67+
/*
68+
* Reusability control:
69+
* `null` -> no reusability at all, false
70+
* If current state is not CCI, then we are within `suspendAtomicCancellableCoroutineReusable`, true
71+
* Else, if result is CCI === requester.
72+
* Identity check my fail for the following pattern:
73+
* ```
74+
* loop:
75+
* suspendAtomicCancellableCoroutineReusable { } // Reusable, outer coroutine stores the child handle
76+
* suspendCancellableCoroutine { } // **Not reusable**, handle should be disposed after {}, otherwise
77+
* it will leak because it won't be freed by `releaseInterceptedContinuation`
78+
* ```
79+
*/
80+
val value = _reusableCancellableContinuation.value ?: return false
81+
if (value is CancellableContinuationImpl<*>) return value === requester
82+
return true
83+
}
6884

6985
/**
7086
* Claims the continuation for [suspendAtomicCancellableCoroutineReusable] block,

kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,17 @@ class ReusableCancellableContinuationTest : TestBase() {
192192
FieldWalker.assertReachableCount(0, receiver) { it is CancellableContinuation<*> }
193193
finish(3)
194194
}
195+
196+
@Test
197+
fun testReusableAndRegularSuspendCancellableCoroutineMemoryLeak() = runTest {
198+
val channel = produce {
199+
repeat(10) {
200+
send(Unit)
201+
}
202+
}
203+
for (value in channel) {
204+
delay(1)
205+
}
206+
FieldWalker.assertReachableCount(1, coroutineContext[Job], { it is ChildContinuation })
207+
}
195208
}

0 commit comments

Comments
 (0)