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]