shiboken6 6.11.1: SIGSEGV in QUndoStack::clear() — pushed QUndoCommand double-deleted when stack and command are cyclic garbage
-
Environment: PySide6 / shiboken6 6.11.1, CPython 3.14.5, Linux x86_64 (glibc 2.43)
Summary: when a parentless
QUndoStackcreated from Python (so the wrapper owns the C++ object) holds a pushedQUndoCommand, and both wrappers become unreachable in the same Python reference-cycle collection,gc.collect()segfaults insideQUndoStack::clear(). NoQApplicationand no Python subclassing required.Deterministic reproducer (crashes 10/10, exit 139):
import gc from PySide6.QtGui import QUndoCommand, QUndoStack cmd = QUndoCommand("cmd") # command wrapper allocated BEFORE the stack wrapper stack = QUndoStack() stack.me = stack # any cycle: the stack must die inside the GC pass stack.push(cmd) # shiboken parentship: stack owns command del cmd, stack gc.collect() # SIGSEGVMechanism (shiboken6 6.11.1,
sources/shiboken6/libshiboken/basewrapper.cpp):When CPython's
delete_garbage()tp_clears the command wrapper first,SbkObject_tp_clear(line 298) callsShiboken::Object::removeParent(child)with the default argumentsgiveOwnershipBack=true, keepReference=false(line 1746), handing C++ ownership of the command back to Python. The follow-up DECREF frees the wrapper andSbkDeallocWrapperCommonrunscpp_dtor, deleting the C++QUndoCommandwhile the C++QUndoStack::d->command_liststill holds the raw pointer (push()is an unconditional ownership transfer; there is no removal API). When the stack wrapper is torn down later in the same pass,~QUndoStack()→clear()→qDeleteAll(d->command_list)makes a virtual call through the freed command's clobbered vptr → SIGSEGV (faulting pc is0x0in gdb).The opposite tp_clear order is handled correctly (
_destroyParentInfoinvalidates children withgiveOwnershipBack=false), which is why this presents as selection-dependent "flaky" crashes in real applications. We first hit it as intermittent pytest-teardown segfaults in a widgets app whose panels hold a parentless undo stack.Faulthandler C stack of the crash:
libQt6Gui.so.6 QUndoStack::clear() + 0x142 libQt6Gui.so.6 QUndoStack::~QUndoStack() + 0x36 QtGui.abi3.so (wrapper dealloc) libshiboken6.abi3.so.6.11 SbkDeallocWrapperCommon python _Py_Dealloc python gc_collect_main ("Garbage-collecting")Workarounds: parent the
QUndoStackto a QObject, or explicitlystack.clear()while both halves are alive before dropping the last Python references. -
Environment: PySide6 / shiboken6 6.11.1, CPython 3.14.5, Linux x86_64 (glibc 2.43)
Summary: when a parentless
QUndoStackcreated from Python (so the wrapper owns the C++ object) holds a pushedQUndoCommand, and both wrappers become unreachable in the same Python reference-cycle collection,gc.collect()segfaults insideQUndoStack::clear(). NoQApplicationand no Python subclassing required.Deterministic reproducer (crashes 10/10, exit 139):
import gc from PySide6.QtGui import QUndoCommand, QUndoStack cmd = QUndoCommand("cmd") # command wrapper allocated BEFORE the stack wrapper stack = QUndoStack() stack.me = stack # any cycle: the stack must die inside the GC pass stack.push(cmd) # shiboken parentship: stack owns command del cmd, stack gc.collect() # SIGSEGVMechanism (shiboken6 6.11.1,
sources/shiboken6/libshiboken/basewrapper.cpp):When CPython's
delete_garbage()tp_clears the command wrapper first,SbkObject_tp_clear(line 298) callsShiboken::Object::removeParent(child)with the default argumentsgiveOwnershipBack=true, keepReference=false(line 1746), handing C++ ownership of the command back to Python. The follow-up DECREF frees the wrapper andSbkDeallocWrapperCommonrunscpp_dtor, deleting the C++QUndoCommandwhile the C++QUndoStack::d->command_liststill holds the raw pointer (push()is an unconditional ownership transfer; there is no removal API). When the stack wrapper is torn down later in the same pass,~QUndoStack()→clear()→qDeleteAll(d->command_list)makes a virtual call through the freed command's clobbered vptr → SIGSEGV (faulting pc is0x0in gdb).The opposite tp_clear order is handled correctly (
_destroyParentInfoinvalidates children withgiveOwnershipBack=false), which is why this presents as selection-dependent "flaky" crashes in real applications. We first hit it as intermittent pytest-teardown segfaults in a widgets app whose panels hold a parentless undo stack.Faulthandler C stack of the crash:
libQt6Gui.so.6 QUndoStack::clear() + 0x142 libQt6Gui.so.6 QUndoStack::~QUndoStack() + 0x36 QtGui.abi3.so (wrapper dealloc) libshiboken6.abi3.so.6.11 SbkDeallocWrapperCommon python _Py_Dealloc python gc_collect_main ("Garbage-collecting")Workarounds: parent the
QUndoStackto a QObject, or explicitlystack.clear()while both halves are alive before dropping the last Python references. -
Yes, I would normally file this on Jira, but my account currently can't create issues. Posting here so it doesn't stall — if a maintainer wants to file it, please do.