https://github.com/python/cpython/commit/4ecf6ca1b3d1eb8bb9afd0c0b9dfdbebdc9e05fc
commit: 4ecf6ca1b3d1eb8bb9afd0c0b9dfdbebdc9e05fc
branch: main
author: Timur Mamedov <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2026-10-21T21:52:56-07:00
summary:

gh-151818: Fix double-free in _CALL_LIST_APPEND on allocation failure (#151826)

_CALL_LIST_APPEND stole the arg stackref via PyStackRef_AsPyObjectSteal
and passed it to _PyList_AppendTakeRef. When the list's backing array
failed to grow, _PyList_AppendTakeRef decreffed the item, but the stale
stackref remained on the value stack. The exception unwinder then closed
it a second time, causing a double-free / use-after-free.

Fix by giving _PyList_AppendTakeRef a separate reference via Py_NewRef
and closing the stackref explicitly on success. On the error path the
stackref still holds a valid reference, so the exception unwinder can
safely close it.

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-18-00-00.gh-issue-151818.LsApDf.rst
M Include/internal/pycore_opcode_metadata.h
M Include/internal/pycore_uop_metadata.h
M Lib/test/test_opcache.py
M Modules/_testinternalcapi/test_cases.c.h
M Python/bytecodes.c
M Python/executor_cases.c.h
M Python/generated_cases.c.h

diff --git a/Include/internal/pycore_opcode_metadata.h 
b/Include/internal/pycore_opcode_metadata.h
index d3546119bc9ee0e..04b826ed09144e7 100644
--- a/Include/internal/pycore_opcode_metadata.h
+++ b/Include/internal/pycore_opcode_metadata.h
@@ -1146,7 +1146,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 86b449158f00d0d..0cd82c53aa932ae 100644
--- a/Include/internal/pycore_uop_metadata.h
+++ b/Include/internal/pycore_uop_metadata.h
@@ -338,7 +338,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 60879e2774e7077..357956a4b661b3d 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_call_c_function_extra_flags(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 3bdc16437e2bc61..2c5af6afd17f004 100644
--- a/Modules/_testinternalcapi/test_cases.c.h
+++ b/Modules/_testinternalcapi/test_cases.c.h
@@ -3984,6 +3984,22 @@
                 int err = _PyList_AppendTakeRef((PyListObject *)self_o, 
PyStackRef_AsPyObjectSteal(arg));
                 UNLOCK_OBJECT(self_o);
                 if (err) {
+                    _PyFrame_SetStackPointer(frame, stack_pointer);
+                    _PyFrame_StackPointerValidate(frame);
+                    _PyStackRef tmp = self;
+                    self = PyStackRef_NULL;
+                    stack_pointer[-2] = self;
+                    PyStackRef_CLOSE(tmp);
+                    _PyFrame_StackPointerInvalidate(frame);
+                    assert(stack_pointer == _PyFrame_GetStackPointer(frame));
+                    _PyFrame_StackPointerValidate(frame);
+                    tmp = callable;
+                    callable = PyStackRef_NULL;
+                    stack_pointer[-3] = callable;
+                    PyStackRef_CLOSE(tmp);
+                    _PyFrame_StackPointerInvalidate(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 31eaeab0d67841f..484820911309007 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -5038,7 +5038,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 c5b2dfcf5f618f3..78d97c36de4324b 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -19083,6 +19083,22 @@
             int err = _PyList_AppendTakeRef((PyListObject *)self_o, 
PyStackRef_AsPyObjectSteal(arg));
             UNLOCK_OBJECT(self_o);
             if (err) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                _PyFrame_StackPointerValidate(frame);
+                _PyStackRef tmp = self;
+                self = PyStackRef_NULL;
+                stack_pointer[-2] = self;
+                PyStackRef_CLOSE(tmp);
+                _PyFrame_StackPointerInvalidate(frame);
+                assert(stack_pointer == _PyFrame_GetStackPointer(frame));
+                _PyFrame_StackPointerValidate(frame);
+                tmp = callable;
+                callable = PyStackRef_NULL;
+                stack_pointer[-3] = callable;
+                PyStackRef_CLOSE(tmp);
+                _PyFrame_StackPointerInvalidate(frame);
+                stack_pointer += -3;
+                ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
                 SET_CURRENT_CACHED_VALUES(0);
                 JUMP_TO_ERROR();
             }
@@ -19125,7 +19141,21 @@
             int err = _PyList_AppendTakeRef((PyListObject *)self_o, 
PyStackRef_AsPyObjectSteal(arg));
             UNLOCK_OBJECT(self_o);
             if (err) {
-                stack_pointer += 1;
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                _PyFrame_StackPointerValidate(frame);
+                _PyStackRef tmp = self;
+                self = PyStackRef_NULL;
+                stack_pointer[-1] = self;
+                PyStackRef_CLOSE(tmp);
+                _PyFrame_StackPointerInvalidate(frame);
+                assert(stack_pointer == _PyFrame_GetStackPointer(frame));
+                _PyFrame_StackPointerValidate(frame);
+                tmp = callable;
+                callable = PyStackRef_NULL;
+                stack_pointer[-2] = callable;
+                PyStackRef_CLOSE(tmp);
+                _PyFrame_StackPointerInvalidate(frame);
+                stack_pointer += -2;
                 ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
                 SET_CURRENT_CACHED_VALUES(0);
                 JUMP_TO_ERROR();
@@ -19171,8 +19201,21 @@
             int err = _PyList_AppendTakeRef((PyListObject *)self_o, 
PyStackRef_AsPyObjectSteal(arg));
             UNLOCK_OBJECT(self_o);
             if (err) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                _PyFrame_StackPointerValidate(frame);
+                _PyStackRef tmp = self;
+                self = PyStackRef_NULL;
                 stack_pointer[0] = self;
-                stack_pointer += 2;
+                PyStackRef_CLOSE(tmp);
+                _PyFrame_StackPointerInvalidate(frame);
+                assert(stack_pointer == _PyFrame_GetStackPointer(frame));
+                _PyFrame_StackPointerValidate(frame);
+                tmp = callable;
+                callable = PyStackRef_NULL;
+                stack_pointer[-1] = callable;
+                PyStackRef_CLOSE(tmp);
+                _PyFrame_StackPointerInvalidate(frame);
+                stack_pointer += -1;
                 ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
                 SET_CURRENT_CACHED_VALUES(0);
                 JUMP_TO_ERROR();
@@ -19220,10 +19263,21 @@
             int err = _PyList_AppendTakeRef((PyListObject *)self_o, 
PyStackRef_AsPyObjectSteal(arg));
             UNLOCK_OBJECT(self_o);
             if (err) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                _PyFrame_StackPointerValidate(frame);
+                _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);
+                _PyFrame_StackPointerInvalidate(frame);
+                assert(stack_pointer == _PyFrame_GetStackPointer(frame));
+                _PyFrame_StackPointerValidate(frame);
+                tmp = callable;
+                callable = PyStackRef_NULL;
+                stack_pointer[0] = callable;
+                PyStackRef_CLOSE(tmp);
+                _PyFrame_StackPointerInvalidate(frame);
                 SET_CURRENT_CACHED_VALUES(0);
                 JUMP_TO_ERROR();
             }
diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h
index dd0ce41e4b06b4a..21f3a63fb715f87 100644
--- a/Python/generated_cases.c.h
+++ b/Python/generated_cases.c.h
@@ -3984,6 +3984,22 @@
                 int err = _PyList_AppendTakeRef((PyListObject *)self_o, 
PyStackRef_AsPyObjectSteal(arg));
                 UNLOCK_OBJECT(self_o);
                 if (err) {
+                    _PyFrame_SetStackPointer(frame, stack_pointer);
+                    _PyFrame_StackPointerValidate(frame);
+                    _PyStackRef tmp = self;
+                    self = PyStackRef_NULL;
+                    stack_pointer[-2] = self;
+                    PyStackRef_CLOSE(tmp);
+                    _PyFrame_StackPointerInvalidate(frame);
+                    assert(stack_pointer == _PyFrame_GetStackPointer(frame));
+                    _PyFrame_StackPointerValidate(frame);
+                    tmp = callable;
+                    callable = PyStackRef_NULL;
+                    stack_pointer[-3] = callable;
+                    PyStackRef_CLOSE(tmp);
+                    _PyFrame_StackPointerInvalidate(frame);
+                    stack_pointer += -3;
+                    ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
                     JUMP_TO_LABEL(error);
                 }
                 c = callable;

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to