diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 0930ef403c6c4b..77c70aaa7b986e 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -381,6 +381,7 @@ async def serve_forever(self): except exceptions.CancelledError: try: self.close() + self.close_clients() await self.wait_closed() finally: raise diff --git a/Lib/test/test_asyncio/test_server.py b/Lib/test/test_asyncio/test_server.py index 5bd0f7e2af4f84..581ea47d2dec97 100644 --- a/Lib/test/test_asyncio/test_server.py +++ b/Lib/test/test_asyncio/test_server.py @@ -266,6 +266,38 @@ async def serve(rd, wr): await asyncio.sleep(0) self.assertTrue(task.done()) + async def test_close_with_hanging_client(self): + # Synchronize server cancellation only after the socket connection is + # accepted and this event is set + conn_event = asyncio.Event() + class Proto(asyncio.Protocol): + def connection_made(self, transport): + conn_event.set() + + loop = asyncio.get_running_loop() + srv = await loop.create_server(Proto, socket_helper.HOSTv4, 0) + + # Start the server + serve_forever_task = asyncio.create_task(srv.serve_forever()) + await asyncio.sleep(0) + + # Create a connection to server + addr = srv.sockets[0].getsockname() + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(addr) + self.addCleanup(sock.close) + + # Send a CancelledError into the server to emulate a Ctrl+C + # KeyboardInterrupt whilst the server is handling a hanging client + await conn_event.wait() + serve_forever_task.cancel() + + # Ensure the client is closed within a timeout + async with asyncio.timeout(0.5): + await srv.wait_closed() + + self.assertFalse(srv.is_serving()) + # Test the various corner cases of Unix server socket removal class UnixServerCleanupTests(unittest.IsolatedAsyncioTestCase): diff --git a/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst b/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst new file mode 100644 index 00000000000000..04e6a377dd816c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst @@ -0,0 +1,5 @@ +asyncio: Fix :func:`asyncio.Server.serve_forever` shutdown regression. Since +3.12, cancelling ``serve_forever()`` could hang waiting for a handler blocked +on a read from a client that never closed (effectively requiring two +interrupts to stop); the shutdown sequence now ensures client streams are +closed so ``serve_forever()`` exits promptly and handlers observe EOF. diff --git a/Modules/_xxtestfuzz/fuzzer.c b/Modules/_xxtestfuzz/fuzzer.c index 6cb11562476e40..02afb01d3731fb 100644 --- a/Modules/_xxtestfuzz/fuzzer.c +++ b/Modules/_xxtestfuzz/fuzzer.c @@ -38,23 +38,18 @@ static int fuzz_builtin_float(const char* data, size_t size) { static int fuzz_builtin_int(const char* data, size_t size) { /* Ignore test cases with very long ints to avoid timeouts int("9" * 1000000) is not a very interesting test caase */ - if (size > MAX_INT_TEST_SIZE) { + if (size < 1 || size > MAX_INT_TEST_SIZE) { return 0; } - /* Pick a random valid base. (When the fuzzed function takes extra - parameters, it's somewhat normal to hash the input to generate those - parameters. We want to exercise all code paths, so we do so here.) */ - int base = Py_HashBuffer(data, size) % 37; + // Use the first byte to pick a base + int base = ((unsigned char) data[0]) % 37; if (base == 1) { // 1 is the only number between 0 and 36 that is not a valid base. base = 0; } - if (base == -1) { - return 0; // An error occurred, bail early. - } - if (base < 0) { - base = -base; - } + + data += 1; + size -= 1; PyObject* s = PyUnicode_FromStringAndSize(data, size); if (s == NULL) {