Skip to content

Commit bd2c60f

Browse files
[3.12] gh-91227: Ignore ERROR_PORT_UNREACHABLE in proactor recvfrom() (GH-32011) (GH-117209)
(cherry picked from commit f11d0d8) Co-authored-by: Erik Soma <stillusingirc@gmail.com>
1 parent 193a24b commit bd2c60f

File tree

5 files changed

+174
-12
lines changed

5 files changed

+174
-12
lines changed

Lib/asyncio/windows_events.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import _overlapped
99
import _winapi
1010
import errno
11+
from functools import partial
1112
import math
1213
import msvcrt
1314
import socket
@@ -466,6 +467,18 @@ def finish_socket_func(trans, key, ov):
466467
else:
467468
raise
468469

470+
@classmethod
471+
def _finish_recvfrom(cls, trans, key, ov, *, empty_result):
472+
try:
473+
return cls.finish_socket_func(trans, key, ov)
474+
except OSError as exc:
475+
# WSARecvFrom will report ERROR_PORT_UNREACHABLE when the same
476+
# socket is used to send to an address that is not listening.
477+
if exc.winerror == _overlapped.ERROR_PORT_UNREACHABLE:
478+
return empty_result, None
479+
else:
480+
raise
481+
469482
def recv(self, conn, nbytes, flags=0):
470483
self._register_with_iocp(conn)
471484
ov = _overlapped.Overlapped(NULL)
@@ -500,7 +513,8 @@ def recvfrom(self, conn, nbytes, flags=0):
500513
except BrokenPipeError:
501514
return self._result((b'', None))
502515

503-
return self._register(ov, conn, self.finish_socket_func)
516+
return self._register(ov, conn, partial(self._finish_recvfrom,
517+
empty_result=b''))
504518

505519
def recvfrom_into(self, conn, buf, flags=0):
506520
self._register_with_iocp(conn)
@@ -510,17 +524,8 @@ def recvfrom_into(self, conn, buf, flags=0):
510524
except BrokenPipeError:
511525
return self._result((0, None))
512526

513-
def finish_recv(trans, key, ov):
514-
try:
515-
return ov.getresult()
516-
except OSError as exc:
517-
if exc.winerror in (_overlapped.ERROR_NETNAME_DELETED,
518-
_overlapped.ERROR_OPERATION_ABORTED):
519-
raise ConnectionResetError(*exc.args)
520-
else:
521-
raise
522-
523-
return self._register(ov, conn, finish_recv)
527+
return self._register(ov, conn, partial(self._finish_recvfrom,
528+
empty_result=0))
524529

525530
def sendto(self, conn, buf, flags=0, addr=None):
526531
self._register_with_iocp(conn)

Lib/test/test_asyncio/test_events.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,6 +1379,80 @@ def test_create_datagram_endpoint_sock(self):
13791379
tr.close()
13801380
self.loop.run_until_complete(pr.done)
13811381

1382+
def test_datagram_send_to_non_listening_address(self):
1383+
# see:
1384+
# https://github.com/python/cpython/issues/91227
1385+
# https://github.com/python/cpython/issues/88906
1386+
# https://bugs.python.org/issue47071
1387+
# https://bugs.python.org/issue44743
1388+
# The Proactor event loop would fail to receive datagram messages after
1389+
# sending a message to an address that wasn't listening.
1390+
loop = self.loop
1391+
1392+
class Protocol(asyncio.DatagramProtocol):
1393+
1394+
_received_datagram = None
1395+
1396+
def datagram_received(self, data, addr):
1397+
self._received_datagram.set_result(data)
1398+
1399+
async def wait_for_datagram_received(self):
1400+
self._received_datagram = loop.create_future()
1401+
result = await asyncio.wait_for(self._received_datagram, 10)
1402+
self._received_datagram = None
1403+
return result
1404+
1405+
def create_socket():
1406+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1407+
sock.setblocking(False)
1408+
sock.bind(('127.0.0.1', 0))
1409+
return sock
1410+
1411+
socket_1 = create_socket()
1412+
transport_1, protocol_1 = loop.run_until_complete(
1413+
loop.create_datagram_endpoint(Protocol, sock=socket_1)
1414+
)
1415+
addr_1 = socket_1.getsockname()
1416+
1417+
socket_2 = create_socket()
1418+
transport_2, protocol_2 = loop.run_until_complete(
1419+
loop.create_datagram_endpoint(Protocol, sock=socket_2)
1420+
)
1421+
addr_2 = socket_2.getsockname()
1422+
1423+
# creating and immediately closing this to try to get an address that
1424+
# is not listening
1425+
socket_3 = create_socket()
1426+
transport_3, protocol_3 = loop.run_until_complete(
1427+
loop.create_datagram_endpoint(Protocol, sock=socket_3)
1428+
)
1429+
addr_3 = socket_3.getsockname()
1430+
transport_3.abort()
1431+
1432+
transport_1.sendto(b'a', addr=addr_2)
1433+
self.assertEqual(loop.run_until_complete(
1434+
protocol_2.wait_for_datagram_received()
1435+
), b'a')
1436+
1437+
transport_2.sendto(b'b', addr=addr_1)
1438+
self.assertEqual(loop.run_until_complete(
1439+
protocol_1.wait_for_datagram_received()
1440+
), b'b')
1441+
1442+
# this should send to an address that isn't listening
1443+
transport_1.sendto(b'c', addr=addr_3)
1444+
loop.run_until_complete(asyncio.sleep(0))
1445+
1446+
# transport 1 should still be able to receive messages after sending to
1447+
# an address that wasn't listening
1448+
transport_2.sendto(b'd', addr=addr_1)
1449+
self.assertEqual(loop.run_until_complete(
1450+
protocol_1.wait_for_datagram_received()
1451+
), b'd')
1452+
1453+
transport_1.close()
1454+
transport_2.close()
1455+
13821456
def test_internal_fds(self):
13831457
loop = self.create_event_loop()
13841458
if not isinstance(loop, selector_events.BaseSelectorEventLoop):

