sneaky4oe

KRFB Fix Black Screen Kubuntu 26.04, Protocol error: bad desktop size 0x0

Jul 17th, 2026 (edited)
762
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Markdown 16.60 KB | Fixit | 0 0

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)

# 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.

Comments
  • sneaky4oe
    120 days
    # text 0.14 KB | 0 0
    1. 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.
Add Comment
Please, Sign In to add comment