Skip to content

[3.7] bpo-32734: Fix asyncio.Lock multiple acquire safety issue (GH-5466) #5501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions Lib/asyncio/locks.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,16 +183,22 @@ async def acquire(self):

fut = self._loop.create_future()
self._waiters.append(fut)

# Finally block should be called before the CancelledError
# handling as we don't want CancelledError to call
# _wake_up_first() and attempt to wake up itself.
try:
await fut
self._locked = True
return True
try:
await fut
finally:
self._waiters.remove(fut)
except futures.CancelledError:
if not self._locked:
self._wake_up_first()
raise
finally:
self._waiters.remove(fut)

self._locked = True
return True

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

def _wake_up_first(self):
"""Wake up the first waiter who isn't cancelled."""
for fut in self._waiters:
if not fut.done():
fut.set_result(True)
break
"""Wake up the first waiter if it isn't done."""
try:
fut = next(iter(self._waiters))
except StopIteration:
return

# .done() necessarily means that a waiter will wake up later on and
# either take the lock, or, if it was cancelled and lock wasn't
# taken already, will hit this again and wake up a new waiter.
if not fut.done():
fut.set_result(True)


class Event:
Expand Down
50 changes: 50 additions & 0 deletions Lib/test/test_asyncio/test_locks.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,56 @@ async def lockit(name, blocker):
self.assertTrue(tb.cancelled())
self.assertTrue(tc.done())

def test_cancel_release_race(self):
# Issue 32734
# Acquire 4 locks, cancel second, release first
# and 2 locks are taken at once.
lock = asyncio.Lock(loop=self.loop)
lock_count = 0
call_count = 0

async def lockit():
nonlocal lock_count
nonlocal call_count
call_count += 1
await lock.acquire()
lock_count += 1

async def lockandtrigger():
await lock.acquire()
self.loop.call_soon(trigger)

def trigger():
t1.cancel()
lock.release()

t0 = self.loop.create_task(lockandtrigger())
t1 = self.loop.create_task(lockit())
t2 = self.loop.create_task(lockit())
t3 = self.loop.create_task(lockit())

# First loop acquires all
test_utils.run_briefly(self.loop)
self.assertTrue(t0.done())

# Second loop calls trigger
test_utils.run_briefly(self.loop)
# Third loop calls cancellation
test_utils.run_briefly(self.loop)

# Make sure only one lock was taken
self.assertEqual(lock_count, 1)
# While 3 calls were made to lockit()
self.assertEqual(call_count, 3)
self.assertTrue(t1.cancelled() and t2.done())

# Cleanup the task that is stuck on acquire.
t3.cancel()
test_utils.run_briefly(self.loop)
self.assertTrue(t3.cancelled())



def test_finished_waiter_cancelled(self):
lock = asyncio.Lock(loop=self.loop)

Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ Milton L. Hankins
Stephen Hansen
Barry Hantman
Lynda Hardman
Bar Harel
Derek Harland
Jason Harper
David Harrigan
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fixed ``asyncio.Lock()`` safety issue which allowed acquiring and locking
the same lock multiple times, without it being free. Patch by Bar Harel.