Commit 1221b59f for guacamole.apache.org

commit 1221b59fd12f28b1d2a5db49bd51b626d340568f
Author: Michael Jumper <mjumper@apache.org>
Date:   Sat Jul 11 17:36:09 2026 -0700

    GUACAMOLE-2118: Defer destruction of removed layers until no operations reference them.

diff --git a/src/libguac/display-flush.c b/src/libguac/display-flush.c
index c3a403d4..aef8208d 100644
--- a/src/libguac/display-flush.c
+++ b/src/libguac/display-flush.c
@@ -304,6 +304,7 @@ void guac_display_end_mouse_frame(guac_display* display) {
 void guac_display_end_multiple_frames(guac_display* display, int frames) {

     guac_display_plan* plan = NULL;
+    guac_display_layer* removed_layers = NULL;

     guac_rwlock_acquire_write_lock(&display->pending_frame.lock);
     display->pending_frame.frames += frames;
@@ -380,6 +381,16 @@ void guac_display_end_multiple_frames(guac_display* display, int frames) {

     guac_rwlock_release_lock(&display->last_frame.lock);

+    /* The last frame layer list has now been rebuilt from the pending frame
+     * layer list, so any layers removed since the previous flush are no longer
+     * part of either frame. Detach them for destruction below.
+     *
+     * IMPORTANT: These layers MUST NOT be freed until after the frame is fully
+     * flushed. The display plan may reference a removed layer's buffer as the
+     * source of a copy operation, and that plan has not yet been applied. */
+    removed_layers = display->pending_frame_removed_layers;
+    display->pending_frame_removed_layers = NULL;
+
     /* Awaken worker threads to perform the rest of the tasks required for the
      * frame (if any such tasks remain) */
     if (plan != NULL) {
@@ -403,4 +414,8 @@ void guac_display_end_multiple_frames(guac_display* display, int frames) {
 finished_with_pending_frame_lock:
     guac_rwlock_release_lock(&display->pending_frame.lock);

+    /* Free any layers detached above. NOTE: This is intentionally done outside
+     * the pending frame lock to avoid contending with drawing operations. */
+    guac_display_free_removed_layers(display, removed_layers);
+
 }
diff --git a/src/libguac/display-layer-list.c b/src/libguac/display-layer-list.c
index 82e47bac..6e61dec8 100644
--- a/src/libguac/display-layer-list.c
+++ b/src/libguac/display-layer-list.c
@@ -23,6 +23,7 @@
 #include "guacamole/display.h"
 #include "guacamole/layer.h"
 #include "guacamole/mem.h"
+#include "guacamole/protocol.h"
 #include "guacamole/rwlock.h"

 #include <cairo/cairo.h>
@@ -308,64 +309,77 @@ void guac_display_remove_layer(guac_display_layer* display_layer) {
     if (display_layer->pending_frame.next != NULL)
         display_layer->pending_frame.next->pending_frame.prev = display_layer->pending_frame.prev;

+    /* Clear the now-stale list pointers so that an erroneous second removal of
+     * the same layer trips the assertion above rather than corrupting the
+     * list */
+    display_layer->pending_frame.prev = NULL;
+    display_layer->pending_frame.next = NULL;
+
+    /* The layer is deliberately NOT unlinked from the last_frame list here, as
+     * full removal and release of memory is deferred until after frame flush */
+    display_layer->next_removed = display->pending_frame_removed_layers;
+    display->pending_frame_removed_layers = display_layer;
+
     guac_rwlock_release_lock(&display->pending_frame.lock);

-    /*
-     * Remove layer from last frame
-     */
+}

-    guac_rwlock_acquire_write_lock(&display->last_frame.lock);
+void guac_display_free_removed_layers(guac_display* display,
+        guac_display_layer* removed_layers) {

-    /* Update previous element, if it exists */
-    if (display_layer->last_frame.prev != NULL)
-        display_layer->last_frame.prev->last_frame.next = display_layer->last_frame.next;
+    guac_client* client = display->client;

-    /* If there is no previous element, then this element is the list head if
-     * the list has any elements at all. Update the list head accordingly. */
-    else if (display->last_frame.layers != NULL) {
-        GUAC_ASSERT(display->last_frame.layers == display_layer);
-        display->last_frame.layers = display_layer->last_frame.next;
-    }
+    guac_display_layer* current = removed_layers;
+    while (current != NULL) {

-    /* Update next element, if it exists */
-    if (display_layer->last_frame.next != NULL)
-        display_layer->last_frame.next->last_frame.prev = display_layer->last_frame.prev;
+        guac_display_layer* next_removed = current->next_removed;
+        const guac_layer* layer = current->layer;

-    guac_rwlock_release_lock(&display->last_frame.lock);
+        guac_client_free_buffer(client, current->last_frame_buffer);

-    /*
-     * Layer has now been removed from both pending and last frame lists and
-     * can be safely freed
-     */
+        /* Release any Cairo resources */
+        guac_display_layer_cairo_context* cairo_context = &(current->pending_frame_cairo_context);
+        if (cairo_context->surface != NULL) {

-    guac_client* client = display->client;
-    guac_client_free_buffer(client, display_layer->last_frame_buffer);
+            cairo_surface_destroy(cairo_context->surface);
+            cairo_context->surface = NULL;

-    /* Release any Cairo resources */
-    guac_display_layer_cairo_context* cairo_context = &(display_layer->pending_frame_cairo_context);
-    if (cairo_context->surface != NULL) {
+            cairo_destroy(cairo_context->cairo);
+            cairo_context->cairo = NULL;

-        cairo_surface_destroy(cairo_context->surface);
-        cairo_context->surface = NULL;
+        }

-        cairo_destroy(cairo_context->cairo);
-        cairo_context->cairo = NULL;
+        /* Free memory for underlying image surface and change tracking cells.
+         * Note that we do NOT free the associated memory for the pending frame
+         * if it was replaced with an external buffer. */

-    }
+        if (!current->pending_frame.buffer_is_external)
+            guac_mem_free(current->pending_frame.buffer);
+
+        guac_mem_free(current->last_frame.buffer);
+        guac_mem_free(current->pending_frame_cells);

-    /* Free memory for underlying image surface and change tracking cells. Note
-     * that we do NOT free the associated memory for the pending frame if it
-     * was replaced with an external buffer. */
+        pthread_mutex_destroy(&current->path_lock);

-    if (!display_layer->pending_frame.buffer_is_external)
-        guac_mem_free(display_layer->pending_frame.buffer);
+        if (layer->index != 0) {

-    guac_mem_free(display_layer->last_frame.buffer);
-    guac_mem_free(display_layer->pending_frame_cells);
+            /* Free the corresponding layer/buffer on the remote side */
+            guac_protocol_send_dispose(client->socket, layer);

-    pthread_mutex_destroy(&display_layer->path_lock);
+            /* As long as this isn't the display layer, it's safe to cast away
+             * the constness and free the underlying layer/buffer. Only the
+             * default layer (layer #0) is truly const. */
+            if (layer->index > 0)
+                guac_client_free_layer(client, (guac_layer*) layer);
+            else
+                guac_client_free_buffer(client, (guac_layer*) layer);

-    guac_mem_free(display_layer);
+        }
+
+        guac_mem_free(current);
+        current = next_removed;
+
+    }

 }

diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h
index d6082e1e..5980a861 100644
--- a/src/libguac/display-priv.h
+++ b/src/libguac/display-priv.h
@@ -609,6 +609,17 @@ struct guac_display_layer {
      */
     size_t pending_frame_cells_height;

+    /**
+     * The next layer within the list of layers that have been removed from
+     * the pending frame and are awaiting destruction, or NULL if this is the
+     * last such layer. This value is meaningful only for layers that have been
+     * removed from the pending frame layer list.
+     *
+     * IMPORTANT: The display-level pending_frame.lock MUST be acquired before
+     * modifying or reading this member.
+     */
+    guac_display_layer* next_removed;
+
 };

 typedef struct guac_display_state {
@@ -646,12 +657,13 @@ typedef struct guac_display_state {
      *  - NEXT: layer->pending_frame.next
      *  - PREV: layer->pending_frame.prev
      *
-     * Existing layers are deleted only at the time a frame is flushed when a
-     * layer in the last frame layer list is found to no longer exist in the
-     * pending frame layer list. The same goes for the addition of new layers:
-     * they are added only during flush when a layer that was not present in
-     * the last frame layer list is found to be present in the pending frame
-     * layer list.
+     * Layers are added to and removed from only the pending frame layer list.
+     * The last frame layer list is rebuilt from the pending frame layer list
+     * each time a frame is flushed, so an added or removed layer takes effect
+     * on the last frame only at that point. A removed layer is freed only
+     * after a flush has dropped it from the last frame layer list (see the
+     * pending_frame_removed_layers member of guac_display and
+     * guac_display_free_removed_layers()).
      */
     guac_display_layer* layers;

@@ -738,6 +750,16 @@ struct guac_display {
      */
     int pending_frame_dirty_excluding_mouse;

+    /**
+     * The head of the list of layers that have been removed from the pending
+     * frame but cannot be freed until the frame is flushed. This list is
+     * iterated via the next_removed member of guac_display_layer.
+     *
+     * IMPORTANT: The display-level pending_frame.lock MUST be acquired before
+     * modifying or reading this member.
+     */
+    guac_display_layer* pending_frame_removed_layers;
+
     /* ---------------- WELL-KNOWN LAYERS / BUFFERS ---------------- */

     /**
@@ -830,14 +852,33 @@ struct guac_display {
 guac_display_layer* guac_display_add_layer(guac_display* display, guac_layer* layer, int opaque);

 /**
- * Removes the given layer from all linked lists containing that layer and
- * frees all associated memory.
+ * Removes the given layer from the pending frame layer list. The layer is not
+ * immediately freed, but is instead scheduled to be freed via the
+ * pending_frame_removed_layers list of the display. It will finally be freed
+ * and removed from the last frame list when the frame is flushed.
  *
  * @param display_layer
  *     The layer to remove.
  */
 void guac_display_remove_layer(guac_display_layer* display_layer);

+/**
+ * Frees the list of layers that are awaiting removal from a previous call to
+ * guac_display_remove_layer(). This function is safe to call only after all
+ * removed layers are not part of any outstanding frame, including any
+ * operations that may not yet have been flushed by the worker threads.
+ *
+ * @param display
+ *     The display that the layers were removed from.
+ *
+ * @param removed_layers
+ *     The head of the list of removed layers to free, linked via the
+ *     next_removed member of guac_display_layer, or NULL if there are no such
+ *     layers.
+ */
+void guac_display_free_removed_layers(guac_display* display,
+        guac_display_layer* removed_layers);
+
 /**
  * Resizes the given layer to the given dimensions, including any underlying
  * image buffers.
diff --git a/src/libguac/display.c b/src/libguac/display.c
index cd8d6271..8d259b97 100644
--- a/src/libguac/display.c
+++ b/src/libguac/display.c
@@ -122,8 +122,8 @@ guac_display* guac_display_alloc(guac_client* client) {
     display->last_frame.timestamp = display->pending_frame.timestamp = guac_timestamp_current();

     /* It's safe to discard const of the default layer here, as
-     * guac_display_free_layer() function is specifically written to consider
-     * the default layer as const */
+     * guac_display_free_removed_layers() is specifically written to consider the
+     * default layer as const */
     display->default_layer = guac_display_add_layer(display, (guac_layer*) GUAC_DEFAULT_LAYER, 1);
     display->cursor_buffer = guac_display_alloc_buffer(display, 0);

@@ -218,15 +218,15 @@ void guac_display_free(guac_display* display) {
     guac_flag_destroy(&display->render_state);
     guac_fifo_destroy(&display->ops);

-    /* Free all layers within the pending_frame list (NOTE: This will also free
-     * those layers from the last_frame list) */
+    /* Remove any layers remaining in the pending frame (by definition, all other
+     * layers must already have been marked for removal) */
     while (display->pending_frame.layers != NULL)
         guac_display_free_layer(display->pending_frame.layers);

-    /* Free any remaining layers that were present only on the last_frame list
-     * and not on the pending_frame list */
-    while (display->last_frame.layers != NULL)
-        guac_display_free_layer(display->last_frame.layers);
+    /* All layers are now part of the pending_frame_removed_layers list and can
+     * be freed */
+    guac_display_free_removed_layers(display, display->pending_frame_removed_layers);
+    display->pending_frame_removed_layers = NULL;

     guac_rwlock_destroy(&display->last_frame.lock);
     guac_rwlock_destroy(&display->pending_frame.lock);
@@ -376,25 +376,5 @@ guac_display_layer* guac_display_alloc_buffer(guac_display* display, int opaque)
 }

 void guac_display_free_layer(guac_display_layer* display_layer) {
-
-    guac_display* display = display_layer->display;
-    const guac_layer* layer = display_layer->layer;
-
     guac_display_remove_layer(display_layer);
-
-    if (layer->index != 0) {
-
-        guac_client* client = display->client;
-        guac_protocol_send_dispose(client->socket, layer);
-
-        /* As long as this isn't the display layer, it's safe to cast away the
-         * constness and free the underlying layer/buffer. Only the default
-         * layer (layer #0) is truly const. */
-        if (layer->index > 0)
-            guac_client_free_layer(client, (guac_layer*) layer);
-        else
-            guac_client_free_buffer(client, (guac_layer*) layer);
-
-    }
-
 }