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.
This commit is contained in:
2026-07-20 09:52:08 +09:00
parent 865beb82d5
commit e4c4cb59e4
+471
View File
@@ -0,0 +1,471 @@
# 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.