Lib/test/test_asyncio/test_sock_lowlevel.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,12 +555,93 @@ class SelectEventLoopTests(BaseSockTestsMixin,
555555
def create_event_loop(self):
556556
return asyncio.SelectorEventLoop()
557557

558+
558559
class ProactorEventLoopTests(BaseSockTestsMixin,
559560
test_utils.TestCase):
560561

561562
def create_event_loop(self):
562563
return asyncio.ProactorEventLoop()
563564

565+
566+
async def _basetest_datagram_send_to_non_listening_address(self,
567+
recvfrom):
568+
# see:
569+
# https://github.com/python/cpython/issues/91227
570+
# https://github.com/python/cpython/issues/88906
571+
# https://bugs.python.org/issue47071
572+
# https://bugs.python.org/issue44743
573+
# The Proactor event loop would fail to receive datagram messages
574+
# after sending a message to an address that wasn't listening.
575+
576+
def create_socket():
577+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
578+
sock.setblocking(False)
579+
sock.bind(('127.0.0.1', 0))
580+
return sock
581+
582+
socket_1 = create_socket()
583+
addr_1 = socket_1.getsockname()
584+
585+
socket_2 = create_socket()
586+
addr_2 = socket_2.getsockname()
587+
588+
# creating and immediately closing this to try to get an address
589+
# that is not listening
590+
socket_3 = create_socket()
591+
addr_3 = socket_3.getsockname()
592+
socket_3.shutdown(socket.SHUT_RDWR)
593+
socket_3.close()
594+
595+
socket_1_recv_task = self.loop.create_task(recvfrom(socket_1))
596+
socket_2_recv_task = self.loop.create_task(recvfrom(socket_2))
597+
await asyncio.sleep(0)
598+
599+
await self.loop.sock_sendto(socket_1, b'a', addr_2)
600+
self.assertEqual(await socket_2_recv_task, b'a')
601+
602+
await self.loop.sock_sendto(socket_2, b'b', addr_1)
603+
self.assertEqual(await socket_1_recv_task, b'b')
604+
socket_1_recv_task = self.loop.create_task(recvfrom(socket_1))
605+
await asyncio.sleep(0)
606+
607+
# this should send to an address that isn't listening
608+
await self.loop.sock_sendto(socket_1, b'c', addr_3)
609+
self.assertEqual(await socket_1_recv_task, b'')
610+
socket_1_recv_task = self.loop.create_task(recvfrom(socket_1))
611+
await asyncio.sleep(0)
612+
613+
# socket 1 should still be able to receive messages after sending
614+
# to an address that wasn't listening
615+
socket_2.sendto(b'd', addr_1)
616+
self.assertEqual(await socket_1_recv_task, b'd')
617+
618+
socket_1.shutdown(socket.SHUT_RDWR)
619+
socket_1.close()
620+
socket_2.shutdown(socket.SHUT_RDWR)
621+
socket_2.close()
622+
623+
624+
def test_datagram_send_to_non_listening_address_recvfrom(self):
625+
async def recvfrom(socket):
626+
data, _ = await self.loop.sock_recvfrom(socket, 4096)
627+
return data
628+
629+
self.loop.run_until_complete(
630+
self._basetest_datagram_send_to_non_listening_address(
631+
recvfrom))
632+
633+
634+
def test_datagram_send_to_non_listening_address_recvfrom_into(self):
635+
async def recvfrom_into(socket):
636+
buf = bytearray(4096)
637+
length, _ = await self.loop.sock_recvfrom_into(socket, buf,
638+
4096)
639+
return buf[:length]
640+
641+
self.loop.run_until_complete(
642+
self._basetest_datagram_send_to_non_listening_address(
643+
recvfrom_into))
644+
564645
else:
565646
import selectors
566647

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix the asyncio ProactorEventLoop implementation so that sending a datagram to an address that is not listening does not prevent receiving any more datagrams.

Modules/overlapped.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2057,6 +2057,7 @@ overlapped_exec(PyObject *module)
20572057
WINAPI_CONSTANT(F_DWORD, ERROR_OPERATION_ABORTED);
20582058
WINAPI_CONSTANT(F_DWORD, ERROR_SEM_TIMEOUT);
20592059
WINAPI_CONSTANT(F_DWORD, ERROR_PIPE_BUSY);
2060+
WINAPI_CONSTANT(F_DWORD, ERROR_PORT_UNREACHABLE);
20602061
WINAPI_CONSTANT(F_DWORD, INFINITE);
20612062
WINAPI_CONSTANT(F_HANDLE, INVALID_HANDLE_VALUE);
20622063
WINAPI_CONSTANT(F_HANDLE, NULL);

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy