# Fixing KRFB (KDE Desktop Sharing) on Kubuntu 26.04 / Plasma 6 Wayland
A practical guide to making **KRFB** work unattended on a **Wayland** Plasma 6 session —
fixing the two bugs that cause a **black screen** (or `Protocol error: bad desktop size 0x0`)
when connecting with any VNC viewer (RealVNC, TigerVNC, Remmina, etc.).
Tested on:
- **Kubuntu 26.04 LTS** (codename *resolute*)
- **KDE Plasma 6** / **KWin Wayland**
- **krfb** `4:25.12.3-0ubuntu1`
- **libkpipewire6** `6.6.4-0ubuntu1`
- **xdg-desktop-portal-kde** `6.6.5-0ubuntu0.1`
- Intel iGPU (N150) — but the DMA-BUF issue affects other GPUs too
The fix attaches VNC to the **existing** Wayland desktop session (the one you're already
logged into). It does **not** create a virtual session and does **not** use X11.
---
## TL;DR — what was wrong
KRFB on Wayland captures the screen through `xdg-desktop-portal` (ScreenCast + RemoteDesktop
portal interfaces → PipeWire). Two separate bugs break it when running unattended:
1. **Portal consent is never granted.** The RemoteDesktop portal shows an interactive
"Remote Control Requested" dialog on every fresh session. With no one at the physical
console to click "Share", no screencast stream is created → black screen.
2. **DMA-BUF frame download fails on the negotiated Intel-tiled modifier.** Even after the
portal consent is bypassed, KPipeWire negotiates a DMA-BUF format with an Intel tiled
modifier that KRFB's `DmaBufHandler` cannot download → KRFB receives empty (cursor-only)
buffers → still black screen (or `bad desktop size 0x0`).
3. **Red/blue colour swap.** Once frames finally flow, KRFB's `PWFrameBuffer` doesn't set
the R/G/B bit shifts in the VNC pixel format, so libvncserver defaults to RGB byte order
while PipeWire delivers BGRx → red and blue are swapped on the client.
---
## Attempts — what worked and what didn't
### ❌ Restarting KRFB
Killing and relaunching KRFB (via `systemctl --user restart`, `systemd-run --user`, or the
autostart generator) made no difference. The screencast node was never created because the
portal consent dialog still needed a click.
### ❌ `KDE_APPLICATIONS_AS_SCOPE=1` / clean KDE session env
Launching KRFB with a full KDE session environment (`KDE_FULL_SESSION=true`,
`XDG_CURRENT_DESKTOP=KDE`, `KDE_APPLICATIONS_AS_SCOPE=1`, etc.) did **not** fix the
`Failed to register with host portal … Connection already associated with an application ID`
error. KRFB always sends `app_id: ""` (empty) to the portal regardless.
### ❌ `wayvnc`
`wayvnc` is in the Ubuntu repos but is for **wlroots-based** compositors (Sway, Hyprland,
Cage). It does not work with KWin/Plasma. Not an option.
### ❌ RealVNC Server / TigerVNC Server / x11vnc
These either create a **virtual X11** session (failing the "existing session + no X11"
requirement) or don't support Wayland at all.
### ❌ Manually linking the PipeWire nodes (`pw-link`)
`pw-link kwin_wayland:output_1 krfb:input_1` failed with `Operation not permitted` —
portal-managed screencast streams cannot be linked manually.
### ❌ `QT_QPA_PLATFORM=offscreen` to disable EGL/DMA-BUF
This disabled DMA-BUF but also broke the portal screencast entirely — KRFB never created
a screencast session and didn't listen on port 5900. The portal needs a real Wayland
platform connection.
### ❌ `LIBGL_ALWAYS_SOFTWARE=1`
Did not disable KPipeWire's DMA-BUF path — `eglChooseConfig` still succeeded and KRFB still
negotiated the Intel-tiled DMA-BUF modifier → still empty buffers.
### ✅ Granting portal "mega-authorization" for the empty app-id
The `xdg-desktop-portal-kde` source (`src/remotedesktop.cpp`) has an
`isAppMegaAuthorized(app_id)` check that does
`permissionStore.Lookup("kde-authorized", "remote-desktop", …)`. If the app-id has a `"yes"`
permission, the consent dialog is **skipped** and the screencast starts unattended. The
source explicitly supports authorizing the **empty** app-id (`""`) for host applications
("the user may authorize the empty app_id to cover generic host applications"). Since KRFB
always sends `app_id: ""`, granting `"" → ["yes"]` is the sanctioned unattended path.
**This fixed bug #1** (the portal consent dialog).
### ✅ `LD_PRELOAD` shim to force shm frames instead of DMA-BUF
There's no env var or config option in `libkpipewire6` 6.6.4 to disable DMA-BUF. The fix is
a tiny `LD_PRELOAD` shared library that stubs out the EGL DMA-BUF format/modifier query
functions (`eglQueryDmaBufFormatsEXT`, `eglQueryDmaBufModifiersEXT`) and
`epoxy_has_egl_extension("EGL_EXT_image_dma_buf_import")` to return empty/false. This makes
KPipeWire's `createFormatsParams()` offer **only** shared-memory formats (no modifier), so
KWin negotiates plain `BGRx` over `SPA_DATA_MemPtr` and KRFB receives real image data via
`frame.image` → `memcpy` into the VNC framebuffer. **This fixed bug #2** (empty buffers).
### ✅ Same shim also fixes the R/B colour swap
The shim intercepts `rfbGetScreen()` and `rfbNewFramebuffer()` (libvncserver) and sets the
server pixel format shifts to match BGRx memory layout
(`redShift=16, greenShift=8, blueShift=0`). **This fixed bug #3** (swapped colours).
### ❌ Intercepting only `rfbNewFramebuffer` (without `rfbGetScreen`)
The first version of the colour fix only intercepted `rfbNewFramebuffer`. It didn't work
because KRFB's initial pixel format is set via `rfbGetScreen()` → `getServerFormat()`, and
the shifts defaulted to RGB. Both functions needed interception.
---
## Symptoms & how to confirm which bug you're hitting
### Bug #1 — portal consent (no screencast stream at all)
```bash
# As the desktop user, with XDG_RUNTIME_DIR set:
pw-cli ls Node | grep krfb # → nothing (no krfb Stream/Input/Video node)
journalctl -t krfb --user -b | grep "no outputs"
# → "qt.qpa.wayland: There are no outputs - creating placeholder screen"
journalctl -t xdg-desktop-portal-kde --user -b | grep MegaAuth
# → "MegaAuth: Failed to lookup permissions: No entry for remote-desktop"
```
### Bug #2 — DMA-BUF empty buffers (screencast node exists, but black)
```bash
# Enable KRFB pipewire debug logging first (see below), restart krfb, then:
journalctl -t krfb --user -b | grep "empty buffer"
# → "Got empty buffer. The buffer possibly carried only information about the mouse cursor."
# (repeated many times = bug #2)
# Check the negotiated PipeWire format on the kwin screencast node:
pw-dump | grep -A5 '"Format"'
# → "format": "BGRx", "modifier": 72057594037927944 (= 0x0100000000000008, Intel tiled)
# A modifier present = DMA-BUF was negotiated = bug #2
```
To enable KRFB debug logging temporarily:
```bash
mkdir -p ~/.config/systemd/user/krfb.service.d
cat > ~/.config/systemd/user/krfb.service.d/debug.conf <)
#############################################################################
# 2. Build the LD_PRELOAD shim (forces shm frames + fixes R/B colour swap).
#############################################################################
sudo apt-get update
sudo apt-get install -y gcc libegl-dev libvncserver-dev
cat > ~/krfb-nodmabuf-shim.c <<'SHIM'
#define _GNU_SOURCE
#include
#include
#include
#include
#include
/* --- 1. Force PipeWire/KPipeWire to use shared-memory (shm) frames instead of
* DMA-BUF. kwin's monitor screencast negotiates an Intel-tiled DMA-BUF
* modifier that krfb's DmaBufHandler cannot download on this iGPU,
* producing empty (cursor-only) buffers -> black screen.
* By defeating the EGL dmabuf-format/modifier queries, KPipeWire's
* createFormatsParams() offers only shm formats and kwin falls back to
* sending real BGRx/BGRA image data. */
static EGLBoolean fail_formats(EGLDisplay d, EGLint max, EGLint *fmts, EGLint *num)
{ if (num) *num = 0; return EGL_TRUE; }
static EGLBoolean fail_mods(EGLDisplay d, EGLint fmt, EGLint max,
EGLuint64KHR *mods, EGLBoolean *ext, EGLint *num)
{ if (num) *num = 0; return EGL_TRUE; }
EGLBoolean eglQueryDmaBufFormatsEXT(EGLDisplay d, EGLint max, EGLint *f, EGLint *n)
{ return fail_formats(d, max, f, n); }
EGLBoolean eglQueryDmaBufModifiersEXT(EGLDisplay d, EGLint fmt, EGLint max,
EGLuint64KHR *mods, EGLBoolean *ext, EGLint *n)
{ return fail_mods(d, fmt, max, mods, ext, n); }
/* Make KPipeWire think EGL_EXT_image_dma_buf_import is unavailable so
* queryDmaBufModifiers() returns empty modifier lists (shm-only offers). */
int epoxy_has_egl_extension(EGLDisplay d, const char *ext)
{
if (ext && strstr(ext, "EXT_image_dma_buf_import")) return 0;
static int (*real)(EGLDisplay, const char *);
if (!real) real = (int (*)(EGLDisplay, const char *))dlsym(RTLD_NEXT, "epoxy_has_egl_extension");
return real ? real(d, ext) : 0;
}
/* --- 2. Fix the R/B colour swap. PipeWire delivers BGRx/BGRA in memory
* ([B,G,R,x]) but krfb's PWFrameBuffer::getServerFormat() does not set
* the R/G/B shifts, so libvncserver defaults to RGB byte order and the
* client sees red and blue swapped. We intercept rfbNewFramebuffer()
* and rfbGetScreen() and set the server format to match the BGRx memory
* layout: redShift=16 greenShift=8 blueShift=0. */
void rfbNewFramebuffer(rfbScreenInfoPtr screen, char *framebuffer,
int width, int height, int bitsPerSample,
int samplesPerPixel, int bytesPerPixel)
{
static void (*real)(rfbScreenInfoPtr, char *, int, int, int, int, int);
if (!real) real = (void (*)(rfbScreenInfoPtr, char *, int, int, int, int, int))
dlsym(RTLD_NEXT, "rfbNewFramebuffer");
if (real) real(screen, framebuffer, width, height, bitsPerSample, samplesPerPixel, bytesPerPixel);
if (screen && screen->serverFormat.bitsPerPixel == 32) {
screen->serverFormat.redShift = 16;
screen->serverFormat.greenShift = 8;
screen->serverFormat.blueShift = 0;
screen->serverFormat.redMax = 255;
screen->serverFormat.greenMax = 255;
screen->serverFormat.blueMax = 255;
screen->serverFormat.trueColour = 1;
screen->serverFormat.bigEndian = 0;
}
}
rfbScreenInfoPtr rfbGetScreen(int *argc, char **argv,
int width, int height, int bitsPerSample,
int samplesPerPixel, int bytesPerPixel)
{
static rfbScreenInfoPtr (*real)(int*, char**, int, int, int, int, int);
if (!real) real = (rfbScreenInfoPtr (*)(int*, char**, int, int, int, int, int))
dlsym(RTLD_NEXT, "rfbGetScreen");
rfbScreenInfoPtr screen = real ? real(argc, argv, width, height, bitsPerSample, samplesPerPixel, bytesPerPixel) : NULL;
if (screen && screen->serverFormat.bitsPerPixel == 32) {
screen->serverFormat.redShift = 16;
screen->serverFormat.greenShift = 8;
screen->serverFormat.blueShift = 0;
screen->serverFormat.redMax = 255;
screen->serverFormat.greenMax = 255;
screen->serverFormat.blueMax = 255;
screen->serverFormat.trueColour = 1;
screen->serverFormat.bigEndian = 0;
}
return screen;
}
SHIM
gcc -shared -fPIC -o ~/krfb-nodmabuf-shim.so ~/krfb-nodmabuf-shim.c -ldl
#############################################################################
# 3. Replace the autostart entry with a proper systemd user service so KRFB
# starts after the Plasma workspace, auto-restarts on failure, and loads
# the shim via LD_PRELOAD.
#############################################################################
# Disable the old autostart desktop entry:
rm -f ~/.config/autostart/krfb.desktop
# Create the systemd user service:
cat > ~/.config/systemd/user/krfb.service <<'UNIT'
[Unit]
Description=Krfb VNC Server (Wayland, existing session)
After=plasma-workspace.target plasma-xdg-desktop-portal-kde.service
[Service]
Type=simple
ExecStart=/usr/bin/krfb -qwindowtitle Krfb
Restart=on-failure
RestartSec=3
Environment=QT_QPA_PLATFORM=wayland
Environment=KDE_APPLICATIONS_AS_SCOPE=1
Environment=KDE_FULL_SESSION=true
Environment=KDE_SESSION_VERSION=6
Environment=XDG_CURRENT_DESKTOP=KDE
Environment=XDG_SESSION_DESKTOP=KDE
Environment=XDG_SESSION_TYPE=wayland
[Install]
WantedBy=plasma-workspace.target
UNIT
# Create the drop-in that loads the shim:
mkdir -p ~/.config/systemd/user/krfb.service.d
cat > ~/.config/systemd/user/krfb.service.d/shim.conf <<'DROPIN'
[Service]
Environment=LD_PRELOAD=%h/krfb-nodmabuf-shim.so
DROPIN
systemctl --user daemon-reload
systemctl --user enable --now krfb.service
#############################################################################
# 4. Verify it works.
#############################################################################
sleep 5
echo "krfb active: $(systemctl --user is-active krfb.service)"
echo "5900 listen: $(ss -tlnp 2>/dev/null | grep -c 5900) listener(s)"
echo "pw link: $(pw-link -l 2>/dev/null | grep -c 'krfb:input_1') link(s)"
echo "empty frames: $(journalctl -t krfb --user -b --no-pager 2>/dev/null | grep -c 'empty buffer')"
# Expected: active / 1+ / 1+ / 0
```
After this, connect with any VNC viewer to `:5900`. You should see the live
Plasma Wayland desktop with correct colours.
---
## What persists across reboots
| Component | Where it's stored | Persists? |
|---|---|---|
| Portal mega-auth (`"" → ["yes"]`) | `~/.local/share/flatpak/db/kde-authorized` (via `xdg-permission-store.service`) | ✅ Yes |
| KRFB systemd service | `~/.config/systemd/user/krfb.service` (enabled) | ✅ Yes |
| LD_PRELOAD shim drop-in | `~/.config/systemd/user/krfb.service.d/shim.conf` | ✅ Yes |
| Shim binary | `~/krfb-nodmabuf-shim.so` (built from `~/krfb-nodmabuf-shim.c`) | ✅ Yes |
| Old autostart entry | removed (`~/.config/autostart/krfb.desktop`) | ✅ N/A |
The `krfb.service` starts after `plasma-workspace.target`, so it launches automatically on
login. `Restart=on-failure` brings it back if it crashes.
---
## After a kernel/mesa/KPipeWire upgrade
If a future update fixes the DMA-BUF download path natively, you can remove the shim:
```bash
rm ~/.config/systemd/user/krfb.service.d/shim.conf
systemctl --user daemon-reload
systemctl --user restart krfb.service
```
If colours break again after removing the shim, the R/B-swap part of the shim is still
needed (that's a KRFB bug, not a DMA-BUF issue) — keep the `rfbGetScreen`/`rfbNewFramebuffer`
interception and only drop the EGL stubs.
---
## KRFB password
KRFB's VNC password is stored in `~/.config/krfbrc` under
`[Security] UninvitedConnectionsPassword`. To change it, edit that file and restart
`krfb.service`. Make sure `AllowUninvitedConnections=true` and `allowUnattendedAccess=true`
are set for unattended access.