From 4043069108144c60903a685f497a3283033ec873 Mon Sep 17 00:00:00 2001 From: Timur Mamedov Date: Thu, 22 Oct 2026 00:52:56 -0400 Subject: [PATCH] [3.15] gh-151818: Fix double-free in _CALL_LIST_APPEND on allocation failure (GH-151826) On allocation failure, close the remaining live inputs and pop all three inputs before unwinding, so the consumed argument is not closed twice. Backport the regression test and regenerate the interpreter files using the 3.15 cases generator. (cherry picked from commit 4ecf6ca1b3d1eb8bb9afd0c0b9dfdbebdc9e05fc) --- Include/internal/pycore_opcode_metadata.h | 2 +- Include/internal/pycore_uop_metadata.h | 2 +- Lib/test/test_opcache.py | 34 ++++++++++++++ ...-06-20-18-00-00.gh-issue-151818.LsApDf.rst | 3 ++ Modules/_testinternalcapi/test_cases.c.h | 12 +++++ Python/bytecodes.c | 5 +- Python/executor_cases.c.h | 46 +++++++++++++++++-- Python/generated_cases.c.h | 12 +++++ 8 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-18-00-00.gh-issue-151818.LsApDf.rst diff --git a/Include/internal/pycore_opcode_metadata.h b/Include/internal/pycore_opcode_metadata.h index 7d9babf358971df..c25d296d69a7464 100644 --- a/Include/internal/pycore_opcode_metadata.h +++ b/Include/internal/pycore_opcode_metadata.h @@ -1158,7 +1158,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = { [CALL_KW_NON_PY] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [CALL_KW_PY] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, [CALL_LEN] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [CALL_LIST_APPEND] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CALL_LIST_APPEND] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [CALL_METHOD_DESCRIPTOR_FAST] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, [CALL_METHOD_DESCRIPTOR_NOARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, diff --git a/Include/internal/pycore_uop_metadata.h b/Include/internal/pycore_uop_metadata.h index 43bb21bd61f55c9..52a20343f293a5e 100644 --- a/Include/internal/pycore_uop_metadata.h +++ b/Include/internal/pycore_uop_metadata.h @@ -342,7 +342,7 @@ const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = { [_GUARD_CALLABLE_ISINSTANCE] = HAS_EXIT_FLAG, [_CALL_ISINSTANCE] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_GUARD_CALLABLE_LIST_APPEND] = HAS_EXIT_FLAG, - [_CALL_LIST_APPEND] = HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG, + [_CALL_LIST_APPEND] = HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG, [_GUARD_CALLABLE_METHOD_DESCRIPTOR_O] = HAS_ARG_FLAG | HAS_EXIT_FLAG, [_CALL_METHOD_DESCRIPTOR_O] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_CHECK_RECURSION_LIMIT] = HAS_EXIT_FLAG, diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index 7946550ec0db637..7bc8eea772c1a53 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -2,6 +2,7 @@ import copy import pickle import dis +import sys import threading import types import unittest @@ -2171,6 +2172,39 @@ class MyList(list): pass self.assert_no_opcode(my_list_append, "CALL_LIST_APPEND") self.assert_no_opcode(my_list_append, "CALL") + @cpython_only + @requires_specialization + def test_call_list_append_memory_error(self): + # gh-151818: if CALL_LIST_APPEND fails to resize the list, + # the argument must be released exactly once. + _testcapi = import_module("_testcapi") + + class Holder: + pass + + def list_append(l, holder): + # holder.item is pushed as a new reference, which + # CALL_LIST_APPEND steals. + l.append(holder.item) + + holder = Holder() + holder.item = item = object() + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + list_append([], holder) + self.assert_specialized(list_append, "CALL_LIST_APPEND") + + refcount = sys.getrefcount(item) + # An empty list has no spare capacity, so append() must resize it. + lst = [] + with self.assertRaises(MemoryError): + _testcapi.set_nomemory(0) + try: + list_append(lst, holder) + finally: + _testcapi.remove_mem_hooks() + self.assertEqual(lst, []) + self.assertEqual(sys.getrefcount(item), refcount) + @cpython_only @requires_specialization def test_load_attr_module_with_getattr(self): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-18-00-00.gh-issue-151818.LsApDf.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-18-00-00.gh-issue-151818.LsApDf.rst new file mode 100644 index 000000000000000..f1e56c81f2ff029 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-18-00-00.gh-issue-151818.LsApDf.rst @@ -0,0 +1,3 @@ +Fix a double-free crash in ``list.append()`` (the specialized +``CALL_LIST_APPEND`` instruction) when growing the list fails with +:exc:`MemoryError`. diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 3e909c21e440445..8740595e4280172 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -3812,6 +3812,18 @@ int err = _PyList_AppendTakeRef((PyListObject *)self_o, PyStackRef_AsPyObjectSteal(arg)); UNLOCK_OBJECT(self_o); if (err) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyStackRef tmp = self; + self = PyStackRef_NULL; + stack_pointer[-2] = self; + PyStackRef_CLOSE(tmp); + tmp = callable; + callable = PyStackRef_NULL; + stack_pointer[-3] = callable; + PyStackRef_CLOSE(tmp); + stack_pointer = _PyFrame_GetStackPointer(frame); + stack_pointer += -3; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); JUMP_TO_LABEL(error); } c = callable; diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 77aaa8a2f446261..ad400ec5e1fb455 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -5075,7 +5075,10 @@ dummy_func( int err = _PyList_AppendTakeRef((PyListObject *)self_o, PyStackRef_AsPyObjectSteal(arg)); UNLOCK_OBJECT(self_o); if (err) { - ERROR_NO_POP(); + // arg is consumed even on failure: close the other inputs + // and pop them all, so that arg is not closed a second time. + DECREF_INPUTS(); + ERROR_IF(true); } c = callable; s = self; diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 38da59a0f9bf3af..a76524a63f1c1e2 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -19004,6 +19004,18 @@ int err = _PyList_AppendTakeRef((PyListObject *)self_o, PyStackRef_AsPyObjectSteal(arg)); UNLOCK_OBJECT(self_o); if (err) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyStackRef tmp = self; + self = PyStackRef_NULL; + stack_pointer[-2] = self; + PyStackRef_CLOSE(tmp); + tmp = callable; + callable = PyStackRef_NULL; + stack_pointer[-3] = callable; + PyStackRef_CLOSE(tmp); + stack_pointer = _PyFrame_GetStackPointer(frame); + stack_pointer += -3; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); SET_CURRENT_CACHED_VALUES(0); JUMP_TO_ERROR(); } @@ -19046,7 +19058,17 @@ int err = _PyList_AppendTakeRef((PyListObject *)self_o, PyStackRef_AsPyObjectSteal(arg)); UNLOCK_OBJECT(self_o); if (err) { - stack_pointer += 1; + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyStackRef tmp = self; + self = PyStackRef_NULL; + stack_pointer[-1] = self; + PyStackRef_CLOSE(tmp); + tmp = callable; + callable = PyStackRef_NULL; + stack_pointer[-2] = callable; + PyStackRef_CLOSE(tmp); + stack_pointer = _PyFrame_GetStackPointer(frame); + stack_pointer += -2; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); SET_CURRENT_CACHED_VALUES(0); JUMP_TO_ERROR(); @@ -19092,8 +19114,17 @@ int err = _PyList_AppendTakeRef((PyListObject *)self_o, PyStackRef_AsPyObjectSteal(arg)); UNLOCK_OBJECT(self_o); if (err) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyStackRef tmp = self; + self = PyStackRef_NULL; stack_pointer[0] = self; - stack_pointer += 2; + PyStackRef_CLOSE(tmp); + tmp = callable; + callable = PyStackRef_NULL; + stack_pointer[-1] = callable; + PyStackRef_CLOSE(tmp); + stack_pointer = _PyFrame_GetStackPointer(frame); + stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); SET_CURRENT_CACHED_VALUES(0); JUMP_TO_ERROR(); @@ -19141,10 +19172,17 @@ int err = _PyList_AppendTakeRef((PyListObject *)self_o, PyStackRef_AsPyObjectSteal(arg)); UNLOCK_OBJECT(self_o); if (err) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyStackRef tmp = self; + self = PyStackRef_NULL; stack_pointer[0] = callable; stack_pointer[1] = self; - stack_pointer += 3; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + PyStackRef_CLOSE(tmp); + tmp = callable; + callable = PyStackRef_NULL; + stack_pointer[0] = callable; + PyStackRef_CLOSE(tmp); + stack_pointer = _PyFrame_GetStackPointer(frame); SET_CURRENT_CACHED_VALUES(0); JUMP_TO_ERROR(); } diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index d312fef398794f1..a6d034a716d521c 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -3812,6 +3812,18 @@ int err = _PyList_AppendTakeRef((PyListObject *)self_o, PyStackRef_AsPyObjectSteal(arg)); UNLOCK_OBJECT(self_o); if (err) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyStackRef tmp = self; + self = PyStackRef_NULL; + stack_pointer[-2] = self; + PyStackRef_CLOSE(tmp); + tmp = callable; + callable = PyStackRef_NULL; + stack_pointer[-3] = callable; + PyStackRef_CLOSE(tmp); + stack_pointer = _PyFrame_GetStackPointer(frame); + stack_pointer += -3; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); JUMP_TO_LABEL(error); } c = callable;