Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Sources/WindowDragHandleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ func windowDragHandleShouldTreatTopHitAsPassiveHost(_ view: NSView) -> Bool {
return false
}

/// Re-entrancy guard for the sibling hit-test walk. When `sibling.hitTest()`
/// triggers SwiftUI view-body evaluation, AppKit can call back into this
/// function before the outer invocation finishes, causing a Swift
/// exclusive-access violation (SIGABRT). Main-thread only, no lock needed.
private var _windowDragHandleIsResolvingSiblingHits = false

/// Returns whether the titlebar drag handle should capture a hit at `point`.
/// We only claim the hit when no sibling view already handles it, so interactive
/// controls layered in the titlebar (e.g. proxy folder icon) keep their gestures.
Expand Down Expand Up @@ -295,6 +301,20 @@ func windowDragHandleShouldCaptureHit(
return true
}

// Bail out if we're already inside a sibling hit-test walk. This happens
// when sibling.hitTest() re-enters SwiftUI layout, which calls hitTest on
// this drag handle again. Proceeding would trigger an exclusive-access
// violation in the Swift runtime.
guard !_windowDragHandleIsResolvingSiblingHits else {
#if DEBUG
dlog("titlebar.dragHandle.hitTest capture=false reason=reentrant point=\(windowDragHandleFormatPoint(point))")
#endif
return false
}

_windowDragHandleIsResolvingSiblingHits = true
defer { _windowDragHandleIsResolvingSiblingHits = false }

let siblingSnapshot = Array(superview.subviews.reversed())

#if DEBUG
Expand Down
41 changes: 41 additions & 0 deletions cmuxTests/CmuxWebViewKeyEquivalentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6521,6 +6521,24 @@ final class WindowDragHandleHitTests: XCTestCase {
}
}

/// A sibling view whose hitTest re-enters windowDragHandleShouldCaptureHit,
/// simulating the crash path where sibling.hitTest triggers a SwiftUI layout
/// pass that calls back into the drag handle's hit resolution.
private final class ReentrantSiblingView: NSView {
weak var dragHandle: NSView?
var reenteredResult: Bool?

override func hitTest(_ point: NSPoint) -> NSView? {
guard bounds.contains(point), let dragHandle else { return nil }
// Simulate the re-entry: during sibling hit test, SwiftUI layout
// calls windowDragHandleShouldCaptureHit on the drag handle again.
reenteredResult = windowDragHandleShouldCaptureHit(
point, in: dragHandle, eventType: .leftMouseDown, eventWindow: dragHandle.window
)
return nil
}
}

func testDragHandleCapturesHitWhenNoSiblingClaimsPoint() {
let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36))
let dragHandle = NSView(frame: container.bounds)
Expand Down Expand Up @@ -6751,6 +6769,29 @@ final class WindowDragHandleHitTests: XCTestCase {
)
}

func testDragHandleSiblingHitTestReentrancyDoesNotCrash() {
let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36))
let dragHandle = NSView(frame: container.bounds)
container.addSubview(dragHandle)

let reentrantSibling = ReentrantSiblingView(frame: container.bounds)
reentrantSibling.dragHandle = dragHandle
container.addSubview(reentrantSibling)

// The outer call enters the sibling walk, which calls
// reentrantSibling.hitTest(), which re-enters
// windowDragHandleShouldCaptureHit. Without the re-entrancy guard
// this would trigger a Swift exclusive-access violation (SIGABRT).
let outerResult = windowDragHandleShouldCaptureHit(
NSPoint(x: 110, y: 18), in: dragHandle, eventType: .leftMouseDown
)
XCTAssertTrue(outerResult, "Outer call should still capture when sibling returns nil")
XCTAssertEqual(
reentrantSibling.reenteredResult, false,
"Re-entrant call should bail out (return false) instead of crashing"
)
}

func testDragHandleTopHitResolutionSurvivesSameWindowReentrancy() {
let point = NSPoint(x: 180, y: 18)
let window = NSWindow(
Expand Down