https://github.com/python/cpython/commit/f8672b4ef46eabfc2f67b99a6e5ae7fe3298ef3c
commit: f8672b4ef46eabfc2f67b99a6e5ae7fe3298ef3c
branch: 3.15
author: Jelle Zijlstra <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2026-10-22T06:23:06-07:00
summary:

[3.15] gh-151818: Fix double-free in _CALL_LIST_APPEND on allocation failure 
(GH-151826) (#160200)

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)

Co-authored-by: Timur Mamedov <[email protected]>

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 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;

_______________________________________________
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