Repository navigation
Double free / use-after-free in list.append() when the list grows under MemoryError (_CALL_LIST_APPEND) #151818
Description
Activity
- addedinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)type-crashA hard crash of the interpreter, possibly with a core dumpA hard crash of the interpreter, possibly with a core dump
on Jun 20, 2026 Related to #151119 / #151538, but I believe this is a distinct bug.
Here's Claude analysis:
#151119 / #151538 fix the missing eval-stack sync before
_PyList_AppendTakeRef— the_Py_Deallocstackpointer != NULLassert, which fires when the appended item has no other reference and is deallocated immediately.This issue is the double-free / use-after-free:
_CALL_LIST_APPENDstealsarg,_PyList_AppendTakeRefdecrefs it whenlist_resizefails, but the op then takesERROR_NO_POP()and leaves the consumedargon the value stack, soexception_unwindcloses it a second time. It surfaces when the item has another live reference (so that first decref doesn't free it).#151538 marks the append ops
HAS_ESCAPES, which also adds the SP-sync to_CALL_LIST_APPEND— but its_CALL_LIST_APPENDhunk only adds the sync; the error path stillJUMP_TO_ERROR()s withargon the stack, so the double-free survives that PR. Notably_CALL_LIST_APPENDis the only append op usingERROR_NO_POP;LIST_APPEND/SET_ADD/MAP_ADDuse the codegen-accountedERROR_IF, which drops the consumed input. So the fix here is to account for the stolenargon the error path (and could be folded into #151538).- added 3 commits that reference this issue
on Jun 21, 2026 Oh, I created a duplicate issue: #152090.
According to git bisect, the regression was introduced by commit 72f5665:
commit 72f56654d06a6d23c91e892c05f9e4d70009315b Author: Mark Shannon <mark@hotpy.org> Date: Wed Feb 12 17:44:59 2025 +0000 GH-128682: Account for escapes in `DECREF_INPUTS` (GH-129953) * Handle escapes in DECREF_INPUTS * Mark a few more functions as escaping * Replace DECREF_INPUTS with PyStackRef_CLOSE where possibleA simpler reproducer:
import _testcapi import sys import os import struct sizeof_empty_list = sys.getsizeof([]) sizeof_pointer = struct.calcsize('P') def capacity(lst): size = (sys.getsizeof(lst) - sizeof_empty_list) if size % sizeof_pointer: raise ValueError("wrong list size") capacity = size // sizeof_pointer return capacity def func(offset): L=[1,2,3] memory_error = False small = 1024 * 1024 for i in range(1000): # run a few iterations (25) to specialize the bytecode # to CALL_LIST_APPEND if i > 25 and capacity(L) == len(L): # the list is full: L.append() will have to resize the list os.uname() # used as gdb traceback _testcapi.set_nomemory(offset, 0) try: try: L.append(b'x' * small) finally: _testcapi.remove_mem_hooks() except MemoryError: memory_error = True break for i in range(0, 10): func(i) print("ok")
- marked ceval: CALL_LIST_APPEND bytecode leaves a dangling stack reference on the stack on error #152090 as a duplicate of this issue
on Jun 24, 2026 - added a commit that references this issue
on Jun 25, 2026 - marked Use-after-free under MemoryError: opcode error path leaves stale stackref that
_PyFrame_ClearLocalsover-decrefs #152147 as a duplicate of this issueon Jun 25, 2026 FYI — blast radius / presentations index. fusil's OOM-injection fuzzing keeps surfacing this as superficially-unrelated crashes, but every one
rr-reduces to this same_CALL_LIST_APPENDdouble-free (_PyList_AppendTakeRefListResize,listobject.c:531). Recording them here so the next person who hits one of these signatures finds this issue instead of filing a new one.One producer, many victim/detector sites (depending on what the double-freed item was and what reused its freed slot) — on a debug build:
Presentation (assert / crash) victim _Py_NegativeRefcount/<object is freed>any item with a 2nd owner DirEntry_dealloc(posixmodule.c) segv / UAFan os.DirEntry.pathstringpycore_stackref.h:726 PyStackRef_XCLOSEnegrefthe leftover stolen stackref itself traceback.c:313 _PyTraceBack_FromFrameassertan object held via a traceback frame pycore_object.h Py_DECREF_MORTAL—!_Py_IsStaticImmortal(op)freed block read back as static-immortal dictobject.c:961 new_dict—mp == NULL || Py_IS_TYPE(mp, &PyDict_Type)a dict that landed on the dict freelist tupleobject.c:48 tuple_alloc—PyTuple_Check(op)a tuple on the tuple freelist gc.c:380 validate_list(at shutdown)a co_constsconstant whose freed slot a weakref'sPyGC_HeadreusedAnd it's reachable from many ordinary stdlib operations under real memory pressure —
os.walk/os.scandir,pkgutil.get_importer/ the import machinery (importlib'spath.append),sys.pathmutation +__import__— not only a literallist.append. (Several of these arrived as separate reports thatrrlater folded into this one.)Since they all funnel through the single
_CALL_LIST_APPENDsteal-then-ERROR_NO_POP, the fix here should cover every presentation above; flagging mainly because the freelist / GC-head faces don't look like alist.appendbug at a glance.Full triage, per-face
rrledgers, and reproducers for each presentation live in a sibling catalog of fusil's OOM findings — this bug isreports/OOM-0036-list-append-oom-double-free/(report.md+meta.json; per-face backtracesbacktrace_immortal.txt/backtrace_dict_freelist.txt/backtrace_validate_list.txt; reproducersrepro.py,repro_natural.py(realRLIMIT_AS, no test API),repro_get_importer.py).
Investigation and comment draft by Claude Code (Opus 4.8)
- added a commit that references this issue
on Jul 3, 2026
Crash report
What happened?
When
list.append(x)has to grow the list's backing array and that allocation fails (i.e.under
MemoryError), the appended itemxis decref'd twice. Ifxis referencedelsewhere this is a use-after-free: the interpreter aborts with
_Py_NegativeRefcounton adebug build, or segfaults on a release build, instead of raising a recoverable
MemoryError.This is reachable under genuine memory pressure (a real
RLIMIT_ASreproducer with no testAPI is included below), so a program that correctly catches
MemoryErrorcan still be leftwith a corrupted interpreter.
Reproducer
Deterministic, pure Python, using
_testcapi.set_nomemoryto fail the grow allocation at acontrolled point:
On a
--with-pydebugbuild this aborts:On a release build it segfaults.
Without any test API (real
MemoryError)The same double-free fires under a genuine allocation failure. With an
RLIMIT_AScap so thelist's grow allocation returns NULL naturally (run on a non-ASan build):
Under the same cap, when the failing allocation is not a list-append grow (e.g. appending
large
bytes), Python raises a clean, catchableMemoryErrorand does not crash — so thesegfault is specific to the buggy append path.
Root cause
In the specialized append bytecode
_CALL_LIST_APPEND(Python/bytecodes.c):argis stolen viaPyStackRef_AsPyObjectStealand handed to_PyList_AppendTakeRef,which consumes the reference on every path — including decref'ing the item when the grow
fails (
_PyList_AppendTakeRefListResize→if (list_resize(...) < 0) { Py_DECREF(newitem); return -1; },Objects/listobject.c).But on that failure the uop takes
ERROR_NO_POP(), which jumps to exception handlingwithout removing
argfrom the value stack. Sincearg's reference was alreadyconsumed, the stale
argstackref is now dangling. The eval loop'sexception_unwindthenpops the frame's value stack and
PyStackRef_XCLOSEs every slot, closing the staleargslot — a second decref of the item.
(Confirmed with ASan on a
--with-pymallocbuild: the item is freed byPyStackRef_XCLOSE←
_PyEval_EvalFrameDefault(theexception_unwindhandler); both the item's allocation andthe second, use-after-free decref are visible in the report.)
Suggested fix
_CALL_LIST_APPENDmust account for the consumedargon the error path — once_PyList_AppendTakeRefhas taken the reference, theargstackref is dead and must not beleft on the value stack for
exception_unwindto close. The sibling ops already show the twocorrect idioms:
LIST_APPEND,SET_ADD,MAP_ADD) call the same kind ofsteal/
*TakeRefhelper but useERROR_IF(...), so the codegen drops the consumed input onthe error path. Concretely,
[x for x in ...](LIST_APPEND, the same_PyList_AppendTakeRefhelper) does not crash wherelst.append(x)(
_CALL_LIST_APPEND) does;_DO_CALL_FUNCTION_EX,_PY_FRAME_EX) callINPUTS_DEAD(); SYNC_SP();beforeERROR_NO_POP().I audited the other specialized ops:
_CALL_LIST_APPENDis the only one that steals astack input and then takes a bare
ERROR_NO_POP()without either form of accounting, whichis why it is the lone op affected.
Environment
main(3.16.0a0); reproduced on--with-pydebugbuilds (abort) and release builds(segfault), both free-threaded and default GIL.
This report and the reduced reproducers were drafted with the assistance of Claude Code; I
have reviewed and reproduced them.
CPython versions tested on:
CPython main branch, 3.16
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.16.0a0 (heads/main:aec0aed1978, Jun 20 2026, 17:44:00) [Clang 22.1.2 (1ubuntu1)]
Linked PRs