From 8128f3ec2165bbd43157b16fd335a5efa6c6eb3f Mon Sep 17 00:00:00 2001 From: braceyourself Date: Sun, 22 Feb 2026 16:37:19 -0500 Subject: [PATCH] fix: guard tree rendering against destroyed window actors If a window gets destroyed mid-render (app crash, rapid close, async cleanup), nodes in the tree still reference the finalized GObject. Accessing anything on it segfaults gnome-shell (signal 11). Adds isNodeValid() that probes the actor with get_name() in a try/catch (GJS throws on finalized GObjects rather than segfaulting). Filters dead nodes out before layout, and guards the remaining property accesses that can race with window destruction. --- lib/extension/tree.js | 44 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/lib/extension/tree.js b/lib/extension/tree.js index c7cf7fe..7d2c169 100644 --- a/lib/extension/tree.js +++ b/lib/extension/tree.js @@ -459,7 +459,9 @@ export class Node extends GObject.Object { style_class: "window-tabbed-tab", x_expand: true, }); + // Window tracker may not resolve an app for a dying window let app = this.app; + if (!app) return; let labelText = this._getTitle(); let metaWin = this.nodeValue; let titleButton = new St.Button({ @@ -537,6 +539,21 @@ export class Node extends GObject.Object { return null; } + // Check if the underlying window actor is still alive. GJS throws + // on property access of finalized GObjects rather than segfaulting, + // so a cheap get_name() call is enough to detect dead actors. + isNodeValid() { + if (!this.isWindow()) return true; + try { + let actor = this._actor; + if (!actor) return false; + actor.get_name(); + return true; + } catch (e) { + return false; + } + } + render() { // Always update the title for the tab if (this.tab !== null && this.tab !== undefined) { @@ -1274,8 +1291,12 @@ export class Tree extends Node { tiledChildren.forEach((w) => { if (w.renderRect) { if (w.renderRect.width > 0 && w.renderRect.height > 0) { + // Window may have been destroyed since processNode computed renderRect let metaWin = w.nodeValue; - this.extWm.move(metaWin, w.renderRect); + try { + this.extWm.move(metaWin, w.renderRect); + } catch (e) { + } } else { Logger.debug(`ignoring apply for ${w.renderRect.width}x${w.renderRect.height}`); } @@ -1385,7 +1406,8 @@ export class Tree extends Node { }); } - tiledChildren.forEach((child, index) => { + // Skip windows whose actors were destroyed mid-render + tiledChildren.filter((c) => c.isNodeValid()).forEach((child, index) => { // A monitor can contain a window or container child if (node.layout === LAYOUT_TYPES.HSPLIT || node.layout === LAYOUT_TYPES.VSPLIT) { this.processSplit(node, child, params, index); @@ -1527,10 +1549,17 @@ export class Tree extends Node { if (node.childNodes.length > 1 || alwaysShowDecorationTab) { nodeY = nodeRect.y + params.stackedHeight; nodeHeight = nodeRect.height - params.stackedHeight; - if (node.decoration && child.isWindow()) { + if (node.decoration && child.isWindow() && child.isNodeValid()) { let gap = this.extWm.calculateGaps(node); let renderRect = this.processGap(node); - let borderWidth = child.actor.border.get_theme_node().get_border_width(St.Side.TOP); + // Border actor may be gone if the window was destroyed mid-render + let borderWidth = 0; + try { + if (child.actor?.border) { + borderWidth = child.actor.border.get_theme_node().get_border_width(St.Side.TOP); + } + } catch (e) { + } // Make adjustments to the gaps let adjust = 4 * Utils.dpi(); @@ -1552,7 +1581,12 @@ export class Tree extends Node { } else { decoration.hide(); } - if (!decoration.contains(child.tab)) decoration.add_child(child.tab); + if (child.tab && !decoration.contains(child.tab)) { + try { + decoration.add_child(child.tab); + } catch (e) { + } + } } child.render();