From 724cf80a6e62cfbe3bd410bcf6f9f3c337922e58 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 25 Jun 2026 21:34:46 +0300 Subject: [PATCH 1/2] gh-152228: Fix a crash in `str.replace` under a limited memory case --- Lib/test/test_str.py | 16 ++++++++++++++++ ...6-06-25-21-34-15.gh-issue-152228.a6K14K.rst | 1 + Objects/unicodeobject.c | 18 +++++++++--------- 3 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-25-21-34-15.gh-issue-152228.a6K14K.rst diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index 4f57499af70f4d..3a964b35628357 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -607,6 +607,22 @@ def test_replace_id(self): text = 'abc def' self.assertIs(text.replace(pattern, pattern), text) + @support.cpython_only + @unittest.skipIf(_testcapi is None, reason="_testcapi is required for this test") + def test_replace_oom(self): + # https://github.com/python/cpython/issues/152228 + s1 = "轘" * 4 + s2 = "&" + s3 = "&" + assertion = self.assertRaises(MemoryError) + _testcapi.set_nomemory(0, 0) + try: + # No allocations made in the test itself: + with assertion: + s1.replace(s2, s3) # this line used to crash before + finally: + _testcapi.remove_mem_hooks() + def test_repeat_id_preserving(self): a = '123abc1@' b = '456zyx-+' diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-25-21-34-15.gh-issue-152228.a6K14K.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-25-21-34-15.gh-issue-152228.a6K14K.rst new file mode 100644 index 00000000000000..916fa1b536da5b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-25-21-34-15.gh-issue-152228.a6K14K.rst @@ -0,0 +1 @@ +Fix a crash in :meth:`str.replace` under a limited memory situation. diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index 3c689761de9b19..785620a186c9cd 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -10753,9 +10753,9 @@ replace(PyObject *self, PyObject *str1, } done: - assert(srelease == (sbuf != PyUnicode_DATA(self))); - assert(release1 == (buf1 != PyUnicode_DATA(str1))); - assert(release2 == (buf2 != PyUnicode_DATA(str2))); + assert(srelease == (sbuf != NULL && sbuf != PyUnicode_DATA(self))); + assert(release1 == (buf1 != NULL && buf1 != PyUnicode_DATA(str1))); + assert(release2 == (buf2 != NULL && buf2 != PyUnicode_DATA(str2))); if (srelease) PyMem_Free((void *)sbuf); if (release1) @@ -10767,9 +10767,9 @@ replace(PyObject *self, PyObject *str1, nothing: /* nothing to replace; return original string (when possible) */ - assert(srelease == (sbuf != PyUnicode_DATA(self))); - assert(release1 == (buf1 != PyUnicode_DATA(str1))); - assert(release2 == (buf2 != PyUnicode_DATA(str2))); + assert(srelease == (sbuf != NULL && sbuf != PyUnicode_DATA(self))); + assert(release1 == (buf1 != NULL && buf1 != PyUnicode_DATA(str1))); + assert(release2 == (buf2 != NULL && buf2 != PyUnicode_DATA(str2))); if (srelease) PyMem_Free((void *)sbuf); if (release1) @@ -10779,9 +10779,9 @@ replace(PyObject *self, PyObject *str1, return unicode_result_unchanged(self); error: - assert(srelease == (sbuf != PyUnicode_DATA(self))); - assert(release1 == (buf1 != PyUnicode_DATA(str1))); - assert(release2 == (buf2 != PyUnicode_DATA(str2))); + assert(srelease == (sbuf != NULL && sbuf != PyUnicode_DATA(self))); + assert(release1 == (buf1 != NULL && buf1 != PyUnicode_DATA(str1))); + assert(release2 == (buf2 != NULL && buf2 != PyUnicode_DATA(str2))); if (srelease) PyMem_Free((void *)sbuf); if (release1) From 07f2ff4d6341ee709c8d9e357bee992c9caa083c Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 25 Jun 2026 23:37:45 +0300 Subject: [PATCH 2/2] Update Lib/test/test_str.py Co-authored-by: Stan Ulbrych --- Lib/test/test_str.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index 3a964b35628357..032118b9e483c8 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -609,6 +609,7 @@ def test_replace_id(self): @support.cpython_only @unittest.skipIf(_testcapi is None, reason="_testcapi is required for this test") + @unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build') def test_replace_oom(self): # https://github.com/python/cpython/issues/152228 s1 = "轘" * 4