Skip to content

GTK/X11 crashes from GTK calls inside @async_function worker threads #409

Description

@ebey317

Hypnotix 4.3 has GTK thread-safety violations that can crash on X11.

Environment:

  • Linux Mint Cinnamon X11
  • Hypnotix 4.3
  • Python 3.10.12
  • GTK 3.24.33

Symptoms:

  • Channel selection can crash with:
    Gdk-ERROR: received X Window System error
    BadRequest (invalid request code or no such operation)
    request_code 0 minor_code 0
  • Launching from desktop icon can abort with SIGABRT/SIGTRAP.
  • coredumps show crashes in libgdk/libX11/cairo while Python worker threads are active.

Root causes found:

  1. LiveMainWindow.play_async is decorated with @async_function but calls GTK/GDK-sensitive code:

    • self.info_menu_item.set_sensitive(False)
    • self.before_play(channel)
    • self.reinit_mpv()
    • self.mpv_drawing_area.get_window().get_xid()
  2. LiveMainWindow.reload is decorated with @async_function; in the Xtream provider path it directly calls GTK/GDK window cursor APIs from the worker thread:

    • self.window.get_window().get_cursor()
    • self.window.get_window().set_cursor(...)
    • Gdk.Cursor.new_from_name(...)

GTK must only be touched from the GTK main loop.

Proposed fixes:

  • Keep GTK setup and mpv window/XID creation on the main thread.
  • Move only blocking/network/wait operations to worker threads.
  • Marshal cursor changes through GLib.idle_add / the existing @idle_function.

Patch direction:

  • Remove @async_function from play_async.
  • Move only mpv.wait_until_playing() into a worker helper.
  • Add idle-thread helpers for Xtream wait cursor set/restore.

Activity

  1. jebjarrell1974-debug commented on Sep 15, 2026

    @jebjarrell1974-debug

    Still present in master (0e0fa1c), and not X11-specific — here is a Wayland
    reproduction with a core dump that shows the damage at the memory level.

    Environment
    hypnotix 5.6 · GTK 3.24.52 · Python 3.14.7 · PyGObject 3.56.3 · glib 2.88.3
    Arch Linux, Wayland (Hyprland), no X11 session — mpv runs through XWayland.

    The violations this issue lists are still unmarshalled in master

    before_play and after_play are @idle_function, but these are not:

    • L885 self.info_menu_item.set_sensitive(False) — called directly from the
      @async_function worker in play_async (L877).
    • L887 self.reinit_mpv() — also from that worker. reinit_mpv (L1612) then calls
      Gtk.events_pending() (L1630) and self.mpv_drawing_area.get_window().get_xid()
      (L1646) off the main thread. Gtk.events_pending() inspects main-loop state from
      a thread that does not own it.
    • L1549/L1551 self.window.get_window().get_cursor() /
      set_cursor(Gdk.Cursor.new_from_name(...)) — from the @async_function reload
      (L1500).

    What the crash looks like on Wayland: heap corruption, not a BadRequest

    SIGSEGV, logged by the kernel as a general protection fault (a non-canonical
    pointer, not a null dereference):

    traps: hypnotix[1683518] general protection fault ip:7fd5627d344f
           in libgtk-3.so.0.2420.32[1d344f,...]
    
    #0  gtk_label_set_label       (libgtk-3.so.0 + 0x1d344f)
    #1  gtk_header_bar_set_title  (libgtk-3.so.0 + 0x1b17da)
    #2  ffi_call -> _gi -> libpython3.14 ...
    #14 g_main_context_iteration
    #15 g_application_run
    

    The faulting instruction is the inlined GTK_LABEL() cast in
    gtk_label_set_label: mov (%rbx),%rax ; cmp %rsi,(%rax), with %rbx =
    priv->title_label. Its class pointer read back as 0x69f1699a694368ec, which is
    not an address at all.

    Inspecting the core: the GtkHeaderBar itself is intact and live —
    priv->title = the provider name, priv->subtitle = "Movies > <category>" — but all three of
    its internal widgets (title_label, subtitle_label, label_box) have been
    freed while priv still points at them. A search of the whole 56 MB heap finds
    exactly one surviving pointer to the dead title label: the header bar's own private
    struct. The freed label's memory had already been reissued to the image loader —
    it now holds a 16-bit monotonic gamma curve (fitted exponent ≈ 2.4), i.e. an lcms2
    tone-response table from glycin decoding channel artwork.

    So the widget was destroyed out from under a live parent, and the crash surfaced on
    the next headerbar.set_title() from navigate_to.

    What was happening at the time

    Browsing an Xtream provider, Movies → a category, with 188 channel-logo JPEGs
    downloading concurrently (download_channel_logos worker threads) while navigating
    between pages and starting playback. 29 threads live at the moment of the crash:
    the GTK main thread, ~5 glycin image threads, several mpv threads, several
    requests/SSL download threads.

    Caveat, stated plainly

    The core proves the corruption and localises it precisely. It does not prove which
    pair of racing calls caused it — I could not pin the freeing thread from a single
    dump. I am reporting it here rather than as a new issue because the mechanism
    (GObject state mutated from @async_function workers) is the one this issue
    already identifies, and because it shows the failure is not limited to X11.

    Authored by Claude Opus 5 via Claude Code, from a systemd-coredump core dump.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions