Skip to content

Commit 7924f6c

Browse files
committed
Tweak and enforce Mutex.unlock contract
* Ensure that when unlock without an owner cannot successfully unlock Mutex that was originally locked with an owner Fixes #2401
1 parent c8e7749 commit 7924f6c

File tree

2 files changed

+69
-19
lines changed

2 files changed

+69
-19
lines changed

kotlinx-coroutines-core/common/src/sync/Mutex.kt

+30-15
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import kotlinx.coroutines.internal.*
1010
import kotlinx.coroutines.intrinsics.*
1111
import kotlinx.coroutines.selects.*
1212
import kotlin.contracts.*
13-
import kotlin.coroutines.*
1413
import kotlin.jvm.*
1514
import kotlin.native.concurrent.*
1615

@@ -20,11 +19,6 @@ import kotlin.native.concurrent.*
2019
* Mutex has two states: _locked_ and _unlocked_.
2120
* It is **non-reentrant**, that is invoking [lock] even from the same thread/coroutine that currently holds
2221
* the lock still suspends the invoker.
23-
*
24-
* JVM API note:
25-
* Memory semantic of the [Mutex] is similar to `synchronized` block on JVM:
26-
* An unlock on a [Mutex] happens-before every subsequent successful lock on that [Mutex].
27-
* Unsuccessful call to [tryLock] do not have any memory effects.
2822
*/
2923
public interface Mutex {
3024
/**
@@ -81,8 +75,8 @@ public interface Mutex {
8175
* Unlocks this mutex. Throws [IllegalStateException] if invoked on a mutex that is not locked or
8276
* was locked with a different owner token (by identity).
8377
*
84-
* @param owner Optional owner token for debugging. When `owner` is specified (non-null value) and this mutex
85-
* was locked with the different token (by identity), this function throws [IllegalStateException].
78+
* @param owner Optional owner token for debugging. When `owner` does not match the owner (or its absence)
79+
* that locked this mutex, this function throws [IllegalStateException].
8680
*/
8781
public fun unlock(owner: Any? = null)
8882
}
@@ -136,6 +130,12 @@ private val EMPTY_LOCKED = Empty(LOCKED)
136130
private val EMPTY_UNLOCKED = Empty(UNLOCKED)
137131

