Skip to content

ceval: CALL_LIST_APPEND bytecode leaves a dangling stack reference on the stack on error #152090

Description

@vstinner

Bug report

Script bug.py (from issue gh-149146, but modified):

importresourceimportsignalimportosdefsegfault_handler(signum, frame):
os.abort()
raiseMemoryError#signal.signal(signal.SIGSEGV, segfault_handler)# deallocation the chain uses a lot of C stack memoryCHAIN_LENGTH=1024*1024# limit process memory to not crash the whole machinemax_memory=1024*1024*1024resource.setrlimit(resource.RLIMIT_AS, (max_memory, max_memory))
print("create long chain...")
long_chain=Nonefor_inrange(CHAIN_LENGTH):
long_chain= (long_chain, None)
print("fill the process memory...")
memory= []
try:
large=1024*1024whileTrue:
memory.append(b'x'*large)
exceptMemoryError:
passtry:
small=1024os.uname()
whileTrue:
memory.append(b'x'*small)
exceptMemoryError:
passprint("delete the long chain...")
long_chain=None# at exit (on error), deallocating long_chain will use a lot of C stack memory# ... and does crash!

Sometimes, running this script triggers a bug when memory.append(b'x' * small) raises a MemoryError. Sadly, the reproducer is very fragile. So far, I failed to write a more reliable reproducer.

The fix is to set stack_pointer[-1] to PyStackRef_NULL in _CALL_LIST_APPEND before calling JUMP_TO_LABEL(error);.

Steps:

  • PyErr_NoMemory() is called by _PyList_AppendTakeRefListResize() in _CALL_LIST_APPEND opcode
  • At this point, stack_pointer[-1] (arg) points to freed memory (all bytes are set to 0xdd)
  • _PyList_AppendTakeRef() returns non-zero result, so _CALL_LIST_APPEND opcode goes to JUMP_TO_LABEL(error);
  • The error label calls JUMP_TO_LABEL(exception_unwind);
  • exception_unwind label clears the stack: call ref = _PyFrame_StackPop() and PyStackRef_XCLOSE(ref) until the stack is empty
  • exception_unwind: PyStackRef_XCLOSE(ref) fails with an assertion error (Py_DECREF_MORTAL: Assertion !_Py_IsStaticImmortal(op)') because stack_pointer[-1]` points to freed memory
$ gdb -args ./python bug.py (gdb) b os_uname
(gdb) run
Breakpoint 2, os_uname (...)
(gdb) b PyErr_NoMemory
(gdb) c
Continuing.
Breakpoint 3, PyErr_NoMemory () at Python/errors.c:803
803	{
(gdb) n
(...)
(gdb) _PyList_AppendTakeRefListResize (...)
531 Py_DECREF(newitem);
(gdb) 532 return -1;
(gdb) _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7ffff7fb0020, throwflag=0) at Python/generated_cases.c.h:3983
3983 if (err) {
(gdb) p stack_pointer[-1]
$2 = <unknown at remote 0xcc08e0>
(gdb) p /x *(PyObject*)stack_pointer[-1]
$3 = {
{
ob_refcnt_full = 0xdddddddddddddddd,
{
ob_refcnt = 0xdddddddd,
ob_overflow = 0xdddd,
ob_flags = 0xdddd
},
_aligner = 0xdd
},
ob_type = 0xdddddddddddddddd
}
(gdb) n
3984 JUMP_TO_LABEL(error);
(gdb) n
(...)
(gdb) 13843 JUMP_TO_LABEL(exception_unwind);
(gdb) 13849 int offset = INSTR_OFFSET()-1;
(...)
(gdb) 13865 while (frame->stackpointer > new_top) {
(gdb) 13866 _PyStackRef ref = _PyFrame_StackPop(frame);
(gdb) 13867 PyStackRef_XCLOSE(ref);
(gdb) p ref
$5 = <unknown at remote 0xcc08e0>
(gdb) p /x *(PyObject*)ref
$6 = {
{
ob_refcnt_full = 0xdddddddddddddddd,
{
ob_refcnt = 0xdddddddd,
ob_overflow = 0xdddd,
ob_flags = 0xdddd
},
_aligner = 0xdd
},
ob_type = 0xdddddddddddddddd
}
(gdb) n
python: ./Include/internal/pycore_object.h:414: Py_DECREF_MORTAL: Assertion `!_Py_IsStaticImmortal(op)' failed.
Program received signal SIGABRT, Aborted.
0x00007ffff7cfedcc in __pthread_kill_implementation () from /lib64/libc.so.6

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions