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(¤t->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);
-
- }
-
}