From e43e2424a4bdac47bf664607c2c9bf44abea220c Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Tue, 30 Jun 2026 10:33:43 -0700 Subject: [PATCH] Fix use-after-free crash during workspace switch animation During workspace switch animations, references to the source and target workspaces (data->from and data->to) are tracked. If a workspace is destroyed while the animation is active (e.g., when a series of workspace switches is executed in a single transaction, leaving intermediate workspaces empty and unfocused so they are garbage collected at the transaction commit), these references become dangling. Upon subsequent animation steps or completion of the animation, accessing the workspaces triggers a use-after-free crash. To address this, we register wl_listener callbacks on the node destroy signals of the source and target workspaces. If either workspace is destroyed while the animation is active, we null out the corresponding reference. All callback and filter code paths are updated to safely handle null pointers. Here is a minimal shell script to reproduce the crash on a build without this fix: ```bash #!/bin/bash set -x # Setup isolated run environment TEMP_DIR=$(mktemp -d) export XDG_RUNTIME_DIR="$TEMP_DIR" export WLR_BACKENDS=headless export WLR_LIBINPUT_NO_DEVICES=1 export LSAN_OPTIONS=detect_leaks=0 unset DISPLAY unset WAYLAND_DISPLAY # Create minimal configuration cat < "$TEMP_DIR/config" animations enabled on EOF # Start scroll compositor headlessly ./build/sway/scroll -c "$TEMP_DIR/config" -d > "$TEMP_DIR/scroll.log" 2>&1 & SCROLL_PID=$! # Wait for the compositor to start up and open its wayland socket for i in {1..50}; do SOCKET_PATH=$(find "$XDG_RUNTIME_DIR" -name "wayland-*" | head -n 1) if [ -n "$SOCKET_PATH" ] && [ -S "$SOCKET_PATH" ]; then break fi sleep 0.1 done if [ -z "$SOCKET_PATH" ] || [ ! -S "$SOCKET_PATH" ]; then echo "Error: scroll failed to start or open socket" cat "$TEMP_DIR/scroll.log" kill $SCROLL_PID || true rm -rf "$TEMP_DIR" exit 1 fi SOCKET_NAME=$(basename "$SOCKET_PATH") # Switch to workspace 2 and immediately switch back to workspace 1 to trigger UAF export SWAYSOCK="$XDG_RUNTIME_DIR/i3-ipc.sock" if [ ! -S "$SWAYSOCK" ]; then # Look for the IPC socket SWAYSOCK=$(find "$XDG_RUNTIME_DIR" -name "*.sock" | head -n 1) fi echo "Using SWAYSOCK=$SWAYSOCK" ./build/swaymsg/scrollmsg -s "$SWAYSOCK" "workspace 2; workspace 1" 2>&1 # Wait a brief moment for the crash to complete and the process to terminate sleep 0.5 # Check if the compositor crashed if kill -0 $SCROLL_PID 2>/dev/null; then echo "Compositor is still running! Reproduction FAILED." echo "=== COMPOSITOR LOG ===" cat "$TEMP_DIR/scroll.log" kill $SCROLL_PID rm -rf "$TEMP_DIR" exit 1 else echo "Compositor has terminated (expected)!" cat "$TEMP_DIR/scroll.log" rm -rf "$TEMP_DIR" exit 0 fi ``` --- sway/tree/workspace.c | 52 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c index 56b4f834..588608b1 100644 --- a/sway/tree/workspace.c +++ b/sway/tree/workspace.c @@ -795,8 +795,22 @@ struct workspace_switch_data { list_t *from_containers; list_t *to_containers; struct sway_root_filters *root_filters; + struct wl_listener from_destroy; + struct wl_listener to_destroy; }; +static void workspace_switch_from_destroy(struct wl_listener *listener, void *data) { + struct workspace_switch_data *ws_data = wl_container_of(listener, ws_data, from_destroy); + ws_data->from = NULL; + wl_list_remove(&listener->link); +} + +static void workspace_switch_to_destroy(struct wl_listener *listener, void *data) { + struct workspace_switch_data *ws_data = wl_container_of(listener, ws_data, to_destroy); + ws_data->to = NULL; + wl_list_remove(&listener->link); +} + static void workspace_switch_callback_end(void *callback_data) { struct workspace_switch_data *data = callback_data; root_filters_destroy(root, data->root_filters); @@ -815,10 +829,17 @@ static void workspace_switch_callback_end(void *callback_data) { if (data->from && data->from->output) { node_set_dirty(&data->from->node); } - if (data->to->output) { + if (data->to && data->to->output) { node_set_dirty(&data->to->node); } + if (data->from_destroy.link.next) { + wl_list_remove(&data->from_destroy.link); + } + if (data->to_destroy.link.next) { + wl_list_remove(&data->to_destroy.link); + } + list_free_items_and_destroy(data->from_containers); list_free_items_and_destroy(data->to_containers); free(data); @@ -840,7 +861,7 @@ static bool switching_output(struct sway_workspace *workspace, } struct sway_output *output = workspace->output; struct sway_output *from_output = data->from ? data->from->output : NULL; - struct sway_output *to_output = data->to->output; + struct sway_output *to_output = data->to ? data->to->output : NULL; if (!output || !from_output || !to_output) { return false; } @@ -1028,6 +1049,20 @@ static void animate_workspace_switch(struct sway_output *output, data->from_containers = create_list(); data->to_containers = create_list(); + data->from_destroy.notify = workspace_switch_from_destroy; + if (data->from) { + wl_signal_add(&data->from->node.events.destroy, &data->from_destroy); + } else { + wl_list_init(&data->from_destroy.link); + } + + data->to_destroy.notify = workspace_switch_to_destroy; + if (data->to) { + wl_signal_add(&data->to->node.events.destroy, &data->to_destroy); + } else { + wl_list_init(&data->to_destroy.link); + } + animation_end(); animation_set_type(ANIMATION_WORKSPACE_SWITCH); @@ -1102,12 +1137,13 @@ bool workspace_switch(struct sway_workspace *workspace) { seat_set_focus(seat, next); // old_ws may not have an output because it is being destroyed if empty - if (!layout_overview_workspaces_enabled() && old_ws != workspace && ( - (old_ws->output && old_ws->output == workspace->output) || - (old_ws->output && !workspace->output) || - (!old_ws->output && workspace->output)) && - workspace->split.sibling != old_ws && - animation_enabled() && animation_path_enabled(ANIMATION_WORKSPACE_SWITCH)) { + if (!layout_overview_workspaces_enabled() && old_ws != workspace && + ((old_ws->output && old_ws->output == workspace->output) || + (old_ws->output && !workspace->output) || + (!old_ws->output && workspace->output)) && + workspace->split.sibling != old_ws && animation_enabled() && + animation_path_enabled(ANIMATION_WORKSPACE_SWITCH) && !workspace->node.destroying && + !old_ws->node.destroying) { struct sway_output *output = old_ws->output ? old_ws->output : workspace->output; animate_workspace_switch(output, old_ws, workspace); } else {