From 99be176c3c4be470748e27293bcb097414d941a7 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Sun, 21 Jun 2026 23:47:30 +0800 Subject: [PATCH 1/2] gh-151862: Fix NULL deref on non-str AttributeError when unpickling An object crossing interpreters via a queue or channel is unpickled on the receive side. When that unpickling raised an ``AttributeError`` whose first argument was not a UTF-8-encodable string, ``PyUnicode_AsUTF8()`` returned ``NULL`` and the subsequent ``strncmp(NULL, ...)`` in ``check_missing___main___attr()`` dereferenced it, crashing the interpreter. Guard against the ``NULL`` result and treat it as not a missing-``__main__``-attribute error. --- Lib/test/test_interpreters/test_queues.py | 47 +++++++++++++++++++ ...-06-21-12-00-00.gh-issue-151862.Xq7vTm.rst | 3 ++ Python/crossinterp.c | 5 ++ 3 files changed, 55 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-21-12-00-00.gh-issue-151862.Xq7vTm.rst diff --git a/Lib/test/test_interpreters/test_queues.py b/Lib/test/test_interpreters/test_queues.py index 77334aea3836b9..47dbfcfdc14ce4 100644 --- a/Lib/test/test_interpreters/test_queues.py +++ b/Lib/test/test_interpreters/test_queues.py @@ -381,6 +381,53 @@ def test_put_get_full_fallback(self): self.assertEqual(obj, obj2) self.assertIsNot(obj, obj2) + def _check_unpickle_attributeerror_arg(self, arg): + # Put an object whose class is then removed so that get() must + # unpickle it via a module __getattr__ that raises AttributeError + # with the given arg, then confirm get() raises NotShareableError + # without crashing. + source = dedent(""" + _attrerr_arg = None + + class Thing: + pass + + def break_module(mod, arg): + del mod.Thing + mod._attrerr_arg = arg + def __getattr__(name): + raise AttributeError(mod._attrerr_arg) + mod.__getattr__ = __getattr__ + """) + with import_helper.ready_to_import('_xi_attrerr', source) as (name, _): + mod = importlib.import_module(name) + queue = queues.create() + queue.put(mod.Thing()) + mod.break_module(mod, arg) + with self.assertRaises(interpreters.NotShareableError): + queue.get() + + def test_get_unpickle_fails_with_bad_attributeerror_arg(self): + # gh-151862: getting an object that fails to unpickle with an + # AttributeError whose first argument cannot be encoded to UTF-8 + # used to crash (NULL dereference in check_missing___main___attr()). + # Two distinct branches NULL the PyUnicode_AsUTF8() result: a + # non-str arg (fails the type check) and a surrogate str (is unicode + # but fails UTF-8 encoding). + for arg in [42, b'x', None, '\ud800']: + with self.subTest(arg=arg): + self._check_unpickle_attributeerror_arg(arg) + + def test_get_unpickle_fails_with_str_attributeerror_arg(self): + # Positive control: a normal str arg (including the genuine missing + # __main__ attribute message shape) must not crash and is handled + # normally. This locks in the non-NULL strncmp() path so a future + # "skip the guard when the arg is a str" change cannot silently + # reintroduce the NULL dereference. + for arg in ['boom', "module '__main__' has no attribute 'Thing'"]: + with self.subTest(arg=arg): + self._check_unpickle_attributeerror_arg(arg) + def test_put_get_same_interpreter(self): interp = interpreters.create() interp.exec(dedent(""" diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-21-12-00-00.gh-issue-151862.Xq7vTm.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-21-12-00-00.gh-issue-151862.Xq7vTm.rst new file mode 100644 index 00000000000000..95b2a89a4a3aa0 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-21-12-00-00.gh-issue-151862.Xq7vTm.rst @@ -0,0 +1,3 @@ +Fixed a crash (``NULL`` dereference) when an object passed between +interpreters via :mod:`concurrent.interpreters` fails to unpickle with an +:exc:`AttributeError` whose first argument is not a string. diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 6b489bf03f86ec..2d11d7442cf283 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -664,6 +664,11 @@ check_missing___main___attr(PyObject *exc) } } const char *err = PyUnicode_AsUTF8(msgobj); + if (err == NULL) { + PyErr_Clear(); + Py_DECREF(msgobj); + return 0; + } // Check if it's a missing __main__ attr. int cmp = strncmp(err, "module '__main__' has no attribute '", 36); From 48ad5c4fcbef715557447881e821f067766b4905 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Mon, 22 Jun 2026 00:53:34 +0800 Subject: [PATCH 2/2] Trim verbose test comments --- Lib/test/test_interpreters/test_queues.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_interpreters/test_queues.py b/Lib/test/test_interpreters/test_queues.py index 47dbfcfdc14ce4..e5761d4ad8b5bd 100644 --- a/Lib/test/test_interpreters/test_queues.py +++ b/Lib/test/test_interpreters/test_queues.py @@ -382,10 +382,8 @@ def test_put_get_full_fallback(self): self.assertIsNot(obj, obj2) def _check_unpickle_attributeerror_arg(self, arg): - # Put an object whose class is then removed so that get() must - # unpickle it via a module __getattr__ that raises AttributeError - # with the given arg, then confirm get() raises NotShareableError - # without crashing. + # Cross an object through a queue where get() must re-import its + # class via a module __getattr__ that raises AttributeError(arg). source = dedent(""" _attrerr_arg = None @@ -408,22 +406,15 @@ def __getattr__(name): queue.get() def test_get_unpickle_fails_with_bad_attributeerror_arg(self): - # gh-151862: getting an object that fails to unpickle with an - # AttributeError whose first argument cannot be encoded to UTF-8 - # used to crash (NULL dereference in check_missing___main___attr()). - # Two distinct branches NULL the PyUnicode_AsUTF8() result: a - # non-str arg (fails the type check) and a surrogate str (is unicode - # but fails UTF-8 encoding). + # gh-151862: an AttributeError arg that can't be UTF-8 encoded used + # to crash (NULL deref); covers both non-str and surrogate-str args. for arg in [42, b'x', None, '\ud800']: with self.subTest(arg=arg): self._check_unpickle_attributeerror_arg(arg) def test_get_unpickle_fails_with_str_attributeerror_arg(self): - # Positive control: a normal str arg (including the genuine missing - # __main__ attribute message shape) must not crash and is handled - # normally. This locks in the non-NULL strncmp() path so a future - # "skip the guard when the arg is a str" change cannot silently - # reintroduce the NULL dereference. + # Positive control: a normal str arg (incl. the real missing-__main__ + # message) must not crash, locking in the non-NULL strncmp() path. for arg in ['boom', "module '__main__' has no attribute 'Thing'"]: with self.subTest(arg=arg): self._check_unpickle_attributeerror_arg(arg)