From c25ac11f0937a7979140de02b79c4dffa3b2960b Mon Sep 17 00:00:00 2001 From: Steve Stagg Date: Mon, 29 Jun 2026 23:46:33 +0100 Subject: [PATCH 1/6] Fix: gh-152635 _interpchannels channel lock alloc failure now raises MemoryError Previously, an allocation failure when creating the lock for a channel in _interpchannels would trigger an assert. Caused by `handle_channel_error` being passed an error code of -1 which is only allowed if an exception has been set. (in this case, no exception was set) `channelsmod_create` now forwards the error code from `channel_create` which `handle_channel_error` already handled. Because the only way to get a -7 error code (was `ERR_CHANNEL_MUTEX_INIT`) is via an allocation failure in `PyThread_allocate_lock`, I made `handle_channel_error` raise a `MemoryError` for this case rather than a `ChannelError`. I also renamed the constant from `ERR_CHANNEL_MUTEX_INIT` to `ERR_CHANNEL_ALLOC_LOCK` to make this clearer for future changes. --- Lib/test/test_interpreters/test_channels.py | 17 +++++++++++++++++ ...26-06-29-23-29-14.gh-issue-152635.O21J0O.rst | 2 ++ Modules/_interpchannelsmodule.c | 11 +++++------ 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-29-14.gh-issue-152635.O21J0O.rst diff --git a/Lib/test/test_interpreters/test_channels.py b/Lib/test/test_interpreters/test_channels.py index 5437792b5a7014..04e537595e43e4 100644 --- a/Lib/test/test_interpreters/test_channels.py +++ b/Lib/test/test_interpreters/test_channels.py @@ -11,6 +11,7 @@ from concurrent import interpreters from test.support import channels from .utils import _run_output, TestBase +import _testcapi class LowLevelTests(TestBase): @@ -30,6 +31,22 @@ def test_highlevel_reloaded(self): # See gh-115490 (https://github.com/python/cpython/issues/115490). importlib.reload(channels) + def test_lock_allocation_failure(self): + # see gh-152635 (https://github.com/python/cpython/issues/152635) + # The first allocation to happen is the lock, which + # historically triggered an assert if alloc failed. + cid = None + _testcapi.set_nomemory(0, 1) + try: + cid = _channels.create() + except MemoryError: + pass # MemoryError is expected behavior + finally: + _testcapi.remove_mem_hooks() + if cid is not None: + _channels.close(cid, force=True) + _channels.destroy(cid) + class TestChannels(TestBase): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-29-14.gh-issue-152635.O21J0O.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-29-14.gh-issue-152635.O21J0O.rst new file mode 100644 index 00000000000000..dadb25fc7c4477 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-29-14.gh-issue-152635.O21J0O.rst @@ -0,0 +1,2 @@ +Fix a crash caused when running out of memory creating a +:mod:`!_interpchannels` channel. Now a :exc:`MemoryError` is correctly raised. diff --git a/Modules/_interpchannelsmodule.c b/Modules/_interpchannelsmodule.c index 05957081079d4a..1ec09488b6e544 100644 --- a/Modules/_interpchannelsmodule.c +++ b/Modules/_interpchannelsmodule.c @@ -354,7 +354,7 @@ clear_module_state(module_state *state) #define ERR_CHANNEL_INTERP_CLOSED -4 #define ERR_CHANNEL_EMPTY -5 #define ERR_CHANNEL_NOT_EMPTY -6 -#define ERR_CHANNEL_MUTEX_INIT -7 +#define ERR_CHANNEL_MUTEX_ALLOC_FAIL -7 #define ERR_CHANNELS_MUTEX_INIT -8 #define ERR_NO_NEXT_CHANNEL_ID -9 #define ERR_CHANNEL_CLOSED_WAITING -10 @@ -427,9 +427,8 @@ handle_channel_error(int err, PyObject *mod, int64_t cid) "if not empty (try force=True)", cid); } - else if (err == ERR_CHANNEL_MUTEX_INIT) { - PyErr_SetString(state->ChannelError, - "can't initialize mutex for new channel"); + else if (err == ERR_CHANNEL_MUTEX_ALLOC_FAIL) { + PyErr_NoMemory(); } else if (err == ERR_CHANNELS_MUTEX_INIT) { PyErr_SetString(state->ChannelError, @@ -1744,7 +1743,7 @@ channel_create(_channels *channels, struct _channeldefaults defaults) { PyThread_type_lock mutex = PyThread_allocate_lock(); if (mutex == NULL) { - return ERR_CHANNEL_MUTEX_INIT; + return ERR_CHANNEL_MUTEX_ALLOC_FAIL; } _channel_state *chan = _channel_new(mutex, defaults); if (chan == NULL) { @@ -2938,7 +2937,7 @@ channelsmod_create(PyObject *self, PyObject *args, PyObject *kwds) int64_t cid = channel_create(&_globals.channels, defaults); if (cid < 0) { - (void)handle_channel_error(-1, self, cid); + (void)handle_channel_error(cid, self, cid); return NULL; } module_state *state = get_module_state(self); From b61a06fc59baeab52a14b43077c3638d05cfd4c8 Mon Sep 17 00:00:00 2001 From: Steve Stagg Date: Tue, 30 Jun 2026 00:08:19 +0100 Subject: [PATCH 2/6] Add @nomemtest decorator to the test, move the _testcapi call into the test method to avoid import issues, improve the syntax for handling the expected MemoryError --- Lib/test/test_interpreters/test_channels.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_interpreters/test_channels.py b/Lib/test/test_interpreters/test_channels.py index 04e537595e43e4..73f7cc57a2e03d 100644 --- a/Lib/test/test_interpreters/test_channels.py +++ b/Lib/test/test_interpreters/test_channels.py @@ -9,9 +9,8 @@ # Raise SkipTest if subinterpreters not supported. _channels = import_helper.import_module('_interpchannels') from concurrent import interpreters -from test.support import channels +from test.support import channels, nomemtest from .utils import _run_output, TestBase -import _testcapi class LowLevelTests(TestBase): @@ -31,16 +30,18 @@ def test_highlevel_reloaded(self): # See gh-115490 (https://github.com/python/cpython/issues/115490). importlib.reload(channels) + @nomemtest def test_lock_allocation_failure(self): # see gh-152635 (https://github.com/python/cpython/issues/152635) # The first allocation to happen is the lock, which # historically triggered an assert if alloc failed. + import _testcapi + cid = None _testcapi.set_nomemory(0, 1) try: - cid = _channels.create() - except MemoryError: - pass # MemoryError is expected behavior + with self.assertRaises(MemoryError): + cid = _channels.create() finally: _testcapi.remove_mem_hooks() if cid is not None: From 191f46a5aea2d3cb0392c241a6bb233d3a4be8ce Mon Sep 17 00:00:00 2001 From: Steve Stagg Date: Tue, 30 Jun 2026 00:19:48 +0100 Subject: [PATCH 3/6] self.assertRaises causes an allocation, so the _testcapi call has been moved to immediately before the _channels.create() call to ensure we correctly target the allocation failure. --- Lib/test/test_interpreters/test_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_interpreters/test_channels.py b/Lib/test/test_interpreters/test_channels.py index 73f7cc57a2e03d..79b38a394c0a2c 100644 --- a/Lib/test/test_interpreters/test_channels.py +++ b/Lib/test/test_interpreters/test_channels.py @@ -38,9 +38,9 @@ def test_lock_allocation_failure(self): import _testcapi cid = None - _testcapi.set_nomemory(0, 1) try: with self.assertRaises(MemoryError): + _testcapi.set_nomemory(0, 1) cid = _channels.create() finally: _testcapi.remove_mem_hooks() From aeb50c0639b86e3c028c1be5e9310db7c73fdf85 Mon Sep 17 00:00:00 2001 From: Steve Stagg Date: Tue, 30 Jun 2026 09:24:56 +0100 Subject: [PATCH 4/6] Remove the test as it's not reliable enough, and testing allocation failues seems to be rarely done --- Lib/test/test_interpreters/test_channels.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/Lib/test/test_interpreters/test_channels.py b/Lib/test/test_interpreters/test_channels.py index 79b38a394c0a2c..5437792b5a7014 100644 --- a/Lib/test/test_interpreters/test_channels.py +++ b/Lib/test/test_interpreters/test_channels.py @@ -9,7 +9,7 @@ # Raise SkipTest if subinterpreters not supported. _channels = import_helper.import_module('_interpchannels') from concurrent import interpreters -from test.support import channels, nomemtest +from test.support import channels from .utils import _run_output, TestBase @@ -30,24 +30,6 @@ def test_highlevel_reloaded(self): # See gh-115490 (https://github.com/python/cpython/issues/115490). importlib.reload(channels) - @nomemtest - def test_lock_allocation_failure(self): - # see gh-152635 (https://github.com/python/cpython/issues/152635) - # The first allocation to happen is the lock, which - # historically triggered an assert if alloc failed. - import _testcapi - - cid = None - try: - with self.assertRaises(MemoryError): - _testcapi.set_nomemory(0, 1) - cid = _channels.create() - finally: - _testcapi.remove_mem_hooks() - if cid is not None: - _channels.close(cid, force=True) - _channels.destroy(cid) - class TestChannels(TestBase): From b9182d3f008dff046745b6d7ea42d0a07f75c71e Mon Sep 17 00:00:00 2001 From: Steve Stagg Date: Tue, 30 Jun 2026 09:40:26 +0100 Subject: [PATCH 5/6] Code review suggestions Just set MemoryError and return directly on alloc failure, rather than go via the handler Leave existing error constant as-is for completeness Co-authored-by: sobolevn --- Modules/_interpchannelsmodule.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/_interpchannelsmodule.c b/Modules/_interpchannelsmodule.c index 1ec09488b6e544..c5d76746486e5a 100644 --- a/Modules/_interpchannelsmodule.c +++ b/Modules/_interpchannelsmodule.c @@ -354,7 +354,7 @@ clear_module_state(module_state *state) #define ERR_CHANNEL_INTERP_CLOSED -4 #define ERR_CHANNEL_EMPTY -5 #define ERR_CHANNEL_NOT_EMPTY -6 -#define ERR_CHANNEL_MUTEX_ALLOC_FAIL -7 +#define ERR_CHANNEL_MUTEX_INIT -7 // currently unused #define ERR_CHANNELS_MUTEX_INIT -8 #define ERR_NO_NEXT_CHANNEL_ID -9 #define ERR_CHANNEL_CLOSED_WAITING -10 @@ -1743,7 +1743,8 @@ channel_create(_channels *channels, struct _channeldefaults defaults) { PyThread_type_lock mutex = PyThread_allocate_lock(); if (mutex == NULL) { - return ERR_CHANNEL_MUTEX_ALLOC_FAIL; + PyErr_NoMemory(); + return -1; } _channel_state *chan = _channel_new(mutex, defaults); if (chan == NULL) { From 25b178a70153ac2753e9b0056ac515cbc17be1ad Mon Sep 17 00:00:00 2001 From: Steve Stagg Date: Tue, 30 Jun 2026 09:48:06 +0100 Subject: [PATCH 6/6] Remove the now-redundant (and incorrect) branch from handle_channel_error Tested locally with: def test_lock_allocation_failure(self): import _testcapi for n in range(0, 100): cid = None try: _testcapi.set_nomemory(n, n+1) cid = _channels.create() except MemoryError: pass finally: _testcapi.remove_mem_hooks() if cid is not None: _channels.close(cid, force=True) _channels.destroy(cid) --- Modules/_interpchannelsmodule.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/Modules/_interpchannelsmodule.c b/Modules/_interpchannelsmodule.c index c5d76746486e5a..358d51cf13f1af 100644 --- a/Modules/_interpchannelsmodule.c +++ b/Modules/_interpchannelsmodule.c @@ -427,9 +427,6 @@ handle_channel_error(int err, PyObject *mod, int64_t cid) "if not empty (try force=True)", cid); } - else if (err == ERR_CHANNEL_MUTEX_ALLOC_FAIL) { - PyErr_NoMemory(); - } else if (err == ERR_CHANNELS_MUTEX_INIT) { PyErr_SetString(state->ChannelError, "can't initialize mutex for channel management");