[kwin] [Bug 523909] New: Entire Wayland session freezes 2-3.6s when a short-lived X11 window with an unresolvable WM_CLIENT_MACHINE is destroyed (blocking getaddrinfo wait in KWin::GetAddrInfo destructor on main thread)

"Dmitrii P" <[email protected]> Wed, 05 Aug 2026 22:44:59 +0000
Newsgroups gmane.comp.kde.devel.bugs
Message-ID <[email protected]/>
https://bugs.kde.org/show_bug.cgi?id=523909

            Bug ID: 523909
           Summary: Entire Wayland session freezes 2-3.6s when a
                    short-lived X11 window with an unresolvable
                    WM_CLIENT_MACHINE is destroyed (blocking getaddrinfo
                    wait in KWin::GetAddrInfo destructor on main thread)
    Classification: Plasma
           Product: kwin
      Version First 6.7.3
       Reported In:
          Platform: Fedora RPMs
                OS: Linux
            Status: REPORTED
          Severity: normal
          Priority: NOR
         Component: xwayland
          Assignee: [email protected]
          Reporter: [email protected]
  Target Milestone: ---

Summary

Entire Wayland session freezes 2–3.6 s when a short-lived X11 window with an
unresolvable WM_CLIENT_MACHINE is destroyed (blocking getaddrinfo wait in
KWin::GetAddrInfo destructor on the main thread)

Description

When an X11 (XWayland) client whose `WM_CLIENT_MACHINE` property contains a
hostname that is not resolvable on the host creates a short-lived window -
typically a tooltip or popup - and that window is destroyed shortly after
mapping, KWin's main thread blocks for the full DNS resolver timeout
(2–3.6 s here). The whole desktop stops rendering: all outputs freeze,
Wayland round-trips stall. XWayland itself stays responsive during the
freeze (verified with an independent X11 client measuring GetInputFocus
round-trips: < 2 ms throughout).

This happens in practice for **any GUI app running inside a container**
(podman/docker/toolbox-style sandboxes) whose container hostname differs
from the host's, and would equally affect `ssh -X` clients from hosts not
in DNS. Qt and GTK set `WM_CLIENT_MACHINE` on every window, and tooltips
live shorter than the failed lookup, so every tooltip shown+hidden freezes
the compositor for seconds. Wayland-native apps are unaffected; the same
apps on the host (resolvable hostname) are unaffected.

Analysis

`KWin::ClientMachine` starts an async `getaddrinfo()` (`KWin::GetAddrInfo`)
to decide whether the client is local. If the X11 window is destroyed while
the lookup is still pending, the cleanup path blocks the compositor:

Main thread (TID of kwin_wayland), identical in ~30 consecutive samples
(eu-stack, 0.1 s interval) across three reproduced freezes:


#0  __syscall_cancel_arch
#1  __internal_syscall_cancel
#2  __syscall_cancel
#3  __GI___futex_abstimed_wait_cancelable64
#4  pthread_cond_wait@@GLIBC_2.3.2
#5  QWaitCondition::wait(QMutex*, QDeadlineTimer)
#6  QFutureInterfaceBase::waitForFinished()
#7  KWin::GetAddrInfo::~GetAddrInfo()
#8  KWin::GetAddrInfo::~GetAddrInfo()
#9  QObjectPrivate::deleteChildren()
#10 QObject::~QObject()
#11 KWin::ClientMachine::~ClientMachine()
#12 QObjectPrivate::deleteChildren()
#13 QObject::~QObject()
#14 KWin::X11Window::~X11Window()


The freeze duration equals the resolver timeout for the nonexistent name.
Expected behavior: the pending lookup should be cancelled/detached (or its
result delivered via the event loop and discarded), never awaited
synchronously on the compositor thread.

Steps to Reproduce

1. Run a Plasma Wayland session.
2. Run the self-contained Python reproducer below (no dependencies beyond
   python3) against the session's XWayland display. It creates an
   override-redirect window, sets `WM_CLIENT_MACHINE` to an unresolvable
   name, maps it, then unmaps+destroys it 0.3 s later - exactly a tooltip's
   lifecycle.