138132
private class Empty(
133+
/*
134+
* State of the lock:
135+
* - LOCKED
136+
* - UNLOCKED
137+
* - owner (of the lock, passed via 'lock(owner)')
138+
*/
139139
@JvmField val locked: Any
140140
) {
141141
override fun toString(): String = "Empty[$locked]"
@@ -163,7 +163,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2<Any?, Mutex> {
163163
return state is LockedQueue && state.isEmpty
164164
}
165165

166-
public override fun tryLock(owner: Any?): Boolean {
166+
override fun tryLock(owner: Any?): Boolean {
167167
_state.loop { state ->
168168
when (state) {
169169
is Empty -> {
@@ -183,7 +183,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2<Any?, Mutex> {
183183
}
184184
}
185185

186-
public override suspend fun lock(owner: Any?) {
186+
override suspend fun lock(owner: Any?) {
187187
// fast-path -- try lock
188188
if (tryLock(owner)) return
189189
// slow-path -- suspend
@@ -304,16 +304,23 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2<Any?, Mutex> {
304304
_state.loop { state ->
305305
when (state) {
306306
is Empty -> {
307-
if (owner == null)
307+
if (owner == null) {
308308
check(state.locked !== UNLOCKED) { "Mutex is not locked" }
309-
else
310-
check(state.locked === owner) { "Mutex is locked by ${state.locked} but expected $owner" }
309+
expectNoOwner(state.locked)
310+
}
311+
else {
312+
val actualOwner = state.locked
313+
expectOwner(actualOwner, owner)
314+
}
311315
if (_state.compareAndSet(state, EMPTY_UNLOCKED)) return
312316
}
313317
is OpDescriptor -> state.perform(this)
314318
is LockedQueue -> {
315-
if (owner != null)
316-
check(state.owner === owner) { "Mutex is locked by ${state.owner} but expected $owner" }
319+
if (owner != null) {
320+
expectOwner(state.owner, owner)
321+
} else {
322+
expectNoOwner(state.owner)
323+
}
317324
val waiter = state.removeFirstOrNull()
318325
if (waiter == null) {
319326
val op = UnlockOp(state)
@@ -332,6 +339,14 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2<Any?, Mutex> {
332339
}
333340
}
334341

342+
private fun expectNoOwner(l: Any) {
343+
check(l == LOCKED) { "Mutex is locked by $l but expected no owner" }
344+
}
345+
346+
private fun expectOwner(actualOwner: Any, owner: Any?) {
347+
check(actualOwner === owner) { "Mutex is locked by $actualOwner but expected $owner" }
348+
}
349+
335350
override fun toString(): String {
336351
_state.loop { state ->
337352
when (state) {

kotlinx-coroutines-core/common/test/sync/MutexTest.kt

+39-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
package kotlinx.coroutines.sync
66

7-
import kotlinx.atomicfu.*
87
import kotlinx.coroutines.*
98
import kotlin.test.*
109

@@ -32,7 +31,7 @@ class MutexTest : TestBase() {
3231
}
3332

3433
@Test
35-
fun tryLockTest() {
34+
fun testTryLock() {
3635
val mutex = Mutex()
3736
assertFalse(mutex.isLocked)
3837
assertTrue(mutex.tryLock())
@@ -50,7 +49,7 @@ class MutexTest : TestBase() {
5049
}
5150

5251
@Test
53-
fun withLockTest() = runTest {
52+
fun testWithLock() = runTest {
5453
val mutex = Mutex()
5554
assertFalse(mutex.isLocked)
5655
mutex.withLock {
@@ -60,7 +59,7 @@ class MutexTest : TestBase() {
6059
}
6160

6261
@Test
63-
fun holdLock() = runTest {
62+
fun testHoldsLock() = runTest {
6463
val mutex = Mutex()
6564
val firstOwner = Any()
6665
val secondOwner = Any()
@@ -91,4 +90,40 @@ class MutexTest : TestBase() {
9190
assertFalse(mutex.holdsLock(firstOwner))
9291
assertFalse(mutex.holdsLock(secondOwner))
9392
}
93+
94+
@Test
95+
fun testUnlockWithoutOwner() {
96+
val owner = Any()
97+
val mutex = Mutex()
98+
assertTrue(mutex.tryLock(owner))
99+
assertFailsWith<IllegalStateException> { mutex.unlock(Any()) }
100+
assertFailsWith<IllegalStateException> { mutex.unlock() }
101+
assertFailsWith<IllegalStateException> { mutex.unlock(null) }
102+
assertTrue(mutex.holdsLock(owner))
103+
mutex.unlock(owner)
104+
assertFalse(mutex.isLocked)
105+
}
106+
107+
@Test
108+
fun testUnlockWithoutOwnerWithLockedQueue() = runTest {
109+
val owner = Any()
110+
val owner2 = Any()
111+
val mutex = Mutex()
112+
assertTrue(mutex.tryLock(owner))
113+
expect(1)
114+
launch {
115+
expect(2)
116+
mutex.lock(owner2)
117+
}
118+
yield()
119+
expect(3)
120+
assertFailsWith<IllegalStateException> { mutex.unlock(owner2) }
121+
assertFailsWith<IllegalStateException> { mutex.unlock() }
122+
assertFailsWith<IllegalStateException> { mutex.unlock(null) }
123+
assertTrue(mutex.holdsLock(owner))
124+
mutex.unlock(owner)
125+
assertTrue(mutex.isLocked)
126+
assertTrue(mutex.holdsLock(owner2))
127+
finish(4)
128+
}
94129
}

0 commit comments

Comments
 (0)