Commit 89cff7b7 for guacamole.apache.org

commit 89cff7b717648948f1d8a1f5087cf0844d75b8da
Author: Stephen Schiffli <sschiffli@keepersecurity.com>
Date:   Thu Feb 26 05:40:17 2026 -0800

    GUACAMOLE-2234: Fix race condition in non-opaque layer clearing across display worker threads.

diff --git a/src/libguac/display-layer-list.c b/src/libguac/display-layer-list.c
index f80aa3a4..b876d255 100644
--- a/src/libguac/display-layer-list.c
+++ b/src/libguac/display-layer-list.c
@@ -259,6 +259,8 @@ guac_display_layer* guac_display_add_layer(guac_display* display, guac_layer* la
     display_layer->layer = layer;
     display_layer->opaque = opaque;

+    pthread_mutex_init(&display_layer->path_lock, NULL);
+
     /* Init tracking of pending and last frames (NOTE: We need not acquire the
      * display-wide last_frame.lock here as this new layer will not actually be
      * part of the last frame layer list until the pending frame is flushed) */
@@ -364,6 +366,8 @@ void guac_display_remove_layer(guac_display_layer* display_layer) {
     guac_mem_free(display_layer->last_frame.buffer);
     guac_mem_free(display_layer->pending_frame_cells);

+    pthread_mutex_destroy(&display_layer->path_lock);
+
     guac_mem_free(display_layer);

 }
diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h
index 255fb897..d6082e1e 100644
--- a/src/libguac/display-priv.h
+++ b/src/libguac/display-priv.h
@@ -516,6 +516,12 @@ struct guac_display_layer {
      */
     int opaque;

+    /**
+     * Lock used to ensure sequences of Guacamole protocol path instructions
+     * for this layer are not interleaved when sent from concurrent threads.
+     */
+    pthread_mutex_t path_lock;
+
     /* ---------------- LAYER PREVIOUS FRAME STATE ---------------- */

     /**
diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c
index 813306fd..eb251263 100644
--- a/src/libguac/display-worker.c
+++ b/src/libguac/display-worker.c
@@ -101,15 +101,21 @@ static void guac_display_layer_clear_non_opaque(guac_display_layer* display_laye
     guac_socket* socket = client->socket;

     /* Clear destination region only if necessary due to the relevant layer
-     * being non-opaque */
+     * being non-opaque. The rect+cfill pair must be sent atomically to
+     * prevent interleaving with concurrent workers operating on the same
+     * layer. */
     if (!display_layer->opaque) {

+        pthread_mutex_lock(&display_layer->path_lock);
+
         guac_protocol_send_rect(socket, layer, dirty->left, dirty->top,
                 guac_rect_width(dirty), guac_rect_height(dirty));

         guac_protocol_send_cfill(socket, GUAC_COMP_ROUT, layer,
                 0x00, 0x00, 0x00, 0xFF);

+        pthread_mutex_unlock(&display_layer->path_lock);
+
     }

 }