diff --git a/local/docs/QT6-WAYLAND-NULL8-DIAGNOSIS.md b/local/docs/QT6-WAYLAND-NULL8-DIAGNOSIS.md new file mode 100644 index 0000000000..02be321c59 --- /dev/null +++ b/local/docs/QT6-WAYLAND-NULL8-DIAGNOSIS.md @@ -0,0 +1,471 @@ +# Qt6 Wayland `wl_proxy_add_listener` null+8 Crash — Static Diagnosis (Phases A–C) + +**Status:** STATIC DIAGNOSIS COMPLETE. Patch verdict issued. Instrumented-rebuild +runbook below for the orchestrator to execute (Phase A instrumentation + forced +rebuild chain + QEMU observation). No QEMU runs or multi-hour Qt rebuilds were +performed in this diagnosis pass — host contention constraints honoured. + +**Date:** 2026-07-20 +**Branch:** 0.3.1 +**Scope:** Phases A–C of `WAYLAND-IMPLEMENTATION-PLAN.md` § "Current Blocker" + +--- + +## 1. VERDICT (root cause statement) + +**The NULL proxy at the crash is the `wl_registry*` returned by +`wl_display_get_registry(mDisplay)` in +`QWaylandDisplay::setupConnection()` (`qwaylanddisplay.cpp:340`), and the crash +site is the qtwaylandscanner-generated `QtWayland::wl_registry::init_listener()` +calling `wl_registry_add_listener(NULL, ...)` → `wl_proxy_add_listener(NULL, ...)` +→ `proxy->object.implementation = listener` faulting at address `0x8`.** + +A secondary, alternative crash site (consistent with the commit-message root +cause but NOT with the "before any compositor events" signature) is any +generated `init_listener()` for a global bound via `wlRegistryBind()` during +`QWaylandDisplay::registry_global()` (`qwaylanddisplay.cpp:634`). + +The candidate patch +(`local/patches/qtbase/qtwaylandscanner-null-guard-listeners.patch`) guards +**both** sites because it modifies the code generator to emit +`if (m_) _add_listener(...)` for **every** generated +`init_listener()`. + +**Patch verdict: the candidate patch guards the CORRECT site.** It is necessary +but not sufficient alone — see §4. + +--- + +## 2. EVIDENCE CHAIN (every claim cites file:line) + +### 2.1 Crash signature confirmation + +| Claim | Evidence | +|-------|----------| +| Fault address `0x8` = write to `proxy->object.implementation` | `local/recipes/wayland/libwayland/source/src/wayland-client.c:665` — `proxy->object.implementation = implementation;` | +| `wl_object.implementation` is at offset 8 | `local/recipes/wayland/libwayland/source/src/wayland-private.h:70-74` — `struct wl_object { const struct wl_interface *interface; /* off 0 */ const void *implementation; /* off 8 */ uint32_t id; /* off 16 */ };` | +| `wl_proxy.object` is at offset 0 | `local/recipes/wayland/libwayland/source/src/wayland-client.c:63-74` — `struct wl_proxy { struct wl_object object; ... };` (first member) | +| Therefore `proxy==NULL` faults at `NULL+8` on the implementation write | Confirmed: `*(uintptr_t*)((char*)NULL + 8) = listener` = SIGSEGV at `0x8` | + +### 2.2 Qt6 client init sequence (where the first add_listener fires) + +Qt 6.11.1 ships the **entire Wayland QPA client plugin inside qtbase** (not +qtwayland). Confirmed locations: + +| File | Role | +|------|------| +| `local/recipes/qt/qtbase/source/src/plugins/platforms/wayland/qwaylanddisplay.cpp` | QWaylandDisplay (the main client-side display object) | +| `local/recipes/qt/qtbase/source/src/plugins/platforms/wayland/qwaylanddisplay_p.h:105` | `class QWaylandDisplay : public QObject, public QtWayland::wl_registry` — **QWaylandDisplay IS-A wl_registry wrapper** | +| `local/recipes/qt/qtbase/source/src/tools/qtwaylandscanner/qtwaylandscanner.cpp` | The code generator (vendored into qtbase, not qtwayland) | + +Init sequence (`qwaylanddisplay.cpp`): + +``` +Line 322: QWaylandDisplay::QWaylandDisplay(...) +Line 327: mDisplay = wl_display_connect(nullptr); +Line 328: if (mDisplay) { ← rules out NULL display +Line 329: setupConnection(); +Line 330: } + +Line 338: void QWaylandDisplay::setupConnection() +Line 340: struct ::wl_registry *registry = wl_display_get_registry(mDisplay); ← CAN return NULL +Line 341: init(registry); ← calls generated QtWayland::wl_registry::init(wl_registry*) +``` + +The generated `init(obj)` (`qtwaylandscanner.cpp:1155-1160`): +```cpp +void wl_registry::init(struct ::wl_registry *obj) +{ + m_wl_registry = obj; // line 1157 — assigned BEFORE init_listener + if (hasEvents) + init_listener(); // line 1159 — called AFTER assignment +} +``` + +The generated `init_listener()` (`qtwaylandscanner.cpp:1295-1298`, **PATCHED**): +```cpp +void wl_registry::init_listener() +{ + if (m_wl_registry) wl_registry_add_listener(m_wl_registry, &m_wl_registry_listener, this); +} +``` + +**Therefore:** if `wl_display_get_registry()` returns NULL → `m_wl_registry = NULL` +→ patched `init_listener()` skips the add_listener call → **no crash**. +Unpatched (upstream Qt 6.11.1) → `wl_registry_add_listener(NULL, ...)` → +`wl_proxy_add_listener(NULL, ...)` → **page fault at 0x8**. + +### 2.3 Why `wl_display_get_registry()` can return NULL + +`wl_display_get_registry()` is generated code (see +`local/recipes/wayland/libwayland/source/tests/data/example-client.h:1027-1036`): + +```c +static inline struct wl_registry * +wl_display_get_registry(struct wl_display *wl_display) +{ + struct wl_proxy *registry; + registry = wl_proxy_marshal_flags((struct wl_proxy *) wl_display, + WL_DISPLAY_GET_REGISTRY, &wl_registry_interface, ...); + return (struct wl_registry *) registry; // NULL if marshal_flags returns NULL +} +``` + +`wl_proxy_marshal_flags` → `wl_proxy_marshal_array_flags` +(`wayland-client.c:901-963`) returns NULL when: + +| Condition | Source | +|-----------|--------| +| `create_outgoing_proxy()` → `proxy_create()` OOM (`zalloc` fails) | `wayland-client.c:487` | +| `wl_map_insert_new` returns 0 (client ID space exhausted) | `wayland-client.c:496-499` | +| `display->last_error` already set (fatal display error) | `wayland-client.c:922-924` | + +The third condition is the most plausible: if the implicit display handshake +during `wl_display_connect()` encounters a protocol or socket error, +`display->last_error` is set, and the subsequent `wl_display_get_registry()` +silently returns NULL without even attempting proxy creation. + +### 2.4 Candidate patch guards the correct site + +The candidate patch (`local/patches/qtbase/qtwaylandscanner-null-guard-listeners.patch`) +modifies the generator at `qtwaylandscanner.cpp:1297` to emit +`if (m_%s) %s_add_listener(...)` instead of `%s_add_listener(...)`. + +This single generator change guards the `init_listener()` of **every** generated +Wayland wrapper class — `wl_registry`, `wl_compositor`, `wl_shm`, `wl_seat`, +`wl_output`, `xdg_wm_base`, and all `zwp_*`/`wp_*` extensions. + +The patch is: +- **Confirmed applied** to the live source tree (`qtwaylandscanner.cpp:1297` + shows the patched 4-argument printf). +- **Confirmed wired** in `local/recipes/qt/qtbase/recipe.toml` patches list + (line 14: `"qtwaylandscanner-null-guard-listeners.patch"`). +- **Confirmed applicable** to Qt 6.11.1 (context lines match; 3-arg → 4-arg + printf transformation is unique). + +### 2.5 Complementary guard already in libwayland + +A **second, independent NULL guard** exists in +`local/patches/libwayland/redox.patch` (added 2026-05-06 in commit +`ea125b0b45`), which modifies `wl_proxy_add_listener` itself +(`wayland-client.c:652-656`): + +```c +if (!proxy) { + fprintf(stderr, "FATAL: wl_proxy_add_listener(NULL) caller=%p -- returning error\n", + __builtin_return_address(0)); + return -1; +} +``` + +This converts the page fault into a logged error + `return -1`. **If this guard +is active, the null+8 page fault CANNOT occur** — instead you see the FATAL log +line. The presence of the page-fault crash signature therefore implies the +libwayland guard was not yet compiled into the running binary at the time of +observation (stale libwayland in qtbase's sysroot — see §6). + +The two guards are **complementary, not redundant in purpose**: +- qtwaylandscanner patch: prevents Qt from *calling* `wl_*_add_listener(NULL)` + (clean — Qt proceeds with a no-op listener) +- libwayland guard: prevents the *crash* if any caller (Qt or otherwise) passes + NULL (defensive — logs + returns error, caller may misbehave) + +Neither guard addresses the **root cause**: why proxy creation returns NULL. + +--- + +## 3. CROSS-CHECK OF THE FOUR RULED-OUT HYPOTHESES + +| # | Hypothesis | Plan's Verdict | Re-evaluation | Evidence | +|---|-----------|----------------|---------------|----------| +| 1 | relibc `calloc` doesn't zero | FALSE | **SOUND** | `local/sources/relibc/src/header/stdlib/mod.rs:281` — `ptr.write_bytes(0, size)` runs unconditionally on every successful `malloc`. Overflow path returns NULL + ENOMEM (line 287-288). Verified. | +| 2 | Compositor protocol bug | FALSE | **SOUND** | Compositor is pure-Rust (`redbear-compositor`), protocol-compliant, doesn't crash. Client crashes during its own init. | +| 3 | `wl_display_connect()` returns NULL | FALSE | **SOUND** | `qwaylanddisplay.cpp:327-329` — Qt checks `if (mDisplay)` before calling `setupConnection()`. A NULL display never reaches `get_registry`. | +| 4 | `wl_display_get_registry()` returns NULL | FALSE | **PARTIALLY UNSOUND** — the plan claims "My null guard `if (!registry) _exit(1)` catches this", but **NO such guard exists** at `qwaylanddisplay.cpp:340-341`. The code is `struct ::wl_registry *registry = wl_display_get_registry(mDisplay); init(registry);` — unconditional. The hypothesis is only effectively ruled out because the *candidate patch*'s `init_listener()` guard implicitly handles NULL. **Without the patch, this is the leading crash cause.** | +| 5 | libwayland `proxy_create()` returns NULL | UNLIKELY | **WEAKENED** — the plan says "Qt would catch NULL", but Qt does NOT check `wlRegistryBind()` / `wl_display_get_registry()` return values. That's exactly the gap the candidate patch closes. The hypothesis is plausible, not unlikely. | + +**Net effect:** hypotheses 1–3 are soundly ruled out. Hypothesis 4's cited +evidence (explicit `_exit` guard) does not exist in the source — the ruling +holds only because of the candidate patch. Hypothesis 5 is under-weighted: +NULL returns from proxy creation are not caught by Qt without the patch. + +--- + +## 4. PATCH VERDICT + +### Candidate: `local/patches/qtbase/qtwaylandscanner-null-guard-listeners.patch` + +**Verdict: CORRECT SITE, NECESSARY, BUT NOT SUFFICIENT ALONE.** + +| Question | Answer | +|----------|--------| +| Does the crash happen inside scanner-generated `init_listener()`? | **YES** — `wl_proxy_add_listener` is only reachable from `wl_*_add_listener` wrappers, which in Qt's client init path are only emitted by qtwaylandscanner inside `init_listener()`. The hand-written `wl_callback_add_listener` / `wl_buffer_add_listener` sites (see §5) all fire LATER, after the first roundtrip. | +| Does the patch guard the actual crash site? | **YES** — the generator change emits `if (m_)` before every `wl_*_add_listener(m_, ...)` in every generated `init_listener()`. | +| Is the patch the same as upstream commits `de2d74c37e`/`882c2974ec`? | **`de2d74c37e`/`882c2974ec` ARE the Red Bear commits that introduced this patch.** They are not upstream Qt commits. `de2d74c37e` (2026-05-06 17:18) made the direct source edit; `882c2974ec` (2026-05-07) extracted it into the patch file + wired recipe.toml. | +| Was the patch runtime-validated? | **NO — not in isolation.** Commit `de2d74c37e` claims "VERIFIED: greeter UI boots", but this was done with THREE fixes simultaneously applied (qtwaylandscanner patch + libwayland NULL guard from `ea125b0b45` 13:44 same day + kded6 workaround renaming `libqwayland.so` to `.disabled`). The qtwaylandscanner patch alone was never proven. The "7 globals" claim in the commit message also disagrees with the compositor's actual 8 globals, suggesting incomplete observation. | +| Is a corrected patch needed? | **NO** for the reported crash. The candidate guards the correct site. Additional hardening for non-scanner sites is recommended (§5) but is a separate concern. | + +### Why "not sufficient alone" + +The patch prevents Qt from calling `wl_*_add_listener(NULL)`, but it does NOT +fix the root cause: **why `wl_display_get_registry()` or `wlRegistryBind()` +returns NULL in the first place.** With the patch applied, Qt silently proceeds +with `m_wl_registry == nullptr`, the registry listener is never attached, Qt +receives no global announcements, and the desktop session fails to initialise +(no screens, no compositor, no input devices) — a different failure mode than a +page fault, but still a non-functional desktop. + +The root cause requires Phase A instrumentation + Phase C libwayland audit to +determine which of the three NULL-return paths (OOM, map exhaustion, or +`display->last_error`) is firing. + +### Relationship to existing patches + +| Patch | Location | Status | Relationship | +|-------|----------|--------|--------------| +| `qtwaylandscanner-null-guard-listeners.patch` | `local/patches/qtbase/` | **Candidate (this diagnosis)** | Generator-level guard — prevents the call | +| libwayland `redox.patch` hunk 1 | `local/patches/libwayland/redox.patch` | Applied (commit `ea125b0b45`) | Library-level guard — prevents the crash if call happens | +| `patch-wayland-gen.sh` | `local/recipes/qt/qtbase/` | **ORPHANED** — never invoked from recipe.toml | Legacy sed-based post-build patcher (replaced by the generator patch) | +| `qtwayland-empty-cursor-guards.patch` | `local/recipes/qt/qtbase/` | **ORPHANED** — not in recipe.toml patches list | Unrelated (cursor theme empty-image guards) | +| `P0-fix-broken-include.patch`, `P1-qplatformopengl-guard.patch` | `local/patches/qtbase/` | **ORPHANED** — symlinks exist in recipe dir but not in patches list | Not applied to current builds | + +**No file changes are needed to the candidate patch.** It is correct. The +runbook below (§7) validates it end-to-end. + +--- + +## 5. UNGUARDED `add_listener` SITES (hardening recommendations — NOT the crash site) + +These hand-written `wl_*_add_listener` calls are NOT guarded by the candidate +patch (which only covers scanner-generated code). All fire AFTER the first +roundtrip, so they are NOT the "before any compositor events" crash site. Listed +for completeness; a separate hardening patch could address them: + +| File:Line | Call | When it fires | +|-----------|------|---------------| +| `qwaylanddisplay.cpp:1049` | `wl_callback_add_listener(mSyncCallback, &syncCallbackListener, this)` | `requestWaylandSync()` — during sync/roundtrip | +| `qwaylandbuffer.cpp:27` | `wl_buffer_add_listener(buf, &listener, this)` | Buffer creation — during surface commit | +| `qwaylandwindow.cpp:1807` | `wl_callback_add_listener(mFrameCallback, &callbackListener, this)` | Frame callback — during surface render | +| `qwaylandtextinputv2.cpp:178` | `wl_callback_add_listener(m_resetCallback, &callbackListener, this)` | Text input reset — during IME interaction | +| `qwaylandbrcmeglwindow.cpp:50` | `wl_buffer_add_listener(mBuffer, &buffer_listener, this)` | Broadcom EGL — hardware integration (not loaded on Redox) | + +These are all covered by the libwayland-level NULL guard (`redox.patch` hunk 1), +so they will log FATAL + return -1 rather than crash. A future hardening patch +to Qt would add explicit `if (ptr)` checks at each site for cleaner diagnostics. + +--- + +## 6. WHY THE CRASH PERSISTED DESPITE "VERIFIED FIXED" (May 2026) + +The v5.5 changelog (`CONSOLE-TO-KDE-DESKTOP-PLAN.md:38-46`) and four other +locations claim the crash is "verified FIXED" via commits `de2d74c37e` + +`882c2974ec`. This claim is **overstated** for three reasons: + +1. **Never tested in isolation.** The May 6 verification had three fixes + applied simultaneously. The qtwaylandscanner patch was never proven to fix + the crash on its own. + +2. **The kded6 workaround masked it.** Commit `176624a008` (2026-05-06 16:34, + one hour before `de2d74c37e`) deployed a wrapper script that renames + `libqwayland.so` to `.disabled` before launching kded6, preventing Qt from + loading the Wayland plugin at all. The "greeter UI boots" verification may + have been of a greeter running WITHOUT the Wayland plugin (offscreen QPA), + not WITH the patched Wayland plugin. + +3. **Stale-sysroot rebuild chain.** The `WAYLAND-IMPLEMENTATION-PLAN.md:157-171` + documents that "Modifying only libwayland does NOT update qtbase's static + copy of `libwayland-client.a`. The old binary persists in qtbase's sysroot." + If the full chain (libwayland → qtbase → qtdeclarative → qtwayland) was not + force-rebuilt, the running binary may have linked against an unpatched + libwayland, making the page fault reappear. + +The crash re-appears as the active blocker in `WAYLAND-IMPLEMENTATION-PLAN.md` +(dated 2026-07-08, two months after the "fix") and +`CONSOLE-TO-KDE-DESKTOP-PLAN.md:234` (2026-06-20, "WIRED, UNVALIDATED"). The +"verified FIXED" line at `CONSOLE-TO-KDE-DESKTOP-PLAN.md:43` directly +contradicts line 240-243 of the same document. + +**The v5.5 claim should be corrected to "WIRED, NEVER RUNTIME-VALIDATED IN +ISOLATION" until the Phase A runbook below produces positive QEMU evidence.** + +--- + +## 7. INSTRUMENTED-REBUILD RUNBOOK (Phase A diagnostics + forced chain) + +This runbook is for the orchestrator to execute when host contention allows. +It produces definitive evidence of WHICH proxy is NULL and WHY. + +### 7.1 Phase A: Add fprintf diagnostics (3 files) + +All edits are to the live source trees. They will be discarded on next +`repo fetch`, so mirror them into patches if durability is needed. + +**Edit 1: `local/recipes/qt/qtbase/source/src/plugins/platforms/wayland/qwaylanddisplay.cpp`** + +In `setupConnection()` (around line 340), add: +```cpp +void QWaylandDisplay::setupConnection() +{ + struct ::wl_registry *registry = wl_display_get_registry(mDisplay); + fprintf(stderr, "[RB-DIAG] setupConnection: mDisplay=%p registry=%p\n", (void*)mDisplay, (void*)registry); + init(registry); + fprintf(stderr, "[RB-DIAG] setupConnection: init() returned, m_wl_registry=%p\n", (void*)object()); +``` + +**Edit 2: `local/recipes/qt/qtbase/source/src/tools/qtwaylandscanner/qtwaylandscanner.cpp`** + +In the `init_listener()` generator (around line 1295), add a diagnostic printf +INSIDE the generated function. Change the patched line to: +```cpp +printf(" fprintf(stderr, \"[RB-DIAG] init_listener m_%s=%p\\n\", (void*)m_%s);\n", interfaceName, interfaceName); +printf(" if (m_%s) %s_add_listener(m_%s, &m_%s_listener, this);\n", interfaceName, interfaceName, interfaceName, interfaceName); +printf(" else fprintf(stderr, \"[RB-DIAG] SKIPPED add_listener for %s (NULL proxy)\\n\");\n", interfaceName); +``` + +**Edit 3: `local/recipes/wayland/libwayland/source/src/wayland-client.c`** + +In `wl_proxy_marshal_array_flags` (around line 920), add before the +`display->last_error` check: +```c +if (display->last_error) + fprintf(stderr, "[RB-DIAG] marshal_flags: display->last_error=%d, opcode=%u\n", + display->last_error, opcode); +``` + +And in `proxy_create` (around line 487), add: +```c +if (proxy == NULL) { + fprintf(stderr, "[RB-DIAG] proxy_create: zalloc FAILED (OOM)\n"); + return NULL; +} +``` + +### 7.2 Phase B: Force-rebuild chain (in strict order) + +The chain MUST be rebuilt in this order. Skipping any step leaves a stale +binary in the next recipe's sysroot. + +```bash +# Step 0: Rebuild prefix (relibc headers if Phase A edit 3 touched wayland-client.c) +touch relibc && make prefix + +# Step 1: Force-rebuild libwayland (Phase A edit 3) +rm -rf local/recipes/wayland/libwayland/target +rm -f repo/x86_64-unknown-redox/libwayland.pkgar +./target/release/repo cook libwayland --force-rebuild + +# Step 2: Force-rebuild qtbase (Phase A edits 1 + 2; picks up new libwayland) +rm -rf local/recipes/qt/qtbase/target +rm -f repo/x86_64-unknown-redox/qtbase.pkgar +./target/release/repo cook qtbase --force-rebuild + +# Step 3: Force-rebuild qtdeclarative (links against new qtbase) +rm -rf local/recipes/qt/qtdeclarative/target +rm -f repo/x86_64-unknown-redox/qtdeclarative.pkgar +./target/release/repo cook qtdeclarative --force-rebuild + +# Step 4: Force-rebuild qtwayland (uses qtwaylandscanner from new qtbase) +rm -rf local/recipes/qt/qtwayland/target +rm -f repo/x86_64-unknown-redox/qtwayland.pkgar +./target/release/repo cook qtwayland --force-rebuild + +# Step 5: Rebuild the full image +./local/scripts/build-redbear.sh redbear-full +``` + +If `repo cook` is unavailable, use `make r.` + `make cr.` +(clean + rebuild) equivalents. + +### 7.3 Phase C: QEMU invocation + serial-log markers + +```bash +# Boot the instrumented image with serial console captured +qemu-system-x86_64 \ + -cdrom build/x86_64/redbear-full.iso \ + -m 4096 \ + -serial file:/tmp/rb-wayland-diag.log \ + -netdev user,id=n0 -device e1000,netdev=n0 \ + -display none \ + 2>&1 | tee /tmp/rb-qemu-stdout.log + +# After boot, trigger a Wayland client (greeter or smoke test) +# Inside the guest (via serial console): +# export QT_QPA_PLATFORM=wayland +# export WAYLAND_DISPLAY=wayland-0 +# /usr/lib/qt6/examples/wayland/minimal-qml # or any Qt6 Wayland client +``` + +### 7.4 Markers to observe in `/tmp/rb-wayland-diag.log` + +| Marker | Meaning | Action | +|--------|---------|--------| +| `[RB-DIAG] setupConnection: mDisplay=0x... registry=0x...` | Connection established; check if registry is NULL | If `registry=(nil)`, root cause is in `wl_display_get_registry` | +| `[RB-DIAG] setupConnection: init() returned, m_wl_registry=0x...` | Registry wrapper initialised | If `(nil)`, the patched init_listener skipped | +| `[RB-DIAG] init_listener m_wl_registry=0x...` | Generated init_listener executed for wl_registry | Confirms the scanner-generated code runs | +| `[RB-DIAG] SKIPPED add_listener for wl_registry (NULL proxy)` | The candidate patch fired — NULL was caught | **Patch works. Root cause is upstream (proxy creation).** | +| `[RB-DIAG] marshal_flags: display->last_error=N` | Display had a prior fatal error | **Root cause: display handshake failure.** Investigate socket/IPC. | +| `[RB-DIAG] proxy_create: zalloc FAILED (OOM)` | Out of memory during proxy allocation | **Root cause: OOM.** Increase guest RAM or audit relibc allocator. | +| `FATAL: wl_proxy_add_listener(NULL) caller=0x...` | libwayland guard fired (some caller passed NULL) | **The candidate patch did NOT catch this site** — the NULL came from a non-scanner call site (§5). Use `addr2line` on the caller address. | +| `Page fault: 0000000000000008 US` | **The crash persisted** — BOTH guards failed to apply | Stale binary in sysroot. Re-run the forced rebuild chain. | + +### 7.5 Decision tree after observation + +``` +Observe the FIRST [RB-DIAG] or crash marker: + +├── registry=(nil) in setupConnection +│ ├── display->last_error set → ROOT CAUSE: display handshake failure (IPC/socket) +│ ├── zalloc FAILED → ROOT CAUSE: OOM in proxy_create +│ └── neither → ROOT CAUSE: wl_map_insert_new returned 0 (investigate) +│ +├── registry=0x... (non-NULL) but SKIPPED add_listener +│ └── Patch works; root cause is elsewhere in global binding (registry_global path) +│ +├── FATAL: wl_proxy_add_listener(NULL) caller=0x... +│ └── addr2line -e → identifies the exact unguarded call site (§5) +│ +└── Page fault at 0x8 (no markers) + └── Stale binary — repeat forced rebuild chain with --no-cache +``` + +--- + +## 8. SUMMARY TABLE + +| Question | Answer | +|----------|--------| +| Which proxy is NULL? | `wl_registry*` from `wl_display_get_registry()` (primary) or any `wl_*` from `wlRegistryBind()` (secondary) | +| Why is it NULL? | `wl_proxy_marshal_array_flags` returns NULL: OOM, map exhaustion, or `display->last_error` set | +| Where is the crash? | Generated `init_listener()` → `wl_*_add_listener(NULL)` → `wl_proxy_add_listener(NULL)` → `proxy->object.implementation = listener` at `wayland-client.c:665` | +| Does the patch guard the crash site? | **YES** — `qtwaylandscanner.cpp:1297` emits `if (m_)` before every generated `add_listener` | +| Is the patch sufficient alone? | **NO** — prevents crash but not the root cause (NULL proxy creation); also redundant with libwayland `redox.patch` hunk 1 | +| Is a corrected patch needed? | **NO** — the candidate is correct for the reported crash | +| Was the v5.5 "verified FIXED" sound? | **NO** — never tested in isolation; kded6 workaround masked it; stale-sysroot risk | +| Are the ruled-out hypotheses sound? | Hypotheses 1–3 sound; 4 cites non-existent evidence; 5 is under-weighted | + +--- + +## 9. FILES CHANGED IN THIS DIAGNOSIS + +| File | Change | +|------|--------| +| `local/docs/QT6-WAYLAND-NULL8-DIAGNOSIS.md` | **NEW** — this report | +| `local/patches/qtbase/` | **No changes** — candidate patch is correct | +| `local/recipes/qt/qtbase/recipe.toml` | **No changes** — patch is already wired (line 14) | + +--- + +## 10. NEXT STEPS (for the orchestrator) + +1. **Execute the Phase A runbook (§7)** when host contention allows. The + fprintf diagnostics will definitively identify which NULL-return path fires. +2. **Do NOT remove the candidate patch** — it is correct and necessary. +3. **Do NOT trust the "verified FIXED" claims** in + `CONSOLE-TO-KDE-DESKTOP-PLAN.md:43`, `config/redbear-full.toml:183`, + `local/recipes/kde/kwin/recipe.toml:2`, or + `local/docs/archived/IMPLEMENTATION-MASTER-PLAN.md:61` until positive QEMU + evidence from §7.4 confirms the crash is gone. +4. **If the runbook reveals `display->last_error` as the root cause**, the fix + is in the IPC/socket layer (kernel or relibc), not in Qt or libwayland. +5. **If the runbook reveals OOM**, increase guest RAM or audit the relibc + allocator (`dlmalloc`) for fragmentation under Qt6's allocation pattern.