From bef84d6317ffefea171e68b0e14508d2882d1d89 Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 21 Mar 2026 16:32:27 +0800 Subject: [PATCH 1/7] Fix reference and buffer leaks audit hook in socket module --- Modules/socketmodule.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index d4df40c78e8a4f..8215d51baf30e8 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -4808,6 +4808,7 @@ sock_sendto(PyObject *self, PyObject *args) } if (PySys_Audit("socket.sendto", "OO", s, addro) < 0) { + PyBuffer_Release(&pbuf); return NULL; } @@ -6982,7 +6983,7 @@ socket_getaddrinfo(PyObject *self, PyObject *args, PyObject* kwargs) if (PySys_Audit("socket.getaddrinfo", "OOiii", hobj, pobj, family, socktype, protocol) < 0) { - return NULL; + goto err; } memset(&hints, 0, sizeof(hints)); From 8c837e7ca5008416d0b98cafe844a3b7ba3604d6 Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 21 Mar 2026 16:42:00 +0800 Subject: [PATCH 2/7] Add tests for socket audit hook leak --- Lib/test/test_socket.py | 54 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 9e03069494345b..ecb357adef21f9 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -2,7 +2,7 @@ from unittest import mock from test import support from test.support import ( - cpython_only, is_apple, os_helper, refleak_helper, socket_helper, threading_helper + cpython_only, is_apple, os_helper, refleak_helper, script_helper, socket_helper, threading_helper ) from test.support.import_helper import ensure_lazy_imports import _thread as thread @@ -28,6 +28,7 @@ import struct import sys import tempfile +import textwrap import threading import time import traceback @@ -7498,6 +7499,57 @@ def close_fds(fds): self.assertEqual(data, str(index).encode()) +@support.requires_subprocess() +@unittest.skipUnless(hasattr(sys, "gettotalrefcount"), + "requires sys.gettotalrefcount()") +class AuditHookLeakTests(unittest.TestCase): + # gh-146245: Reference and buffer may leaks in audit hook's failures path. + + def test_getaddrinfo_audit_hook_leak(self): + code = textwrap.dedent(""" + import socket + import sys + import gc + sys.addaudithook(lambda *a: (_ for _ in ()).throw(RuntimeError("audit"))) + gc.collect() + before = sys.gettotalrefcount() + for _ in range(100): + try: + socket.getaddrinfo(None, 80) + except RuntimeError: + pass + gc.collect() + after = sys.gettotalrefcount() + print(after - before) + """) + rc, out, err = script_helper.assert_python_ok("-c", code) + leaked = int(out.strip()) + self.assertLessEqual(leaked, 2, f"Leaked {leaked} references") + + def test_sendto_audit_hook_leak(self): + code = textwrap.dedent(""" + import socket + import sys + import gc + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sys.addaudithook(lambda *a: (_ for _ in ()).throw(RuntimeError("audit"))) + gc.collect() + before = sys.gettotalrefcount() + for _ in range(100): + try: + s.sendto(bytearray(b"x"), ("127.0.0.1", 80)) + except RuntimeError: + pass + gc.collect() + after = sys.gettotalrefcount() + s.close() + print(after - before) + """) + rc, out, err = script_helper.assert_python_ok("-c", code) + leaked = int(out.strip()) + self.assertLessEqual(leaked, 2, f"Leaked {leaked} references") + + class FreeThreadingTests(unittest.TestCase): def test_close_detach_race(self): From 27f2e2fe99897f4a54add58ebe725db6e0b407e2 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 08:48:31 +0000 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-03-21-08-48-25.gh-issue-146245.cqM3_4.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-03-21-08-48-25.gh-issue-146245.cqM3_4.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-21-08-48-25.gh-issue-146245.cqM3_4.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-21-08-48-25.gh-issue-146245.cqM3_4.rst new file mode 100644 index 00000000000000..b3482c80446122 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-21-08-48-25.gh-issue-146245.cqM3_4.rst @@ -0,0 +1 @@ +Fixed reference leaks in :mod:`socket` when audit hooks raise exceptions in :func:`socket.getaddrinfo` and :meth:`socket.sendto`. From 1321d4770b605e429b35b40469d316c6eb05d9de Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 21 Mar 2026 17:02:45 +0800 Subject: [PATCH 4/7] Fix news entry --- .../2026-03-21-08-48-25.gh-issue-146245.cqM3_4.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-21-08-48-25.gh-issue-146245.cqM3_4.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-21-08-48-25.gh-issue-146245.cqM3_4.rst index b3482c80446122..f52eaa0d6c7277 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-21-08-48-25.gh-issue-146245.cqM3_4.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-21-08-48-25.gh-issue-146245.cqM3_4.rst @@ -1 +1 @@ -Fixed reference leaks in :mod:`socket` when audit hooks raise exceptions in :func:`socket.getaddrinfo` and :meth:`socket.sendto`. +Fixed reference leaks in :mod:`socket` when audit hooks raise exceptions in :func:`socket.getaddrinfo` and :meth:`!socket.sendto`. From 026d272a8082ef7b81192913cca494e40da6fc3a Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 21 Mar 2026 17:11:12 +0800 Subject: [PATCH 5/7] Utilize assertAlmostEqual with delta in tests --- Lib/test/test_socket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index ecb357adef21f9..af669fd9713e9e 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -7524,7 +7524,7 @@ def test_getaddrinfo_audit_hook_leak(self): """) rc, out, err = script_helper.assert_python_ok("-c", code) leaked = int(out.strip()) - self.assertLessEqual(leaked, 2, f"Leaked {leaked} references") + self.assertAlmostEqual(leaked, 0, delta=2, msg=f"Leaked {leaked} references") def test_sendto_audit_hook_leak(self): code = textwrap.dedent(""" @@ -7547,7 +7547,7 @@ def test_sendto_audit_hook_leak(self): """) rc, out, err = script_helper.assert_python_ok("-c", code) leaked = int(out.strip()) - self.assertLessEqual(leaked, 2, f"Leaked {leaked} references") + self.assertAlmostEqual(leaked, 0, delta=2, msg=f"Leaked {leaked} references") class FreeThreadingTests(unittest.TestCase): From 396b8c6f1cc36bd01d96c6ed6716e17c67da6ab3 Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 21 Mar 2026 19:26:35 +0800 Subject: [PATCH 6/7] Simplify the codes --- Lib/test/test_socket.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index af669fd9713e9e..b22f1906f455bc 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -7503,20 +7503,24 @@ def close_fds(fds): @unittest.skipUnless(hasattr(sys, "gettotalrefcount"), "requires sys.gettotalrefcount()") class AuditHookLeakTests(unittest.TestCase): - # gh-146245: Reference and buffer may leaks in audit hook's failures path. + # gh-146245: Reference and buffer leaks in audit hook failure paths. def test_getaddrinfo_audit_hook_leak(self): code = textwrap.dedent(""" import socket import sys import gc - sys.addaudithook(lambda *a: (_ for _ in ()).throw(RuntimeError("audit"))) + + def hook(*args): + raise ValueError("audit") + + sys.addaudithook(hook) gc.collect() before = sys.gettotalrefcount() for _ in range(100): try: socket.getaddrinfo(None, 80) - except RuntimeError: + except ValueError: pass gc.collect() after = sys.gettotalrefcount() @@ -7531,14 +7535,18 @@ def test_sendto_audit_hook_leak(self): import socket import sys import gc + + def hook(*args): + raise ValueError("audit") + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sys.addaudithook(lambda *a: (_ for _ in ()).throw(RuntimeError("audit"))) + sys.addaudithook(hook) gc.collect() before = sys.gettotalrefcount() for _ in range(100): try: s.sendto(bytearray(b"x"), ("127.0.0.1", 80)) - except RuntimeError: + except ValueError: pass gc.collect() after = sys.gettotalrefcount() From 6e079ab5d4e38b319f696c5c57cd0ab3f94f9c5a Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 21 Mar 2026 21:31:49 +0800 Subject: [PATCH 7/7] Revert the tests --- Lib/test/test_socket.py | 62 +---------------------------------------- 1 file changed, 1 insertion(+), 61 deletions(-) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index b22f1906f455bc..9e03069494345b 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -2,7 +2,7 @@ from unittest import mock from test import support from test.support import ( - cpython_only, is_apple, os_helper, refleak_helper, script_helper, socket_helper, threading_helper + cpython_only, is_apple, os_helper, refleak_helper, socket_helper, threading_helper ) from test.support.import_helper import ensure_lazy_imports import _thread as thread @@ -28,7 +28,6 @@ import struct import sys import tempfile -import textwrap import threading import time import traceback @@ -7499,65 +7498,6 @@ def close_fds(fds): self.assertEqual(data, str(index).encode()) -@support.requires_subprocess() -@unittest.skipUnless(hasattr(sys, "gettotalrefcount"), - "requires sys.gettotalrefcount()") -class AuditHookLeakTests(unittest.TestCase): - # gh-146245: Reference and buffer leaks in audit hook failure paths. - - def test_getaddrinfo_audit_hook_leak(self): - code = textwrap.dedent(""" - import socket - import sys - import gc - - def hook(*args): - raise ValueError("audit") - - sys.addaudithook(hook) - gc.collect() - before = sys.gettotalrefcount() - for _ in range(100): - try: - socket.getaddrinfo(None, 80) - except ValueError: - pass - gc.collect() - after = sys.gettotalrefcount() - print(after - before) - """) - rc, out, err = script_helper.assert_python_ok("-c", code) - leaked = int(out.strip()) - self.assertAlmostEqual(leaked, 0, delta=2, msg=f"Leaked {leaked} references") - - def test_sendto_audit_hook_leak(self): - code = textwrap.dedent(""" - import socket - import sys - import gc - - def hook(*args): - raise ValueError("audit") - - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sys.addaudithook(hook) - gc.collect() - before = sys.gettotalrefcount() - for _ in range(100): - try: - s.sendto(bytearray(b"x"), ("127.0.0.1", 80)) - except ValueError: - pass - gc.collect() - after = sys.gettotalrefcount() - s.close() - print(after - before) - """) - rc, out, err = script_helper.assert_python_ok("-c", code) - leaked = int(out.strip()) - self.assertAlmostEqual(leaked, 0, delta=2, msg=f"Leaked {leaked} references") - - class FreeThreadingTests(unittest.TestCase): def test_close_detach_race(self):