3. Observe the whole desktop freeze for 2–3.6 s
   (`journalctl --user -b | grep "The main thread was hanging"` also fires).
   Not every cycle necessarily freezes: the resolver's negative cache can
   absorb immediate repeats of the same name, so allow a few seconds between
   attempts or vary the hostname argument.
4. Control: run it with your host's own hostname as argument - no freeze.

Real-world trigger: `podman run` any Qt/GTK app with X11 socket shared and a
container-specific hostname; hover any tooltip and let it hide (e.g. VLC's
toolbar tooltips).


#!/usr/bin/env python3
# usage: python3 repro.py [hostname]   (default: unresolvable name -> freeze)
import os, socket, struct, sys, time

def cookie(path):
    d = open(path, "rb").read(); i = 0; c = None
    while i + 2 <= len(d):
        i += 2
        r = []
        for _ in range(4):
            n = struct.unpack(">H", d[i:i+2])[0]; r.append(d[i+2:i+2+n]); i +=
2 + n
        if r[2] == b"MIT-MAGIC-COOKIE-1": c = r[3]
    return c

def pad4(b): return b + b"\x00" * ((4 - len(b) % 4) % 4)
def rx(s, n):
    b = b""
    while len(b) < n:
        t = s.recv(n - len(b)); assert t; b += t
    return b

disp = os.environ.get("DISPLAY", ":0").split(":")[1].split(".")[0]
s = socket.socket(socket.AF_UNIX); s.connect(f"/tmp/.X11-unix/X{disp}")
ck = cookie(os.environ["XAUTHORITY"]) if os.environ.get("XAUTHORITY") else None
nm = b"MIT-MAGIC-COOKIE-1" if ck else b""
s.sendall(struct.pack("<BxHHHHxx", 0x6C, 11, 0, len(nm), len(ck or b"")) +
pad4(nm) + pad4(ck or b""))
st, _, _, ln = struct.unpack("<BxHHH", rx(s, 8)); body = rx(s, ln * 4)
assert st == 1, body[:64]
rid, = struct.unpack_from("<I", body, 4)
vlen, = struct.unpack_from("<H", body, 16)
off = 32 + (vlen + 3) // 4 * 4 + body[21] * 8
root, = struct.unpack_from("<I", body, off); depth = body[off + 38]

host = (sys.argv[1] if len(sys.argv) > 1 else "unresolvable-host-xyz").encode()
for i in range(3):
    w = rid + 1 + i
    s.sendall(struct.pack("<BBHIIhhHHHHII", 1, depth, 10, w, root, 100, 100,
300, 60, 0, 1, 0, 0x202)
              + struct.pack("<II", 0xFFFF00, 1))          # CreateWindow,
override-redirect
    s.sendall(struct.pack("<BBHIIIBxxxI", 18, 0, 6 + (len(host) + 3) // 4, w,
36, 31, 8, len(host))
              + pad4(host))                                # WM_CLIENT_MACHINE
= host
    s.sendall(struct.pack("<BxHI", 8, 2, w))               # MapWindow
    time.sleep(0.3)
    s.sendall(struct.pack("<BxHI", 10, 2, w))              # UnmapWindow
    s.sendall(struct.pack("<BxHI", 4, 2, w))               # DestroyWindow
    s.sendall(struct.pack("<BxH", 43, 1)); rx(s, 32)       # sync
    print(f"cycle {i}: window destroyed - desktop should freeze now")
    time.sleep(3)


Observed Result

Compositor main thread blocks in `QFutureInterfaceBase::waitForFinished()`
inside `~GetAddrInfo()`; all outputs frozen 2–3.6 s per destroyed window.

Expected Result

Window destruction never blocks the compositor on name resolution.

Environment

- Operating System: Fedora 44 (host), kernel 7.1.5-201.fc44.x86_64
- KDE Plasma Version: 6.7.3
- KWin version: 6.7.3
- Graphics: AMD Radeon RX 9070 XT (dGPU, display) + Ryzen iGPU, amdgpu/Mesa
- X11 clients ran inside a rootless podman container (hostname differs from
  host); reproducer above needs no container.

-- 
You are receiving this mail because:
You are watching all bug changes.