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.
This commit is contained in:
Timur Mamedov authored and GitHub committed 2026-10-21 21:52:56 -07:00
1 parent 3e13407763
commit 4ecf6ca1b3
8 files changed
+133 -7

No files matched your search

+1 -1
View File
@@ -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 },
+1 -1
View File
@@ -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,
+34
View File
@@ -2,6 +2,7 @@ import collections
import copy
import pickle
import dis
import sys
import threading
import types
import unittest
@@ -2171,6 +2172,39 @@ class TestSpecializer(TestBase):
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):
@@ -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`.
+16
View File
@@ -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;
+4 -1
View File
@@ -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;
+58 -4
View File
@@ -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();
}
+16
View File
@@ -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;