Skip to content

Native crash (0xC0000005) during Text Import Wizard teardown in on-screen test runs #335

Description

@PierreRaybaut

Summary

When the test suite runs with visible Qt windows (native mode, i.e. without QT_QPA_PLATFORM=offscreen), the process sporadically crashes with a Windows access violation (0xC0000005, exit code -1073741819) during the teardown of datalab/tests/features/common/textimport_unit_test.py::test_import_wizard. The crash is a native C++ fault (no Python faulthandler traceback is produced) that occurs while the TextImportWizard's embedded PlotPy preview is being destroyed by the garbage collector.

Impact

  • CI is not affected: continuous integration runs in offscreen mode, where the raster backend allocates no native windows and the suite is 100% green (24/24 matrix cells in our benchmark).
  • Real-world usage is unlikely to hit it: a user opens the import wizard once and closes it; the crash requires many wizard create/destroy cycles (or accumulated native state) in a single process.
  • Test reliability: in on-screen / native test runs it produces flaky crashes (~12–17% over a full native suite run) that abort the whole pytest session, polluting the native test matrix.

Environment

  • OS: Windows
  • Qt: PyQt5 5.15.11 / Qt 5.15.2
  • Plotting stack: PlotPy 2.10, PythonQwt (version-independent — reproduced identically on 0.15.0 and 0.16.2), guidata 3.14
  • Python: 3.9 → 3.14 (version-independent)
  • Mode: native (on-screen) only; never reproduced in offscreen

Symptoms

  • Exit code -1073741819 (0xC0000005, ACCESS_VIOLATION).
  • The crash always occurs at test_import_wizard: the test line is printed but no PASSED/FAILED ever follows.
  • No faulthandler traceback → the fault is in native code (Qt/PlotPy), not in Python.
  • Sporadic: the same cell sometimes passes and sometimes crashes; in isolation the test passes repeatedly, which initially masked the issue.

Root cause

test_import_wizard builds four TextImportWizard instances in a loop. Each wizard embeds a heavy PlotPy preview (GraphicalRepresentationPagePlotWidget with toolbar, item list and contrast panels, plus a connected SIG_ITEMS_CHANGED signal). The wizards are never closed or deleted explicitly, so their native resources are torn down lazily by the garbage collector. In native mode this lazy, unordered teardown of several plot widgets races inside Qt/PlotPy and occasionally dereferences freed memory, producing the access violation. In offscreen mode there are no native windows, so the path is never exercised.

The crash is independent of PythonQwt version (controlled native matrix: v0.15.0 ≡ 0.16.2, both crash at the same rate), of the DataLab branch (develop and main), and of the Python version.

Reproduction (controlled)

A throwaway native harness reproduces the fault deterministically by mimicking the original lazy-GC teardown and amplifying it (accumulate a batch of fully-built wizards, then drop all references at once and force gc.collect()), run directly (not through run_with_env.py, which masks the child exit code). At a bounded load (80 wizards per run, well below the USER-handle cap so the access violation is the only failure mode):

Behaviour Native crashes (0xC0000005)
Original (lazy-GC teardown) 4 / 10 (~40%)
Deterministic cleanup (fix) 0 / 6

Notes for anyone re-running this:

  • Launch python directly with PYTHONPATH set; run_with_env.py swallows the real exit code (always reports 1 on failure), which makes a native crash indistinguishable from a Python error.
  • Do not show() hundreds of wizards in a tight loop without letting the event loop recycle them: this exhausts the per-process USER handle quota (CreateWindowEx failed), a creation-side artifact unrelated to the teardown bug.

Fix

Release the wizard's native resources deterministically instead of relying on the garbage collector:

  • GraphicalRepresentationPage.cleanup() — disconnect SIG_ITEMS_CHANGED, call plot.del_all_items(), and clear the object/item list before destruction.
  • Wizard.cleanup() (base class) — invoke cleanup() on any page that exposes it, wired to the dialog's closeEvent.
  • test_import_wizardclose() + deleteLater() + processEvents() after each wizard so each teardown happens in a controlled, single-threaded order.

Validation

  • Controlled native repro: 4/10 crashes before the fix, 0/6 after, at identical load.
  • 100 sequential native create/destroy cycles with the fix: exit 0, no crash.
  • test_import_wizard unit test: green.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions