Commit 738ebcf8 for guacamole.apache.org

commit 738ebcf8a84e2d88bc874d38690ffeb7bbb5b688
Author: Michael Jumper <mjumper@apache.org>
Date:   Mon Aug 3 01:48:47 2026 -0700

    GUACAMOLE-2313: Synchronize VNC lock key state when supported.

diff --git a/src/protocols/vnc/input.c b/src/protocols/vnc/input.c
index f22d0e53..37d42bf9 100644
--- a/src/protocols/vnc/input.c
+++ b/src/protocols/vnc/input.c
@@ -18,6 +18,7 @@
  */

 #include "display.h"
+#include "input.h"
 #include "vnc.h"

 #include <guacamole/display.h>
@@ -74,6 +75,47 @@ static int guac_vnc_translate_keysym(int keysym) {

 }

+/**
+ * Toggles the lock controlled by the given lock keysym within the VNC session
+ * by pressing and releasing that key. The key will be pressed/released only if
+ * the given lock flag is set within the reported lock key state.
+ *
+ * @param rfb_client
+ *     The VNC client to send key events through.
+ *
+ * @param led_state
+ *     The lock key state reported by the VNC server, as a bitmask of
+ *     rfbKeyboardMask* flags.
+ *
+ * @param led_mask
+ *     The single rfbKeyboardMask* flag to test.
+ *
+ * @param keysym
+ *     The keysym of the lock key that toggles the tested lock.
+ */
+static void guac_vnc_release_lock(rfbClient* rfb_client, int led_state,
+        int led_mask, int keysym) {
+    if (led_state & led_mask) {
+        SendKeyEvent(rfb_client, keysym, TRUE);
+        SendKeyEvent(rfb_client, keysym, FALSE);
+    }
+}
+
+void guac_vnc_keyboard_led_state(rfbClient* rfb_client, int state, int pad) {
+
+    guac_client* client = rfbClientGetClientData(rfb_client, GUAC_VNC_CLIENT_KEY);
+    guac_vnc_client* vnc_client = (guac_vnc_client*) client->data;
+
+    /* Clear lock states once we know which locks need to be cleared */
+    if (!vnc_client->lock_state_synced) {
+        guac_vnc_release_lock(rfb_client, state, rfbKeyboardMaskCapsLock,   0xFFE5 /* Caps_Lock */);
+        guac_vnc_release_lock(rfb_client, state, rfbKeyboardMaskNumLock,    0xFF7F /* Num_Lock */);
+        guac_vnc_release_lock(rfb_client, state, rfbKeyboardMaskScrollLock, 0xFF14 /* Scroll_Lock */);
+        vnc_client->lock_state_synced = 1;
+    }
+
+}
+
 int guac_vnc_user_key_handler(guac_user* user, int keysym, int pressed) {

     guac_vnc_client* vnc_client = (guac_vnc_client*) user->client->data;
@@ -85,9 +127,22 @@ int guac_vnc_user_key_handler(guac_user* user, int keysym, int pressed) {
                 keysym, pressed);

     /* Send VNC event only if finished connecting */
-    if (rfb_client != NULL)
+    if (rfb_client != NULL) {
+
+        /* Ensure the lock key state of the VNC session matches the
+         * all-released state assumed by connecting clients before any key
+         * events are forwarded */
+        if (!vnc_client->lock_state_synced) {
+            vnc_client->lock_state_synced = 1;
+            guac_client_log(user->client, GUAC_LOG_WARNING, "VNC server did "
+                "not report which lock keys are active. Client-side use of "
+                "Caps Lock, etc. may not match server-side lock state.");
+        }
+
         SendKeyEvent(rfb_client, guac_vnc_translate_keysym(keysym), pressed);

+    }
+
     return 0;
 }

diff --git a/src/protocols/vnc/input.h b/src/protocols/vnc/input.h
index cce07168..63f81bc6 100644
--- a/src/protocols/vnc/input.h
+++ b/src/protocols/vnc/input.h
@@ -21,6 +21,7 @@
 #define GUAC_VNC_INPUT_H

 #include <guacamole/user.h>
+#include <rfb/rfbclient.h>

 /**
  * Handler for Guacamole user mouse events.
@@ -32,6 +33,22 @@ guac_user_mouse_handler guac_vnc_user_mouse_handler;
  */
 guac_user_key_handler guac_vnc_user_key_handler;

+/**
+ * Handler for lock key state (KeyboardLedState) updates received from the VNC
+ * server. Where supported, this is used to help ensure the client can assume all
+ * locks are inactive at connection start.
+ *
+ * @param client
+ *     The VNC client associated with the update.
+ *
+ * @param state
+ *     The reported lock key state, as a bitmask of rfbKeyboardMask flags.
+ *
+ * @param pad
+ *     Unused (reserved by libvncclient for future use).
+ */
+void guac_vnc_keyboard_led_state(rfbClient* client, int state, int pad);
+
 /**
  * Handler for Guacamole user resize events.
  */
diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c
index 26af94c6..47102c44 100644
--- a/src/protocols/vnc/vnc.c
+++ b/src/protocols/vnc/vnc.c
@@ -23,6 +23,7 @@
 #include "common/clipboard.h"
 #include "cursor.h"
 #include "display.h"
+#include "input.h"
 #include "log.h"
 #include "settings.h"
 #include "vnc.h"
@@ -242,6 +243,9 @@ rfbClient* guac_vnc_get_client(guac_client* client) {
     vnc_client->rfb_GotCopyRect = rfb_client->GotCopyRect;
     rfb_client->GotCopyRect = guac_vnc_copyrect;

+    /* Lock key state (KeyboardLedState) update handler */
+    rfb_client->HandleKeyboardLedState = guac_vnc_keyboard_led_state;
+
 #ifdef ENABLE_VNC_TLS_LOCKING
     /* TLS Locking and Unlocking */
     rfb_client->LockWriteToTLS = guac_vnc_lock_write_to_tls;
diff --git a/src/protocols/vnc/vnc.h b/src/protocols/vnc/vnc.h
index 3a348ecc..fb0ec6cc 100644
--- a/src/protocols/vnc/vnc.h
+++ b/src/protocols/vnc/vnc.h
@@ -100,6 +100,13 @@ typedef struct guac_vnc_client {
      */
     int finished_frame_logged;

+    /**
+     * Whether the initial lock key state of the VNC session has been received
+     * and synchronized with the all-released state that connecting Guacamole
+     * clients assume.
+     */
+    int lock_state_synced;
+
     /**
      * Client settings, parsed from args.
      */