Skip to content

AltGr (ISO_Level3_Shift) allocated to a dynamic spare keycode instead of the real layout keycode — breaks nested-RDP AltGr handling #370

Open

Description

@N3m3515

Environment

  • KasmVNC 1.4.1 (shipped via kasmweb/core-kasmos:1.18.0-rolling-daily)
  • KDE Plasma desktop, container XKB layout explicitly set to "de" (setxkbmap de + kxkbrc)
  • Browser client: Chrome, connecting to a physical German keyboard
  • Downstream: an RDP client inside the session (Devolutions Remote Desktop
    Manager, bundled FreeRDP 3.16.0) is what surfaces the practical impact,
    but the root cause reproduces with plain xev, no RDP involved.

Problem

With keyboard.raw_keyboard: false (default), pressing the physical AltGr
key produces an X11 KeyPress event with keysym 0xfe03 (ISO_Level3_Shift)
on keycode 92 — a "spare"/dynamically-allocated keycode — instead of
keycode 108, the standard evdev position for the physical Right-Alt/
AltGr key that a "de" XKB layout assigns it to.

xev output:
KeyPress event, ...
state 0x0, keycode 92 (keysym 0xfe03, ISO_Level3_Shift), ...

Any downstream consumer that relies on a fixed/expected keycode for AltGr
(e.g. FreeRDP's internal X11-keycode-to-VK table) never recognizes this as
a modifier and falls back to broken behavior (in FreeRDP 3.x's case,
transmitting the composed character as Unicode input instead of a proper
AltGr-modified keypress, since it gets VK_NONE for keycode 92).

Root cause (as far as I can tell from the source)

In unix/xserver/hw/vnc/XserverDesktop.cc:

void XserverDesktop::keyEvent(rdr::U32 keysym, rdr::U32 keycode, bool down)
{
  if (!rawKeyboard)
    keycode = 0;
  vncKeyboardEvent(keysym, keycode, down);
}

When raw_keyboard is off, the keycode from the client is discarded
entirely and re-derived purely from the keysym elsewhere, presumably by
allocating it onto the next free/unused keycode — which is why it lands on
92 instead of reusing the layout's real 108, regardless of what the
container's actual XKB keymap (via setxkbmap/xmodmap) defines.

We tested three independent workarounds, all unsuccessful, which suggests
this dynamic allocation isn't visible to other X11 clients through normal
means (XChangeKeyboardMapping/MappingNotify):

  1. Forcing keycode 108 = ISO_Level3_Shift via xmodmap at session start —
    zero effect on the keycode delivered in the resulting KeyPress event.
  2. Setting keyboard.raw_keyboard: true — preserves the real keycode
    (108, confirmed via xev), but breaks AltGr modifier-hold semantics
    entirely: AltGr+ no longer composes the Level3 character at all,
    both locally in-session and over RDP. This matches a known, unresolved
    TigerVNC issue ([BUG] AltGr still not working TigerVNC/tigervnc#1715, closed as "notourbug"), so we
    suspect KasmVNC inherited the same limitation in -RawKeyboard mode.
  3. "Priming" the keycode allocation by pressing AltGr once locally before
    ever opening the downstream RDP client (to rule out a stale
    keymap-snapshot-at-connect-time theory on the FreeRDP side) — also had
    no effect.

We also found #357, which describes a different symptom
(Ctrl+Shift+Minus sending the wrong sequence) in the same general area —
introduced by the 1.4.0 "Fixed bug with Ctrl key not working on foreign
language keyboards with Chromium based browsers" change. Given the
overlap, we suspect this whole area (fake-modifier/dynamic-keycode
handling for foreign layouts) may share a common cause.

Ask

Is there a way to make the dynamic keysym allocation check the layout's
existing/expected keycode for well-known modifier keysyms like
ISO_Level3_Shift before allocating a spare one, and/or announce it to
other X11 clients through the standard keymap-change mechanism? Or is
raw_keyboard meant to be the fix here, in which case the AltGr-composition
break in that mode would need to be looked at separately?

Happy to provide a full container reproduction / more xev traces on
request.

Activity

  1. N3m3515 commented on Sep 1, 2026

    @N3m3515
    Author

    Follow-up with a more precise finding, and a correction to something I
    speculated about earlier in the report.

    I initially assumed raw_keyboard: true deterministically breaks AltGr's
    modifier-hold semantics. Comparing three xev traces of the exact same
    action (physically hold AltGr, press ß) shows that's not quite right -
    it's a race condition, not a deterministic break, and it reproduces in
    BOTH keyboard.raw_keyboard settings.

    The traces

    Native X11 (no KasmVNC involved, control test on a separate machine):
    A single, clean press/hold/release cycle - no repeats:

    KeyPress  keycode 108 (ISO_Level3_Shift), state 0x0
    KeyPress  keycode 20  (backslash),        state 0x90   <- AltGr correctly held
    KeyRelease keycode 20  (backslash),        state 0x90
    KeyRelease keycode 108 (ISO_Level3_Shift), state 0x90
    

    KasmVNC, raw_keyboard: false (default): AltGr fires roughly 8 rapid
    KeyPress/KeyRelease cycles on its own keycode before the ß event ever
    arrives - even though the physical key was only held once, not repeatedly
    pressed. In the trace I captured, the ß KeyPress happened to land while
    one of those cycles was still in its "pressed" phase, so it composed
    correctly:

    ... 8x KeyPress/KeyRelease keycode 92 (ISO_Level3_Shift) ...
    KeyPress   keycode 92 (ISO_Level3_Shift), state 0x0   <- 8th press, not yet released
    KeyPress   keycode 20 (backslash),        state 0x80  <- landed while still "down" -> worked
    KeyRelease keycode 92 (ISO_Level3_Shift), state 0x80
    KeyRelease keycode 20 (ssharp),           state 0x0
    

    KasmVNC, raw_keyboard: true: Same flutter, but worse - roughly 19
    cycles this time, now on keycode 108 (confirming raw_keyboard does fix
    the keycode value itself). This time the last release lands 1ms before
    the ß press, so the modifier is no longer considered held when ß arrives,
    producing plain "ß" instead of backslash:

    ... 19x KeyPress/KeyRelease keycode 108 (ISO_Level3_Shift) ...
    KeyRelease keycode 108 (ISO_Level3_Shift), state 0x80, time T
    KeyPress   keycode 20  (ssharp),           state 0x0,  time T+1ms  <- already released -> broken
    

    Revised understanding

    The keycode-92-vs-108 mismatch (described in the original report) is
    still the deterministic reason FreeRDP/RDM specifically always fails,
    since it depends on a fixed keycode number regardless of timing.

    But the local composition failure I attributed to raw_keyboard mode
    breaking modifier semantics looks instead like a symptom of this same
    underlying flutter - a genuine race condition, not a clean behavioral
    difference tied to raw_keyboard. raw_keyboard doesn't introduce the
    flutter (it's present in the default path too, just with fewer cycles:
    8 vs 19 in my traces), it just seems to make the race more likely to
    land the wrong way, and happens to also fix the keycode number, which
    made the composition failure more visible/reproducible for us.

    The open question this raises: why does AltGr fire repeated
    press/release cycles at all while being physically held once? A
    modifier key auto-repeating like a regular character key looks like the
    actual root anomaly here, upstream of both the keycode-92 assignment and
    the raw_keyboard behavior - possibly worth looking at independently of
    the rest of this report.

    Happy to provide the full xev logs for all three traces if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions