#4019·Nuitka

--module: compiled code passes an object pointer 32 bytes outside any valid block (PySide6, CPython 3.13)

Author: DennisK83Created Aug 31, 2026Updated Sep 18, 2026
Labelsbug

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:

  1. builds its main window (~120 parentless top-level widgets exist at this point: combo popups, menus, list views),
  2. closes and deleteLater()s every top-level widget,
  3. drains the delete queue with sendPostedEvents(None, QEvent.Type.DeferredDelete),
  4. 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_..._build

allocateFromFreeList 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: releaseToFreeList caches whenever free_list##_count < max_free_list_count, regardless of use_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 --unstripped gdb 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.