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):

import resource
import signal
import os

def segfault_handler(signum, frame):
    os.abort()
    raise MemoryError

#signal.signal(signal.SIGSEGV, segfault_handler)

# deallocation the chain uses a lot of C stack memory
CHAIN_LENGTH = 1024 * 1024

# limit process memory to not crash the whole machine
max_memory = 1024 * 1024 * 1024
resource.setrlimit(resource.RLIMIT_AS, (max_memory, max_memory))

print("create long chain...")
long_chain = None
for _ in range(CHAIN_LENGTH):
    long_chain = (long_chain, None)

print("fill the process memory...")

memory = []
try:
    large = 1024 * 1024
    while True:
        memory.append(b'x' * large)
except MemoryError:
    pass

try:
    small = 1024
    os.uname()
    while True:
        memory.append(b'x' * small)
except MemoryError:
    pass

print("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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions