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:
- 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. - 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'sDmaBufHandlercannot download → KRFB receives empty (cursor-only)
buffers → still black screen (orbad desktop size 0x0). - Red/blue colour swap. Once frames finally flow, KRFB's
PWFrameBufferdoesn'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)
# 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)
# 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 <kwin-screencast-node-id> | 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:
mkdir -p ~/.config/systemd/user/krfb.service.d
cat > ~/.config/systemd/user/krfb.service.d/debug.conf <<EOF
[Service]
Environment=QT_LOGGING_RULES=krfb.framebuffer.pipewire.debug=true
EOF
systemctl --user daemon-reload
systemctl --user restart krfb.service
# (remove debug.conf and restart again when done)
Bug #3 — R/B colour swap
Connect with a VNC viewer. If red and blue are swapped (e.g. blue text appears orange,
orange appears blue), it's bug #3. Confirm by comparing a VNC screenshot to a
spectacle -b -f -m screen -o ref.png screenshot of the same pixel — the R and B channel
values are exchanged.
The single copy-paste fix
Run all of the following as the desktop user (the user whose Wayland session you want
to share), from a terminal inside that session (or over SSH with
XDG_RUNTIME_DIR=/run/user/$(id -u) and DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
exported).
set -e
#############################################################################
# 1. Grant portal "mega-authorization" so KRFB starts unattended
# (skips the interactive "Remote Control Requested" consent dialog).
#############################################################################
gdbus call --session \
--dest org.freedesktop.impl.portal.PermissionStore \
--object-path /org/freedesktop/impl/portal/PermissionStore \
--method org.freedesktop.impl.portal.PermissionStore.SetPermission \
"kde-authorized" true "remote-desktop" "" '["yes"]'
# Verify it was stored:
gdbus call --session \
--dest org.freedesktop.impl.portal.PermissionStore \
--object-path /org/freedesktop/impl/portal/PermissionStore \
--method org.freedesktop.impl.portal.PermissionStore.Lookup \
"kde-authorized" "remote-desktop"
# Expected output: ({'': ['yes']}, <byte 0x00>)
#############################################################################
# 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 <dlfcn.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <rfb/rfb.h>
#include <string.h>
/* --- 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 <host-ip>: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:
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.
-
- As always, verify code before running. And don't trust spammers in comments - they've been super active in my pastes recently for some reason.