Skip to content

Fix EXC_BREAKPOINT crash from re-entering sendEvent: during cursor movement - #69

Open
KJRRR wants to merge 1 commit into
jvanakker:masterfrom
KJRRR:fix/sendevent-reentrancy-pr
Open

Fix EXC_BREAKPOINT crash from re-entering sendEvent: during cursor movement#69
KJRRR wants to merge 1 commit into
jvanakker:masterfrom
KJRRR:fix/sendevent-reentrancy-pr

Conversation

@KJRRR

@KJRRR KJRRR commented Aug 16, 2026

Copy link
Copy Markdown

Problem

On tvOS 26/27 the app dies with EXC_BREAKPOINT during ordinary use — not on any particular page or action, but while simply moving the cursor. It looks like a random quit back to the Home Screen.

Cause

-[ViewController browserRemoteInputControllerHoverStateAtCursorPoint:] is called from touchesMoved: to decide whether the cursor should show the arrow or the pointer. It answers by querying the page synchronously, via -[BrowserWebView stringByEvaluatingJavaScriptFromString:].

That evaluation is only synchronous by virtue of BrowserPumpRunLoopUntil spinning the run loop until WebKit calls back. WebKit runs the page out of process, so this blocks the main thread — and spinning the run loop from inside UIKit event delivery dispatches the next UIEvent, re-entering -[UIApplication sendEvent:] while the first call is still on the stack.

UIKit traps on that from tvOS 26 onwards. Earlier releases tolerated the re-entrancy, which is why this only shows up now.

The device crash report shows the recursion directly — note browser_sendEvent: appearing twice in one stack:

UIApplication sendEvent:                      ← trap
browser_sendEvent:                            ← event #2, re-entrant
updateCycleEntry
__CFRunLoopRun
NSRunLoop runMode:beforeDate:
BrowserPumpRunLoopUntil                       ← spins the run loop
BrowserWebView stringByEvaluatingJavaScriptFromString:
BrowserDOMInteractionService DOMPointForCursorOrigin:
ViewController browserRemoteInputControllerHoverStateAtCursorPoint:
BrowserRemoteInputController handleTouchesMoved:
ViewController touchesMoved:
UIWindow _sendTouchesForEvent:
UIWindow sendEvent:
UIApplication sendEvent:
browser_sendEvent:                            ← event #1

Fix

1. Never pump the run loop during event delivery. The existing sendEvent: swizzle now tracks its own nesting depth, and stringByEvaluatingJavaScriptFromString: returns nil instead of pumping while an event is in flight. That closes the whole class of bug rather than this one call site.

2. Keep the pointer cursor working. The hover query now answers from the last known value while an event is being delivered, and schedules a refresh on a later turn of the run loop, where blocking is safe. The refresh is throttled to ten per second with one in flight, so the cursor still tracks links — it can lag by a frame, which is not perceptible in use.

3. Bound the pump. BrowserPumpRunLoopUntil had no timeout at all, so a page that never calls back could spin the main thread indefinitely and get the app killed by the watchdog. It now gives up after two seconds.

The click path is unaffected: it already defers through performSelector:withObject:afterDelay:, so it runs outside event delivery. I audited the rest of the input path — the hover query was the only synchronous evaluation reachable from sendEvent:.

Testing

Reproduced and fixed on Apple TV 4K (3rd generation), tvOS 27.0 (24J5325d), built with Xcode 27.0 against the tvOS 27.0 SDK. Before the change, moving the cursor around a page reliably killed the app within a minute or two; ~15 crash reports pulled off the device all show the recursion above. After the change, the same usage no longer crashes. Also builds and runs clean in the tvOS 27 simulator.

Scope of what I actually verified: one device, one tvOS version, Debug configuration. I have not tested Release builds, older Apple TV hardware, or tvOS 26 — though the trapping behaviour is a UIKit change rather than anything device-specific, so I would expect tvOS 26 to be affected the same way.

Note this is independent of #68 and can be merged on its own, in either order.

🤖 Generated with Claude Code

Moving the cursor calls -[ViewController browserRemoteInputControllerHoverStateAtCursorPoint:]
from touchesMoved:, which queried the page synchronously. That evaluation spins the run loop
waiting on the out-of-process web content, and spinning it from inside event delivery
dispatches the next UIEvent, re-entering -[UIApplication sendEvent:] while the first call is
still on the stack. UIKit traps on that from tvOS 27 onwards, so the app dies with
EXC_BREAKPOINT during ordinary cursor movement.

Confirmed by device crash reports, which show the recursion directly:

  UIApplication sendEvent:  <- browser_sendEvent:  <- updateCycleEntry
    <- NSRunLoop runMode:beforeDate: <- BrowserPumpRunLoopUntil
    <- stringByEvaluatingJavaScriptFromString: <- DOMPointForCursorOrigin:
    <- hoverStateAtCursorPoint: <- touchesMoved: <- UIWindow sendEvent:
    <- browser_sendEvent:

Track the depth of sendEvent: in the existing swizzle, refuse to pump the run loop while an
event is being delivered, and answer the hover query from the last known value, refreshing it
on a later turn of the run loop where blocking is safe. The refresh is throttled to ten a
second with one in flight, so the pointer cursor still tracks links.

Also bound the pump at two seconds, so a page that never calls back cannot hang the app.

The click path is unaffected: it already defers through performSelector:afterDelay:, so it
runs outside event delivery.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant