From 4071cbb3c1afe36940de737fe20c69f1246f9fa2 Mon Sep 17 00:00:00 2001 From: Tyler Hebenstreit Date: Wed, 1 Jul 2026 09:39:42 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9E=20Fix=20stack=20overflow=20crash?= =?UTF-8?q?=20on=20window-move=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Window-move actions (moveUp/Down/Left/Right) crashed Loop with EXC_BAD_ACCESS (stack overflow). In WindowFrameResolver.calculateTargetFrame, the `willMove` branch read `context.getTargetFrame()`, whose lazy recompute calls `WindowFrameResolver.getFrame` on the *same* ResizeContext, which re-enters the willMove branch — infinite mutual recursion until the stack guard page is hit. Fix the willMove branch to read `lastAppliedFrame ?? cachedTargetFrame.raw`, matching the grow/shrink branches. This removes the self-recursion at its source and anchors moves to the window's actual position instead of a theoretical (possibly clamped-away) target frame. Also harden ResizeContext.recomputeTargetFrame to clear its `needsRecompute` guard before resolving, so any future re-entrant getTargetFrame() returns the cached frame instead of recursing. --- .../Window Manipulation/ResizeContext.swift | 8 +++++++- .../Window Manipulation/WindowFrameResolver.swift | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Loop/Window Management/Window Manipulation/ResizeContext.swift b/Loop/Window Management/Window Manipulation/ResizeContext.swift index 237db048..2787e39a 100644 --- a/Loop/Window Management/Window Manipulation/ResizeContext.swift +++ b/Loop/Window Management/Window Manipulation/ResizeContext.swift @@ -131,6 +131,13 @@ final class ResizeContext { } private func recomputeTargetFrame() { + // Clear the recompute guard *before* resolving so this method is re-entrancy safe: + // if `WindowFrameResolver.getFrame` ever reads `getTargetFrame()` on this same + // context while it is still being computed, the guard is already clear and the + // re-entrant call returns the cached frame instead of recomputing and recursing + // until the stack overflows. + needsRecompute = false + let result = WindowFrameResolver.getFrame(resizeContext: self) let normalized = CGRect( @@ -152,7 +159,6 @@ final class ResizeContext { normalized: normalized, padded: paddedFrame ) - needsRecompute = false log.info("Computed target frame - raw: \(cachedTargetFrame.raw), normalized: \(cachedTargetFrame.normalized) padded: \(cachedTargetFrame.padded), for action: \(action)") } diff --git a/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift b/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift index bfb5ef69..f0efe634 100644 --- a/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift +++ b/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift @@ -201,7 +201,12 @@ extension WindowFrameResolver { ) } else if direction.willMove { - let frameToResizeFrom = context.getTargetFrame().raw + // Read the last applied frame (falling back to the cached target) instead of + // `context.getTargetFrame()`. `getTargetFrame()` would recompute this very + // context and re-enter here, recursing until the stack overflows. Matching the + // grow/shrink branches above also keeps moves anchored to the window's actual + // position rather than a theoretical (possibly clamped-away) target frame. + let frameToResizeFrom = context.lastAppliedFrame ?? context.cachedTargetFrame.raw result = calculatePositionAdjustment(for: action, frameToResizeFrom: frameToResizeFrom)