Skip to content

Commit 7a2ddc5

Browse files
authored
Merge branch 'master' into windows
2 parents d90e020 + 0582f94 commit 7a2ddc5

8 files changed

Lines changed: 287 additions & 9 deletions

File tree

examples/bench/echoclient.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44

55
import argparse
66
import concurrent.futures
7+
import multiprocessing
78
import socket
89
import ssl
910
import time
1011

1112

1213
if __name__ == '__main__':
14+
multiprocessing.set_start_method("fork")
15+
1316
parser = argparse.ArgumentParser()
1417
parser.add_argument('--msize', default=1000, type=int,
1518
help='message size in bytes')

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ test = [
4545
'mypy>=0.800',
4646
]
4747
dev = [
48-
'packaging',
48+
'packaging>=20',
4949
'setuptools>=60',
50-
'Cython~=3.0',
50+
'Cython~=3.1',
5151
]
5252
docs = [
5353
'Sphinx~=4.1.2',
@@ -57,8 +57,8 @@ docs = [
5757

5858
[build-system]
5959
requires = [
60+
"packaging>=20",
6061
"setuptools>=60",
61-
"wheel",
6262
"Cython~=3.1",
6363
]
6464
build-backend = "setuptools.build_meta"

setup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import pathlib
1414
import platform
1515
import re
16+
import shlex
1617
import shutil
1718
import subprocess
1819
import sys
@@ -22,9 +23,9 @@
2223
from setuptools.command.sdist import sdist
2324

2425

25-
CYTHON_DEPENDENCY = 'Cython~=3.0'
26+
CYTHON_DEPENDENCY = 'Cython~=3.1'
2627
MACHINE = platform.machine()
27-
MODULES_CFLAGS = [os.getenv('UVLOOP_OPT_CFLAGS', '-O2')]
28+
MODULES_CFLAGS = shlex.split(os.getenv('UVLOOP_OPT_CFLAGS', '-O2'))
2829
_ROOT = pathlib.Path(__file__).parent
2930
LIBUV_DIR = str(_ROOT / 'vendor' / 'libuv')
3031
LIBUV_BUILD_DIR = str(_ROOT / 'build' / 'libuv-{}'.format(MACHINE))
@@ -115,7 +116,6 @@ def finalize_options(self):
115116

116117
if need_cythonize:
117118
from packaging.requirements import Requirement
118-
from packaging.version import Version
119119

120120
# Double check Cython presence in case setup_requires
121121
# didn't go into effect (most likely because someone
@@ -131,7 +131,7 @@ def finalize_options(self):
131131
)
132132

133133
cython_dep = Requirement(CYTHON_DEPENDENCY)
134-
if not cython_dep.specifier.contains(Version(Cython.__version__)):
134+
if not cython_dep.specifier.contains(Cython.__version__):
135135
raise RuntimeError(
136136
"uvloop requires {}, got Cython=={}".format(
137137
CYTHON_DEPENDENCY, Cython.__version__

tests/test_base.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,26 @@ async def main():
820820
elif result.returncode != 0:
821821
self.fail(result.stdout.strip())
822822

823+
def test_thread_name_prefix_in_default_executor(self):
824+
if self.implementation == "asyncio" and sys.version_info < (3, 9):
825+
raise unittest.SkipTest(
826+
"thread_name_prefix was added in CPython 3.9"
827+
)
828+
829+
called = []
830+
831+
def cb():
832+
called.append(threading.current_thread().name)
833+
834+
async def runner():
835+
await self.loop.run_in_executor(None, cb)
836+
837+
self.loop.run_until_complete(runner())
838+
839+
self.assertEqual(len(called), 1)
840+
self.assertTrue(called[0] is not None)
841+
self.assertTrue(called[0].startswith(self.implementation))
842+
823843

824844
class TestBaseUV(_TestBase, UVTestCase):
825845

tests/test_context.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,13 @@ def close():
493493
# put the incoming data on-hold
494494
proto.transport.pause_reading()
495495
# send data
496-
await self.loop.run_in_executor(None, ssl_sock.send, b"hello")
496+
await self.loop.run_in_executor(None,
497+
ssl_sock.send, b'hello')
498+
# After gh-105836 run_in_executor may resolve without
499+
# yielding. This is very noticeable when PYTHONASYNCIODEBUG
500+
# is set. Hence, we yield explicitly so that the sent data
501+
# can reach the SSL buffer before close/resume_reading.
502+
await asyncio.sleep(0)
497503
# schedule a proactive transport close which will trigger
498504
# the flushing process to retrieve the remaining data
499505
self.loop.call_soon(close)

tests/test_tcp.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,130 @@ async def test():
775775
with s1, s2:
776776
loop.run_until_complete(test())
777777

778+
def test_create_connection_sock_cancel_detaches(self):
779+
async def client(addr):
780+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
781+
sock.setblocking(False)
782+
try:
783+
sock.connect(addr)
784+
except BlockingIOError:
785+
pass
786+
await asyncio.sleep(0.01)
787+
788+
task = asyncio.ensure_future(
789+
self.loop.create_connection(asyncio.Protocol, sock=sock))
790+
await asyncio.sleep(0)
791+
task.cancel()
792+
with self.assertRaises(asyncio.CancelledError):
793+
await task
794+
795+
# After cancellation the socket must be detached (fd == -1)
796+
# so that its __del__ won't close a recycled fd.
797+
self.assertEqual(sock.fileno(), -1)
798+
799+
def _recv_or_abort(sock):
800+
try:
801+
sock.recv_all(1)
802+
except ConnectionAbortedError:
803+
pass
804+
805+
with self.tcp_server(_recv_or_abort,
806+
max_clients=1,
807+
backlog=1) as srv:
808+
self.loop.run_until_complete(client(srv.addr))
809+
810+
def test_create_connection_sock_cancel_fd_leak(self):
811+
# Regression test for https://github.com/MagicStack/uvloop/issues/645
812+
# and https://github.com/aio-libs/aiohttp/issues/10506
813+
#
814+
# When create_connection(sock=sock) is cancelled, the socket must
815+
# be detached so its close()/`__del__` won't double-close the fd.
816+
# Without the fix, libuv closes the fd but the socket object still
817+
# references it, enabling a chain of fd corruption and data leak:
818+
#
819+
# 1. cancel → libuv closes fd N
820+
# 2. New connection (victim) reuses fd N
821+
# 3. Stale sock.close() closes fd N → breaks the victim
822+
# 4. Another fd N is opened (new connection)
823+
# 5. Victim writev(N) → data goes to the wrong connection
824+
825+
async def test():
826+
srv = await asyncio.start_server(
827+
lambda r, w: w.close(),
828+
'127.0.0.1', 0,
829+
family=socket.AF_INET)
830+
addr = srv.sockets[0].getsockname()
831+
832+
# --- Step 1: create_connection with sock= and cancel it ---
833+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
834+
sock.setblocking(False)
835+
await self.loop.sock_connect(sock, addr)
836+
stale_fd = sock.fileno()
837+
838+
task = self.loop.create_task(
839+
self.loop.create_connection(asyncio.Protocol, sock=sock)
840+
)
841+
await asyncio.sleep(0)
842+
task.cancel()
843+
with self.assertRaises(asyncio.CancelledError):
844+
await task
845+
846+
# --- Step 2: a victim connection reuses the fd ---
847+
victim_tr, _ = await self.loop.create_connection(
848+
asyncio.Protocol, *addr)
849+
victim_fd = victim_tr.get_extra_info('socket').fileno()
850+
if victim_fd != stale_fd:
851+
victim_tr.close()
852+
sock.close()
853+
srv.close()
854+
await srv.wait_closed()
855+
raise unittest.SkipTest(
856+
f'fd not reused (got {victim_fd}, need {stale_fd})')
857+
858+
# --- Step 3: stale sock.close() must NOT kill the victim ---
859+
# Allocate the socketpair BEFORE sock.close() so the pair
860+
# fds don't collide with stale_fd.
861+
spy_a, spy_b = socket.socketpair()
862+
spy_b.setblocking(False)
863+
864+
sock.close()
865+
866+
# Check whether sock.close() broke the victim's fd.
867+
victim_broken = False
868+
try:
869+
os.fstat(victim_fd)
870+
except OSError:
871+
victim_broken = True
872+
873+
if victim_broken:
874+
# The victim's fd was killed — place a spy socket on
875+
# the freed fd (in production this would be a new
876+
# incoming connection).
877+
os.dup2(spy_a.fileno(), stale_fd)
878+
spy_a.close()
879+
880+
# Victim writes. If victim_broken, writev(stale_fd) goes
881+
# to the spy; otherwise it goes to the real connection.
882+
victim_tr.write(b'LEAKED')
883+
884+
try:
885+
leaked = spy_b.recv(4096)
886+
except BlockingIOError:
887+
leaked = b''
888+
889+
if victim_broken:
890+
os.close(stale_fd)
891+
spy_b.close()
892+
victim_tr.close()
893+
srv.close()
894+
await srv.wait_closed()
895+
896+
self.assertEqual(leaked, b'',
897+
f"Data leaked to an unrelated socket: "
898+
f"got {leaked!r}")
899+
900+
self.loop.run_until_complete(test())
901+
778902

779903
class Test_UV_TCP(_TestTCP, tb.UVTestCase):
780904
def test_create_server_buffered_1(self):

tests/test_unix.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,117 @@ def test_create_unix_connection_6(self):
421421
)
422422
)
423423

424+
def test_create_unix_connection_sock_cancel_detaches(self):
425+
async def test():
426+
srv_path = os.path.join(tempfile.mkdtemp(), 'test.sock')
427+
srv = await asyncio.start_unix_server(
428+
lambda r, w: w.close(), path=srv_path)
429+
430+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
431+
sock.setblocking(False)
432+
try:
433+
sock.connect(srv_path)
434+
except BlockingIOError:
435+
pass
436+
await asyncio.sleep(0.01)
437+
438+
task = asyncio.ensure_future(
439+
self.loop.create_unix_connection(
440+
asyncio.Protocol, sock=sock))
441+
await asyncio.sleep(0)
442+
task.cancel()
443+
with self.assertRaises(asyncio.CancelledError):
444+
await task
445+
446+
self.assertEqual(sock.fileno(), -1)
447+
448+
srv.close()
449+
await srv.wait_closed()
450+
if os.path.exists(srv_path):
451+
os.unlink(srv_path)
452+
453+
self.loop.run_until_complete(test())
454+
455+
def test_create_unix_connection_sock_cancel_fd_leak(self):
456+
# Same as test_create_connection_sock_cancel_fd_leak but for
457+
# the create_unix_connection(sock=) path.
458+
459+
async def test():
460+
srv_path = os.path.join(tempfile.mkdtemp(), 'test.sock')
461+
srv = await asyncio.start_unix_server(
462+
lambda r, w: w.close(), path=srv_path)
463+
464+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
465+
sock.setblocking(False)
466+
await self.loop.sock_connect(sock, srv_path)
467+
stale_fd = sock.fileno()
468+
469+
task = self.loop.create_task(
470+
self.loop.create_unix_connection(
471+
asyncio.Protocol, sock=sock))
472+
await asyncio.sleep(0)
473+
task.cancel()
474+
with self.assertRaises(asyncio.CancelledError):
475+
await task
476+
477+
# Create victim that reuses the fd.
478+
victim_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
479+
victim_sock.setblocking(False)
480+
await self.loop.sock_connect(victim_sock, srv_path)
481+
victim_tr, _ = await self.loop.create_unix_connection(
482+
asyncio.Protocol, sock=victim_sock)
483+
victim_fd = victim_tr.get_extra_info('socket').fileno()
484+
if victim_fd != stale_fd:
485+
victim_tr.close()
486+
sock.close()
487+
srv.close()
488+
await srv.wait_closed()
489+
if os.path.exists(srv_path):
490+
os.unlink(srv_path)
491+
raise unittest.SkipTest(
492+
f'fd not reused (got {victim_fd}, need {stale_fd})')
493+
494+
spy_a, spy_b = socket.socketpair()
495+
spy_b.setblocking(False)
496+
497+
sock.close()
498+
499+
victim_broken = False
500+
try:
501+
os.fstat(victim_fd)
502+
except OSError:
503+
victim_broken = True
504+
505+
if victim_broken:
506+
os.dup2(spy_a.fileno(), stale_fd)
507+
spy_a.close()
508+
509+
victim_tr.write(b'LEAKED')
510+
511+
try:
512+
leaked = spy_b.recv(4096)
513+
except BlockingIOError:
514+
leaked = b''
515+
516+
if victim_broken:
517+
os.close(stale_fd)
518+
spy_b.close()
519+
victim_tr.close()
520+
# Let pending callbacks (e.g. server-side connection_lost
521+
# from the cancelled connection) run before closing the
522+
# server, to avoid triggering call_exception_handler().
523+
await asyncio.sleep(0)
524+
srv.close()
525+
await srv.wait_closed()
526+
if os.path.exists(srv_path):
527+
os.unlink(srv_path)
528+
529+
self.assertEqual(leaked, b'',
530+
f"Data leaked to an unrelated socket: "
531+
f"got {leaked!r}")
532+
533+
self.loop.run_until_complete(test())
534+
424535

425536
class Test_UV_Unix(_TestUnix, tb.UVTestCase):
426537
@unittest.skipUnless(hasattr(os, "fspath"), "no os.fspath()")

0 commit comments

Comments
 (0)