Skip to content

fix: guard tree rendering against destroyed window actors#507

Open
braceyourself wants to merge 1 commit intoforge-ext:mainfrom
braceyourself:fix/guard-destroyed-window-actors
Open

fix: guard tree rendering against destroyed window actors#507
braceyourself wants to merge 1 commit intoforge-ext:mainfrom
braceyourself:fix/guard-destroyed-window-actors

Conversation

@braceyourself
Copy link

Got kicked out of my entire GNOME session on Feb 20. No warning, just landed on an error page with a "log out" button.

  1. journalctl -b 0 told the story: signal 11 (SIGSEGV) in gnome-shell, then signal 6 (mutter assertion) right behind it. A double crash.
  2. Signal 11 pointed at the Forge extension. Signal 6 was just mutter reacting to the mess.
  3. Traced it into tree.js - the recursive processNode() traversal, line 1555.
  4. Reproduced it: open a few windows in tabbed layout, kill -9 one of them, watch gnome-shell die on the next render cycle.
  5. The smoking gun was child.actor.border.get_theme_node() accessing a dead window's actor. The render pipeline just assumes everything is alive. GJS doesn't stop you from touching a finalized GObject - it lets the access through to C, and C doesn't ask twice.

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.

Tested by opening 3 windows in tabbed layout, kill -9ing one, and confirming gnome-shell stayed up with no SIGSEGV or forge errors in journalctl.

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.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the window tree rendering/movement logic in the GNOME Shell extension against windows being destroyed mid-render (e.g., invalid/composited actors), aiming to prevent GJS exceptions during tab/decor rendering and move() application.

Changes:

  • Add Node.isNodeValid() to detect finalized window actors safely.
  • Skip processing/rendering for invalid window nodes during layout computation.
  • Add defensive guards/try/catch around window moves, border width lookups, and tab actor attachment.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1409 to 1412
// 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) {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In processNode(), sizes is computed from the unfiltered tiledChildren, but the later loop filters with tiledChildren.filter((c) => c.isNodeValid()) and uses the filtered loop’s index to read params.sizes[index]. If any invalid nodes are filtered out, the indices no longer line up and split/tab/stack calculations will apply the wrong sizes to the remaining windows (and can leave stale decoration state when the filtered list becomes empty). Filter invalid nodes before computing sizes and set params.tiledChildren to that same filtered list so indexing and decoration logic stay consistent.

Copilot uses AI. Check for mistakes.
Comment on lines 458 to 465
let tabContents = new St.BoxLayout({
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();
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early return when this.app is missing happens after creating tabContents. When app is null (e.g., dying/untracked windows), this allocates an unused St.BoxLayout actor and immediately discards it. Consider checking this.app before creating any St widgets so you avoid unnecessary actor allocations on the hot path.

Copilot uses AI. Check for mistakes.
this.extWm.move(metaWin, w.renderRect);
try {
this.extWm.move(metaWin, w.renderRect);
} catch (e) {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply() now swallows all exceptions from this.extWm.move() with an empty catch. This makes unrelated failures (bad rects, API changes, etc.) silent and hard to diagnose. If the intention is to ignore “window destroyed mid-render” errors, consider (a) filtering invalid windows before calling move and/or (b) logging at least a debug/warn message (possibly rate-limited) inside the catch so unexpected errors are visible.

Suggested change
} catch (e) {
} catch (e) {
Logger.debug(
`extWm.move failed for window ${metaWin} with rect ${JSON.stringify(
w.renderRect
)}: ${e}`
);

Copilot uses AI. Check for mistakes.
Comment on lines +1555 to +1562
// 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) {
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The try/catch around child.actor.border.get_theme_node() has an empty catch block. If this is expected when windows are destroyed mid-render, please add at least a brief comment or debug-level log in the catch so unexpected theme/actor errors don’t get silently ignored (and consider narrowing the guarded code to only what can throw).

Copilot uses AI. Check for mistakes.
if (child.tab && !decoration.contains(child.tab)) {
try {
decoration.add_child(child.tab);
} catch (e) {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The try/catch around decoration.add_child(child.tab) currently ignores all errors with an empty catch. This can hide real UI issues (e.g., actor already disposed for reasons other than window destruction). Consider logging at debug/warn (or adding a clarifying comment) so unexpected failures are diagnosable, or restructure to avoid calling add_child when the tab actor is no longer valid.

Suggested change
} catch (e) {
} catch (e) {
Logger.debug(`Failed to add tab actor to decoration: ${e}`);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants