Files
RedBear-OS/local/docs/QT6-WAYLAND-NULL8-DIAGNOSIS.md
T
vasilito e4c4cb59e4 docs: Qt6 Wayland null+8 static diagnosis — patch verdict + instrumented runbook
Phases A-C static diagnosis of the wl_proxy_add_listener null+8 crash.
Evidence-backed root cause, candidate patch verdict, ruled-out hypothesis
cross-check, and a copy-pasteable instrumented-rebuild runbook for the
orchestrator.

Verdict: candidate patch qtwaylandscanner-null-guard-listeners.patch guards
the CORRECT site (every generated init_listener). Necessary but not
sufficient alone — complementary to libwayland redox.patch hunk 1.
v5.5 'verified FIXED' claim overstated: never tested in isolation,
kded6 workaround masked it, stale-sysroot risk documented.
2026-07-20 09:52:08 +09:00

472 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Qt6 Wayland `wl_proxy_add_listener` null+8 Crash — Static Diagnosis (Phases AC)
**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 AC 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_<iface>) <iface>_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 13 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_<iface>)` before every `wl_*_add_listener(m_<iface>, ...)` 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.<recipe>` + `make cr.<recipe>`
(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 <binary> <caller_addr> → 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_<iface>*` 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_<iface>)` 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 13 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.