--module: compiled code passes an object pointer 32 bytes outside any valid block (PySide6, CPython 3.13)
Summary
With nuitka --module, a compiled module that builds Qt widgets eventually
hands CPython an object pointer that lies 32 bytes outside any valid heap
block, on an ordinary attribute access. _PyType_LookupRef then writes a
refcount into that unowned memory. The process segfaults some time later,
in an unrelated allocation.
The same code interpreted is clean under the identical driver.
Environment
| Nuitka | 4.1.3, and reproduced unchanged on 4.2 |
| Python | 3.13.14 (distro build, /usr/lib64) |
| PySide6 | 6.11.0 |
| gcc | 15.3.0 |
| OS | openSUSE Leap 16.0, kernel 7.2.0 |
| Mode | --module (single module compiled; rest of the app interpreted) |
Symptom
A ~700-line PySide6 module is compiled with nuitka --module, its .py
removed so the .so is imported. The application then:
- builds its main window (~120 parentless top-level widgets exist at this point: combo popups, menus, list views),
- closes and
deleteLater()s every top-level widget, - drains the delete queue with
sendPostedEvents(None, QEvent.Type.DeferredDelete), - builds the main window again.
Round 2 or 3 segfaults. Interpreted, the same cycle survives indefinitely.
Narrowing on the trigger:
- Stopping after
close()+deleteLater()is clean. Draining the delete queue is the fatal step, so it is the actual C++ destruction. - Skipping any single widget type from the teardown does not help.
- Building the panel from that module alone (7 top-levels) does not reproduce; it needs the full application's widget population.
Root cause (valgrind memcheck, PYTHONMALLOC=malloc)
Invalid write of size 4
at 0x498FBD2: ??? (in libpython3.13.so.1.0)
by 0x498FF20: _PyType_LookupRef (in libpython3.13.so.1.0)
by 0x4AB1C70: _PyObject_GenericGetAttrWithDict (in libpython3.13.so.1.0)
by 0xDB32C1C: ??? (in <compiled module>.so)
by 0xDBD94EE: ??? (in <compiled module>.so)
by 0x49C603E: PyObject_Vectorcall (in libpython3.13.so.1.0)
Address 0x373641f0 is 32 bytes BEFORE a block of size 64 in arena "client"A plain attribute access in compiled code passes an object pointer that is
32 bytes below a live block. The constant 32-byte offset is what makes this
look like an object/GC header layout mismatch in --module codegen against
CPython 3.13, rather than a lifetime bug.
No frame inside the compiled module appears as the faulting frame: the bad writes happen in CPython, on memory the generated code mis-addressed.
Second, subordinate path (--unstripped + gdb)
With the compiled-function free list active, the crash usually surfaces here instead:
#0 _PyObject_GC_Resize
#1 Nuitka_Function_New CompiledFunctionType.c:1552
#2 MAKE_FUNCTION_..._build_..._lambda
#3 impl_..._buildallocateFromFreeList reuses an entry and calls PyObject_GC_Resize,
whose realloc moves the function object and frees the old block while
stale pointers to it survive — valgrind reports the later write as landing
in that freed block.
Forcing MAX_FUNCTION_FREE_LIST_COUNT to 0 removes this path from the log
entirely, and the 32-bytes-before write above remains, and it still
segfaults. So the free list amplifies the corruption; it is not the
origin.
Ruled out (each measured)
--enable-plugin=pyside6— still crashes.--experimental=disable-allocators— still crashes.--experimental=disable-freelist-all— still crashes. Note this flag looks like a no-op on this path:releaseToFreeListcaches wheneverfree_list##_count < max_free_list_count, regardless ofuse_freelists.- Patched Nuitka with
MAX_FUNCTION_FREE_LIST_COUNT 0, built with--disable-cache=all— still crashes. - Nuitka 4.2 — still crashes.
Reproducer
I don't have a shareable runnable case, and I'm sorry — the application is proprietary and I can't publish it. Attempts to synthesise one failed: a stand-in module building ~140 parentless top-level widgets in the same pattern does not reproduce, so something about the real application's size or widget structure matters.
What I can offer:
- the full valgrind log and the
--unstrippedgdb session, - the exact trigger conditions above.
Happy to run any instrumented build or patch you'd like against the real case and report back — it reproduces deterministically here, so it is a fast turnaround.
Impact
It rules out --module for any Qt-importing module in this application:
the compiled binary itself opens and runs, but the test suite tears widgets
down and rebuilds between tests, which is exactly the pattern that
corrupts. That currently keeps ~34k lines out of native compilation.
Source: Nuitka/Nuitka