Skip to content

Commit 2b5937e

Browse files
miss-islingtonbharel
authored andcommitted
bpo-32734: Fix asyncio.Lock multiple acquire safety issue (GH-5466) (#5501)
(cherry picked from commit 2f79c01) Co-authored-by: Bar Harel <bzvi7919@gmail.com>
1 parent c7de1d7 commit 2b5937e

File tree

4 files changed

+75
-10
lines changed

4 files changed

+75
-10
lines changed

Lib/asyncio/locks.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -183,16 +183,22 @@ async def acquire(self):
183183

184184
fut = self._loop.create_future()
185185
self._waiters.append(fut)
186+
187+
# Finally block should be called before the CancelledError
188+
# handling as we don't want CancelledError to call
189+
# _wake_up_first() and attempt to wake up itself.
186190
try:
187-
await fut
188-
self._locked = True
189-
return True
191+
try:
192+
await fut
193+
finally:
194+
self._waiters.remove(fut)
190195
except futures.CancelledError:
191196
if not self._locked:
192197
self._wake_up_first()
193198
raise
194-
finally:
195-
self._waiters.remove(fut)
199+
200+
self._locked = True
201+
return True
196202

197203
def release(self):
198204
"""Release a lock.
@@ -212,11 +218,17 @@ def release(self):
212218
raise RuntimeError('Lock is not acquired.')
213219

214220
def _wake_up_first(self):
215-
"""Wake up the first waiter who isn't cancelled."""
216-
for fut in self._waiters:
217-
if not fut.done():
218-
fut.set_result(True)
219-
break
221+
"""Wake up the first waiter if it isn't done."""
222+
try:
223+
fut = next(iter(self._waiters))
224+
except StopIteration:
225+
return
226+
227+
# .done() necessarily means that a waiter will wake up later on and
228+
# either take the lock, or, if it was cancelled and lock wasn't
229+
# taken already, will hit this again and wake up a new waiter.
230+
if not fut.done():
231+
fut.set_result(True)
220232

221233

222234
class Event:

Lib/test/test_asyncio/test_locks.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,56 @@ async def lockit(name, blocker):
200200
self.assertTrue(tb.cancelled())
201201
self.assertTrue(tc.done())
202202

203+
def test_cancel_release_race(self):
204+
# Issue 32734
205+
# Acquire 4 locks, cancel second, release first
206+
# and 2 locks are taken at once.
207+
lock = asyncio.Lock(loop=self.loop)
208+
lock_count = 0
209+
call_count = 0
210+
211+
async def lockit():
212+
nonlocal lock_count
213+
nonlocal call_count
214+
call_count += 1
215+
await lock.acquire()
216+
lock_count += 1
217+
218+
async def lockandtrigger():
219+
await lock.acquire()
220+
self.loop.call_soon(trigger)
221+
222+
def trigger():
223+
t1.cancel()
224+
lock.release()
225+
226+
t0 = self.loop.create_task(lockandtrigger())
227+
t1 = self.loop.create_task(lockit())
228+
t2 = self.loop.create_task(lockit())
229+
t3 = self.loop.create_task(lockit())
230+
231+
# First loop acquires all
232+
test_utils.run_briefly(self.loop)
233+
self.assertTrue(t0.done())
234+
235+
# Second loop calls trigger
236+
test_utils.run_briefly(self.loop)
237+
# Third loop calls cancellation
238+
test_utils.run_briefly(self.loop)
239+
240+
# Make sure only one lock was taken
241+
self.assertEqual(lock_count, 1)
242+
# While 3 calls were made to lockit()
243+
self.assertEqual(call_count, 3)
244+
self.assertTrue(t1.cancelled() and t2.done())
245+
246+
# Cleanup the task that is stuck on acquire.
247+
t3.cancel()
248+
test_utils.run_briefly(self.loop)
249+
self.assertTrue(t3.cancelled())
250+
251+
252+
203253
def test_finished_waiter_cancelled(self):
204254
lock = asyncio.Lock(loop=self.loop)
205255

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,7 @@ Milton L. Hankins
602602
Stephen Hansen
603603
Barry Hantman
604604
Lynda Hardman
605+
Bar Harel
605606
Derek Harland
606607
Jason Harper
607608
David Harrigan
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed ``asyncio.Lock()`` safety issue which allowed acquiring and locking
2+
the same lock multiple times, without it being free. Patch by Bar Harel.

0 commit comments

Comments
 (0)